一、前情回顾 & 背景
上一篇小作文作为 patch
阶段的第一篇主要做了以下工作:
重新修改 test.html
加入了可以修改响应式数据的 button#btn
元素,以及绑定点击事件修改 data.forProp.a
;
重新梳理了完整的响应式流程,包含依赖收集、修改数据、派发更新的过程;并且明确了 Watcher
、Dep
以及响应式数据间的依赖和被依赖关系以及三者协作过程;
通过修改 this.forProp.a
进入到了 dep.notify()
,接着看到了作为计算属性
的 lazy watcher
和 普通 watcher
在 watcher.update()
方法中的不同处理方式;
因为作为就算属性的 lazy watcher
要等到用到的时候才会求值,所以放到后面再说,本篇小作文的接着讲把要更新的 watcher
作为参数传递给 queueWatcher
方法后的事情;
二、queueWatcher
方法位置:src/core/observer/scheduler.js -> function queueWatcher
方法参数:watcher
,待更新的 Watcher
实例
方法作用:将 watcher
推入 watcher
队列,id
相同的 watcher
将会被忽略,但是当队列正在被刷新时例外,具体如下:
获取 watcher.id
,watcher.id
是一个自增的数字,数字越小标识这个 watcher
的创建的顺序越靠前
判重,如果不存在该 id
再处理,并且缓存该 watcher.id
;
2.1 如果队列未处于正在刷新状态,即 flushing
不为 true
,则将该 watcher
推入队列
2.2 否则,从队列末尾向前遍历找到比当前 watcher.id
小的那个,把当前 watcher
插入id较小
的那个后面;
判断 waiting
标识符,第一次执行 queueWatcher
时 waiting
是 false
,但是执行过一次 queueWatcher
后就被置为 true
了。这么做确保本次事件循环中只会在下一个循环中添加一个 flushSchedulerQueue
任务;这也是常说的 Vue
会合并更新,然后在下个事件循环中全量更新。在当前循环中收集要更新的 watcher
放入队列,而不是立刻执行这个 watcher
。
经过第一个 watcher.update
调用 queueWatcher
的三步骤后,全局变量 waiting
变为 false
,如果 dep.notify
中还有 watcher
需要 update
,那么仍然会调用 queueWatcher
,那这个时候咋办呢?
因为 dep.notify
是 for
循环这种同步代码,连续调用 subs[i].update()
,对于 queueWatcher
来说,浏览器的下一个事件循环中已经有刷新队列的任务了 —— flushSchedulerQueue
;只管向队列中添加 watcher
就好了,当下一个事件循环开始的时候就会消耗这个队列;
export function queueWatcher (watcher: Watcher) { const id = watcher.id // 如果 watcher 已经存在,就不处理,保证不会重复进入队列 if (has[id] == null) { // 缓存 watcher.id,用于判断 watcher 是否已经进入队列 has[id] = true if (!flushing) { // flushing 标识当前队列是否正在被刷新 // 当前没处在刷新队列状态, watcher 直接进入队列 queue queue.push(watcher) } else { // 如果已经在刷新队列正处于被刷新的状态, // 从 queue 末尾开始遍历,根据当前 watcher.id,找到id 比它小的 watcher 位置, // 然后将自己插入到这 小 id 的 watcher 的下一个位置 // 即将当前 watcher 放入到已经排序的队列 queue 中 // 至于为啥是队列,后面会解释的 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { // 直接同步刷新队列,不是重点,忽略 flushSchedulerQueue() return } // nextTick 就是 Vue.nextTick 或者 this.$nextTick // 其主要作用有两点: // 1.就是把刷新 queue 队列的 flushSchdulerQueue 放入 callbacks 列表 // 2. 通过 pending 控制浏览器中只有一个刷新 callbacks 的 flushCallbacks 任务 nextTick(flushSchedulerQueue) } }}
2.1 flushSchedulerQueue
方法位置:src/core/observer/scheduler.js
方法参数:无
方法作用:
维护 flushing
为 true
;
给 queue
里面的 watcher
进行排序,排序的意义在于:
2.1 确保组件更新顺序从父级到子级,因为父组件总是在子组件之前被创建
2.2 一个组件的用户 watcher(你自己
写在代码里面的 watch
叫做用户watcher
)在渲染 watcher
之前被执行,因为用户 watcher
先于渲染 watcher
创建
2.3 如果一个组件在其父组件
的 watcher
执行期间被销毁,则它的 watcher
会被跳过
遍历 queue
,逐个调用 queue
中的每个 watcher.before
(如有),然后调用 watcher.run
重新求值;
调用 resetSchedulerState
重置 wating
和 flushing
标识符;
触发 activated
和 updated
组件的生命周期钩子
function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = truelet watcher, id
// 刷新队列之前先给队列排序(升序),可以保证: // 1. 组件更新顺序从父级到子级,因为父组件总是在子组件之前被创建 // 2. 一个组件的用户 watcher(你自己写在代码里面的 watcher 叫做用户watcher) // 在渲染 watcher 之前被执行,因为用户 watcher 先于渲染 watcher 创建 // 3. 如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 会被跳过 // 排序以后再刷新队列期间新进来的 watcher 也会按顺序放入队列的合适位置
queue.sort((a, b) => a.id - b.id)
// 不要缓存 queue.length // 简介利用了 数组长度是个动态更新的值,这有啥好处呢? // 因为在执行当前 watcher 时, // 队列中可能会被 push 进来更多 watcher for (index = 0; index < queue.length; index++) { watcher = queue[index] // 执行 before 钩子,在使用 vm.$watch // 或者 watch 选项时可以选配 options.before 传递 if (watcher.before) { watcher.before() } // 将缓存的 watcher 清除 id = watcher.id has[id] = null // 执行 watcher.run(),最终触发更新函数, // 比如渲染 watcher 的 updateComponent watcher.run() }
// 在重置状态(flushing/wating)前复制保存激活的子列表 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice()
resetSchedulerState() // 这里会把 waiting 重置为 false
// 调用组件的 updated 和 activated 钩子 callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }
#### 2.1.1 wathcer.before上面的 `flushSecheduleQueue` 中调用了 `watcher.before`,下面就是一个创建`渲染 watcher` 时传递的 `before` 选项;```jsexport function mountComponent (): Component { let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 这个玩意儿就是渲染 watcher new Watcher(vm, updateComponent, noop, { before () { // 这个 before 方法将会称为 watcher.before // 在响应式更新后 watcher 被重新求值前调用 if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true)}
2.1.2 Watcher.prototype.run
这个方法就是被上面 flushSchedulerQueue
调用的 watcher.run
,其主要作用就是调用创建 watcher
时传递的回调函数:
对于渲染 watcher
就是 updateComponent
方法;
对于用户 watcher
就是监听到值变化时要执行的回调函数,所谓用户 watcher
就是我们在 Vue
组件中传递的 watch
选项例如,{ watch: { someVal (newVal, oldVal) { ....} } }
;
export default class Watcher { constructor () { this.before = options.before this.getter = expOrFn this.value = this.lazy ? undefined : this.get() } get () { pushTarget(this) let value const vm = this.vm try { // 执行回调函数 updateComponent,进入 patch 阶段 value = this.getter.call(vm, vm) } catch (e) { } finally { } return value } update () { queueWatcher(this) } run () { if (this.active) { // 调用 this.get 方法对 watcher 重新求值 const value = this.get() if ( value !== this.value || // deep wathcer 和 Object/Array 的 watcher 即便是同一个值也要触发重新计算 // 因为有可能其中的key value 已经发生了变化 isObject(value) || this.deep ) { // set new value // 缓存旧值为之前的 value const oldValue = this.value // 更新 value 为最新求得的 value this.value = value if (this.user) { // 如果是用户 watcher,则执行用户传递的第三个参数——回调函数, // 参数为 val 和 oldVal const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { // 更新一个渲染 watcher 时, // 也就是说这个 run 方法是由渲染 watcher.run 调用, // 其 cb 是调用了 updateComponent 方法 this.cb.call(this.vm, value, oldValue) } } } }}
2.2 nextTick
方法位置:src/core/util/next-tick.js -> function nextTick
方法参数:
cb
,下一个 tick
需要调用的回调函数,经过包装放到 callbacks
列表中;
ctx
,cb
触发时指定的上下文对象
方法作用:
包装 cb
函数,放入 callbacks
队列中,这队列将会由 flushCallbacks
消耗,在我们目前 patch
阶段中的 cb
是 flushQueueWatcher
方法,这个方法被放到 callbacks
队列中,当触发时执行 watcher.run
方法对 watcher
重新求值;
维护 pending
,前面说了 nextTick
需要保证浏览器在下个事件环的任务队列中只有 flushCallback
;保证方法也很简单,第一次执行置标识符 pending
为 true
,后面再执行的时候判断 pending
为 true
就不添加了。当 flushCallbacks
执行后再将 pending
置为 false
就可以了。
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { // 维护 pending 为 true, // 确保这个下个事件循环中只有一个 flushCallbacks pending = true // timerFunc 负责把 flushCallbacks 放入到下个事件循环中 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }}
为啥叫 nextTick
呢? tick
是个事件循环的概念,表示的浏览器从收到通知后从任务队列中取出一个任务,然后执行它这个全套过程叫做一个 tick
。所以 next tick
顾名思义,放到下一次 tick
执行;
有很多人估计看到过一个经典面试题:说说 $nextTick
的原理。估计很多人都知道 $nextTick
中关于如何把回调函数放到下一个 tick
中的降级过程,优先使用 Promise.then
,如果没有 Promise
则使用 MutationObserver
,如果前两个都没有尝试 setImmediate
,如果前面都没有就用 setTimeout
;
那么这些逻辑都是在哪里处理的呢?没错 timerFunc
方法~
2.1.1 timerFunc
方法位置:src/core/util/next-tick.js -> let timerFunc
方法参数:无
方法作用:通过 js
的异步任务,将 flushCallbacks
放到下一个事件循环。在处理这个问题的时候是存在优先级的,优先使用微任务,实在不行再使用宏任务,优先级按顺序如下:
原生的 Promise.then
优先级最高,将 flushCallbacks
放到下一个事件循环开始前的微任务队列;
如果原生 Promise
不被支持,则降级到 MuatationObserver
;
前面两个微任务都不被支持,看下 setImmedaite
这个宏任务是否支持,若支持则使用;
最后用 setTimeout
作为兜底选项使用;
let timerFunc// nextTick 行为充分利用微任务对队列,// 通过原生 Promise 或者 MutationObserver 实现// MutationObserver 虽然被广泛支持,// 但是在 ios>= 9.3.3 的UIWebView 仍然存在严重问题if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { // 在微任务队列中放入 flushCallbacks 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]')) { // 在原生的 Promise 不可用的时候,MutationsObserver 次之 // 比如 PhantomJS, ios 7, android 4.4 // IE11 仍不可用 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 = String(counter) } isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 再次之是 setImmediate // 虽然是一个宏任务了,但仍比 setTimeout 要好 timerFunc = () => { setImmediate(flushCallbacks) }} else { // 最后用 setTimeout 兜底 timerFunc = () => { setTimeout(flushCallbacks, 0) }}
2.2.2 flushCallbacks
方法位置:src/core/util/next-tick.js
方法参数:无
方法作用:消耗 callbacks
队列,赋值 callback
中的函数,然后清空 callbacks
队列;
function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 // 遍历 copies 数组, // 数组中存储的是 flushSchedulerQueue 包装函数 for (let i = 0; i < copies.length; i++) { copies[i]() }}
callbacks
存放就是上面 2.1
的 flushSchedulerQueue
函数,这么说其实并不准确,它存放的是一个被包装过的函数,这个包装过程发生在 nextTick
中:
export function nextTick (cb?: Function, ctx?: Object) { // ... // cb 就是 flushSchedulerQueue 方法 callbacks.push(() => { // 就是这个包装函数,主要是处理 cb 执行时的错误 if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // ....}
三、总结
本篇小作文讲述了 Vue
如何组织队列更新的,主要依托于下面几个方法:
Watcher.prototype.update
,当响应式数据发生变化,其对应的 dep.notify
执行,watcher.update
会调用 queueWatcher
;
queueWatcher
负责把 watcher
实例加入到待求值的 watcher
队列 queue
中,添加到队列需要根据当前队列是否处于刷新状态做不同的处理;
queueWatcher
还会调用 nextTick
方法,传入消耗 queue
队列的 flushSchedulerQueue
方法;
nextTick
会把 flushSchedulerQueue
包装然后放到 callbacks
队列,nextTick
另一个重要任务就是把消耗 callbacks
队列的 flushCallback
放入到下一个事件循环(或者下一个事件循环的开头,即微任务);