nextTick Vue中的nextTick
涉及到Vue中DOM的异步更新,感受颇有意思,特地了解了一下。其中关于nextTick
的源码涉及到很多知识,nextTick
是 Vue 的一个核心实现,在介绍 Vue 的 nextTick
以前,为了方便你们理解,我先简单介绍一下 JS 的运行机制。html
JS 执行是单线程的,它是基于事件循环的。事件循环大体分为如下几个步骤:vue
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。react
(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。ios
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。git
(4)主线程不断重复上面的第三步。github
主线程的执行过程就是一个 tick,而全部的异步结果都是经过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task
和 micro task
,而且每一个 macro task
结束后,都要清空全部的 micro task。数组
关于 macro task
和 micro task
的概念,这里不会细讲,简单经过一段代码演示他们的执行顺序:promise
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
复制代码
在浏览器环境中,常见的 macro task
有 setTimeout
、MessageChannel
、postMessage
、setImmediate
;常见的 micro task
有 MutationObsever
和 Promise.then
。浏览器
在 Vue 源码 2.5+ 后,nextTick 的实现单独有一个 JS 文件来维护它,它的源码并很少,总共也就 100 多行。接下来咱们来看一下它的实现,在 src/core/util/next-tick.js
中:app
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]()
}
}
// 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).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// 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)
}
}
// 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
}
/** * 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
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
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
})
}
}
复制代码
next-tick.js
申明了 microTimerFunc
和 macroTimerFunc
2 个变量,它们分别对应的是micro task
的函数和 macro task
的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel
,若是也不支持的话就会降级为 setTimeout 0;而对于micro task
的实现,则检测浏览器是否原生支持 Promise
,不支持的话直接指向 macro task
的实现。
next-tick.js
对外暴露了 2 个函数,先来看 nextTick
,这就是咱们在上一节执行 nextTick(flushSchedulerQueue) 所用到的函数。它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑很是简单,对 callbacks 遍历,而后执行相应的回调函数。
这里使用 callbacks
而不是直接在 nextTick
中执行回调函数的缘由是保证在同一个 tick 内屡次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
nextTick
函数最后还有一段逻辑:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
复制代码
这是当 nextTick
不传 cb 参数的时候,提供一个 Promise 化的调用,好比:
nextTick().then(() => {})
复制代码
当 _resolve
函数执行,就会跳到 then 的逻辑中。
next-tick.js
还对外暴露了 withMacroTask
函数,它是对函数作一层包装,确保函数执行过程当中对数据任意的修改,触发变化执行 nextTick
的时候强制走 macroTimerFunc
。好比对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task
。
Vue.js 提供了 2 种调用 nextTick 的方式,一种是全局 API Vue.nextTick,一种是实例上的方法 vm.$nextTick,不管咱们使用哪种,最后都是调用 next-tick.js 中实现的 nextTick 方法。
/** * Defer a task to execute it asynchronously. */
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 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 if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// 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 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
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
复制代码
首先,先了解nextTick
中定义的三个重要变量。
callbacks
用来存储全部须要执行的回调函数
pending
用来标志是否正在执行回调函数
timerFunc
用来触发执行回调函数 接下来,了解nextTickHandler()
函数。
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
复制代码
这个函数用来执行callbacks里存储的全部回调函数。 接下来是将触发方式赋值给timerFunc。
先判断是否原生支持promise,若是支持,则利用promise来触发执行回调函数; 不然,若是支持MutationObserver
,则实例化一个观察者对象,观察文本节点发生变化时,触发执行全部回调函数。 若是都不支持,则利用setTimeout设置延时为0。
最后是queueNextTick
函数。由于nextTick是一个即时函数,因此queueNextTick
函数是返回的函数,接受用户传入的参数,用来往callbacks里存入回调函数。
上图是整个执行流程,关键在于timeFunc(),该函数起到延迟执行的做用。 从上面的介绍,能够得知timeFunc()一共有三种实现方式。
Promise
MutationObserver
setTimeout
其中Promise和setTimeout很好理解,是一个异步任务,会在同步任务以及更新DOM的异步任务以后回调具体函数。 下面着重介绍一下MutationObserver。 MutationObserver是HTML5中的新API,是个用来监视DOM变更的接口。他能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。 调用过程很简单,可是有点不太寻常:你须要先给他绑回调:
var mo = new MutationObserver(callback)
复制代码
复制代码经过给MutationObserver
的构造函数传入一个回调,能获得一个MutationObserver
实例,这个回调就会在MutationObserver
实例监听到变更时触发。 这个时候你只是给MutationObserver
实例绑定好了回调,他具体监听哪一个DOM、监听节点删除仍是监听属性修改,尚未设置。而调用他的observer方法就能够完成这一步:
var domTarget = 你想要监听的dom节点
mo.observe(domTarget, {
characterData: true //说明监听文本内容的修改。
})
复制代码
在nextTick中 MutationObserver
的做用就如上图所示。在监听到DOM更新后,调用回调函数。 其实使用 MutationObserver
的缘由就是 nextTick想要一个异步API,用来在当前的同步代码执行完毕后,执行我想执行的异步回调,包括Promise和 setTimeout都是基于这个缘由。其中深刻还涉及到microtask等内容。
@2.5后版本 $nextTick was using setImmediate > MessageChannel > setTimeout
@2.5前版本 $nextTick was using Promise > MutationObserver > setTimeout
经过这一节对 nextTick
的分析,并结合上一节的 setter 分析,咱们了解到数据的变化到 DOM 的从新渲染是一个异步过程,发生在下一个 tick。这就是咱们平时在开发的过程当中,好比从服务端接口去获取数据的时候,数据作了修改,若是咱们的某些方法去依赖了数据修改后的 DOM 变化,咱们就必须在 nextTick
后执行。好比下面的伪代码:
getData(res).then(()=>{
this.xxx = res.data
this.$nextTick(() => {
// 这里咱们能够获取变化后的 DOM
})
})
复制代码