📝个人主页:爱吃炫迈
💌系列专栏:Vue
🧑💻座右铭:道阻且长,行则将至💗
文章目录
- nextTick原理
- nextTick
- timerFunc
- flushCallbacks
- 异步更新流程
- update
- queueWatcher
- flushSchedulerQueue
- resetSchedulerState
nextTick原理
nextTick
export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc/*推送到队列中下一个tick时执行cb 回调函数ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {let _resolve// 第一步 传入的cb会被push进callbacks中存放起来callbacks.push(() => {if (cb) { try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})// 第二步:判断用什么方法// 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁if (!pending) {// 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)pending = true// 调用判断Promise,MutationObserver,setTimeout的优先级timerFunc()}// 第三步:nextTick 函数会返回一个Promise对象。该Promise对象在异步任务执行完毕后会resolve,可以让用户在异步任务执行完毕后进行处理。if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => {_resolve = resolve})}
}
解释:
第二步:pending
的作用就是一个锁,防止后续的 nextTick
重复执行 timerFunc
(换句话说:当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次)。timerFunc
内部创建会一个微任务或宏任务,等待所有的 nextTick
同步执行完成后,再去执行 callbacks
内的回调。
timerFunc
💡 timerFunc函数,主要通过一些兼容判断来创建合适的
timerFunc
,最优先肯定是微任务,其次再到宏任务。 优先级为promise.then
>MutationObserver
>setImmediate
>setTimeout
。
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promiseconst p = Promise.resolve()timerFunc = () => {// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务p.then(flushCallbacks)if (isIOS) setTimeout(noop)}// 标记当前 nextTick 使用的微任务isUsingMicroTask = true// 如果不支持 promise,就判断是否支持 MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter) // 数据更新}isUsingMicroTask = true // 标记当前 nextTick 使用的微任务// 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}// 以上三种都不支持就选择 setTimeout
} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}
}
我们发现无论那种timerFunc
最终都会执行flushCallbacks
函数
flushCallbacks
💡
flushCallbacks
里做的事情很简单,它就负责执行callbacks
里的回调。
// flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {pending = falseconst copies = callbacks.slice(0) // 拷贝一份 callbackscallbacks.length = 0 // 清空 callbacksfor (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调copies[i]()}
}
异步更新流程
Vue
使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。
update
💡
触发某个数据的setter方法后,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。
/*调度者接口,当依赖发生改变的时候进行回调 */update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {/*同步则执行run直接渲染视图*/this.run()} else {/*异步推送到观察者队列中,下一个tick时调用。*/queueWatcher(this) // this为当前实例watcher}}
queueWatcher
💡 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送
export function queueWatcher (watcher: Watcher) {/*获取watcher的id*/const id = watcher.id/*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/if (has[id] == null) {has[id] = true// 不是刷新if (!flushing) {queue.push(watcher) // 将多个渲染watcher去重后放到队列中} else {// 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 - 1while (i >= 0 && queue[i].id > watcher.id) {i--}queue.splice(Math.max(i, index) + 1, 0, watcher)}// 是刷新if (!waiting) {waiting = truenextTick(flushSchedulerQueue) //这里会产生一个nextTick,队列刷新函数(flushSchedulerQueue)}}
}
从queueWatcher代码中看出Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时将这个队列queue全部拿出来run一遍,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去。这也解释了同一个watcher被多次触发,只会被推入到队列中一次。
flushSchedulerQueue
💡
flushSchedulerQueue
内将刚刚加入queue
的watcher
逐个run
更新。
function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = truelet watcher, id// 在刷新之前对队列进行排序。// 这确保了:// 1. 组件从父级更新到子级。(因为父母总是在子进程之前创建)// 2. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察者是在渲染观察者之前创建的)// 3. 如果组件在父组件的观察程序运行期间被销毁,可以跳过它的观察者。queue.sort((a, b) => a.id - b.id)// do not cache length because more watchers might be pushed// as we run existing watchersfor (index = 0; index < queue.length; index++) {watcher = queue[index]if (watcher.before) {watcher.before()}id = watcher.idhas[id] = nullwatcher.run()}// keep copies of post queues before resetting stateconst activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()// call component updated and activated hookscallActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)
}
resetSchedulerState
💡
resetSchedulerState
重置状态,等待下一轮的异步更新。
function resetSchedulerState () {index = queue.length = activatedChildren.length = 0has = {}if (process.env.NODE_ENV !== 'production') {circular = {}}waiting = flushing = false
}
要注意此时 flushSchedulerQueue
还未执行,它只是作为回调传入而已。因为用户可能也会调用 nextTick
方法。这种情况下,callbacks
里的内容为 [“flushSchedulerQueue”, “用户的nextTick回调”],当所有同步任务执行完成,才开始执行 callbacks
里面的回调。
由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick
回调执行。这也是为什么我们能在 nextTick
中获取到更新后DOM的原因。
参考文章:
Vue你不得不知道的异步更新机制和nextTick原理 - 掘金
通俗易懂的Vue异步更新策略及 nextTick 原理 - 掘金