持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
nextTick 原理与实现
nextTick 功能
在开发当中我们经常会遇到一种场景:当我们更新了状态(数据)的时候,需要立即对更改 DOM 进行一些操作,但是此时,我们是获取不到更新后的 DOM 的,因为在本次的更新操作当中,DOM 并没有重新渲染,那么这个时候我们就需要使用到nextTick方法来了。
nextTick方法接收两个参数cb和ctx,其中cb是一个回调函数,我们在这里面执行我们需要在下次 DOM 更新后执行的操作,而ctx则是一个上下文对象,保存着回调函数的执行环境。
new Vue({
//...
methods: {
//...
change() {
//修改数据改变状态
this.msg = "hello";
this.nextTick(() => {
//获取更新后的DOM然后执行相对应的操作
this.changeDom();
});
},
},
});
现在我们需要知道一个问题,下次 DOM 更新之后具体指哪个时间段呢?要搞懂这个问题,我们首先要明白什么是下次 DOM 更新。
在 VueJS 当中,当状态发生改变的时候,会通知到 Watcher,Watcher 会通知页面发生更新,触发虚拟 Dom 的 patch 流程,然后更新页面视图。但是,现在 Watcher 触发虚拟 Dom 的流程不是同步的,而是异步的,Vue 当中有一个队列,每当需要渲染时,会将 Watcher 推送到这个队列当中,在下次事件循环中再让 Watcher 触发渲染流程。
那么为什么需要将渲染流程变成异步的呢?并且需要推送到队列当中。因为在 vue 当中,我们使用虚拟 Dom 的方式来进行渲染,当数据发生变化的时候,会通知到组件层次,然后组件内部使用 diff 算法来比对哪里发生了变化。但是当一个组件有两个数据发生变化的时候,组件内的 Watcher 会收到两份通知,这时候要进行两次更新渲染吗?这显然是不合理的。虚拟 Dom 对整个组件进行渲染,只需要等所有的状态都修改完毕,一次性将整个组件的 DOM 渲染到最新的即可。
要解决这个问题,我们只需要在状态发生变化的时候,通知到对应的 Watcher,然后将 Watcher 添加到缓存队列当中,如果队列当中已经有相同的 Watcher 则不添加,如果没有则添加。这样队列当中只有一个 Watcher,当下次事件循环的时候,就会触发这个 Watcher 的渲染流程,所有的数据状态都会发生更新。
nextTick 原理
nextTick 的实现原理也是利用事件循环来进行异步操作,然后等 vue 的事件循环结束之后,再执行回调函数。首先大多数情况下,nextTick 会通过 Promise.resolve()来创建一个成功的 Promise,然后再通过 Promise.then()来将回调函数添加入微任务队列。同时,nextTick 还设置了状态锁pedding,通过pedding来判断当前队列当中是否已经存在一个nextTick的任务。这样就可以避免多次执行nextTick的任务,降低系统资源的使用。
前面我们说过,大多数情况下,我们使用的是Promise.then()将回调函数添加入事件队列。但是有些情况,我们无法使用这种方式。假如我们的浏览器不支持 Promise 的话,我们就无法使用 Promise 了。这种情况,我们会选择将 nextTick 加入到宏任务队列。另外,当 nextTick 的回调执行的时候,下一次事件循环还没有执行,这时候我们是获取不到更新渲染之后的数据的,因此,这时候我们会将 nextTick 的回调函数添加到宏任务队列当中。加入宏任务队列的方式有很多种,按照优先级来依次为setImmediate、MessageChannel、setTimeout。
nextTick 实现
// 存放回调函数的数组
const callbacks = [];
// 判断当前事件循环当中是否存在nextTick
let padding = false;
// 在事件循环当中执行回调函数
function flushCallbacks() {
// 本轮事件循环当中的nextTick已经执行完成,将pedding的状态变成false
pedding = false;
// 拷贝回调函数的数组
const copies = callbacks.slice(0);
// 将存放回调函数的数组清空
callbacks.length = 0;
// 执行回调函数
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
// 微任务
let microTimerFunc;
// 宏任务
let macroTimerFunc;
// 判断当前是否使用宏任务
let useMacroTask = false;
// 进行环境验证
//如果当前环境支持setImmediate,则使用setImmediate将回调函数添加到宏任务队列
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks);
};
}
// 如果当前环境支持MessageChannel,则使用MessageChannel将回调函数添加到宏任务队列
else if (
typeof MessageChannel !== "undefined" &&
(isNative(MessageChannel) ||
MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = () => {
port.postMessage(1);
};
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 判断当前环境是否支持Promise
if (typeof Promise !== "undefined" && isNative(Promise)) {
//创建一个Promise对象
const p = Promise.resolve();
// 将flushCallbacks添加到微任务
microTimerFunc = () => {
p.then(flushCallbacks);
};
} else {
//当前环境不支持Promise,则直接添加到宏任务当中
microTimerFunc = macroTimerFunc;
}
export function withMacroTask(fn) {
return (
fn._withTask ||
(fn.withTask = function () {
useMacroTask = true;
const res = fn.apply(null, arguments);
useMacroTask = false;
return res;
})
);
}
/**
*
* @param {*} cb 下一次Dom更新后执行的回调函数
* @param {*} ctx 回调函数的上下文对象
*/
export function nextTick(cb, ctx) {
let _resolve;
//将回调函数缓存到callbacks当中
callbacks.push(() => {
//判断当前是否有回调函数
if (cb) {
// 让当前回调函数的this指向ctx上下文对象
cb.call(ctx);
} else if (_resolve) {
// 将Promise返回出去
return _resolve(ctx);
}
});
//判断当前事件循环当中是否存在nextTick
if (!pedding) {
//将当前状态更改为true,表示事件队列当中已经有nextTick了
pedding = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
//如果回调函数不存在,并且环境支持Promise,则nextTick会返回一个Promise
if (!cb && typeof Promise !== "undefined") {
//返回一个成功状态的回调函数
return new Promise((resolve) => {
_resolve = resolve;
});
}
}