Vue 3响应式解析

276 阅读3分钟

job 是什么?

job 是依赖响应式对象属性值变更触发执行的函数(执行时响应式对象属性值建立依赖)。

举几个 job 的栗子

这里看不懂可以先跳过,下面有详细介绍

  • redner job

渲染组件实例,首次挂载立即执行,执行阶段获取响应式对象属性时和其建立依赖,

生成依赖后,其他响应式对象属性值变更时触发 render job

或可以通过 $forceUpdate 强制触发。

  • computed job:

某 job 执行段需要计算值,会先触发生成 comoputed job

生成时,立即执行计算函数,computed job 会和计算函数使用到响应式对象的属性值建立依赖

生成后,某 job 会和当前计算属性值建立依赖

Vue 会在计算属性值被某 job 使用时在内部使用 ComputedRefImpl 创建一个只有 value 属性的对象 o。

value 的 get 函数生成 computed job,并执行计算函数,并使用 track 函数将 value 和触发 get 函数 的某 job 建立依赖。

computed job 的作用是,打开控制缓存的开关变量,并触发依赖 value 的 job

Vue 还会在实例上创建一个与计算函数同名的属性,其 get 函数为 () => o.value

当响应式对象变更,触发 computed job,computed job 又触发 value 的 job,value 的 job 使用计算值,又重新触发 value 的 get 函数

  • watch job 组件实例子初始化,获取 oldValue 时立即生成 watch job

后续依赖的响应式对象属性变更时,触发 watch job

在首次挂载阶段同步执行 watch job

在挂载后,异步执行 watch jop(存于 pendingPreFlushCbs),先于 render job

存储待执行 job 的容器

以下容器皆为数组:

  • pendingPreFlushCbs
  • queue
  • pendingPostFlushCbs

注:以上排序也是执行顺序

操作 job 的 api

flushJobs 依次执行 pendingPreFlushCbs queue  pendingPostFlushCbs 三个数组中的 job

queueFlush 执行 Promise.resolve().then(flushJobs)

queueJob 将 job 推入 queue,并立马执行 queueFlush

queuePreFlushCb 将 job 推入 pendingPreFlushCbs,并立马执行 queueFlush

queuePostFlushCb 将 job 推入 pendingPostFlushCbs,并立马执行 queueFlush

nextTick queueFlush.then(fn) 或 Promise.resolve().then(fn)

可以看出 nextTick 永远在当前 job 执行完毕后执行。

创建 job 的 effect 方法

每一次创建时,都会清除和响应式对象属性已有的依赖,并重新建立依赖

// 接收要执行的fn,并返回一个 job。
function effect(fn, options = EMPTY_OBJ) {
    if (isEffect(fn)) {
        fn = fn.raw;
    }
    /* 
        若 options 配置了 scheduler 方法,
        响应式对象属性值变更引起 job 执行时会执行 scheduler(job),
        否则,直接执行 job 函数(也就是执行fn)
    */
    const effect = createReactiveEffect(fn, options);
    /*
        生成 render job 时,lazy 为 false,立即执行 render job
    */
    if (!options.lazy) {
        effect();
    }
    return effect;
}

function createReactiveEffect(fn, options) {
    // 为了方便称呼这个 effect 函数为 job
    const effect = function reactiveEffect() {
        if (!effect.active) {
            return options.scheduler ? undefined : fn();
        }
        
        /* 
           假如
        */
        if (!effectStack.includes(effect)) {
            /* 
                清除 effect 对响应式对象属性值的依赖
                因为 fn 每次执行时,都会和响应式对象重新建立依赖
                所以在执行前,要将已存在的依赖关系清除
            */
            cleanup(effect);
            try {
                enableTracking();
                effectStack.push(effect);
                // 使用全局变量存储当前 job,在执行 fn 时,关联响应式对象会需要
                activeEffect = effect;
                return fn();
            }
            finally {
                effectStack.pop();
                resetTracking();
                activeEffect = effectStack[effectStack.length - 1];
            }
        }
    };
    effect.id = uid++;
    effect.allowRecurse = !!options.allowRecurse;
    effect._isEffect = true;
    effect.active = true;
    effect.raw = fn;
    effect.deps = [];
    /* 
        1、若 options 有 scheduler 方法,
           则响应式对象引起 job 执行阶段,会执行 scheduler(job),若无,执行 job。
        2、举个例子:render job 配置 scheduler 为 queueJob,将渲染任务放在微任务中异步执行。
    */
    effect.options = options;
    return effect;
}

render job

instance.update = effect(function componentEffect() {
    render...
}, {
     scheduler: queueJob
})

computed job

