乐趣区

关于javascript:Vue3-nextTick-源码分析

Vue3 nextTick 源码剖析

vue 版本

3.2.37

前言

在之前的 Vue2 剖析中,提到了 Vue2 的 nextTick 是保护了一个 callbacks 数组,每次更新过程中只插入一个微工作,执行放在 callbacks 数组中的回调。

而 Vue3 不同,Vue3 的 nextTick 和 Promise 根本没什么区别,set 过程的更新看似也不再依赖,nextTick 进行。仅仅只是将创立一个 resovled 状态的 Promise,将传入的函数放入回调中罢了。以下是 vue3 的 nextTick 源码:

const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

正因为如此,同样的代码,在 Vue2 和 Vue3 中会有不同的体现。参考以下代码:

Promise.resolve().then(()=>{console.log('开始的 Promise 回调')
            })
            this.$nextTick(()=>{console.log('第一次 nextTick 的回调')
            })
            Promise.resolve().then(()=>{console.log('批改数据之前的 Promise 回调')
            })
            this.name = 'kirito' // 这里进行赋值操作
            Promise.resolve().then(()=>{console.log('批改数据之后的 Promise 回调')
            })
            this.$nextTick(()=>{console.log('最初的 nextTick 的回调')
            })

以上代码的运行后果,在 Vue2 中是:

开始的 Promise 回调
第一次 nextTick 的回调
最初的 nextTick 的回调
批改数据之前的 Promise 回调
批改数据之后的 Promise 回调

只有调用了 nextTick 或者对数据进行了变更,那么放在之后的 Promise 回调,肯定是排在前面执行的。

而同样的代码:

        const name = ref('yuuki')
        const test2 = () => {Promise.resolve().then(()=>{console.log('开始的 Promise 回调')
            })
            nextTick(()=>{console.log('第一次 nextTick 的回调')
            })
            Promise.resolve().then(()=>{console.log('批改数据之前的 Promise 回调')
            }) 
            name.value = 'kirito'  // 这里进行赋值操作
            Promise.resolve().then(()=>{console.log('批改数据之后的 Promise 回调')
            })
            nextTick(()=>{console.log('最初的 nextTick 的回调')
            })
        }

在 Vue3 中的执行后果是:

开始的 Promise 回调
第一次 nextTick 的回调
批改数据之前的 Promise 回调
批改数据之后的 Promise 回调
最初的 nextTick 的回调

看上去齐全是依照 Promise 退出微工作队列的逻辑,一次 nextTick 就是插入一个微工作队列,不保护 callbacks 数组。

实例剖析

上述示例代码,赋值操作如果稍稍扭转一下地位,又会与料想的输入截然不同:

        const name = ref('yuuki')
        const age = ref(18)
        const test2 = () => {Promise.resolve().then(()=>{console.log('开始的 Promise 回调')
            })
            name.value = 'kirito'  // 这里进行赋值操作
            nextTick(()=>{console.log('第一次 nextTick 的回调')
            })
            Promise.resolve().then(()=>{console.log('批改数据之前的 Promise 回调')
            }) 
            Promise.resolve().then(()=>{console.log('批改数据之后的 Promise 回调')
            })
            nextTick(()=>{console.log('最初的 nextTick 的回调')
            })
        }

以上的代码,执行了之后输入变成:

开始的 Promise 回调
批改数据之前的 Promise 回调
批改数据之后的 Promise 回调
第一次 nextTick 的回调
最初的 nextTick 的回调

仿佛在进行赋值操作之后,nextTick 的优先程序又产生了变动,就算在前面的 Promise 回调也会在 nextTick 之前调用,与 Vue2 的赋值操作之后,nextTick 优先级降级比起来,Vue3 中进行了赋值操作,也就是说数据更新之后,nextTick 回调的优先级反而降落了一个等级。

nextTick 源码剖析

