4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)

引言

前几天写了一篇关于Vue 3.0 reactive API 源码实现的文章,发现你们仍是蛮有兴趣对于源码这一块的。阅读的人数虽然很少,可是 200 屡次阅读,仍是阔以的!而且,在当时阿里的一位前辈也指出了文章存在的不足,就是没有分析 Proxy 是如何配合 Effect 实现响应式的原理,即依赖收集和派发更新的过程。javascript

因此,此次咱们就来完全了解一下,Vue 3.0 依赖收集和派发更新的整个过程。vue

值得一提的是在 Vue 3.0 中没有了 watcher 的概念,取而代之的是 effect ,因此接下来会接触不少和 effect 相关的函数

1、开始前准备

在文章的开始前,咱们先准备这样一个简单的 case,以便后续分析具体逻辑:java

main.js 项目入口node

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue 组件react

<template>
  <button @click="inc">Clicked {{ count }} times.</button>
</template>

<script>
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
    })
    const inc = () => {
      state.count++
    }

    return {
      inc,
      ...toRefs(state)
    }
  }
}
</script>

2、安装渲染 Effect

首先,咱们你们都知道在一般状况下,咱们的页面会使用当前实例的一些属性、计算属性、方法等等。因此,在组件渲染的过程就会发生依赖收集的这个过程。也所以,咱们先从组件的渲染过程开始分析。数组

在组件的渲染过程当中,会安装(建立)一个渲染 reactive effect,即 Vue 3.0 在编译 template 的时候,对是否有订阅数据作出相应的判断,建立对应的渲染 reactive effect,它的定义以下:缓存

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG) => {
    // create reactive effect for rendering
    instance.update = effect(function componentEffect() {
            ....
            instance.isMounted = true;
        }
        else {
            ...
        }
    }, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions);
};

咱们来大体分析一下 setupRenderEffect()。它传入几个参数,它们分别为:app

  • instance 当前 vm 实例
  • initialVNode 能够是组件 VNode 或者普通 VNode
  • container 挂载的模板,例如 div#app 对应的节点
  • anchor, parentSuspense, isSVG 普通状况下都为 null

而后在当前实例 instance 上建立属性 update 赋值为 effect() 函数的执行结果,effect() 函数传入两个参数:函数

  • componentEffect() 函数,它会在具体逻辑以后提到,这里咱们先不讲
  • createDevEffectOptions(instance) 用于后续的派发更新,它会返回一个对象:
{
    scheduler: queueJob(job) {
                    if (!queue.includes(job)) {
                        queue.push(job);
                        queueFlush();
                    }
                },
    onTrack: instance.rtc ? e => invokeHooks(instance.rtc, e) : void 0,
    onTrigger: instance.rtg ? e => invokeHooks(instance.rtg, e) : void 0
}

而后,咱们再来看看effect() 函数定义:post

function effect(fn, options = EMPTY_OBJ) {
    if (isEffect(fn)) {
        fn = fn.raw;
    }
    const effect = createReactiveEffect(fn, options);
    if (!options.lazy) {
        effect();
    }
    return effect;
}

effect() 函数的逻辑较为简单,首先判断是否已经为 effect,是则取出以前定义的。不是则经过 ceateReactiveEffect() 建立一个 effect,而 creatReactiveEffect() 的逻辑会是这样:

function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect(...args) {
        return run(effect, fn, args);
    };
    effect._isEffect = true;
    effect.active = true;
    effect.raw = fn;
    effect.deps = [];
    effect.options = options;
    return effect;
}

能够看到在 createReactiveEffect() 中先定义了一个 reactiveEffect() 函数赋值给 effect,它又调用了 run()方法。而 run() 方法中传入三个参数,分别为:

  • effect,即 reactiveEffect() 函数自己
  • fn,即在刚开始 instance.update 是调用 effect 函数时,传入的函数 componentEffect()
  • args 为一个空数组

而且,对 effect 进行了一些初始化,例如咱们最熟悉Vue 2x 中的 deps 就出如今 effect 这个对象上。

而后,咱们分析一下 run() 函数的逻辑:

