Vue2.0 —— Vue.nextTick(this.$nextTick)源码探秘
《工欲善其事,必先利其器》
一、知识储备
在学习这个 API 之前,我们需要进行一定量的知识储备,并且是从最基础的开始:
nextTick ,译为:下一个刻度,可理解为下一个事件,下一个要去做的事情;- 浏览器的进程和线程,进程包含着线程,需理解多进程的概念;
- 了解 JavaScript 的事件循环机制,包括宏任务和微任务的区别。
(截图来自极客时间网站)
首先说明,这里不是广告,也不是盗图。只是这位老师的课程讲的实在是特别好!强烈建议还没学习的小伙伴可以去学习这门课程,我希望看到的是大家一起进步,而不是得过且过。
- 现代的浏览器分为多个模块的进程,它们之间互不干扰又部分通信共享,这种底层的技术称为
IPC (Inter Process Communication) ,译为:进程间通信,属于半双工通信,例如我们现实生活中的对讲机。 - 现代浏览器属于多进程架构,分别有主进程、网络进程、渲染进程、
GPU 进程和插件进程。其中,渲染进程运行在沙箱模式下,即:一个 Tabs 就代表一个渲染进程。我们熟知的 JavaScript 线程就运行在这个进程之中。 - JavaScript 线程又将执行任务分为宏任务和微任务。在 JS 引擎工作的过程中,会产生一个
执行栈 ,里面用于执行宏任务;如果在宏任务执行的过程中遇见微任务,JS 引擎会将微任务提炼到 任务队列 中,当执行栈栈顶的宏任务执行完之后,在 GPU 渲染之前,执行任务队列中属于该宏任务的微任务。如此循环以往,称之为 事件循环机制 。 - 宏任务有:主代码块、
setTimeout 、setInterval 、setImmidiate 以及 I/O流 和 requestAnimationFrame 。 - 微任务有:
Promise 、Object.observe 和 MutationObserver 以及 process.nextTick (node)。
好了,下面开始进入正题,话不多说,上号!
二、为什么会有这个 API?
由官方的解释引入:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
在之前我写过一篇文章 —— 《Vue2.0 —— 关于虚拟节点和 Diff 算法的浅析》 一文中提到,开发者们为了提升 SPA 的性能可谓是绞尽脑汁,不仅应用了虚拟节点的技术,还实现了 Diff 算法 ,目的就是提升更为优异的性能。最后我们得出的结论是:Vue 只会在执行完 Diff 算法 之后渲染一次 DOM。
但你有没有想过,这与我们上面做的知识储备是否背道而驰?明明每次执行完宏任务,就会进行一次 GPU 渲染,那为什么官网还倡导我们在数据修改之后立即使用这个方法去获取更新后的 DOM 呢?
那我们不妨大胆猜想,Vue 如果没有指定立即刷新视图(sync 关键字),那么他的 render 调用视图更新方法,极有可能就是异步的,而且是属于 微任务 (事实上也的确如此,后面的源码会分析)。Fine,事情开始变的有趣了起来。
三、使用方式
vm.msg = "Hello";
Vue.nextTick(function() {
})
这个方法属于全局应用,值得注意的是,如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise 。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不是原生支持 Promise (IE:你们都看我干嘛),你得自行 polyfill。
new Vue({
methods: {
example: function () {
this.message = 'changed'
this.$nextTick(function () {
this.doSomethingElse()
})
}
}
})
这个方法是应用在组件内的方式,与上面的全局方法在本质上实现并无二致,后者仅仅是前者的一个别名。
四、源码探秘
接下来是官方的原话:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick ”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then 、MutationObserver 和 setImmediate ,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
皇天不负有心人,我们上面的猜想被验证了。无独有偶,在 JavaScript 的发展历程中,鉴于 JS 单线程引擎的工作原理,我们的前辈也想要在浏览器主代码执行完之后、浏览器渲染之前,可以做一些操作。因此 JavaScript 的 事件循环机制 以及 宏任务微任务 的概念就诞生了。
而我们的 Vue 框架,为此也向程序猿们提供了本文这个 API (Vue.nextTick)。并且,这个方法最终也成为了 Vue 渲染视图的主要手段,造成了异步更新 DOM 的现象,间接的提升了 Vue 框架的性能。
- 第一,我们修改响应式数据之后,触发
dep.notify ;
- 第二,
Dep 利用观察者模式通知已经收集好的 Watcher 进行视图更新;
- 第三,每个
Watcher 将自己推入到任务队列里面;
- 第四,
Sheduler 对 Watcher 进行识别判断,如果属于同一 Watcher 则会被忽略;
- 第五,重点来了:
Vue.config.async 默认是 true ,如果没有设置,那么 Vue 会利用 nextTick 方法,将 flushSchedulerQueue() 处理流函数,提取到异步任务队列之中。如果设置了,那么就是立即同步执行,这就是典型的,“异步渲染机制”。
flushScheduleQueue() 处理流函数用于执行 Watcher 的 run 回调方法,以及部分生命周期的更新,重置 Schedule 实例等。
- 第六,才是我们今天的主题,
Vue.nextTick 。
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
捋一下吧:Vue 内部默认使用异步渲染机制,最终调用 nextTick 方法,这个 API 的作用就是,先是把所有的回调函数加入“回调函数集合”数组,再把不同浏览器可用的微任务做了一个判断、适配和循环赋值(flushCallbacks ),最终添加完了之后,执行回调函数集合。
当然了,这个 API 也是对外暴露的,任何开发者都可以使用。
五、用例测试
输出:40,这个应该是没有什么问题对吧,我们再看一组。
这里即使 age 在 nextTick 函数后面,但你前面已经执行修改 gender 触发了收集依赖,所以,微任务就会等主代码执行完之后,再执行回调,所以这里打印出来的,依然是更新之后的 DOM ,输出: 40。
同样,这里也会输出:40;即便你多次执行同一个 Watcher 的更新,Vue 会对其进行去重操作,并不会修改一次属性就更新一次视图,这部分是为了性能做的优化。
这个输出:20。因为 nextTick 的回调在异步渲染的回调之前执行,所以获取不到更新后的 DOM。
最后,感谢你的阅读,愿你的未来一片光明~
|