不同于 Vue2 的 nextTick 的实现,Vue3 相比起来,nextTick 的实现代码异样简略,只有短短几行:

const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
let currentPreFlushParentJob = null;
const RECURSION_LIMIT = 100;
function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

resolvedPromise 只是一个处于 fulfilled 状态的 Promise 对象,也就是说如果 p 变量是 resolvedPromise,那么只会立刻执行 then 回调并退出到微工作队列中。那么要关怀的就是 currentFlushPromise 变量。也就是说正是因为 currentFlushPromise 的值不为 null 了,导致的 nextTick 执行优先级降落。

那么逐渐剖析响应式数据的 set 过程,肯定能找到 currentFlushPromise 何时产生了变动。

set 过程源码剖析

对 ref 创立的响应式数据,进行赋值操作: 首先会进入 RefImpl 的 set 函数

class RefImpl {constructor(value, __v_isShallow) {
        this.__v_isShallow = __v_isShallow;
        this.dep = undefined;
        this.__v_isRef = true;
        this._rawValue = __v_isShallow ? value : toRaw(value);
        this._value = __v_isShallow ? value : toReactive(value);
    }
    get value() {trackRefValue(this);
        return this._value;
    }
    set value(newVal) {newVal = this.__v_isShallow ? newVal : toRaw(newVal);
        if (hasChanged(newVal, this._rawValue)) { // 判断是否变动
            this._rawValue = newVal;
            this._value = this.__v_isShallow ? newVal : toReactive(newVal);
            triggerRefValue(this, newVal); // 
        }
    }
}

值更新后进入 triggerRefValue 函数(追踪 ref 值变动),这里将 ref 转为一般数据或者说原始对象,这意味着勾销了数据代理,不会因为值的读取和批改而造成额定开销,

function triggerRefValue(ref, newVal) {ref = toRaw(ref);
    if (ref.dep) {if ((process.env.NODE_ENV !== 'production')) {
            triggerEffects(ref.dep, {
                target: ref,
                type: "set" /* SET */,
                key: 'value',
                newValue: newVal
            });
        }
        else {triggerEffects(ref.dep);
        }
    }
}

之后进入 triggerEffects 函数,ref.dep 是一个 ReactiveEffect 类的 Set 汇合

function triggerEffects(dep, debuggerEventExtraInfo) {
    // spread into array for stabilization
    const effects = isArray(dep) ? dep : [...dep];
    for (const effect of effects) {if (effect.computed) {triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
    for (const effect of effects) {if (!effect.computed) {triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
}

进入 triggerEffect 函数,且不论 onTrigger 和 run 是何种状况会调用,这里进入的是 effect.scheduler 函数

function triggerEffect(effect, debuggerEventExtraInfo) {if (effect !== activeEffect || effect.allowRecurse) {if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {effect.onTrigger(extend({ effect}, debuggerEventExtraInfo));
        }
        if (effect.scheduler) {effect.scheduler();
        }
        else {effect.run();
        }
    }
}

effect 是一个 ReactiveEffect 类型对象,其构造函数以及,effect 创立过程如下:

class ReactiveEffect {constructor(fn, scheduler = null, scope) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.active = true;
        this.deps = [];
        this.parent = undefined;
        recordEffectScope(this, scope);
    }
.......
}
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope
        ));
        const update = (instance.update = () => effect.run());

也就是说,调用 queueJob 函数,如果全局变量 queue 为空或者 queue 中不蕴含该 job 并且以后 job 不等于正在筹备 flush 的 job,则往 queue 推入一个 job

function queueJob(job) {
    if ((!queue.length ||
        !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
        job !== currentPreFlushParentJob) {if (job.id == null) {queue.push(job);
        }
        else {queue.splice(findInsertionIndex(job.id), 0, job);
        }
        queueFlush();}
}

接着执行 queueFlush 函数,这里将 flushJobs 函数退出微工作队列,并且要害的标记 bool 变量,置为 true,代表如果不是第一次进行数据更新就跳过这个操作,currentFlushPromise 是一个状态为pending 的 Promise 对象,期待回调执行胜利,这也解释了为什么之前的示例代码中,nextTick 的优先级会升高一级。

function queueFlush() {if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs);
    }
}

