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混着并且反复用的中央。