为什么Vue有一个API叫nextTick?
Vue.nextTick 的原理和用途
用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。
具体来说,异步执行的运行机制如下。
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
用途——应用场景:需要在视图更新之后,基于新的视图进行操作。 可以看这篇:Vue.nextTick 的原理和用途
vue中nextTick的源码分析
vue中nextTick源码(vue\src\core\util\next-tick.js)
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]()
}
}
let timerFunc // timerFunc函数是重点!
// task的执行优先级
// Promise -> MutationObserver -> setImmediate -> setTimeout
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 {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// nextTick 主函数
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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick函数
nextTick主函数
var callbacks = [];
var pending = false;
function nextTick(cb, ctx) { //cb?: Function, ctx?: Object
var _resolve;
callbacks.push(function() {
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(function(resolve) {
_resolve = resolve;
})
}
}
nextTick函数中通过参数cb传入的函数,包装后push到callbacks数组中。变量pending是用来保证执行一个事件循环中只执行一次timerFunc()函数。最后if (!cb && typeof Promise !== 'undefined'),判断参数cb不存在(即没有参数再传入了),且浏览器支持Promise,就会return一个Promise实例对象。例如 nextTick().then(() => {}),当 _resolve 函数执行,就会执行 then 的逻辑中。
通过参数cb传入的函数是如何包装的?
当参数cb有值,在try语句中执行cb.call(ctx),参数ctx是传入函数的参数。如果执行失败错误会被catch捕获。如果参数cb没有值。执行_resolve(ctx),因为cb不存在时,会return一个Promise实例对象,那么执行_resolve(ctx),就会执行then的逻辑。
- timerFunc()函数
来看一下
timerFunc()函数,先只看用 Promise 创建一个异步执行的ptimerFunc函数。其中timerFunc函数就是用各种异步执行的方法调用flushCallbacks函数。
let timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) //异步执行方法调用flushCallbacks
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
- flushCallbacks函数。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
执行pending=false使下个事件循环中nextTick函数中能调用timerFunc函数。执行const copies = callbacks.slice(0)是把要异步执行的函数集合 callbacks克隆到常量copies,然后把callbacks清空。再遍历copies执行每一项函数。
总结一下nextTick函数的逻辑
定义了一个callbacks数组来模拟事件队列,通过参数cb传入的函数经过一个函数包装,在这个包装过程中会执行传入的函数,处理执行失败的情况,以及参数cb不存在的情景,然后添加到callbacks数组中。再调用timerFunc函数,该函数就是用各种异步执行的方法调用flushCallbacks 函数,在flushCallbacks 函数中拷贝callbacks中的每个函数,并执行。定义了一个变量 pending来保证一个事件循环中只调用一次 timerFunc 函数。
那么其中的关键还是怎么定义 timerFunc 函数。因为在各浏览器下对创建异步执行函数的方法各不相同,要做兼容处理,下面来介绍一下各种方法。
Promise 创建异步执行函数
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
//timerFunc函数就是用各种异步执行的方法调用flushCallbacks函数。
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
首先通过isNative方法判断浏览器是否支持Promise,其中 typeof Promise 支持的话为 function,故该条件满足,直接返回一个resolved状态的 Promise 对象。在 timerFunc 函数中执行 p.then(flushCallbacks),会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务 (micro task)类型,故这些函数就变成异步执行了。执行 if (isIOS) { setTimeout(noop)} 来在 IOS 浏览器下添加空的计时器强制刷新微任务队列。
isNative方法先判断浏览器是否支持Promise,另一个条件,当 Ctor 是函数类型时,执行 /native code/.test(Ctor.toString()),检测函数 toString 之后的字符串中是否带有 native code 片段,那为什么要这么监测。这是因为这里的 toString 是 Function 的一个实例方法,如果是浏览器内置函数调用实例方法 toString 返回的结果是function Promise() { [native code] }。
function isNative(Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
调用timerFunc函数时,通过 p.then(flushCallbacks) 会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务(micro task)类型,故这些函数就变成异步执行了。
MutationObserver 创建异步执行函数
if (!isIE && typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function() {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
}
创建并返回一个新的 MutationObserver,并且把 flushCallbacks 作为回到函数传入,它会在指定的 DOM 发生变化时被调用,在其中会遍历去执行每个 nextTick 传入的函数,因为MutationObserver 是个微任务 (micro task)类型,故这些函数就变成异步执行了。
setImmediate 创建异步执行函数
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
setImmediate 只兼容 IE10 以上浏览器,其他浏览器均不兼容。其是个宏任务 (macro task),消耗的资源比较小
setTimeout 创建异步执行函数
timerFunc = () => {
setTimeout(flushCallbacks, 0);
}
兼容 IE10 以下的浏览器,创建异步任务,其是个宏任务 (macro task),消耗资源较大。
参考文献1:你真的理解$nextTick么
参考文献2:Vue源码——nextTick实现原理