接着来看回调的 flushJobs 函数干了什么事:

1、将标识 isFlushPending 还原,代表这次的回调曾经胜利开始执行了; 将 isFlushing 标识置为 true,代表接下来要进行更新操作,其余一些操作无奈失效。

2、对更新队列进行排序。

3、调用 callWithErrorHandling 执行更新的具体操作

4、最初重置标识,以及一些全局变量等,为下一次更新做好筹备,其中包含 currentFlushPromise 置为 null。

function flushJobs(seen) {
    isFlushPending = false;
    isFlushing = true;
    if ((process.env.NODE_ENV !== 'production')) {seen = seen || new Map();
    }
    flushPreFlushCbs(seen);
    // 排序队列的意义:1、保障组件的更新是从父到子组件,因为父组件肯定先于子组件创立,所以父组件的渲染优先级更小。2、如果父组件在更新期间卸载组件,可能跳过他的更新。queue.sort((a, b) => getId(a) - getId(b));

    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;}
                // console.log(`running:`, job.id)
                callWithErrorHandling(job, null, 14 /* SCHEDULER */);
            }
        }
    }
    finally {
        flushIndex = 0;
        queue.length = 0;
        flushPostFlushCbs(seen);
        isFlushing = false;
        currentFlushPromise = null;
        // some postFlushCb queued jobs!
        // keep flushing until it drains.
        if (queue.length ||
            pendingPreFlushCbs.length ||
            pendingPostFlushCbs.length) {flushJobs(seen);
        }
    }
}

前面没太看明确了,总之会进入 ReactiveEffect 的 run 函数中, 接着 this.fn()的函数调用

