• 分析 Vue 3.0 响应式原理


    引言

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

    所以,这次我们就来彻底了解一下,vue 3.0 依赖收集和派发更新的整个过程。

    值得一提的是在 vue 3.0 中没有了watcher 的概念,取而代之的是 effect ,所以接下来会接触很多和 effect 相关的函数

    一、开始前准备

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

    main.js 项目入口

    import { createApp } from 'vue'
    import App from './App.vue'
    
    createApp(App).mount('#app')

    App.vue 组件

    <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>

    二、安装渲染 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()。它传入几个参数,它们分别为:

    • 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() 函数定义:

    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 2x,Vue 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);
    而接下来就会进入组件的渲染过程,其中涉及 renderComponnetRoot、patch 等等,这次我们并不会分析组件渲染具体细节。

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

    三、依赖收集

    get

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

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

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

    那么,我想这个时候大家会立即反应,此时会触发这个 count 对应 Proxy 的 get。但是,在我们这个 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 = true 和 activeEffect = 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 添加到 activeEffect 的 deps 数组中。

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

    最后,再回到 get(),会返回 res 的值,在我们这个 case 是 res 的值是 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。然后再往 targetMap 的 depMap 中添加对应属性的 Map,即 depsMap。

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

    电脑刺绣绣花厂 http://www.szhdn.com 广州品牌设计公司https://www.houdianzi.com

    四、派发更新

    set

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

    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;
        };

    可以看到,oldValue 为 0,而我们的 shallow 此时为 false,value 为 1。那么,我们看一下 toRaw() 函数的逻辑:

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

    toRaw() 中有两个 WeakMap 类型的变量 reactiveToRaw 和 readonlyRaw。前者是在初始化 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() 函数,首先获取当前 targetMap 中 data 对应的主题对象的 depsMap,而这个 depsMap 即我们在依赖收集时在 track 中定义的。

    然后,初始化两个 Set 集合 effects 和 computedRunners ,用于记录普通属性或计算属性的 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() 记录该 job 即 effect() 触发的次数。如果超过 100 次会抛出错误。
    • 然后调用 callWithErrorHandling(),执行 job 即 effect(),而我们都知道的是这个 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() 等等

  • 相关阅读:
    ASE19 团队项目 模型组 scrum report集合
    ASE19团队项目alpha阶段model组 scrum2 记录
    ASE19团队项目alpha阶段model组 scrum1 记录
    ASE第二次结对编程——Code Search
    jdk中集成的jre和单独安装的jre有什么区别?
    window, linux, mac 比较文件和文件夹的区别
    Java 调用python、ruby 等脚本引擎
    微软软件工程 第一周博客作业
    绩效考核(2018.5.28~2018.6.3)
    数据库需求文档
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/13825914.html
Copyright © 2020-2023  润新知