vue通过使用数据劫持+发布订阅实现数据的双向绑定。
首先,先要搞懂两个问题:
- 什么是数据劫持(Object.defineProperty)。
- 什么是发布/订阅模式。
1、什么是数据劫持:
举个例子,现在有个obj对象,在他身上有个name属性是’vue’,像这样: var obj = { name: ‘vue’ },现在我们想通过监听obj.name的变化,当name属性发生变化时,我们可以实时得到一个反馈,那么如何来做呢, Object.defineProperty可以实现上述需求:
利用Object.defineProperty重写obj.name的setter,和getter函数
可以看到,通过Object.defineProperty() 监控到obj的name属性,当对name属性发生变化的时候,便会执行setter函数在控制台输出 ‘change’, 并且data.name的值为‘3’ 。
2、什么是发布订阅:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
举个例子,A、B、C三个人都订了一期报纸,那么邮局送报纸的话,一定是在某个时间点,给他们订的这一期一次送完,这个过程就是发布-订阅(反过来说更形象订阅-发布)映射到vue中就是,我们可以有很多个div绑定了data中的name属性(订阅),当数据发生变化,将会通知所有绑定name属性的视图进行更新(发布)。
自定义事件便是一个比较简单的例子,这里不做赘述了。
明白这两个核心思想之后,我们看下整个过程:
那么知道了这些之后,实现双向绑定,需要做什么?
- 入口函数,数据监听器_observer 函数,对数据对象的所有属性进行监听,如有变动拿到最新值并通知订阅者
指令解析器_compile函数,对每个元素节点进行遍历,根据指令绑定数据。 - Watcher函数,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
有了基本思路后,我们接下来一步步来实现。
1、入口函数:
入口函数不多做解释,唯一需要理解的就是watcher的用处,定义watcher池,目的是为了将多个订阅者对象存入,如果这些订阅者订阅的某个属性发生变化,则通知所有的订阅者进行视图更新。比如我们有多个div绑定了同一个name,那么我们就需要将所有绑定name的dom元素存入watcher池,如果name发生变化,则通知这些订阅者(也就是绑定name的div)进行视图更新,这个wacther大概呢,长这样
可以看到,绑定name的dom元素,有三个。
2、实现_observer:
上边也说了,_observer函数需要对data里边所有数据对象进行监控,这个实现起来也简单,不过有些细节需要注意:
在对data进行遍历的时候,要将data里边的每个属性先存入watcher池占个坑位,这样,我们就可以在编译模板时,把对应的绑定值直接存入。
在set中,需要通知对应绑定属性的dom元素节点,更新视图数据。
上述代码主要作了三件事:
1、遍历vue实例上的data对象(注意:此处没有深度遍历,vue实际上会对data进行深度遍历) 重写data上每个属性的getter和setter。
2、将每个属性的key都存入watcher池,默认为一个数组。
3、当修改data中的某一属性时,从watcher池中取出监控这个属性的数组进行遍历(也就是this._data[key])对所有绑定这个属性的元素update ( 通知视图进行更新 )。
3、实现_compile:
首先需要深度遍历dom树,将带有v-model属性和v-bind属性的dom元素拿到。
如果发现input或textarea标签绑定有v-model属性,则获取到v-model绑定的值,将push进_observer函数中data对应的的数据watcher池,并为其添加input事件。
如果发现绑定有v-bind属性的标签,直接push进_observer函数中data对应的的数据watcher池。
到了这一步,基本上已经写的差不多了,我们只需要一个桥梁,把_compile和_observer联系起来,就大功告成。
3、Watcher:
watcher是个独立的构造函数,在上边的实例代码中我们也能看到,每个数据的watcher池中push的都是Watcher的实例。Watcher函数需要四个参数:当前的vue实例对象,dom元素绑定data的key,当前dom元素,绑定到当前dom元素的哪个属性上。以及一个update方法,因为我们需要在new Watcher()的时候进行视图更新(因为首先页面初始化时,遍历到绑有v-bind或v-model的元素,总要进行push Watcher实例的操作,且此时也应该将data[key]赋值给当前元素,也就是说,页面初始化过程中,遇到有v-bind的元素需要将数据更新到dom元素中去)
现在只需要在入口函数中,执行_compile和_observer即可
到目前为止,我们已经可以使用了,就像下边这样
参考链接:
https://javascript.ruanyifeng.com/stdlib/attributes.html Object.defineProperty
https://blog.csdn.net/q1056843325/article/details/53353850 发布/订阅模式
https://github.com/DMQ/mvvm vue双向绑定实现原理
流程图来源:知乎,具体哪篇找不到了
总结:
代码主要介绍了vue对于双向绑定的实现思路,vue真正在实现双向绑定的细节还有多未涉及的,比如重写了数组的一些原型方法如:push等。
在模版编译阶段也只是作了简单的演示,主要目的还在于理解vue双向绑定的实现思路。