this.effect = effect(getter, {
    lazy: true,
    scheduler: () => {
        if (!this._dirty) {
            this._dirty = true;
            // 触发依赖 value 值的 job
            trigger$1(toRaw(this), "set" /* SET */, 'value');
        }
    }
});

watch job

const runner = effect(getter, {
    lazy: true,
    scheduler = () => {
        // 组件渲染阶段触发同步执行,组件更新阶段触发异步执行
        if (!instance || instance.isMounted) {
            queuePreFlushCb(job);
        }
        else {
            // with 'pre' option, the first call must happen before
            // the component is mounted so it is called synchronously.
            job();
        }
   }    
});

什么是响应式对象

举个栗子

  • data 方法创建的对象
  • comouted 使用 ComputedRefImpl 类创建的一个对象 o,其只有一个属性值 value(注:vue 又手动在 this 上定义一个与 fn 同名的属性,其 get 函数 为 () => o.value, set 函数为 (v) => o.value = v )
  • props 所有传入的 prop 值组成一个响应式对象

响应式对象属性值变更,会触发依赖该属性值的 job。

job 如何关联 响应式对象?

响应式对象属性值变更会触发 job 执行

总结如下两点:

  • 在 job 执行阶段,主动获取响应式对象属性时,使用 track 建立响应式对象属性和当前 job 的依赖。
  • 在响应式对象属性值变更时,使用 trigger$1 根据已有的映射,触发依赖改属性值得 job。

track

track(target, type, key) { ... }

track 设置依赖响应式对象 target 的 key 属性变更的 job,存于 targetMap。

  • targetMap 是一个 weakMap,键值为响应式对象 target,值为 depsMap
  • depsMap 是一个 weakMap, 键值为响应式对象的某键值 key,值为依赖该键值得 job 集合

怎么知道当前 job 呢?上文有介绍,在 job 执行阶段,会设置全局变量 activeEffect 为当前job。

track 什么时候执行呢?比如 render job 渲染模版阶段,获取 proxy(data) 的 test 属性,触发 get 劫持函数 fn,在 fn 中判断当前存在 activeEffect,即执行 track 函数绑定依赖。

trigger

trigger$1(target, type, key, newValue, oldValue, oldTarget){ ... }

trigger 会根据 targetMap 找出要执行的 job,若 job 无特殊处理,直接执行。若有配置 scheduler ,执行scheduler(effect)

举个栗子

<template>
    <div>{{ test }} {{ cTest }} </div>
</tempalte>

export default {
    data() {
        return {
            test: '姑且称 data 为响应式对象'
        }
    },
    computed() {
        cTest() {
            return this.test
        }
    }
}

Vue 使用 proxy 劫持 data,生成 proxy data

const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
target 为要监听的对象,普通对象使用 baseHandlers,集合使用 collectionHandlers

渲染模时,获取 data 属性,会触发 baseHandlers 的 get 函数,get 函数会触发 track(target, "get" , key);

redner job

render job 即组件初始化后的渲染流程,首次挂载立即执行,或者在响应式对象属性变更时执行。

render job 渲染模版阶段,获取 proxy data 属性值时,触发 proxy data 的 get 方法这中,使用 track 建立依赖。在 proxy data 的 set 方法中,使用 trigger 触发依赖。

computed job

class ComputedRefImpl {
    constructor(getter, _setter, isReadonly) {
        this._setter = _setter;
        this._dirty = true;
        this.__v_isRef = true;
        this.effect = effect(getter, {
            lazy: true,
            scheduler: () => {
                if (!this._dirty) {
                    this._dirty = true;
                    trigger$1(toRaw(this), "set" /* SET */, 'value');
                }
            }
        });
        this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
    }
    get value() {
        // the computed ref may get wrapped by other proxies e.g. readonly() #3376
        const self = toRaw(this);
        if (self._dirty) {
            self._value = this.effect();
            self._dirty = false;
        }
        track(self, "get" /* GET */, 'value');
        return self._value;
    }
    set value(newValue) {
        this._setter(newValue);
    }
}

Object.defineProperty(ctx, key, {
    enumerable: true,
    configurable: true,
    get: () => c.value,
    set: v => (c.value = v)
});

computed 有些复杂,以上面的 cTest 为栗子

render job 阶段,获取 cTest,触发 cTest job,cTest job 执行阶段,使用了 data 中的 test,建立了 test 的 cTest job 的依赖关系,cTest job 执行完后,又主动建立了 cTest 计算值和 render job 的依赖关系。

当 test 变更时,触发 cTest job,执行 cTest job 的 scheduler,又主动触发 render job,render job 又重新计算 cTest 的值,也就是执行 ComputedRefImpl 中的 get 方法