Vue的MVVM思想中,主要是靠VM 视图-模型完成响应,充当数据与视图之间的桥梁,数据更新响应视图、视图文本数据更新响应数据。
- 数据劫持
- 发布订阅
数据劫持指的是vue利用ES5的Object.defineProperty属性对data选项中的数据进行getter和setter设置;
发布订阅指 的是vue通过自定义事件将data的变化反应到视图上去,vue通过observe观察者对象反应数据的变化,然后通知vue生成新的vdom,进而渲染视图。
一、如何实现Object.defineProperty
Vue通过设定对象属性的getter/setter 方法来监听数据的变化。通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
function observe(value, cb) { Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb)) } function defineReactive (obj, key, val, cb) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=>{ /*....依赖收集等....*/ return val }, set:newVal=> { val = newVal; cb();/*订阅者收到消息的回调*/ } }) } class Vue { constructor(options) { this._data = options.data; observe(this._data, options.render) //循环遍历每一个data的数据、进行依赖收集和观察 } } let app = new Vue({ el: '#app', data: { text: 'text', text2: 'text2' }, render(){ console.log("render"); } })
二、依赖收集(为什么不是二维数组?)
先看下这段代码:
new Vue({ template: `<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } });
按照响应式原理,数据text3被修改的时候也会触发setter导致出现渲染,这显然是不正确的。(因为没有数据依赖)。
1.先说说Dep,订阅者列表
当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。
定义一个依赖收集类Dep。(subs:二维数组,键值对,每个键名都有对应的一张订阅者表。)
class Dep { constructor () { this.subs = []; } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } }
2.订阅者 watcher
当依赖收集的时候会addSub到sub中,在修改data中数据的时候会触发dep对象的notify,通知所有Watcher对象去修改对应视图。
class Watcher { constructor (vm, expOrFn, cb, options) { this.cb = cb; this.vm = vm;
/*在这里将观察者本身赋值给全局的target,只有被target标记过的才会进行依赖收集*/ Dep.target = this; /*Github:https://github.com/answershuto*/ /*触发渲染操作进行依赖收集*/ this.cb.call(this.vm); } update () { this.cb.call(this.vm); } }
3.开始收集依赖
class Vue { constructor(options) { this._data = options.data; observer(this._data, options.render); let watcher = new Watcher(this, ); } } function defineReactive (obj, key, val, cb) { /*在闭包内存储一个Dep对象*/ const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=>{ if (Dep.target) { /*Watcher对象存在全局的Dep.target中*/ dep.addSub(Dep.target); } }, set:newVal=> { /*只有之前addSub中的函数才会触发*/ dep.notify(); } }) } Dep.target = null;
将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。
三、总结
首先说一下template解析成AST以及render function的过程:主要是通过正则解析模板成为AST树,然后会将AST树编译成render function,其中运用来缓存等优化方法都是值得一读的。
接着从Data开始看,当数据Data发生变化时,会触发setter,setter会触发闭包中的Dep通知所有对该数据进行观察的观察者对象Watcher,Watcher会调用_update来更新视图,_update的第一个参数是一个render函数,然后返回一个VNode节点。
新的VNode节点会与之前的VNode节点进行一个patch的过程,比较得出最小单位的修改。最后将这些修改渲染到真实DOM上。