【实操学习】nextTick的实现原理

113 阅读2分钟

背景

最近在翻看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浏览器控制台打印结果:

image.png

为什么nextTick:1 的输出在Promise: 2Promise: 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 中执行。

控制台输出结果均为

image.png

可以看到 nextTick "恢复正常"了

最后

目前写文章的目的,就是记录一下自己的小收获,还有困惑。

欢迎大家指点和讨论