前面我们在 vue2源码系列-响应式原理 中介绍了 vue
中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番。
学习目标
我们先来梳理下本篇文章的学习目标
- 整明白
Watcher
的每一行代码(PS:有点夸张了) - 明白
renderWatcher
和userWatcher
- 清楚
watch
和computed
选项实现原理,两者的参数实现及差异 - 清楚
asyncWatcher
的排队执行机制
Watcher实现
前面学习 vue响应式原理
的时候其实是有对 Watcher
的实现进行一个比较完整的分析的,只是部分细节没有深入。这次争取弄明白全部实现。
let uid = 0 export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // renderWatcher 一个实例对应唯一一个renderWatcher if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep // deepWatcher this.user = !!options.user // userWatcher this.lazy = !!options.lazy // computed实现 this.sync = !!options.sync // syncWatcher this.before = options.before // 前置函数 比如在渲染前会调用 hook:mounted } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers // deps newDeps 用于dep收集 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 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } // 非computed执行get() this.value = this.lazy ? undefined : this.get() } get () { pushTarget(this) let value const vm = this.vm try { // 执行getter添加订阅 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // watch deep 参数实现原理 if (this.deep) { // traverse就是递归遍历value触发各个属性的getter traverse(value) } popTarget() // 清空本轮deps this.cleanupDeps() } return value } // 添加订阅 addDep (dep: Dep) { const id = dep.id // 防止重复收集 if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } // 清空本轮deps cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } // 接收订阅中心的更新通知 update () { // computed使用 if (this.lazy) { this.dirty = true // 同步watcher } else if (this.sync) { this.run() // 异步watcher队列 } else { queueWatcher(this) } } run () { // 执行回调 if (this.active) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { this.cb.call(this.vm, value, oldValue) } } } } // computed的更新通知 evaluate () { this.value = this.get() this.dirty = false } // 用于computed依赖其它属性deps depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } // 移除deps teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } } 复制代码
有了上次的基础,Watcher
的代码理解起来其实不难。上面我们对每个函数及重点代码都有注释分析,下面我们再看看一些容易看不明白的点。
newDepIds/depIds
值得一说的是收集 dep
的作用。很简单,就是防止重复收集。那为什么需要两个数组来实现了,单单防止重复收集的话,一个 Set
数组无疑是最简单的。
我们知道每个更新的时候都会执行 this.get() -> this.getter.call(vm, vm)
, 其实使用两个数组的原因就在于在 getter
函数中,是会触发不同属性的 getter
的,他们会将属性的对应的 dep
添加当前 watcher
订阅。
所以每次更新的时候 getter
函数有可能触发不同属性的 getter
,这时候应该添加新一轮的订阅,而老一轮的订阅可能已经不存在了,所以需要及时移除,这就是使用两个数组而不是一个数组实现的原因。具体复现可以通过指令 v-if
来试试,加深理解。
cleanupDeps () { let i = this.deps.length while (i--) { // 移除上一轮不必要的订阅 const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 这里的实现有点绕 将newDepIds赋值为depIds,而后再通过clear方法清空 // 让人难免觉得多此一举,为什么不直接 newDepIds = [] 呢 // 按照我个人的理解是 newDepIds = [] 势必要新建数组 // 而下面的方式不需要开辟新内存,也不用回收旧内存,一直是原来两个内存地址,属于优化内存的写法 let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } 复制代码
computed的实现
上面 Watcher
的实现其实很多是和实现 computed
相关的,所以我们来看看 computed
的实现原理
initComputed
我们之前分析 vue
初始化的时候,知道在 initState
中会调用 initComputed
初始计算属性
const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { // 收集计算属性 const watchers = vm._computedWatchers = Object.create(null) // 判断服务端渲染 const isSSR = isServerRendering() // 遍历computed for (const key in computed) { const userDef = computed[key] // 兼容函数及对象写法 const getter = typeof userDef === 'function' ? userDef : userDef.get if (!isSSR) { // 实例化watcher // 特别 watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // 定义getter if (!(key in vm)) { defineComputed(vm, key, userDef) } } } 复制代码
可以看到 initComputed
主要是对 computed
选项进行遍历初始化 watcher
实例,和其它 watcher
不同之处就在于选项中传了参数 lazy:true
。我们再看看 defineComputed
defineComputed
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { // 这段代码比较多但是逻辑不复杂 主要就是判断计算属性的set get设置 // 我们重点关注 get 就行 // 其实主要逻辑就是通过createComputedGetter创建getter函数再定义到vm对应属性中 const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { // 这边有个cache参数可以用于关闭缓存 sharedPropertyDefinition.get = userDef.get ? shouldCache && cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } // 定义到vm对应属性中 Object.defineProperty(target, key, sharedPropertyDefinition) } 复制代码
createComputedGetter
createComputedGetter
函数应该是精华之处了,这边我们能看到其和 watcher
的联系,以及方法 evaluate
和 depend
的使用,而这也是实现计算属性缓存的原理。
function createComputedGetter (key) { return function computedGetter () { // 获取我们在函数initComputed中设置的watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 我们之前说过computed watcher设置了lazy=true // 这样同时dirty也会设置为true if (watcher.dirty) { // 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值 // 而计算属性需要在属性的get函数中主动计算值 watcher.evaluate() } // 这里是实现computed数据驱动的原理 // 我们举个例子 name() {return this.firtName + this.lastName} // 在渲染watcher中因为会调用到 this.name 就会走到当前函数 // 在当前函数中 Dep.target 则会指向渲染 watcher // 而在计算属性new Watcher的时候 // 我们说过其会收集对应的dep数组也就是firtName和lastName对应的dep // 此时调用watcher.depend()则会将收集的dep分别订阅当前的渲染watcher // 所以当触发firtName或者lastName的时候,就会触发dep.notify进而通知渲染watcher更新 if (Dep.target) { watcher.depend() } return watcher.value } } } 复制代码
computed缓存原理
上面我们解析了计算属性依赖于其它属性更新的原理,我们再来分析分析其缓存实现
缓存原理的实现其实在于 dirty
属性的运用。
if (watcher.dirty) { // 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值 // 而计算属性需要在属性的get函数中主动计算值 watcher.evaluate() } 复制代码
如果访问计算属性的 get
函数,会进行 watcher.dirty
的判断,如果为 true
才会调用 evaluate
获取新值,否则则返回旧值,那么他是如何判断值更新呢?
update () { // 和普通watcher不同的是 // 依赖的值更新时不会调用run函数 // 而是仅仅将dirty设置true来表明值已更新 // 这样就不会调用get()进行更新 if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } 复制代码
在实际使用到计算属性的访问函数 get
中,才会通过 dirty
判断进入 evaluate
evaluate () { // 在此才会调用get更新value 同时将dirty设置为fasle表明值已经更新 this.value = this.get() this.dirty = false } 复制代码
watch实现
我们再来看看 watch
选项的实现,会比计算属性简单许多
initWatch
同样从入口开始 initState -> initWatch
function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] // 数组处理 if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { // 创建watcher createWatcher(vm, key, handler) } } } 复制代码
createWatcher
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { // 处理兼容不同的写法 if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } // 最终调用vm.$watch return vm.$watch(expOrFn, handler, options) } 复制代码
$watch
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} // 将user参数设置为true 用于标志开发者watcher options.user = true // 正常的实例化watcher const watcher = new Watcher(vm, expOrFn, cb, options) // 一个值得注意的参数 // 如果配置了immediate则在此时(vue初始化)进行调用回调cb if (options.immediate) { const info = `callback for immediate watcher "${watcher.expression}"` // 为啥这边要进行pushTarget呢 // 而且target为undefined // 因为当前可能处于其它watcher实例化当中,例如渲染watcher // 如果这边不推入其它watcher得话会导致cb中得某些属性dep添加了渲染watcher // 则可能引起没必要得渲染watcher get执行 pushTarget() invokeWithErrorHandling(cb, vm, [watcher.value], vm, info) popTarget() } // 移除deps解除订阅 return function unwatchFn () { watcher.teardown() } } } 复制代码
异步watcher
在 Watcher
的 update
中有这么个判断
update () { // lazy用于computed if (this.lazy) { } else if (this.sync) { // 同步watcher this.run() // 异步watcher } else { queueWatcher(this) } } 复制代码
看来只有同步 watcher
才会直接调用 run
进行更新。而异步 watcher
则会调用 queueWatcher
,实际上大部分 watcher
都是异步 watcher
,因为 sync
默认值就是 false
queueWatcher
那么我们主要来研究 queueWatcher
是怎么个实现,watcher
更新函数是以怎么样一个排队机制来进行更新的。
export function queueWatcher (watcher: Watcher) { const id = watcher.id // 通过watcherID判断防止重复执行 // 这里也是设计为异步的一个重要原因吧 // 开发中肯定会存在某些修改触发同一个渲染watcher的情况 // 通过异步队列则可以很好的防止重复,大大优化效率 if (has[id] == null) { has[id] = true // flushing表明当前queue正在执行更新 if (!flushing) { // 如果不是正在更新,则推入队列即可 queue.push(watcher) } else { // 如果正在更新,则要对比watcherID的大小 将watcher推入正确位置 // 这种情况是由于watcher的回调cb又通知了新的watcher更新 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // 异步执行 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 使用nextTick来实现异步执行 // 其中执行函数flushSchedulerQueue通过回调方式传递、 // nextTick的原理以后分析 知道它是将函数添加到异步队列中就行了 nextTick(flushSchedulerQueue) } } } 复制代码
flushSchedulerQueue
我们再来看看实际更新函数 flushSchedulerQueue
function flushSchedulerQueue () { // 这边会设置flushing=true // 表明当前正在执行更新 flushing = true let watcher, id // 注意这边会将watcher进行排序 // 和上面的排序其实是对应的,正因为上面的queueWatcher在排序之后 // 所以才需要对比watcherID插入特定位置 // 这边的排序主要为了处理 // 1. 父组件的更新总是应该先于子组件 // 2. userWatcher总是应该先于渲染watcher // 3. 如果父组件已经销毁,其实不再需要执行更新 queue.sort((a, b) => a.id - b.id) // 这边要注意queue.length不会通过len=queue.length的方式缓存 // 因为queue的长度其实是实时变化的,和前面说的原因一样 for (index = 0; index < queue.length; index++) { watcher = queue[index] // before执行 if (watcher.before) { watcher.before() } id = watcher.id // 重置has[id] 不然下一轮不会添加到队列了 has[id] = null // 执行更新函数 watcher.run() } // keepAlive组件 const activatedQueue = activatedChildren.slice() // 用于获取普通组件 通过watcher.vm const updatedQueue = queue.slice() // 重置参数 见下文 resetSchedulerState() // 触发组件update及activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) } 复制代码
resetSchedulerState
正常的重置队列参数
function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} waiting = flushing = false } 复制代码
总结
本文主要分析了 Watcher
的实现原理及与其相关的 computed
和 watch
选项的实现。同时分析了 watcher
的更新函数是如何添加到异步队列中及其执行机制。内容实际上是比较多的,可能有些地方没说明白,希望多多理解。good good staduy day day up