前一章节,通过对prop,data所定义的属性建立观察类和发布器,但此时的发布类Dep中sub空空如也,如何实现watcher的注册,并在属性发生变化时,实现更新。本章节将继续介绍订阅Watcher。
在介绍前,我们先思考下,用户定义的prop,data在哪些方法或者表达式是需要实现响应式变化的?
首先用户自定义的watch,computed是需要的,另外我们的视图也会用到大量的属性表达式(如前面的实例{{item.id}}),也是需要的。method中虽然用到了这些数据,但是都是及时调用,所以不需要的。
事实上,源码中也是在这三个场景创建watcher,如果阅读过我前面的博客,在initWatcher,initComputer(第五篇),以及mount(第六篇)简单的介绍过,做过一些知识的铺垫。
这三种watcher分别为user watcher,compute watcher,以及render watcher,本篇就结合前面的知识,详细介绍这几种watcher实现原理。
我们以下面的的watch为运行实例:
data:{ msg:'this is msg', items:[ {id:1}, {id:2}, {id:3} ] } watch: { msg: function (val, oldVal) { console.log('new: %s, old: %s', val, oldVal) }, .... }回顾下第五篇initWatcher的创建watcher过程,如下:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { ... //创建watcher对象,进行监听 const watcher = new Watcher(vm, expOrFn, cb, options) ... }
其中主要入参,vm,即vue对象;expOrFn,待观察的表达式,即本例中属性msg对象;cb,回调函数,即本例中msg属性的对应的方法。watcher的定义位于src/core/observer/watcher.js。
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; computed: boolean; sync: boolean; dirty: boolean; active: boolean; dep: Dep; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component,//组件对象 expOrFn: string | Function,//待观察的表达式 cb: Function,//回调函数,更新的时候调用 options?: ?Object, isRenderWatcher?: boolean ) { //1、初始化变量 this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter //2、解析表达式,获取getter方法 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } //3、依赖收集 if (this.computed) { //对于计算属性watcher,此时没有立即进行依赖收集,在执行render函数时收集 this.value = undefined this.dep = new Dep() } else { //对于普通watcher,调用get方法,进行依赖收集 this.value = this.get() } } ... }1、构造方法
我们先看下其构造方法。主要过程包括:
(1)、初始化变量。主要变量如下:
deep,表示是否要进行深度监听,即属性嵌套的变量进行监听。
computed,表示是否是计算属性。
sync,表示更新时,是否需要同步执行。
deps,newDeps,该watcher对应的维护的发布器数组。
(2)、解析表达式,获取getter方法,如果expOrFn是function类型,则直接设置为getter方法,否则调用parsePath方法返回属性对象作为getter方法,本例中则使用后一种,执行结果为vm[msg]。
export function parsePath (path: string): any { ... return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }(3)、依赖收集,这部分处理的很巧妙,对于非计算属性,直接调用了get方法(对于计算属性的watcher我们等会在表)。继续看下get方法。
2、get方法
get () { //1、将当前的watcher压栈 pushTarget(this) let value const vm = this.vm try { //2、核心代码,依赖收集 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching //3、后置处理 if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }(1)将自身的watcher对象压入栈,设置全局的变量Dep.target为当前的watcher对象。
export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target }(2)执行getter方法,触发该属性的get劫持,我们回顾前一章节定义的劫持方法。
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val //依赖收集 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value } ...全局变量Dep.target表示的就是当前的watcher对象,非空,继续调用Dep类的depend方法
depend () { if (Dep.target) { Dep.target.addDep(this) } }最终又回调了watcher中的addDep方法,
addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { //加入到watcher的dep数组 this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { //加入到dep的sub数组 dep.addSub(this) } } }该方法,首先将dep保存到watcher的newDeps数组中,然后调用Dep的addSub,将watcher对象加入到sub数组中。
addSub (sub: Watcher) { this.subs.push(sub) }整个调用过程比较绕,这样做的目的,就是为了建立dep和watcher间的双向链表。
(3)后置处理,包括deep处理,清除当前的watcher对象等。
整个过程完成后,msg属性的Dep对象的sub中就添加了watcher对象。msg的依赖模型如下:
以下面的计算属性computeMsg为运行实例:
<div id="app"> {{computeMsg}} ... </div> var vm = new Vue({ data:{ msg:'this is msg', items:[ {id:1}, {id:2}, {id:3} ] } watch: { msg: function (val, oldVal) { console.log('new: %s, old: %s', val, oldVal) }, computed:{ computeMsg:function(){ return "this is computed msg:"+this.msg } } .... }该实例中,创建computeMsg计算属性,并在模板(template)中使用。computeMsg计算属性表达式中调用了msg属性。
我们来回顾下第五章节的initComputed方法。
function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() //1、循环计算属性 for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get ... if (!isSSR) { // create internal watcher for the computed property. //2、为每个属性创建watcher watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } ... //3、劫持数据变化,创建监听方法 defineComputed(vm, key, userDef) } } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { .... } //4、并对计算属性的getter和setter进行劫持 Object.defineProperty(target, key, sharedPropertyDefinition) } } //getter方法劫持 function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { //依赖收集,将订阅类添加到 watcher.depend() //返回值 return watcher.evaluate() } } }在该方法中,循环所定义的计算属性,为每个计算属性创建watcher,并设置getter方法,监听数据变化(计算属性setter方法用的较少)。watcher的主要入参:vm,即vue对象;getter,计算属性的表达式,即本例中computeMsg对应的表达式。继续看下watcher的构造方法。
1、构造函数
constructor ( vm: Component,//组件对象 expOrFn: string | Function,//待观察的表达式 cb: Function,//回调函数 options?: ?Object, isRenderWatcher?: boolean ) { ... // parse expression for getter //1、对于计算属性,表达式即为getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } if (this.computed) { //2、对于计算属性,创建了dep,此时没有立即进行依赖收集, this.value = undefined this.dep = new Dep() } else { this.value = this.get() } }watcher的入参,vm表示vue对象,getter为待观察的表达式,即计算属性函数。
与user watcher相比,有两个不同的地方。
(1)、由于expOrFn 是计算属性的表达式,类型是function,所以走一个分支,将getter设置为计算属性表达式。
(2)、并没有调用get方法进行依赖收集,只是创建了dep对象,该对象保存依赖该计算属性的watcher。
计算属性的依赖关系不同于普通的属性,它即依赖于其表达式包含的普通属性,比如说本例中的属性msg,同时又被其他调用者依赖,如本例中调用computeMsg的模板表达式({{computeMsg}})。本例的依赖模型如下:
那么问题来了,既然这些依赖关系没有在定义的时候进行收集,那是什么时候做的呢?答案就是在模板调用的时候。
计算属性的设计初衷是简化模板的表达式,避免太多的逻辑导致模板的复杂。所以,只有在模板调用的情况下才会触发依赖,如果只定义不调用,进行依赖就会造成浪费。
下面我们来介绍与模板render相关的watcher(render watcher),并看下如何完成红色标注的依赖收集。
我们先回顾下第六章节介绍挂载时的mountComponent方法。
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { .... //2、定义updateComponent,vm._render将render表达式转化为vnode,vm._update将vnode渲染成实际的dom节点 updateComponent = () => { vm._update(vm._render(), hydrating) } //3、首次渲染,并监听数据变化,并实现dom的更新 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) .... }当时我们说这个watcher有两个作用,1、实现首次dom渲染,并完成依赖收集。2、监听模板所包含表达式的变化,实现update。
该watcher是数据"render"模板的"桥梁",称之为render watcher,其核心部分体现在入参expression(第二个参数,即updateComponent)。我们看下该watcher的初始化的主要过程:
constructor ( vm: Component,//组件对象 expOrFn : string | Function,//待观察的表达式 cb: Function,//回调函数 options?: ?Object, isRenderWatcher?: boolean ) { .... if (typeof expOrFn === 'function') {//1、设置getter方法为表达式 this.getter = expOrFn } else { ..... } if (this.computed) { .... } else { //2、执行get方法,进行依赖收集 this.value = this.get() } }1、expOrFn为function,所以设置getter方法为表达式,即为updateComponent方法。
2、由于不是计算属性,与user watcher一样,执行get方法,实际就是执行updateComponent方法。
updateComponent = () => { vm._update(vm._render(), hydrating) }该方法包含vm._render(),vm._update两个执行阶段,我们在第六章重点分析过,我们不再做详细介绍。今天我们重点回答前面提出的问题,如何实现依赖关系的收集。
以上面的计算属性为例,在模板中,我们使用了"{{computeMsg}}"。
<div id="app"> {{computeMsg}} ... </div>该模板经过编译(参见前面的编译部分)后,该部分的render表达式"_s(computeMsg)",在执行vm._render时,就会触发computeMsg所设置的getter方法。
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { //1、依赖收集,将订阅类添加到dep中 watcher.depend() //2、返回值 return watcher.evaluate() } } }1、调用watcher.depend,将当前的render watcher对象添加到compteMsg的dep中,完成依赖关系的收集。
depend () { if (this.dep && Dep.target) { this.dep.depend() } }注意这里的Dep.target指的是render watcher。依赖模型如下:
2、调用watcher.evaluate()。
evaluate () { //如果有更新,则重新计算,否则返回缓存值 if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }这是this指的是compute watcher。其get方法就是属性表达式。
function(){ return "this is computed msg:"+this.msg }由于表达式中包含了msg属性,在执行过程中,又触发msg的get监听方法,将该compute watcher 添加到msg的dep中,完成被依赖关系的收集。最终的依赖模型如下:
至此,computeMsg的依赖和被依赖关系收集完成。大家也可以尝试分析下模板中调用普通属性的依赖关系收集过程。
目前msg属性的dep发布器中收集了两个依赖的watcher对象,当我们重新设置msg值,会发生什呢?
当重新设置msg值,就会触发set方法。
Object.defineProperty(obj, key, { ... set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } //核心部分,通知更新 childOb = !shallow && observe(newVal) dep.notify() } })调用其发布器的notify方法。
//通知相关的watcher类更新 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }循环属性关联的watcher类,由前面可知,此时dep的sub集合中有user watcher和computed watcher两个对象。分别调用其update方法。
update () { /* istanbul ignore else */ if (this.computed) {//计算属性watcher处理 // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a component's render function. //当没有订阅计算属性时,不需要计算,仅仅设置dirty为true,表示下次重新计算 if (this.dep.subs.length === 0) { // In lazy mode, we don't want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) {//同步处理,立即执行 this.run() } else {//异步处理,进入堆栈 queueWatcher(this) } }update分为三个分支处理,我们分别对三种情况进行分析。
(1)、计算属性处理。
(2)、同步处理,立即执行。
(3)、异步处理,加入到堆栈中。
1、计算属性
对于计算属性的watcher对象,判断是否有订阅过该计算属性,如果没有(即该计算属性的dep的sub集合是否为空),则设置dirty为true,下次将重新计算。
如本例中,computeMsg是被render watcher依赖,所以会进入else分支,执行getAndInvoke方法。
this.getAndInvoke(() => { this.dep.notify() }) getAndInvoke (cb: Function) { //获取最新的值 const value = this.get() if ( // value !== this.value ||//值发生变化 // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) ||//object对象 this.deep//深度watcher ) { // set new value //更新值 const oldValue = this.value this.value = value this.dirty = false //执行回调函数 if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }该方法核心部分是执行传入的回调cb方法,即this.dep.notify(),此时递归调用render watcher的update,又回到了update方法。
2、同步处理
如果不是计算属性(本例中的user watcher,render watcher),并设置了同步处理,则调用run方法。如:
run () { if (this.active) { this.getAndInvoke(this.cb) } }run方法调用getAndInvoke,该方法核心部分是:
(1)执行get方法获取value,对于render watcher ,执行updateComponent方法,重新生成Vnode,并patch,实现dom的更新(下一章节将详细说明)。
(2)回调cb方法,如本例中的msg的watch表达式(注意:render watcher的cb是noon)
3、异步处理
对于非同步,则调用queueWatcher将watcher压入堆栈中。在将这个方法之前,我们先看如果更新queue中的watcher,即flushSchedulerQueue方法,在src/core/observer/scheduler.js中
function flushSchedulerQueue () { //1、设置标识位flush为true,标识正在刷新中 flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. //2、将queue数组从小到大排序 queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers //3、循环队栈queue,执行watcher.run方法,实现更新。 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() //4、重置相关的状态 resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }我们看下核心的步骤:
(1)设置标识位flush,表示queue正在处理中。
(2)根据queue中的watcher的id,从小到大进行排序(也就是创建的先后)
(3)循环queque中的watcher,执行run方法,实现更新。
(4)queue中的执行完成后,则重置相关的状态,包括flush,wait等,等待下一次执行。
整个处理过程还是比较清晰的,这里注意一点,在第3步中,queue是可能动态变化的。
现在回过头来,继续看queueWatcher,是如何将watcher加入到queue中,又是如何触发flushSchedulerQueue执行的。
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) {//对于同一个watcher,不会重复加入到queue,避免多次触发 has[id] = true //1、将watcher加入队列中 if (!flushing) {//尚未刷新,则加入队栈,待执行 queue.push(watcher) } else {//2、正在刷新中,则动态的插入到到对应位置。 // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 //从后往前,查找对应位置 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush //3、通过nexttick执行queue中的watcher if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }将watcher加入queue队栈中,分两种情况,
(1) flushSchedulerQueue未执行,则将watcher加入到queue即可,待下一次执行。
(2)flushSchedulerQueue执行中,则从后往前查找对应的位置,然后插入到queue。
(3)通过nextTick,调用flushSchedulerQueue,实现queue中watcher的更新。vue的DOM是异步更新的,nextTick确保了在DOM更新后再执行,在这里可以认为下一个事件循环的"tick"。nextTick机制实现了"批量"的更新,效率更高。
这里要注意,对于同一个watcher,不能重复的加入到queue中,避免多次触发。
本章节的逻辑还是比较复杂。我们将各个方法间的调用关系总结下,便于大家理解。