Vue 异步更新源码解析
先补充一下 Vue 实现双向数据绑定的原理:
通过Object.defineproperty
拦截对数据的访问 get 和设置 set,当拦截到数据的访问时进行依赖收集,拦截到数据的设置时则执行dep.notify
通知 watcher 进行更新。
notify
/src/core/observer/dep.js
/**
* 通知该依赖收集的所有 watcher 执行 update 方法,进行异步更新
*/
notify () {
// subs 保存的是该依赖收集的所有 watcher
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
update
调用 queueWatcher
方法,将 watcher
加入队列
/src/core/observer/watcher.js
/**
* 根据 watcher 配置,决定接下来怎么走,一般是 queueWatcher
*/
update () {
/* istanbul ignore else */
// 如果是懒执行,设置 dirty 为 true
// computed 就是懒执行,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
// if (watcher.dirty) {
// watcher.evaluate()
// }
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run
this.run()
} else {
// 将 watcher 放入 watcher 队列,一般走这里
queueWatcher(this)
}
}
queueWatcher
- 每个
watcher
都有自己的id
,如果has
记录到对应的watcher
,则跳过,不会重复入队,这在 官网 有提到:如果同一个 watcher 被多次触发,只会被推入到队列中一次
。 - 将
watcher
加入到队列中,等待执行。 - 当队列没有在刷新的时候,执行
nextTick(flushSchedulerQueue)
。这里的waiting
的作用就是一个锁,防止nextTick
重复执行。
ps:flushSchedulerQueue
作为回调传入 nextTick
异步执行。
/src/core/observer/scheduler.js
/**
* 将 watcher 放入 watcher 队列
* 具有重复 id 的 watcher 会跳过
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已经存在,则跳过,不会重复入队
if (has[id] == null) {
has[id] = true
// 不管是否刷新, watcher 都能立即入队 queue
if (!flushing) {
// 如果当前没有处于刷新队列状态,watcher 直接入队
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果已经在刷新队列,把 watcher 放入正确的位置
// 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
// 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
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) {
// 直接刷新调度队列, 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
nextTick
-
cb
即传入的回调,它被push
进一个callbacks
数组,等待调用。 -
pending
的作用和queueWatcher
方法中的waiting
一样,也是一个锁。防止后续的nextTick
重复执行timerFunc
。timerFunc
内部创建会一个微任务或宏任务,等待所有的nextTick
同步执行完成后,再去执行callbacks
内的回调。 - 如果没有传入回调,用户可能使用的是
Promise
形式,返回一个Promise
,_resolve
被调用时进入到then
。
/src/core/util/next-tick.js
/**
* 1、用 try catch 获取 cb ,确保拿到的是函数,然后将其放入 callbacks 数组
* 2、pending = false:表示现在浏览器的任务队列没有 flushCallbacks 函数
* pending = true:表示浏览器的任务队列中有 flushCallbacks 函数
* 待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
* 浏览器的任务队列
* @param {*} cb 接收一个回调函数 => flushSchedulerQueue
* @param {*} ctx 上下文
* @returns
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 因为用户可以采用 Vue.nextTick 和 this.$nextTick 调用 nextTick ,
// 因此在对 callbacks 数组存储 cb 回调之前,做了捕获,确保拿到 cb
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
// 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
pending = true
timerFunc()
}
// 处理采用 promise 进行调用的情况: Vue.nextTick().then(function () { })
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc
将flushCallbacks
函数放入浏览器的异步任务队列中
通过兼容来创建合适的 timerFunc
,最优先肯定是微任务,其次再到宏任务。优先级为 promise.then
(微任务) > MutationObserver
(微任务) > setImmediate
(宏任务) > setTimeout
(宏任务)
/src/core/util/next-tick.js
let timerFunc
// 首选 Promise.resolve().then() 微任务
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' && (
// 第二选择 MutationObserver 微任务
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in 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)) {
// 第三选择 MutationObserver 宏任务,但是依然比 setTimeout 好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 前三种都不支持的情况见,选择 setTimeout 宏任务
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
flushCallbacks
/src/core/util/next-tick.js
/**
* 清空 callbacks 数组、pending 设为 false、
* 执行 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]()
}
}
nextTick 回调flushSchedulerQueue
将刚刚加入 queue
的 watcher
逐个 run
更新。resetSchedulerState
重置状态,等待下一轮的异步更新。
要特别注意: 在执行nextTick(flushSchedulerQueue)
的时候flushSchedulerQueue
并不是马上执行,它只是作为回调传入而已。因为用户可能还会调用 nextTick
方法。这种情况下,callbacks
里的内容为 ["flushSchedulerQueue", "用户传入的nextTick回调"],当所有 nextTick
同步任务执行完才开始执行 callbacks
里面的回调。
/src/core/observer/scheduler.js
/**
* 刷新 watcher, 运行 watcher.run 方法,触发更新函数
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 刷新队列之前先给队列排序(升序),可以保证:
// 1. 父组件比子组件先更新. (因为父组件比子组件先创建)
// 2. 一个组件的用户user watcher 比渲染 watcher 先执行 (用户user watcher 比渲染 watcher 先创建)
// 3. 如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
queue.sort((a, b) => a.id - b.id)
// 直接使用了 queue.length,动态计算队列的长度。不要缓存队列的长度,因为在我们运行存在的 watcher 的时候,别的 watcher 可能会 push 进来
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 执行 before 钩子,在使用 vm.$watch 或者 watch 选项时可以通过配置项(options.before)传递
if (watcher.before) {
watcher.before()
}
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
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
// 重置调度状态
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
resetSchedulerState
重置状态,意味着这一'tick' 刷新队列结束,刷新状态改为 false ,等待下一轮的异步更新
/src/core/observer/scheduler.js
/**
* 重置调度状态
* 1、has = {}
* 2、waiting = flushing = false,表示刷新队列结束
* 可以给 callbacks 数组中放入新的 flushSchedulerQueue 函数
* 并且可以向浏览器的任务队列放入下一个 flushCallbacks 函数
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false