Vue nextTick实现原理
前言
熟悉 vue 的前端,想必对 vue 里的 nextTick 也很熟悉了,用的时候就知道他是延迟回调,有时候用起来甚至和setTimeout 看起来是同样的效果。但他和setTimeout到底有什么区别?他是如何实现的?
本文就nextTick的实现引入,来探讨下js中的异步与同步,微任务与宏任务。
nextTick
用法
先搬运下文档 Vue-nextTick
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
源码实现
在了解原理之前先看下 nextTick
源码实现
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
可以看到上面有几个条件判断 如果支持 Promise 就用 Promise
如果不支持就用 MutationObserver MDN-MutationObserver
MutationObserver 它会在指定的DOM发生变化时被调用
如果不支持 MutationObserver 的话就用 setImmediate MDN-setImmediate
但是这个特性只有最新版IE和node支持,然后是最后一个条件 如果这些都不支持的话就用setTimeout。
看完这一段其实也很懵,为什么要这样设计呢?为什么要这样一个顺序来判断呢?说到这里就不得不讨论JavaScript 运行机制(Event Loop)&微任务宏任务了。
JavaScript 运行机制(Event Loop)
单线程
JS是单线程,同一个时间只能做一件事。至于JS为什么是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
同步和异步
js里的任务分为两种:同步任务(synchronous)和异步任务(asynchronous)。同步阻塞异步非阻塞。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,例如alert,会阻塞后续任务的执行,只有在点击确定之后,才会执行下一个任务。
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。所以会有任务队列的概念。正因为是单线程,所以所有任务都是主线程执行的,异步请求这些也不会开辟新的线程,而是放到任务队列,当这些异步操作被触发时才进入主线程执行。
宏任务和微任务
JS任务又分为宏任务和微任务。
宏任务(macrotask):setTimeout、setInterval、setImmediate、I/O、UI rendering
微任务(microtask):promise.then、process.nextTick、MutationObserver、queneMicrotask(开启一个微任务)
宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...)
微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,比如对一系列动作做出反馈,或或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且当前宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。
何时使用微任务
微任务的执行时机,晚于当前本轮事件循环的 Call Stack(调用栈)中的代码(宏任务),早于事件处理函数和定时器的回调函数
使用微任务的原因
减少操作中用户可感知到的延迟
确保任务顺序的一致性,即便当结果或数据是同步可用的
批量操作的优化
了解了宏任务和微任务的执行顺序,就可以了解到为何nextTick 要优先使用Promise
和MutationObserver
因为他俩属于微任务,会在执行栈空闲的时候立即执行,它的响应速度相比setTimeout会更快,因为无需等渲染。
而setImmediate和setTimeout属于宏任务,执行开始之前要等渲染,即task->渲染->task。
参考:
阮一峰-JavaScript 运行机制详解:再谈Event Loop
译文:JS事件循环机制(event loop)之宏任务、微任务
Generator 函数的含义与用法
浏览器进程、JS事件循环机制、宏任务和微任务