Vue源码解读(四)(更新策略)

之前介绍过初始化时 Vue 对数据的响应式处理是利用了Object.defifineProperty(),通过定义对象属性 getter 方法拦截对象属性的访问,进行依赖的收集,依赖收集的作用就是在数据变更的时候能通知到相关依赖进行更新。
通知更新 setter
当响应式数据发生变更时,会触发拦截的 setter 函数,先来看看 setter :

// src/core/observer/index.js export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // ... Object.defineProperty(obj, key, { enumerable: true, configurable: true, // ... // 劫持修改操作 set: function reactiveSetter (newVal) { // 旧的 obj[key] const value = https://www.it610.com/article/getter ? getter.call(obj) : val // 如果新旧值一样,则直接 return,无需更新 if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !=='production' && customSetter) { customSetter() } // setter 不存在说明该属性是一个只读属性,直接 return if (getter && !setter) return // 设置新值 if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新值进行观察,让新值也是响应式的 childOb = !shallow && observe(newVal) // 依赖通知更新 dep.notify() } }) }

dep.notify()
// src/core/observer/dep.js // 通知更新 notify () { const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }

遍历 dep 中存储的 watcher,执行 watcher.update()
watcher.update()
// src/core/observer/watcher.js export default class Watcher { // ... update () { /* istanbul ignore else */ if (this.lazy) { // 懒执行时走这里,比如 computed watcher // 将 dirty 置为 true,计算属性的求值就会重新计算 this.dirty = true } else if (this.sync) { // 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项, // 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run方法进行更新 // 这个属性在官方文档中没有出现 this.run() } else { // 更新时一般都这里,将 watcher 放入 watcher 队列 queueWatcher(this) } } }

queueWatcher
// src/core/observer/scheduler.js const queue: Array = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false /** * 将 watcher 放入 queue 队列 */ export function queueWatcher (watcher: Watcher) { const id = watcher.id // 如果 watcher 已经存在,则跳过 if (has[id] == null) { // 缓存 watcher.id,用于判断 watcher 是否已经入队 has[id] = true if (!flushing) { // 当前没有处于刷新队列状态,watcher 直接入队 queue.push(watcher) } else { // 正在刷新队列,这时用户可能添加新的 watcher,就会走到这里 // 从后往前找,找到第一个 watcher.id 比当前队列中 watcher.id 大的位置,然后将自己插入到该位置。保持队列是有序的。 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // waiting 保证了 nextTick 的调用只有一次 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { // 直接刷新调度队列 // 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣 flushSchedulerQueue() return } // nextTick => vm.$nextTick、Vue.nextTick nextTick(flushSchedulerQueue) } } }

nextTick 等会再看,它的作用主要就是把 flushSchedulerQueue 使用异步任务去执行,先尝试用微任务,不支持的情况再用宏任务去执行。
那么先看看 flushSchedulerQueue 的作用:
flushSchedulerQueue
// src/core/observer/scheduler.js function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 对队列做了从小到大的排序,目的: // 1. 组件的更新由父到子,因为父组件在子组件之前被创建,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。 // 2. 一个组件的用户 watcher 先于渲染 watcher 执行,以为用户 watcher 创建先于渲染 watcher。 // 3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。 queue.sort((a, b) => a.id - b.id) // 在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher for (index = 0; index < queue.length; index++) { watcher = queue[index] // 执行 beforeUpdate 生命周期钩子,在 mount 阶段创建 Watcher 时传入 if (watcher.before) { watcher.before() } // 将缓存的 watcher 清除 id = watcher.id has[id] = null // 执行 watcher.run,最终触发更新函数 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 } } } // 在重置状态之前保留队列的副本 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() //重置刷新队列状态 resetSchedulerState() // keep-alive 组件相关 callActivatedHooks(activatedQueue) // 执行 updated 生命周期钩子 callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }/** * 把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。 */ function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production') { circular = {} } waiting = flushing = false }/** * 由子组件到父组件依次执行 updated 生命周期钩子 */ function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, 'updated') } } }

上面代码可以看出 flushSchedulerQueue 的作用就是执行更新队列。通过 watcher.run() 触发最终的更新。
watcher.run()
// src/core/observer/watcher.js export default class Watcher { constructor( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.cb = cb } run () { if (this.active) { // 调用 this.get 方法 const value = https://www.it610.com/article/this.get() if ( value !== this.value ||// 新旧值不相等 isObject(value) ||// 新值是对象 this.deep// deep模式 ) { // 更新旧值为新值 const oldValue = this.value this.value = value if (this.user) { // 如果是用户 watcher const info = `callback for watcher"${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { // 渲染 watcher,this.cb = noop,一个空函数 this.cb.call(this.vm, value, oldValue) } } } } }

