js中的getter与setter
要了解vue的响应式原理,必先了解js的getter与setter。下面的例子描述了getter与setter是如何为用户定义的对象o工作的。
1 | var o = { |
js是一个动态语言,可以动态为对象添加一个属性,同时设置它的getter和setter。使用Object.defineProperty扩展Date原型,为预定义好的Date类添加year属性。
1 | var d = Date.prototype; |
综上,getter和setter可以通过两种方式来定义。
- 使用对象初始化器定义
- 使用
Object.defineProperty方法。
vue响应式原理
原来,在vue的设计中,有两个核心的概念:组件和虚拟DOM树。组件其实就是一个js对象,通过vue.extend方法创建的对象就是一个组件(这个方法在vue3.0以后就没有了),而一个组件对应一个虚拟DOM树,虚拟DOM树会最终挂载到真正的浏览器DOM树上。
当你把一个普通的js对象传入vue实例作为data选项,vue将会遍历此对象所有的property,并使用Object.defineProperty把这些property全部转化为getter和setter,在property被访问和修改时通知变更。
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把接触到的数据property记录为依赖,例如:template中有一行html代码<p>{{ message }}</p> ,组件实例中的data选项有对象Data: {message: "zhou"}。当渲染到上面那句html代码时,就会记录依赖项为message和依赖为p结点。之后,当依赖项的setter触发时,会通知watcher,从而使它关联的所有组件重新渲染(本组件和它的关联的子组件,如果在子组件中使用了v-bind命令进行结点和父亲数据的单向数据绑定,那么就有父组件的数据和子组件的结点的关联)。

数据劫持,vue响应式实现
参考 https://segmentfault.com/a/1190000006599500
先上一个代码,看双向数据绑定的应用场景。
1 | <div id="mvvm-app"> |

vue.js采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty来劫持data中各个属性的setter和getter,在数据变动时发布消息给订阅者。
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听(若属性为对象,则继续对属性的属性进行监听,这是一个递归),如有变动可将最新值通知订阅者。
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,构建虚拟DOM,根据指令模板替换数据,绑定响应的更新函数。
- 实现一个Watcher,作为连接Observer和Compile的桥梁,Watcher就是订阅者,能够收到更新消息,并通过Compile对虚拟DOM进行更新,进而更新视图。
observer
Observer对象为data中的所有属性,包括子属性,加入setter和getter,来监听属性的变动,那么需要将data递归遍历。给某个对象赋值,就会触发setter,那么就能监听到属性变化了。
监听子属性:为子属性加入getter和setter。
在这篇文章中,属性一般指的是vue实例中data对象中的属性及其子属性。
1 | //observer.js |
我们已经可以监听每个数据的变化了,那么如何通知订阅者呢?我们就在observer对象里面维护一个订阅者队列dep,数据变动,就触发订阅者队列的notify()方法。
1 | //observer.js |
我们知道,watcher就是订阅者,但是如何向订阅者队列中添加watcher订阅者呢?
1 | //observer.js |
试想,一个watcher要想加入到某个属性的订阅者队列中,就要访问这个属性(因为可以触发它的getter),所以在wathcer的构造过程中,要有一个访问属性的过程。
1 | //watcher.js |
watcher
watcher是observer和compile的桥梁。主要做:
- 在自身实例化时往属性的订阅者队列中添加自己
- 自身有一个updata方法
- 待dep.notify()执行后,能调用自身的updata方法,并触发compile中绑定的回调(具体见compile的实现)
watcher并不会直接改变虚拟DOM和视图,compile做这件事,watcher只是compile的门户。
1 | function watcher(vm, exp, cb){ |
实例化watcher,就意味建立了某个属性与组件中某个结点(元素结点或文本结点)的关联:watcher执行updata()方法,执行compile中定义的回调函数,进而操作对应的DOM结点。
所以,什么时候实例化watcher呢?
compile
compile主要做:
- 建立虚拟DOM
- 将虚拟DOM挂载到浏览DOM上,由浏览器渲染页面
- 给每个vue指令对应的结点绑定更新回调函数,每个回调函数对应一个watcher订阅者,watcher主动触发回调函数,更改虚拟DOM,并重新挂载到浏览器DOM上。
至于什么是虚拟DOM,什么是浏览器DOM,看代码解释。
1 | //compile.js |
MVVM
MVVM作为数据绑定的入口,整合了observer,compile和watcher三者,通过observer监听自己model数据的变化,通过compile解析编译模板指令,最终利用watcher搭起observer和compile之间的通信桥梁,达到数据变化->视图更新,视图交互变化->数据model变化的双向绑定效果。
1 | //mvvm.js |
但是有一个问题,这里监听的数据对象是options.data,每次更新视图,则必须通过vm._data.name = 'dmq'的方式,而不是vm.name = 'dmq'。
所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性。
1 | //mvvm.js |