在掘金刷到有人写$nextTick
,这里我把我之前的这篇分析文章拿出来给你们看看,但愿对你们有所启迪,这里是我写的原文连接地址Vue源码分析 - nextTick。可能文中有些表述不是很严谨,你们见谅。javascript
顺便推荐你们看一篇很是好的文章Tasks, microtasks, queues and schedules,看完绝对有所收获。css
这里的描述不是很详细,后续须要补充Node.js的EventLoop和浏览器的差别。html
nextTick
这里猜想一下为何Vue有一个API叫nextTick
。前端
浏览器(多进程)包含了Browser进程(浏览器的主进程)、第三方插件进程和GPU进程(浏览器渲染进程),其中GPU进程(多线程)和Web前端密切相关,包含如下线程:java
GUI渲染线程和JS引擎线程是互斥的,为了防止DOM渲染的不一致性,其中一个线程执行时另外一个线程会被挂起。ios
这些线程中,和Vue的nextTick
息息相关的是JS引擎线程和事件触发线程。git
浏览器页面初次渲染完毕后,JS引擎线程结合事件触发线程的工做流程以下:github
(1)同步任务在JS引擎线程(主线程)上执行,造成执行栈(Execution Context Stack)。promise
(2)主线程以外,事件触发线程管理着一个任务队列(Task Queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。浏览器
(3)执行栈中的同步任务执行完毕,系统就会读取任务队列,若是有异步任务须要执行,将其加到主线程的执行栈并执行相应的异步任务。
主线程的执行流程以下图所示:
这里多是不够严谨的,在本文中事件队列和任务队列指向同一个概念。
事件触发线程管理的任务队列是如何产生的呢?事实上这些任务就是从JS引擎线程自己产生的,主线程在运行时会产生执行栈,栈中的代码调用某些异步API时会在任务队列中添加事件,栈中的代码执行完毕后,就会读取任务队列中的事件,去执行事件对应的回调函数,如此循环往复,造成事件循环机制,以下图所示:
JS中有两种任务类型:微任务(microtask)和宏任务(macrotask),在ES6中,microtask称为 jobs,macrotask称为 task。
宏任务: script (主代码块)、setTimeout
、setInterval
、setImmediate
、I/O 、UI rendering
微任务:process.nextTick
(Nodejs) 、promise
、Object.observe
、MutationObserver
这里要重点说明一下,宏任务并不是全是异步任务,主代码块就是属于宏任务的一种(Promises/A+规范)。
它们之间区别以下:
setTimeout
(下一个宏任务)会更快,由于无需等待UI渲染。自我灌输一下本身的理解:
根据事件循环机制,从新梳理一下流程:
举个栗子,如下示例没法直观的表述UI渲染线程的接管过程,只是表述了JS引擎线程的执行流程:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .outer { height: 200px; background-color: red; padding: 10px; } .inner { height: 100px; background-color: blue; margin-top: 50px; } </style> </head> <body> <div class="outer"> <div class="inner"></div> </div> </body> <script> let inner = document.querySelector('.inner') let outer = document.querySelector('.outer') // 监听outer元素的attribute变化 new MutationObserver(function() { console.log('mutate') }).observe(outer, { attributes: true }) // click监听事件 function onClick() { console.log('click') setTimeout(function() { console.log('timeout') }, 0) Promise.resolve().then(function() { console.log('promise') }) outer.setAttribute('data-random', Math.random()) } inner.addEventListener('click', onClick) </script> </html> 复制代码
点击inner
元素打印的顺序是:建议放入浏览器验证。
触发的click
事件会加入宏任务队列,MutationObserver
和Promise
的回调会加入微任务队列,setTimeout
加入到宏任务队列,对应的任务用对象直观的表述一下(自我认知的一种表述,只有参考价值):
{
// tasks是宏任务队列
tasks: [{
script: '主代码块'
}, {
script: 'click回调函数',
// microtasks是微任务队列
microtasks: [{
script: 'Promise'
}, {
script: 'MutationObserver'
}]
}, {
script: 'setTimeout'
}]
}
复制代码
稍微增长一下代码的复杂度,在原有的基础上给outer
元素新增一个click
监听事件:
outer.addEventListener('click', onClick)
复制代码
点击inner
元素打印的顺序是:建议放入浏览器验证。
因为冒泡,click
函数再一次执行了,对应的任务用对象直观的表述一下(自我认知的一种表述,只有参考价值):
{
tasks: [{
script: '主代码块'
}, {
script: 'innter的click回调函数',
microtasks: [{
script: 'Promise'
}, {
script: 'MutationObserver'
}]
}, {
script: 'outer的click回调函数',
microtasks: [{
script: 'Promise'
}, {
script: 'MutationObserver'
}]
}, {
script: 'setTimeout'
}, {
script: 'setTimeout'
}]
}
复制代码
process.nextTick
Node.js中有一个nextTick
函数和Vue中的nextTick
命名一致,很容易让人联想到一块儿(Node.js的Event Loop和浏览器的Event Loop有差别)。重点讲解一下Node.js中的nextTick
的执行机制,简单的举个栗子:
setTimeout(function() {
console.log('timeout')
})
process.nextTick(function(){
console.log('nextTick 1')
})
new Promise(function(resolve){
console.log('Promise 1')
resolve();
console.log('Promise 2')
}).then(function(){
console.log('Promise Resolve')
})
process.nextTick(function(){
console.log('nextTick 2')
})
复制代码
在Node环境(10.3.0版本)中打印的顺序: Promise 1
> Promise 2
> nextTick 1
> nextTick 2
> Promise Resolve
> timeout
在Node.js的v10.x版本中对于process.nextTick
的说明以下:
The process.nextTick() method adds the callback to the "next tick queue". Once the current turn of the event loop turn runs to completion, all callbacks currently in the next tick queue will be called. This is not a simple alias to setTimeout(fn, 0). It is much more efficient. It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.
nextTick
Vue官方对nextTick
这个API的描述:
在下次 DOM 更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 尚未更新
Vue.nextTick(function () {
// DOM 更新了
})
// 做为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
复制代码
2.1.0 起新增:若是没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,因此若是你的目标浏览器不原生支持 Promise (IE:大家都看我干吗),你得本身提供 polyfill。 0
可能你尚未注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,若是执行环境不支持,会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value' ,该组件不会当即从新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数状况咱们不须要关心这个过程,可是若是你想在 DOM 状态更新后作点什么,这就可能会有些棘手。虽然 Vue.js 一般鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,可是有时咱们确实要这么作。为了在数据变化以后等待 Vue 完成更新 DOM ,能够在数据变化以后当即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
Vue对于这个API的感情是曲折的,在2.4版本、2.5版本和2.6版本中对于nextTick
进行反复变更,缘由是浏览器对于微任务的不兼容性影响、微任务和宏任务各自优缺点的权衡。
看以上流程图,若是Vue使用setTimeout
等宏任务函数,那么势必要等待UI渲染完成后的下一个宏任务执行,而若是Vue使用微任务函数,无需等待UI渲染完成才进行nextTick
的回调函数操做,能够想象在JS引擎线程和GUI渲染线程之间来回切换,以及等待GUI渲染线程的过程当中,浏览器势必要消耗性能,这是一个严谨的框架彻底须要考虑的事情。
固然这里所说的只是nextTick
执行用户回调以后的性能状况考虑,这中间固然不能忽略flushBatcherQueue
更新Dom的操做,使用异步函数的另一个做用固然是要确保同步代码执行完毕Dom更新性能优化(例如同步操做对响应式数据使用for循环更新一千次,那么这里只有一次DOM更新而不是一千次)。
到了这里,对于Vue中nextTick
函数的命名应该是了然于心了,固然这个命名不知道和Node.js的process.nextTick
还有没有什么必然联系。
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = []
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]()
}
}
// 在2.4中使用了microtasks ,可是仍是存在问题,
// 在2.5版本中组合使用macrotasks和microtasks,组合使用的方式是对外暴露withMacroTask函数
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
// 2.5版本在nextTick中对于调用microtask(微任务)仍是macrotask(宏任务)声明了两个不一样的变量
let microTimerFunc
let macroTimerFunc
// 默认使用microtask(微任务)
let useMacroTask = false
// 这里主要定义macrotask(宏任务)函数
// macrotask(宏任务)的执行优先级
// setImmediate -> MessageChannel -> setTimeout
// setImmediate是最理想的选择
// 最Low的情况是降级执行setTimeout
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 这里主要定义microtask(微任务)函数
// microtask(微任务)的执行优先级
// Promise -> macroTimerFunc
// 若是原生不支持Promise,那么执行macrotask(宏任务)函数
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
// 对外暴露withMacroTask 函数
// 触发变化执行nextTick时强制执行macrotask(宏任务)函数
/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
try {
return fn.apply(null, arguments)
} finally {
useMacroTask = false
}
})
}
// 这里须要注意pending
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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
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]()
}
}
// 在2.5版本中组合使用microtasks 和macrotasks,可是重绘的时候仍是存在一些小问题,并且使用macrotasks在任务队列中会有几个特别奇怪的行为没办法避免,So又回到了以前的状态,在任何地方优先使用microtasks 。
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// 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 */
// task的执行优先级
// Promise -> MutationObserver -> setImmediate -> setTimeout
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.
// Techinically 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)
}
}
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
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
本文的表述可能存在一些不严谨的地方。