我们都知道Vue通过MVVM思想实现数据的双向绑定,数据驱动页面视图。那它到底是如何进行双向绑定的呢?
Vue数据双向绑定是通过采用数据劫持结合发布者-订阅者模式的方式来实现的。通过Object.defineProperty()来劫持各个属性的setter,getter。修改触发set方法赋值,获取触发get方法取值,在数据变动时发布消息给订阅者,触发相应的回调并通过数据劫持发布信息。
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
首先我们看一下Object.defineProperty()方法:
其核心就是通过Object.defineProperty()来实现对属性的劫持,那么在设置或者获取的时候我们就可以在get或者set方法里加入其他的触发函数,达到监听数据变动的目的。
实现Observer
它的作用是将一个正常的object转换为每个层级的属性都是响应式的。
我们定义了__ob__属性用来存储定义的Observer实例。通过walk方法进行当前层的属性遍历并设置为响应式的。
由于存在对象属性嵌套的情况,所以在遍历每一个属性时,我们需要对其子元素进行设置为响应式的,至此形成了多个函数循环调用(递归)。
数组的响应式
我们都知道Vue 实现数据双向绑定通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持。Vue 框架是通过遍历数组和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。Vue的数组响应式是如何实现的?它是以Array.prototype为原型,创建了一个arrayMethods对象,使用Object.setPrototypeOf()强制让数组指向arrayMethods,这样就可以触发我们在arrayMethods中的改写的数组操作方法。
实现Dep和Watcher
依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。
在getter中收集依赖,在setter中触发依赖。我们可以监听每个数据的变化了,那么监听到变化之后触发notify,再调用订阅者的update方法。
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例;