class ReactiveEffect {
    .......
    run() {if (!this.active) {return this.fn();
        }
        let parent = activeEffect;
        let lastShouldTrack = shouldTrack;
        while (parent) {if (parent === this) {return;}
            parent = parent.parent;
        }
        try {
            this.parent = activeEffect;
            activeEffect = this;
            shouldTrack = true;
            trackOpBit = 1 << ++effectTrackDepth;
            if (effectTrackDepth <= maxMarkerBits) {initDepMarkers(this);
            }
            else {cleanupEffect(this);
            }
            return this.fn(); // 函数调用}
        finally {if (effectTrackDepth <= maxMarkerBits) {finalizeDepMarkers(this);
            }
            trackOpBit = 1 << --effectTrackDepth;
            activeEffect = this.parent;
            shouldTrack = lastShouldTrack;
            this.parent = undefined;
            if (this.deferStop) {this.stop();
            }
        }
    }

而后调用一个两百多行的 componentUpdateFn 函数, 这里次要进行生命周期钩子回调的调用,以及进行虚构 DOM 树的比照,以及理论 DOM 树的更新操作。这里会调用 beforeMount,mount,activated,beforeUpdate 以及 updated 这些 hook 和生命周期钩子函数。

const componentUpdateFn = () => {if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const {el, props} = initialVNode
        const {bm, m, parent} = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

        toggleRecurse(instance, false)
        // beforeMount hook
        if (bm) {invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if (
          !isAsyncWrapperVNode &&
          (vnodeHook = props && props.onVnodeBeforeMount)
        ) {invokeVNodeHook(vnodeHook, parent, initialVNode)
        }
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {instance.emit('hook:beforeMount')
        }
        toggleRecurse(instance, true)
          
        // mounted hook
        if (m) {queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if (
          !isAsyncWrapperVNode &&
          (vnodeHook = props && props.onVnodeMounted)
        ) {
          const scopedInitialVNode = initialVNode
          queuePostRenderEffect(() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
            parentSuspense
          )
        }
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {
          queuePostRenderEffect(() => instance.emit('hook:mounted'),
            parentSuspense
          )
        }

        // activated hook for keep-alive roots.
        // #1742 activated hook must be accessed after first render
        // since the hook may be injected by a child keep-alive
        if (
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
          (parent &&
            isAsyncWrapper(parent.vnode) &&
            parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
        ) {instance.a && queuePostRenderEffect(instance.a, parentSuspense)
          if (
            __COMPAT__ &&
            isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
          ) {
            queuePostRenderEffect(() => instance.emit('hook:activated'),
              parentSuspense
            )
          }
        }
        instance.isMounted = true

        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {devtoolsComponentAdded(instance)
        }

        // #2458: deference mount-only object parameters to prevent memleaks
        initialVNode = container = anchor = null as any
      } else {
        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode)
        let {next, bu, u, parent, vnode} = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined
        if (__DEV__) {pushWarningContext(next || instance.vnode)
        }

        // Disallow component effect recursion during pre-lifecycle hooks.
        toggleRecurse(instance, false)
        if (next) {
          next.el = vnode.el
          updateComponentPreRender(instance, next, optimized)
        } else {next = vnode}

        // beforeUpdate hook
        if (bu) {invokeArrayFns(bu)
        }
        // onVnodeBeforeUpdate
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {invokeVNodeHook(vnodeHook, parent, next, vnode)
        }
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {instance.emit('hook:beforeUpdate')
        }
        toggleRecurse(instance, true)

        // render
        if (__DEV__) {startMeasure(instance, `render`)
        }
        const nextTree = renderComponentRoot(instance)
        if (__DEV__) {endMeasure(instance, `render`)
        }
        const prevTree = instance.subTree
        instance.subTree = nextTree

        if (__DEV__) {startMeasure(instance, `patch`)
        }
        patch(
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
        if (__DEV__) {endMeasure(instance, `patch`)
        }
        next.el = nextTree.el
        if (originNext === null) {
          // self-triggered update. In case of HOC, update parent component
          // vnode el. HOC is indicated by parent instance's subTree pointing
          // to child component's vnode
          updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
          queuePostRenderEffect(() => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
            parentSuspense
          )
        }
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {
          queuePostRenderEffect(() => instance.emit('hook:updated'),
            parentSuspense
          )
        }
      }
    }

总结

到此为止的剖析,也只是通俗的理解了响应式数据 set 过程中运行逻辑。

Vue2 与 Vue3,在 nextTick 和响应式数据更新过程中一样的点在于:

1、应用 Promise 形式放入更新的回调。

2、回调中应用通过全局变量等形式,使一次更新回调中,能拜访到这次所有对数据的操作,即自第一次数据更新操作后,就不再应用 Promise 退出更多的微工作。

不同之处在于:

1、数据代理的形式不同了,都是拦挡对数据的 get 和 set 操作,Vue3 通过 Proxy 形式,Vue2 通过 Object.defineProperty 的形式

2、nextTick 的实现形式不同了,Vue2 中的 nextTick 如果在数据赋值操作执行之前调用,是可能影响更新逻辑在微工作队列中的执行程序的。

也就是说,如果执行了 nextTick->Promise.then-> 赋值操作。那么理论的回调程序会是 nextTick,组件更新,Promise.then。

而 Vue3 中不同,即便 nextTick 赋值操作之前执行也不会影响到赋值操作退出的回调的执行程序,并且在执行了赋值操作之后,再执行 nextTick,则会等到组件理论的更新操作实现之后,才会将 nextTick 退出微工作队列中,筹备执行。


然而剖析了这么一通,其实依照 Vue 官网文档中举荐的写法,个别是不会出什么问题的。理论开发中也很少有须要 nextTick 和 Promise 混着并且反复用的中央。

退出移动版