function run(effect, fn, args) {
    if (!effect.active) {
        return fn(...args);
    }
    if (!effectStack.includes(effect)) {
        cleanup(effect);
        try {
            enableTracking();
            effectStack.push(effect);
            activeEffect = effect;
            return fn(...args);
        }
        finally {
            effectStack.pop();
            resetTracking();
            activeEffect = effectStack[effectStack.length - 1];
        }
    }
}

在这里,初次建立 effect,咱们会命中第二个分支逻辑,即当前 effectStack 栈中不包含这个 effect。那么,首先会执行 cleanup(effect),即遍历effect.deps,清空以前的依赖。

cleanup() 的逻辑其实在 Vue 2x的源码中也有的,避免依赖的重复收集。而且,对比 Vue 2xVue 3.0 中的 track 其实至关于 watcher,在 track 中会进行依赖的收集,后面咱们会讲 track 的具体实现

而后,执行enableTracking()effectStack.push(effect),前者的逻辑很简单,便可以追踪,用于后续触发 track 的判断:

function enableTracking() {
    trackStack.push(shouldTrack);
    shouldTrack = true;
}

然后者,即将当前的 effect 添加到 effectStack 栈中。最后,执行 fn() ,即咱们一开始定义的 instance.update = effect() 时候传入的 componentEffect()

instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
        const subTree = (instance.subTree = renderComponentRoot(instance));
        // beforeMount hook
        if (instance.bm !== null) {
            invokeHooks(instance.bm);
        }
        if (initialVNode.el && hydrateNode) {
            // vnode has adopted host node - perform hydration instead of mount.
            hydrateNode(initialVNode.el, subTree, instance, parentSuspense);
        }
        else {
            patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
            initialVNode.el = subTree.el;
        }
        // mounted hook
        if (instance.m !== null) {
            queuePostRenderEffect(instance.m, parentSuspense);
        }
        // activated hook for keep-alive roots.
        if (instance.a !== null &&
            instance.vnode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
            queuePostRenderEffect(instance.a, parentSuspense);
        }
        instance.isMounted = true;
    }
    else {
        ...
    }
}, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions);
而接下来就会进入组件的渲染过程,其中涉及 renderComponnetRootpatch 等等,此次咱们并不会分析组件渲染具体细节。

安装渲染 Effect,是为后续的依赖收集作一个前期的准备。由于在后面会用到 setupRenderEffect 中定义的 effect() 函数,以及会调用 run() 函数。因此,接下来,咱们就正式进入依赖收集部分的分析。

3、依赖收集

get

前面,咱们已经讲到了在组件渲染过程会安装渲染 Effect。而后,进入渲染组件的阶段,即 renderComponentRoot(),而此时会调用 proxyToUse,即会触发 runtimeCompiledRenderProxyHandlersget,即:

get(target, key) {
    ...
    else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) {
        accessCache[key] = 1 /* CONTEXT */;
        return renderContext[key];
    }
    ...
}

能够看出,此时会命中 accessCache[key] = 1renderContext[key] 。对于前者是作一个缓存的做用,后者是从当前的渲染上下文中获取 key 对应的值((对于本文这个 casekey 对应的就是 count,它的值为 0)。

那么,我想这个时候你们会当即反应,此时会触发这个 count 对应 Proxyget。可是,在咱们这个 case 中,用了 toRefs()reactive 包裹导出,因此这个触发 get 的过程会分为两个阶段:

Proxy 对象toRefs() 后获得对象的结构:

{
    value: 0
    _isRef: true
    get: function() {}
    set: ƒunction(newVal) {}
}

咱们先来看看 get() 的逻辑:

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        ...
        const res = Reflect.get(target, key, receiver);
        if (isSymbol(key) && builtInSymbols.has(key)) {
            return res;
        }
        ...
        // ref unwrapping, only for Objects, not for Arrays.
        if (isRef(res) && !isArray(target)) {
            return res.value;
        }
        track(target, "get" /* GET */, key);
        return isObject(res)
            ? isReadonly
                ? // need to lazy access readonly and reactive here to avoid
                    // circular dependency
                    readonly(res)
                : reactive(res)
            : res;
    };
}
两个阶段的不一样点在于,第一阶段的 target 为一个 object(即上面所说的 toRefs的对象结构),而第二阶段的 target 为普通对象 {count: 0}。具体细节能够看我 上篇文章