这里有两种情况,当 this.usertrue 的时候代表用户 watcher,在之前介绍过也就是 user watcher , 否则执行渲染 watcher 的逻辑。
  • user watcher
invokeWithErrorHandling 接收的第一个参数就是我们自定义侦听属性的回调函数,在初始化侦听属性 initWatch 方法过程中,实例化 new Watcher(vm, expOrFn, cb, options) 的时候传入。
第三个参数就是 [value, oldValue] (新值和旧值),这也就是为什么在侦听属性的回调函数中能获得新值和旧值。
// src/core/util/error.js export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { let res // 利用 try catch 做一些错误处理 try { res = args ? handler.apply(context, args) : handler.call(context) if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) // issue #9511 // avoid catch triggering multiple times when nested calls res._handled = true } } catch (e) { handleError(e, vm, info) } return res }

  • 渲染 watcher
【Vue源码解读(四)(更新策略)】如果是渲染 watcher 则执行 this.cb.call(this.vm, value, oldValue)。渲染 Wather 的实例化是在挂载时 mountComponent 方法中执行的:
//src/core/instance/lifecycle.js new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)

export function noop (a?: any, b?: any, c?: any) {} 是一个空函数,所以 this.cb.call(this.vm, value, oldValue),就是在执行一个空函数。
渲染 watcher 在执行 watcher.run 会调用 this.get() ,也就会执行 this.getter.call(vm, vm)this.getter 实际就是实例化时传入的第二个参数 updateComponent
//src/core/instance/lifecycle.js updateComponent = () => { vm._update(vm._render(), hydrating) }

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会进入 patch的过程。
nextTick 前面介绍了 flushSchedulerQueue 的作用就是去执行更新队列,那么我们看看 queueWatcher 中的这段代码是怎么回事:
nextTick(flushSchedulerQueue)

nextTick
// src/core/util/next-tick.js const callbacks = [] let pending = falseexport function nextTick (cb?: Function, ctx?: Object) { let _resolve // 用 callbacks 数组存储经过包装的 cb 函数 callbacks.push(() => { if (cb) { // 用 try catch 包装回调函数,便于错误捕获 try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }

nextTick 第一个参数是一个回调函数,这里的回调函数对应的就是 flushSchedulerQueue 了。通过 try catch 将回调函数包装,用于错误捕获,然后将其放入 callbacks 中。
这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
接下来当 pendingfalse 的时候执行 timerFuncpendingtrue,表示正在将任务放入浏览器的任务队列中;pendingfalse ,表示任务已经放入浏览器任务队列中了。
最后,nextTick 在没有传入 cb 回调函数的时候,会返回 promise,提供了一个 .then 的调用。
nextTick().then(() => {})

timerFunc
// src/core/util/next-tick.js // 可以看到 timerFunc 的作用很简单,就是将 flushCallbacks 函数放入浏览器的异步任务队列中 let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { // 首选 Promise p.then(flushCallbacks) /** * 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态, * 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。 * 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。 */ if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // 然后使用 MutationObserver let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = https://www.it610.com/article/String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !=='undefined' && isNative(setImmediate)) { // 然后 setImmediate,宏任务 timerFunc = () => { setImmediate(flushCallbacks) } } else { // 最后 setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } }

flushCallbacks
// src/core/util/next-tick.js /** *1、将 pending 置为 false *2、清空 callbacks 数组 *3、执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数) */ function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }

不管是全局 API Vue.nextTick,还是实例方法 vm.$nextTick,最后都是调用 next-tick.js 中的 nextTick 方法。
相关链接 Vue源码解读(预):手写一个简易版Vue
Vue源码解读(一):准备工作
Vue源码解读(二):初始化和挂载
Vue源码解读(三):响应式原理
Vue源码解读(四):更新策略
[Vue源码解读(五):render和VNode(待续)]()
[Vue源码解读(六):update和patch(待续)]()
[Vue源码解读(七):模板编译(待续)]()
如果觉得还凑合的话,给个赞吧!!!也可以来我的个人博客逛逛 https://www.mingme.net/

    推荐阅读