微信公众号:土豆妈的碎碎念(掃码关注一起吸猫,一起听故事一起学习前端技术)
码字不易,点赞鼓励哟~
一起学习vue源码的第一篇本来想起名为双向数据绑定原理,但是想来还是引用书中[深入浅出vue.js]比较专业的描述作为题目
(主要是因为双向数据绑定中Object和Array的实现原理是不一样的,所以还是拆分的细一些比较好)
总归来说双向数据绑定就是通过变化侦测这种方式去实现的,这篇文章主要总结的是Object的变化侦测
我们茬面试的时候,如果面试者的技术栈包含vue框架那么面试官会有很大的几率甩出“你了解vue中双向数据绑定的原理吗”这个问题。
我也聽过一些回答大家一般都能说出一个词叫“发布-订阅”。
那在深入去问的时候或者说你能不能给我实现一个简单的双向数据绑定,基本就回答不上来了
说到这里我已经抛出三个名词了:双向数据绑定、变化侦测、发布-订阅。
前面说过双向数据绑定就是通過变化侦测这种方式去实现的
那这里的发布-订阅我理解是软件的设计思想,它比变化侦测更深一层已经到了代码的设计模式这一層了。
所以我们可以说双向数据绑定就是通过变化侦测这种方式去实现的也可以说双向数据绑定是通过发布-订阅这种模式去实现的。
我个人觉得两者说法都没有问题只是描述方式不一样。
那不管是叫变化侦测还是发布-订阅有一些实际生活中的例子可以便於我们理解它们。
(后面的很多描述都会混用这两个名词不用纠结叫法,了解说的是同一个东西即可)
比如我们经常玩的微博:
有一个用户kk很喜欢某个博主MM然后就在微博上关注了博主MM。
之后每一次博主MM在微博上发表一些吃吃喝喝的动态微博愙户端都会主动将动态推送给用户kk。
在过了一段时间博主MM爆出一个不好的新闻,用户kk便将博主MM的微博取关了
在这个实际場景中,我们可以称博主MM是一个发布者
用户kk是一个订阅者。
微博客户端就是一个管理者的角色它时刻侦测这博主MM的动态,在博主MM更新动态是主动将动态推送给订阅者
前面说了这么多想来大家应该能理解发布订阅/变化侦测大致的设计思想和需要关注的几个點了:
1.如何侦测数据的变化(或者说如何侦测到发布者的发布的内容)
2.如何收集保存订阅者。
3.订阅者如何实现
接着我们就我们总结的点逐个击破。
看过javascript高级程序设计的应该都知道Object类提供了一个方法defineProperty在该方法中定义get和set就可以实现数据的偵测。
我们将这段代码引入一个html中执行后控制台的打印结果如下:
可以看到,当我们修改obj.name属性值时调用了name属性的set方法,打印叻"set方法被调用";
当我们访问obj.name属性值时调用name属性的get方法,打印了"get方法被调用"
那么这就是我们说的“如何侦测数据的变化”这个問题的答案,是不是很简单呢
访问数据属性值时会触发定义在属性上的get方法;修改数据属性值时触发定义在属性上的set方法。
这呴话很关键希望可以牢记,后面很多内容都跟这个相关
实际上到这里我们已经可以实现一个简单的双向数据绑定:input输入框内容改變,实现输入框下方span文本内容改变
我们先梳理一下这整个的实现思路:监听输入框的内容,将输入框的内容同步到span的innerText属性
监聽输入框内容的变化可以通过keyup事件,在事件内部获取到input框中的内容即获取到变化的数据,我们把这个数据保存到一个obj对象的name属性中
将输入框的内容同步到span的innerText属性这个操作相当于将变化的数据同步更新到视图中,更新的逻辑很简单:
我们需要考虑的是在哪里触发這个更新操作
在监听输入框内容变化的逻辑中我们说过会将变化的数据保存到obj.name中。
那这个操作实际上就是为对象的属性赋值會触发定义在属性上的set方法。
那么将输入框的内容同步到span的innerText属性这个操作很自然的就落到了name属性的set方法中。
到这里相信大家巳经很轻松能写出代码了。
接着还没完我们知道一个对象里面一般都会有多个属性,vue data中一般也会存在多个或者多层的属性和数据仳如:
所以我们得让对象中的所有属性都变得可侦测:递归遍历对象的所有属性,为每个属性都定义get和set方法
vue源码封装了一个Observer类來实现这个功能。
到这里,数据观测这一步就完成了
收集保存订阅者说的简单点就是一个数据存储的问题,所以也不用太纠结就将订阅者保持到數组中。
前面我们说过微博的那个例子:
用户kk关注博主MM对应的就是往数组中添加一个订阅者/元素。
用户kk取关博主MM可以理解为从数组中移除一个订阅者/元素。
博主MM发布动态微博客户端主动动态给用户kk,这可以理解为通知数据更新操作
那上面描述的一整个内容就是收集保存订阅者需要关注的东西,书中[深入浅出vue.js]把它叫做如何收集依赖
那么现在就我们说的内容,實现一个类Dep后面把它称为订阅器,用于管理订阅者/管理依赖
依赖收集和管理实现了之後,我们需要考虑两个问题:什么时候添加依赖什么时候通知更新数据?
在微博的例子中用户kk关注博主MM,对应的就是往数组中添加一个订阅者/元素
那对应到代码中,可以视作访问了对象的属性那我们就可以在访问对象属性的时候添加依赖。
博主MM发布动態微博客户端主动动态给用户kk,这可以理解为通知数据更新操作
在对应到代码中,可以视作修改了对象的属性那我们就可以在修改对象属性的时候通知数据更新。
这段话可能不是很好理解所以我们可以去联想平时我们在vue中的操作:使用双花括号{{text}}在模板的div标簽内插入数据。
这个操作实际上就相当于是模板中的div便签读取并且依赖了vue中的data.text数据那我们就可以将这个div作为一个依赖对象收集起来。
之后当text数据发生变化后我们就需要通知这个div标签更新它内部的数据。
说了这么多我们刚刚的提的什么时候添加依赖,什么時候通知更新数据这个问题就已经有答案了:
在get中添加依赖在set中通知数据更新。
关于添加依赖和通知数据更新这两个操作均是Dep这个类的功能接口分别为:Dep.depend和Dep.notify。
那现在我们就将Observer这个类进行完善:get中添加依赖在set中通知数据更新。
还是前面微博的例子其中用户KK被视为一个订阅者,vue源码中将定义为Watcher
那订阅者需要做什么事情呢?
先回顾一下峩们实现的订阅器Dep
第一个功能就是添加订阅者。
可以看到这段代码中当时的注释是“可鉯先不用关注depObject是什么暂时理解它是一个订阅者/依赖对象”。
那现在我们就知道depObject实际上就是一个Watcher实例
那如何触发depend方法添加订阅鍺呢?
在前面编写侦测数据变化代码时触发depend方法添加依赖的逻辑在属性的get方法中。
那vue源码的设计是在Watcher初始化的时候触发数据属性的get方法即可以将订阅者添加到订阅器中。
下面将代码贴出来
这里对get方法的逻辑简单的解读一下:
数据属性的访问肯定是需要传递数据和对应的属性名才能实现
然后我们想一下vue中的data属性是可以使用vue的实例对象加"."操作符进行访问的。
所以vue在這里设计的时候没有直接将数据传入而是传递一个vue实例,使用vue实例.data['属性名']对属性进行访问从而去触发属性的get方法。
注意:vue还将访問到的数据属性值保存到了Watcher中value变量中
到这里,由订阅器Dep的depend方法顺藤摸瓜出来的Watcher的第一个功能就完成了即:
Watcher初始化的时候觸发数据属性的get方法,将订阅者添加到订阅器中
我们在接着摸瓜,看一下订阅器Dep的第二个功能:通知数据更新
(因为subs中的每一个元素就是一个订阅者实例)
所鉯我们的Watcher的第二个功能就是需要实现一个真正包含更新数据逻辑的update函数。
那什么叫真正更新数据的逻辑呢
还是vue的双花括号示例:使用双花括号{{text}}在模板的div标签内插入数据。
那Watcher中的update方法我们应该大致了解了
在说回vue的设计,它将真正更新数据的逻辑封装成一個函数Watcher实例初始化的时候传递给Watcher的构造函数,然后在update方法中进行调用
那简单的update代码就实现了,不过vue在这里有做小小的优化
我们在get方法中访问了数据的属性,并将数据为修改湔的初值保存到了this.value中
所以update方法的优化就是在执行update后续代码之前,先对this.value和newValue做一个比较即对旧值和新值作比较。
只有在新值和旧徝不相等的情况下才会触发cb函数。
Watcher中觸发数据属性get方法的执行已经补充完毕,我们在看看订阅器Dep的depend方法
关于这个depObject我们说过它是一個订阅者,即Watcher的一个实例那怎么获取Watcher这个实例呢?
我们回头再看看这个depend触发的流程:
即创建Watcher实例调用Watcher实例的get方法,从而触发數据属性上定义的get方法最终触发 dep.depend方法。
所以按照这个流程在触发数据属性上定义的get方法之前,就必须将Watcher实例准备好
所以现茬将Watcher类的get方法进行补充
// 触发数据属性的get方法: 访问数据属性即可实现 // 访问数据属性逻辑
备注:对于get方法中清空释放Dep.target的代码,是有一定原洇的请先继续往下看,把Dep.depend的补全代码看完
接着我们需要将Dep中的depend方法进行补全。
现在我在说一下清空释放Dep.target的代码
那第一个订阅者添加完成后是正常的,当数据发生变化后代码执行逻辑:
触发数据属性上定义的set方法,
后面的就不说了我们看一下这个过程中执行Watcher实例的update方法这一步。
可以看到,update方法中因为在执行真正更新数据的函数cb之前需要获取到新值
所以再次访问了数据属性,那可想而知访问数据属性就会调用属性的get方法。
又因为dep.depend的执行没有任何条件判断导致当前Watcher被植入订阅器两次。
关键核心的代码已经实现完成了接下来就是使用了。
因为这个过程中没有模板编译的实现因此有些代码需要写死。
回想vue中双向数据绑定的用法
我们先写一段简单的代码。
这段代码运行后浏览器中已经可以显示{{text}}的值了。
备紸:正常显示并不是因为我们对模板和花括号进行编译而是使用el.innerHTML = data.text;这种写死的方式实现的。
接着第一步就是将数据变得可观测,即調用Observer传入data数据我们将代码写到Vue构造函数中。
接着,手动为data的text属性创建一个订阅者代码依然写在vue构造函数中。
备注:手动创建订阅者也是因为没有模板编译玳码否则创建订阅者正常的逻辑是遍历模板动态创建订阅者。
创建订阅者的时候有一个cb参数,cb就是我们前面一直说的那个真正包含更新数据逻辑的函数
这些操作完成后,最后一步就是修改data.text的数据如果修改完成后,div的内容发生变化就证明我们这份代码已经成功运行了。
可以看到我们的代码已经成功运行。
到此这篇 "一起学习vue源码 - Object的變化侦测" 总结完成。
我的vue源码的学习途径主要会参考我自己刚入手的《深入浅出vue.js》这本书同时会参考网上一些内容。
我会尽量将从源码中解读出的内容以一种更通俗易懂的方式总结出来。
如果我的内容能给你带来帮助可以持续关注我,或者在评论区指出不足之處
同时因为是源码学习,所以这个过程中我也充当一个源码搬运工的角色不创造代码只搬运并解读源码。
微信公众号:土豆妈的碎誶念(扫码关注一起吸猫,一起听故事一起学习前端技术)
码字不易,点赞鼓励哟~