第一阶段:触发普通对象的 get

因为此时是第一阶段,因此咱们会命中 isRef() 的逻辑,并返回 res.value 。此时就会触发 reactive 定义的 Proxy 对象的 get。而且须要注意的是 toRefs() 只能用于对象,不然咱们即时触发了 get 也不能获取对应的值(这其实也是看源码的一些好处,深度理解 API 的使用)。

track

第二阶段:触发 Proxy 对象的 get

此时属于第二阶段,因此咱们会命中 get 的最后逻辑:

track(target, "get" /* GET */, key);
return isObject(res)
    ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
            // circular dependency
            readonly(res)
        : reactive(res)
    : res;

能够看到,首先会调用 track() 函数,进行依赖收集,而 track() 函数定义以下:

function track(target, type, key) {
    if (!shouldTrack || activeEffect === undefined) {
        return;
    }
    let depsMap = targetMap.get(target);
    if (depsMap === void 0) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (dep === void 0) {
        depsMap.set(key, (dep = new Set()));
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
        if ((process.env.NODE_ENV !== 'production') && activeEffect.options.onTrack) {
            activeEffect.options.onTrack({
                effect: activeEffect,
                target,
                type,
                key
            });
        }
    }
}

能够看到,第一个分支逻辑不会命中,由于咱们在前面分析 run() 的时候,就已经定义 ishouldTrack = trueactiveEffect = effect。而后,命中 depsMap === void 0 逻辑,往 targetMap 中添加一个键名为 {count: 0} 键值为一个空的 Map:

if (depsMap === void 0) {
    debugger
    targetMap.set(target, (depsMap = new Map()));
}
而此时,咱们也能够对比 Vue 2.x,这个 {count: 0} 其实就至关于 data 选项(如下统称为 data)。因此,这里也能够理解成先对 data 初始化一个 Map,显然这个 Map 中存的就是不一样属性对应的 dep

而后,对 count 属性初始化一个 Map 插入到 data 选项中,即:

let dep = depsMap.get(key);
if (dep === void 0) {
    depsMap.set(key, (dep = new Set()));
}

因此,此时的 dep 就是 count 属性对应的主题对象了。接下来,则判断是否当前 activeEffect 存在于 count 的主题中,若是不存在则往主题 dep 中添加 activeEffect,而且将当前主题 dep 添加到 activeEffectdeps 数组中。

if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
    // 最后的分支逻辑,咱们此次并不会命中
}

最后,再回到 get(),会返回 res 的值,在咱们这个 caseres 的值是 0

return isObject(res)
            ? isReadonly
                ? // need to lazy access readonly and reactive here to avoid
                    // circular dependency
                    readonly(res)
                : reactive(res)
            : res;

总结

好了,整个 reactive 的依赖收集过程,已经分析完了。咱们再来回忆其中几个关键点,首先在组件渲染过程,会给当前 vm 实例建立一个 effect,而后将当前的 activeEffect 赋值为 effect,并在 effect 上建立一些属性,例如很是重要的 deps 用于保存依赖

接下来,当该组件使用了 data 中的变量时,会访问对应变量的 get()。第一次访问 get() 会建立 data 对应的 depsMap,即 targetMap。而后再往 targetMapdepMap 中添加对应属性的 Map,即 depsMap

建立完属性的 depsMap 后,一方面会往该属性的 depsMap 中添加当前 activeEffect,即收集订阅者。另外一方面,将该属性的 depsMap 添加到 activeEffectdeps 数组中,即订阅主题。从而,造成整个依赖收集过程。

4、派发更新

set

分析完依赖收集的过程,那么派发更新的整个过程的分析也将会水到渠成。首先,对应派发更新,是指当某个主题发生变化时,在咱们这个 case 是当 count 发生变化时,此时会触发 dataset(),即 targetdatakeycount

function set(target, key, value, receiver) {
        ...
        const oldValue = target[key];
        if (!shallow) {
            value = toRaw(value);
            if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                oldValue.value = value;
                return true;
            }
        }
        const hadKey = hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        // don't trigger if target is something up in the prototype chain of original
        if (target === toRaw(receiver)) {
            if (!hadKey) {
                trigger(target, "add" /* ADD */, key, value);
            }
            else if (hasChanged(value, oldValue)) {
                trigger(target, "set" /* SET */, key, value, oldValue);
            }
        }
        return result;
    };

