背景
最近在翻看nextTick的实现原理,于是本天命之子喜滋滋地写了一个简单的demo...
<script lang="ts" setup>
import {nextTick} from 'vue';
function runNextTickTest(){
const p = new Promise(resolve => {
resolve(null)
})
p.then(() => {
console.log('Promise: '+ 1)
return
}).then(() => {
console.log('Promise: '+ 3)
})
nextTick(() => {
console.log('nextTick:' + 1)
})
p.then(() => {
console.log('Promise: '+ 2)
})
}
onMounted(() => {
runNextTickTest()
})
</script>
chrome浏览器控制台打印结果:
为什么nextTick:1 的输出在Promise: 2 和 Promise: 3之间?
没记错的话,在环境支持promise的情况下,nextTick应该是一个和Promise差不多的微任务才对 ?
这里贴一下 nextTick 的部分源码
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.
// Technically 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)
}
}
官方源码地址: github.com/vuejs/vue/b…
直接从引用的 nextTick 处寻找
既然从源码部分找不到原因,那就从当前引用的 nextTick 函数入手。
找到引用的 nextTick 位置为 \node\_modules\@vue\runtime-core\dist\runtime-core.esm-bundler.js
这里只贴出顺着 nextTick -> currentFlushPromise -> ...hydrateElement 的部分
const resolvedPromise = /* @__PURE__ */ Promise.resolve();
let currentFlushPromise = null;
const RECURSION_LIMIT = 100;
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise;
return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
...
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
...
function flushJobs(seen) {
isFlushPending = false;
isFlushing = true;
if (!!(process.env.NODE_ENV !== "production")) {
seen = seen || /* @__PURE__ */ new Map();
}
queue.sort(comparator);
const check = !!(process.env.NODE_ENV !== "production") ? (job) => checkRecursiveUpdates(seen, job) : NOOP;
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
if (!!(process.env.NODE_ENV !== "production") && check(job)) {
continue;
}
callWithErrorHandling(job, null, 14);
}
}
} finally {
flushIndex = 0;
queue.length = 0;
flushPostFlushCbs(seen);
isFlushing = false;
currentFlushPromise = null;
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen);
}
}
}
...
function queuePostFlushCb(cb) {
if (!isArray(cb)) {
if (!activePostFlushCbs || !activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)) {
pendingPostFlushCbs.push(cb);
}
} else {
pendingPostFlushCbs.push(...cb);
}
queueFlush();
}
...
function queueEffectWithSuspense(fn, suspense) {
if (suspense && suspense.pendingBranch) {
if (isArray(fn)) {
suspense.effects.push(...fn);
} else {
suspense.effects.push(fn);
}
} else {
queuePostFlushCb(fn);
}
}
...
const queuePostRenderEffect = queueEffectWithSuspense ;
...
const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => {
optimized = optimized || !!vnode.dynamicChildren;
const { type, props, patchFlag, shapeFlag, dirs } = vnode;
const forcePatchValue = type === "input" && dirs || type === "option";
if (!!(process.env.NODE_ENV !== "production") || forcePatchValue || patchFlag !== -1) {
let vnodeHooks;
if (vnodeHooks = props && props.onVnodeBeforeMount) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode);
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
}
if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
queueEffectWithSuspense(() => {
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode);
dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
}, parentSuspense);
}
....
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
...
const { bm, m, parent } = instance;
...
..
if (m) {
queuePostRenderEffect(m, parentSuspense);
}
...
(代码太多, 就没有继续找下去了)
在 mounted 生命周期函数运行前, 调用 nextTick ,等价于调用resolvedPromise;
在 mounted 生命周期函数内部,调用 nextTick,等价于调用 currentFlushPromise ;
在 mounted 生命周期函数之后, 调用 nextTick ,等价于调用resolvedPromise;
其中最难以理解的就是
在 mounted 生命周期函数内部调用 nextTick, 内部回调会在同层级的微任务最末输出
这个现象是如何产生的,本人也没有找到原因。
结合代码中很多props.onVnodeMounted之类的限制条件,只能初步猜测大概和等待页面渲染有关。
修改demo
既然猜测与生命周期有关,那就可以进行如下两个尝试。
1,将runNextTickTest函数在 setup 中执行。
2,将runNextTickTest函数在 setTimeout 中执行。
控制台输出结果均为
可以看到 nextTick "恢复正常"了
最后
目前写文章的目的,就是记录一下自己的小收获,还有困惑。
欢迎大家指点和讨论