原理
可以这么来理解 nextTick
,响应式数据发生改变是同步任务,nextTick
是异步微任务,根据 JS 的事件循环原理,是先执行同步任务,再执行微任务。
假如没有使用 nextTick
会怎样呢?如果不断触发响应式数据的改变,那么就会不断地触发 effect
副作用函数,导致多次执行 render
函数,而我们只是想要最终的那一次更新即可,中间过程的更新对于我们只想渲染最终结果到页面来说是多余的触发。
那么问题就是,怎么才可以在等响应式数据改变完之后,再去执行且只执行一次 render
函数呢?
异步微任务可以做到。
在响应式数据发生改变的时候,不执行 effect
副作用函数 fn
,而是通过 scheduler
把当前的 fn
即 instance.update
添加到异步微任务队列。
等到所有同步的任务执行完,也就是先让响应式数据最终改变完成后,再去执行异步微任务队列的任务。
而在执行了 instance.update
之后,DOM 的数据才是最新的。
vue3 抛出的 nextTick
内部的实现原理是接收一个函数 fn
,返回一个 promise
,该 promise
是 Promise.resolve().then(fn)
的结果,也就是它也是内部实现的是一个异步微任务。
所以我们往往使用 nextTick
来获取 DOM 更新之后的数据。
这个例子也许可以帮我们更好地理解 JS 的微任务在 nextTick 中的使用:
js
const instanceUpdate = () => {
console.log('effect-update')
}
const p1 = Promise.resolve();
const queue1 = [];
function queueJobFn(job) {
if(queue1.includes(job)) {
return;
}
queue1.push(job);
p1.then(() => {
while(queue1.length) {
const job = queue1.shift();
job && job();
}
});
}
let i = 1; // 模拟响应式数据
i++; // 模拟响应式数据发生变化
queueJobFn(instanceUpdate); // 模拟触发 effect scheduler
i++; // 模拟响应式数据发生变化
queueJobFn(instanceUpdate); // 模拟触发 effect scheduler
// 模拟 nextTick
p1.then(() => {
console.log(i);
});
执行的顺序是:
i = 1
,执行同步i++
,执行同步,i = 2
queueJobFn(instanceUpdate);
依据 JS 事件循环,内部遇到Promise.then
,先不执行,放到微任务队列中排队等候,而我们自己也把任务存到queue1
队列中。i++
,执行同步,i = 3
queueJobFn(instanceUpdate);
queue1
队列中已存在于instanceUpdate
,return- 遇到
p1.then
微任务,依据 JS 事件循环,遇到Promise.then
,先不执行,放到微任务队列中排队等候 - 依据 JS 事件循环,同步任务执行完,开始执行异步微任务,先进入队列的是
instanceUpdate
,执行,从queue1
中一一取出任务来执行,打印effect-update
- 接着还有
p1.then
, 执行,打印3
实现
scheduler
diff
function setupRenderEffect(instance, initialVNode, container, anchor) {
instance.update = effect(() => {
if(!instance.isMounted) {
console.log('init');
const { proxy } = instance;
const subTree = (instance.subTree = instance.render.call(proxy, proxy));
patch(null, subTree, container, instance, anchor);
// vnode -> element
initialVNode.el = subTree.el;
instance.isMounted = true;
} else {
console.log('update');
// 更新 props,需要一个 vnode
const { next, vnode } = instance;
if(next) {
// 设置 新虚拟节点的 el
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const { proxy } = instance;
const subTree = instance.render.call(proxy, proxy);
const prevSubTree = instance.subTree;
instance.sbuTree = subTree;
patch(prevSubTree, subTree, container, instance, anchor);
}
+ }, {
+ scheduler() {
+ queueJobs(instance.update);
+ }
+ });
}
queueJobs
queueJobs
的作用是把 instance.update
添加到微任务队列,等同步任务执行完之后,再执行 instance.update
。
外面手动添加 nextTick
写异步回调,则这个异步回调将会以异步微任务的形式执行。
如果要回获取 DOM 渲染后的数据,则在 nextTick
回调中可以获取到。
ts
// scheduler.ts
const queue: any[] = []; // 微任务队列
const p = Promise.resolve(); // 可以防止多次创建
let isFlushPending = false;
export function nextTick(fn?) {
return fn ? p.then(fn) : p; // 返回值是个 promise
}
export function queueJobs(job) {
// 如果已经存在,则不需要再次添加,不存在则添加到队列
if(!queue.includes(job)) {
queue.push(job);
}
queueFlush();
}
function queueFlush () {
if(isFlushPending) return; // 有在执行微任务,则返回
isFlushPending = true;
nextTick(flushJobs); // 微任务
}
function flushJobs() {
isFlushPending = false;
let job;
while(job = queue.shift()) { // 出队
job && job(); // 执行
}
}