能够看到,oldValue0,而咱们的 shallow 此时为 falsevalue 为 1。那么,咱们看一下 toRaw() 函数的逻辑:

function toRaw(observed) {
    return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed;
}

toRaw() 中有两个 WeakMap 类型的变量 reactiveToRawreadonlyRaw。前者是在初始化 reactive 的时候,将对应的 Proxy 对象存入 reactiveToRaw 这个 Map 中。后者,则是存入和前者相反的键值对。即:

function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
    ...
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target);
    ...
}

很显然对于 toRaw() 方法而言,会返回 observer 即 1。因此,回到 set() 的逻辑,调用 Reflect.set() 方法将 data 上的 count 的值修改成 1。而且,接下来咱们还会命中 target === toRaw(receiver) 的逻辑。

target === toRaw(receiver) 的逻辑会处理两个逻辑:

  • 若是当前对象不存在该属性,触发 triger() 函数对应的 add
  • 或者该属性发生变化,触发 triger() 函数对应的 set

trigger

首先,咱们先看一下 trigger() 函数的定义:

function trigger(target, type, key, newValue, oldValue, oldTarget) {
    const depsMap = targetMap.get(target);
    if (depsMap === void 0) {
        // never been tracked
        return;
    }
    const effects = new Set();
    const computedRunners = new Set();
    if (type === "clear" /* CLEAR */) {
        ...
    }
    else if (key === 'length' && isArray(target)) {
        ...
    }
    else {
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
            addRunners(effects, computedRunners, depsMap.get(key));
        }
        // also run for iteration key on ADD | DELETE | Map.SET
        if (type === "add" /* ADD */ ||
            (type === "delete" /* DELETE */ && !isArray(target)) ||
            (type === "set" /* SET */ && target instanceof Map)) {
            const iterationKey = isArray(target) ? 'length' : ITERATE_KEY;
            addRunners(effects, computedRunners, depsMap.get(iterationKey));
        }
    }
    const run = (effect) => {
        scheduleRun(effect, target, type, key, (process.env.NODE_ENV !== 'production')
            ? {
                newValue,
                oldValue,
                oldTarget
            }
            : undefined);
    };
    // Important: computed effects must be run first so that computed getters
    // can be invalidated before any normal effects that depend on them are run.
    computedRunners.forEach(run);
    effects.forEach(run);
}
而且,你们能够看到这里有一个细节,就是计算属性的派发更新要优先于普通属性。

trigger() 函数,首先获取当前 targetMapdata 对应的主题对象的 depsMap,而这个 depsMap 即咱们在依赖收集时在 track 中定义的。

而后,初始化两个 Set 集合 effectscomputedRunners ,用于记录普通属性或计算属性的 effect,这个过程是会在 addRunners() 中进行。

接下来,定义了一个 run() 函数,包裹了 scheduleRun() 函数,并对开发环境和生产环境进行不一样参数的传递,这里因为咱们处于开发环境,因此传入的是一个对象,即:

{
    newValue: 1,
    oldValue: 0,
    oldTarget: undefined
}

而后遍历 effects,调用 run() 函数,而这个过程实际调用的是 scheduleRun()

function scheduleRun(effect, target, type, key, extraInfo) {
    if ((process.env.NODE_ENV !== 'production') && effect.options.onTrigger) {
        const event = {
            effect,
            target,
            key,
            type
        };
        effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event);
    }
    if (effect.options.scheduler !== void 0) {
        effect.options.scheduler(effect);
    }
    else {
        effect();
    }
}

此时,咱们会命中 effect.options.scheduler !== void 0 的逻辑。而后,调用 effect.options.scheduler() 函数,即调用 queueJob() 函数:

scheduler 这个属性是在 setupRenderEffect 调用 effect 函数时建立的。
function queueJob(job) {
    if (!queue.includes(job)) {
        queue.push(job);
        queueFlush();
    }
}
这里使用了一个队列维护全部 effect() 函数,其实也和 Vue 2x 类似,由于咱们 effect() 至关于 watcher,而 Vue 2x 中对 watcher 的调用也是经过队列的方式维护。队列的存在具体是为了保持 watcher 触发的次序,例如先父 watcher 后子 watcher

