咱们在前面推导过程当中实现了一个简单版的watcher。这里面还有一些问题vue
class Watcher { constructors(component, getter, cb){ this.cb = cb // 对应的回调函数,callback this.getter = getter; this.component = component; //这就是执行上下文 } //收集依赖 get(){ Dep.target = this; this.getter.call(this.component) if (this.deep) { traverse(value) } Dep.target = null; } update(){ this.cb() } }
所谓的同步更新是指当观察的主体改变时马上触发更新。而实际开发中这种需求并很少,同一事件循环中可能须要改变好几回state状态,但视图view只须要根据最后一次计算结果同步渲染就行(react中的setState就是典型)。若是一直作同步更新无疑是个很大的性能损耗。
这就要求watcher在接收到更新通知时不能全都马上执行callback。咱们对代码作出相应调整react
constructors(component, getter, cb, options){ this.cb = cb // 对应的回调函数,callback this.getter = getter; this.id = UUID() // 生成一个惟一id this.sync = options.sync; //默认通常为false this.vm = component; //这就是执行上下文 this.value = this.getter() // 这边既收集了依赖,又保存了旧的值 } update(){ if(this.sync){ //若是是同步那就马上执行回调 this.run(); }else{ // 不然把此次更新缓存起来 //可是就像上面说的,异步更新每每是同一事件循环中屡次修改同一个值, // 那么一个wather就会被缓存屡次。由于须要一个id来判断一下, queueWatcher(this) } } run: function(){ //获取新的值 var newValue = this.getter(); this.cb.call(this.vm, newValue, this.value) }
这里的一个要注意的地方是,考虑到极限状况,若是正在更新队列中wather时,又塞入进来该怎么处理。所以,加入一个flushing
来表示队列的更新状态。
若是加入的时候队列正在更新状态,这时候分两种状况:缓存
let flushing = false; let has = {}; // 简单用个对象保存一下wather是否已存在 function queueWatcher (watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 若是以前没有,那么就塞进去吧,若是有了就不用管了 if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // ... 等同一事件循环结束后再依次处理队列里的watcher。具体代码放到后面nexttick部分再说 } } }
这么设计不无道理。咱们之因此为了将wather放入队列中,就是为了较少没必要要的操做。考虑以下代码框架
data: { a: 1 }, computed: { b: function(){ this.a + 1 } } methods: { act: function(){ this.a = 2; // do someting this.a = 1 } }
在act操做中,咱们先改变a,再把它变回来。咱们理想情况下是a没变,b也不从新计算。这就要求,b的wather执行update的时候要拿到a最新的值来计算。这里就是1。若是队列中a的watehr已经更新过,那么就应该把后面的a的wather放到当前更新的wather后面,当即更新。这样能够保证后面的wather用到a是能够拿到最新的值。
同理,若是a的wather尚未更新,那么把新的a的wather放的以前的a的wather的下一位,也是为了保证后面的wather用到a是能够拿到最新的值。dom
之因此把计算属性拿出爱单独讲,是由于异步
所谓的按需计算顾名思义就是用到了才会计算,即调用了某个计算属性的get方法。在前面的方法中,咱们在class Watcher的constructor中直接调用了getter方法收集依赖,这显然是不符合按需加载的原则的。函数
实际开发中,咱们发现一个计算属性每每由另外一个计算属性得来。如,性能
computed: { a: function(){ return this.name; }, b: function(){ return this.a + "123"; } }
对于a而言,它是b的依赖,所以有必要在a的wather执行update操做时也更新b,也就意味着,a的watcher里须要收集着b的依赖。而收集的时机是执行b的回调时,this.a调用了a的get方法的时候
在computed部分,已经对计算属性的get方法进行了改写学习
function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { //调用一个计算属性的get方法时,会在watcher中收集依赖。 watcher.depend() return watcher.evaluate() } }
咱们再修改一下wather代码:this
class Watcher { constructors(component, getter, cb, options){ this.cb = cb this.getter = getter; this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed //因为是对计算属性特殊处理,那确定要给个标识符以便判断 } this.dirty = this.computed // for computed watchers this.value = this.lazy ? undefined : this.get(); } update(){ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run: function(){ //拿到新值 const value = this.get() if (value !== this.value || //基本类型的值直接比较 // 对象没办法直接比较,所以都进行计算 isObject(value)) { // set new value const oldValue = this.value this.value = value this.dirty = false cb.call(this.vm, value, oldValue) } } // 新增depend方法,收集计算属性的依赖 depend () { if (this.dep && Dep.target) { this.dep.depend() } } } //不要忘了还要返回当前computed的最新的值 //因为可能不是当即更新的,所以根据dirty再判断一下,若是数据脏了,调用get再获取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
在绑定依赖以前(computed的get被触发一次),computed用到的data数据改变是不会触发computed的从新计算的。
对于render和computed想要收集依赖,咱们只须要执行一遍回调函数就行,可是对于$watch方法,咱们并不关心他的回调是什么,而更关心咱们须要监听哪一个值。
这里的需求多种多样,
好比单个值监听,监听对象的某个属性(.),好比多个值混合监听(&&, ||)等。这就须要对监听的路径进行解析。
constructors(component, expOrFn, cb, options){ this.cb = cb this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed } if(typeof expOrFn === "function"){ // render or computed this.getter = expOrFn }else{ this.getter = this.parsePath(); } if(this.computed){ this.value = undefined this.dep = new Dep() }else{ this.value = this.get(); //非计算属性是经过调用getter方法收集依赖。 } } parsePath: function(){ // 简单的路径解析,若是都是字符串则不须要解析 if (/[^\w.$]/.test(path)) { return } // 这边只是简单解析了子属性的状况 const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
咱们在watcher乞丐版的基础上,根据实际需求推导出了更健全的watcher版本。下面是完整代码
class Watcher { constructors(component, getter, cb, options){ this.cb = cb this.getter = getter; this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed //因为是对计算属性特殊处理,那确定要给个标识符以便判断 } if(typeof expOrFn === "function"){ // render or computed this.getter = expOrFn }else{ this.getter = this.parsePath(); } this.dirty = this.computed // for computed watchers if(this.computed){ // 对于计算属性computed而言,咱们须要关心preValue吗? ********************* this.value = undefined // 若是是计算属性,就要收集依赖 //同时根据按需加载的原则,这边不会手机依赖,主动执行回调函数。 this.dep = new Dep() }else{ this.value = this.get(); //非计算属性是经过调用getter方法收集依赖。 } } update(){ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run: function(){ //拿到新值 const value = this.get() if (value !== this.value || //基本类型的值直接比较 // 对象没办法直接比较,所以都进行计算 isObject(value)) { // set new value const oldValue = this.value this.value = value this.dirty = false cb.call(this.vm, value, oldValue) } } // 新增depend方法,收集计算属性的依赖 depend () { if (this.dep && Dep.target) { this.dep.depend() } } } //不要忘了还要返回当前computed的最新的值 //因为可能不是当即更新的,所以根据dirty再判断一下,若是数据脏了,调用get再获取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
能够看到,基本vue的实现同样了。VUE中有些代码,好比teardown
方法,清除自身的订阅信息我并无加进来,由于没有想到合适的应用场景。
这种逆推的过程我以为比直接读源码更有意思。直接读源码并不难,但很容易形成似是而非的状况。逻辑很容易理解,可是真正为何这么写,一些细节缘由很容易漏掉。可是无论什么框架都是为了解决实际问题的,从需求出发,才能更好的学习一个框架,并在本身的工做中加以借鉴。
借VUE的生命周期图进行展现
局部图:
从局部图里能够看出,vue收集依赖的入口只有两个,一个是在加载以前处理$wacth方法,一个是render生成虚拟dom。而对于computed,只有在使用到时才会收集依赖。若是咱们在watch和render中都没有使用,而是在methods中使用,那么加载的过程当中是不会计算这个computed的,只有在调用methods中方法时才会计算。