能够看到 咱们会先将 effect() 函数添加到队列 queue 中,而后调用 queueFlush() 清空和调用 queue

function queueFlush() {
    if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        nextTick(flushJobs);
    }
}

熟悉 Vue 2x 源码的同窗,应该知道 Vue 2x 中的 watcher 也是在下一个 tick 中执行,而 Vue 3.0 也是同样。而 flushJobs 中就会对 queue 队列中的 effect() 进行执行:

function flushJobs(seen) {
    isFlushPending = false;
    isFlushing = true;
    let job;
    if ((process.env.NODE_ENV !== 'production')) {
        seen = seen || new Map();
    }
    while ((job = queue.shift()) !== undefined) {
        if (job === null) {
            continue;
        }
        if ((process.env.NODE_ENV !== 'production')) {
            checkRecursiveUpdates(seen, job);
        }
        callWithErrorHandling(job, null, 12 /* SCHEDULER */);
    }
    flushPostFlushCbs(seen);
    isFlushing = false;
    if (queue.length || postFlushCbs.length) {
        flushJobs(seen);
    }
}

flushJob() 主要会作几件事:

  • 首先初始化一个 Map 集合 seen,而后在递归 queue 队列的过程,调用 checkRecursiveUpdates() 记录该 jobeffect() 触发的次数。若是超过 100 次会抛出错误。
  • 而后调用 callWithErrorHandling(),执行 jobeffect(),而咱们都知道的是这个 effect 是在 createReactiveEffect() 时建立的 reactiveEffect(),因此,最终会执行 run() 方法,即执行最初在 setupRenderEffectect 定义的 effect()
const setupRenderEffectect = (instance, initialVNode, container, anchor, parentSuspense, isSVG) => {
        // create reactive effect for rendering
        instance.update = effect(function componentEffect() {
            if (!instance.isMounted) {
                ...
            }
            else {
                ...
                const nextTree = renderComponentRoot(instance);
                const prevTree = instance.subTree;
                instance.subTree = nextTree;
                if (instance.bu !== null) {
                    invokeHooks(instance.bu);
                }
                if (instance.refs !== EMPTY_OBJ) {
                    instance.refs = {};
                }
                patch(prevTree, nextTree, 
                hostParentNode(prevTree.el), 
                getNextHostNode(prevTree), instance, parentSuspense, isSVG);
                instance.vnode.el = nextTree.el;
                if (next === null) {
                    updateHOCHostEl(instance, nextTree.el);
                }
                if (instance.u !== null) {
                    queuePostRenderEffect(instance.u, parentSuspense);
                }
                if ((process.env.NODE_ENV !== 'production')) {
                    popWarningContext();
                }
            }
        }, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions);
    };

即此时就是派发更新的最后阶段了,会先 renderComponentRoot() 建立组件 VNode,而后 patch() ,即走一遍组件渲染的过程(固然此时称为更新更为贴切)。从而,完成视图的更新。

总结

一样地,咱们也来回忆派发更新过程的几个关键点。首先,触发依赖的 set(),它会调用 Reflect.set() 修改依赖对应属性的值。而后,调用 trigger() 函数,获取 targetMap 中对应属性的主题,即 depsMap(),而且将 depsMap 中的 effect() 存进 effect 集合中。接下来,就将 effect 进队,在下一个 tick 中清空和执行全部 effect。最后,和在初始化的时候说起的同样,走组件的更新过程,即 renderComponent()patch() 等等

结语

虽然,整个依赖收集的过程我足足花费了 9 个小时来总结分析,而且整个文章的内容也达到了 4k+ 字。可是,这并不表明了它很复杂。其实整个依赖收集和派发更新的过程,仍是很是简单明了的。首先定义全局的渲染 effect(),而后在 get() 中调用 track() 进行依赖收集。接下来,若是依赖发生变化,就会走派发更新的流程,先更新依赖的值,而后调用 trigger() 收集 effect(),在下一个 tick 中执行 effect(),最后更新组件。

写做不易,若是你以为有收获的话,能够帅气三连击!!!
相关文章
相关标签/搜索