Vue3 响应式reactive和effect 的实现原理

89 阅读15分钟

Vue的响应式系统让人着迷,在Vue2时期使用的是Object.defineProperty,而到了Vue3则变成了Proxy。既然我们想要了解Vue3的响应式原理,就要先明白什么是响应式。

什么是响应式

首先要明白,响应式是一个过程,它有两个参与方:触发者:数据;响应者:引用的数据函数。

当数据发生改变时,引用数据的函数就会自动重新执行。例如:在进行视图渲染时使用了数据,数据发生了改变,视图也会自动更新,这就完成了一个响应的过程。

f25fb8b34a87bb9d4d57e59221cb7fff.png 这张图详细的展示了Vue3中响应式是如何工作的。

Proxy和Reflect

在了解Vue3响应式原理之前,还需要了解Proxy和Reflect

Proxy

顾名思义,Proxy主要是为对象创建一个代理,从而实现对对象基本操作的拦截和自定义。简单来说就是在目标对象前设置一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。因此提供了一种机制,可以对外界的访问进行过滤和改写。基本语法:

let proxy = new Proxy (target,handler);
  • target:需要拦截的对象。
  • handler:也是一个对象,来定制拦截行为。举个栗子:
const obj = {
    name: 'John',
    age: 16
}

const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
// 输出
[Log] obj.age20
[Log] objProxy.age20 
[Log] obj与objProxy是否相等 – false

这里的 objProxy 的 handler 为空,则直接指向代理对象,并且代理对象和数据源对象不完全相等。如果想要更加灵活的拦截对象的操作,就需要在 handler 中添加相应的属性。比如这样:

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        target[key] = value
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return delete target[key]
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

// 输出
[Log] 获取对象属性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 设置对象属性age值 (example01.html, line 26)
[Log] 删除对象属性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)

在上面的代码中,我们在捕获器中定义了set()、get()、deleteProperty()属性,通过对 proxy 的操作实现了对 obj 的操作拦截。其中出现了多种属性,我们来一一看下:

  • target:是目标对象,该对象会作为第一个参数传递给 new Proxy。
  • key:目标属性名称。
  • value:目标属性的值。
  • receiver:指向当前操作 正确的上下文。如果目标属性是一个 getter 访问器属性。那 receiver 就是本次读取属性所指向的 this 对象。通常,receiver 是 Proxy 对象本身,但如果我们从 proxy 继承,那么 receiver 指的是 proxy 继承的对象。
  • has():拦截 in操作符。
  • ownKeys():拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy) Object.keys(proxy)。 construct():拦截 new 操作等。

Reflect

反射,就是将代理的内容反射出去。Reflectproxy 一样,都是 ES6 为了操作对象而提供的新 API 。它提供了拦截 JavaScript 操作的方法。这些方法和 Proxy handlers 提供的方法是一一对应的,也就是说,只要是 Proxy 对象上的方法,就能在 Reflect 对象上找到相应的方法。而且 Reflect 不是函数对象,也就意味着不能实例化。所有的属性和方法都是静态的。

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return Reflect.deleteProperty(target, key)
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

这个例子中 Reflect.get() 代替 target[key]Reflect.set() 代替 target[key]=valueReflect.deleteProperty() 代替 delete target[key]

Reactive 和 Effect

在了解了 Proxy 和 Reflect 之后,再来看下Vue3是如何通过 proxy 来实现响应式的。

Reactive

Reactive是Vue实现响应式的方法之一,根据官网的介绍,有以下特点:

  1. 接受一个普通对象,返回一个响应式的代理对象。
  2. 响应式对象是深层的,会影响对象内部所有嵌套的属性。
  3. 自动对ref对象解包。
  4. 对于数组、对象、Map、Set等原生类型的元素,如果是ref对象不会自动解包。
  5. 返回的对象会通过Proxy进行包装,所以返回的对象不是原始对象。

Reactive 的源码在packages/reactivity/src/reactive.ts中 ,但在这里我们只看编译成js的代码。

function reactive(target) {
    // 如果对只读的代理对象进行再次代理,那么应该返回原始的只读代理对象
    if (isReadonly(target)) {
        return target;
    }
    
    // 通过 createReactiveObject 方法创建响应式对象
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

Reactive 的源码比较简单,就是调用了 createReactiveObject 方法,这是个工厂方法,用来创建响应式对象的,再来看下这个方法的源码:

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 如果 target 不是对象,那么直接返回 target
    if (!isObject(target)) {
        {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    
    // 如果 target 已经是一个代理对象了,那么直接返回 target
    // 异常:如果对一个响应式对象调用 readonly() 方法
    if (target["__v_raw" /* ReactiveFlags.RAW */] &&
        !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
        return target;
    }
    
    // 如果 target 已经有对应的代理对象了,那么直接返回代理对象
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    
    // 对于不能被观察的类型,直接返回 target
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    
    // 创建一个响应式对象
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    
    // 将 target 和 proxy 保存到 proxyMap 中
    proxyMap.set(target, proxy);
    
    // 返回 proxy
    return proxy;
}

这个方法的代码也很简单,最开始是对需要代理的 target 进行判断,判断标准都是 target不是对象和target已经是一个代理对象。

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 对于不能被观察的类型,直接返回 target
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    
    // 创建一个响应式对象
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    
    // 将 target 和 proxy 保存到 proxyMap 中
    proxyMap.set(target, proxy);
    
    // 返回 proxy
    return proxy;
}

这几行代码是核心代码,这里涉及一个targetType的判断,那这个targetType又是什么?让我们来看下getTargetType方法的源码:

// 获取原始数据类型
const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};

// 获取数据类型
function targetTypeMap(rawType) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return 1 /* TargetType.COMMON */;
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return 2 /* TargetType.COLLECTION */;
        default:
            return 0 /* TargetType.INVALID */;
    }
}

// 获取 target 的类型
function getTargetType(value) {
    return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
        ? 0 /* TargetType.INVALID */
        : targetTypeMap(toRawType(value));
}

这里是枚举出原始数据类型,它的返回值是:

const enum TargetType {
    // 无效的数据类型,对应的值是 0,表示 Vue 不会对这种类型的数据进行响应式处理
    INVALID = 0,
    // 普通的数据类型,对应的值是 1,表示 Vue 会对这种类型的数据进行响应式处理
    COMMON = 1,
    // 集合类型,对应的值是 2,表示 Vue 会对这种类型的数据进行响应式处理
    COLLECTION = 2
}

export const enum ReactiveFlags {
    // 用于标识一个对象是否不可被转为代理对象,对应的值是 __v_skip
    SKIP = '__v_skip',
    // 用于标识一个对象是否是响应式的代理,对应的值是 __v_isReactive
    IS_REACTIVE = '__v_isReactive',
    // 用于标识一个对象是否是只读的代理,对应的值是 __v_isReadonly
    IS_READONLY = '__v_isReadonly',
    // 用于标识一个对象是否是浅层代理,对应的值是 __v_isShallow
    IS_SHALLOW = '__v_isShallow',
    // 用于保存原始对象的 key,对应的值是 __v_raw
    RAW = '__v_raw'
}

这里将枚举值和含义都列出来了,把注释和源码结合起来理解,会更容易理解源码。

collectionHandlers和baseHandlers

createReactiveObject方法是返回一个代理对象。而关键点是这个代理对象的handler,而这个handler是由collectionHandlersbaseHandlers这两个对象组成。

在源码中通过targetType来判断使用哪种handler,当targetType的值为2时使用collectionHandlers,否则使用baseHandlers

其实targetType只有三个值,走到代理的也只有两种情况:

  • targetType为1时,target是一个普通对象或数组,这是用baseHandlers
  • targetType为2时,target是一个集合类型,这时用collectionHandlers

这两个handler都是由外部传入的,也就是createReactiveObject方法的第三和第四参数,而传入这两个参数的地方就是reactive方法:

function reactive(target) {
    // ...
    
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

可以看出,mutableHandlersmutableCollectionHandlers分别对应baseHandlerscollectionHandlers

baseHandlers

mutableHandlersbaseHandlers的一个export。来看以下代码:

const mutableHandlers = {
    get: get$1,
    set: set$1,
    deleteProperty,
    has: has$1,
    ownKeys
};

这里设置了几个拦截器,我们来分别介绍下作用:

  • get:拦截getter操作,如obj.name。
  • set:拦截setter操作,如obj.name="张三"。
  • deleteProperty:拦截delete操作。
  • has:拦截in操作。
  • ownKeys:拦截Object.getOwnPropertyNamesObject.getOwnPropertySymbolsObject.keys等操作。 接下来详细介绍每个拦截器。
get
function get(target, key, receiver) {
    // 如果访问的是 __v_isReactive 属性,那么返回 isReadonly 的取反值
    if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
        return !isReadonly;
    }
    
    // 如果访问的是 __v_isReadonly 属性,那么返回 isReadonly 的值
    else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
        return isReadonly;
    }
    
    // 如果访问的是 __v_isShallow 属性,那么返回 shallow 的值
    else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
        return shallow;
    }
    
    // 如果访问的是 __v_raw 属性,并且有一堆条件满足,那么返回 target
    else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
        receiver ===
        (isReadonly
            ? shallow
                ? shallowReadonlyMap
                : readonlyMap
            : shallow
                ? shallowReactiveMap
                : reactiveMap).get(target)) {
        return target;
    }
   
    // ...
};

这段代码是来处理一些特殊的属性,这都是Vue内定义好的,也就是上文提到的枚举值。来判断reactive、readonly、shallow等。

接下来的这些代码才是至关重要的。

首先是对数组的访问处理:

function get(target, key, receiver) {
    // ...
    
    // target 是否是数组
    const targetIsArray = isArray(target);
    
    // 如果不是只读的
    if (!isReadonly) {
        // 如果是数组,并且访问的是数组的一些方法,那么返回对应的方法
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
        
        // 如果访问的是 hasOwnProperty 方法,那么返回 hasOwnProperty 方法
        if (key === 'hasOwnProperty') {
            return hasOwnProperty;
        }
    }
    
    // ...
};

这段代码是一些处理数组的方法,比如push、pop等。当调用这些方法时,就会执行这段代码并返回对应方法。例如:

const arr = reactive([1, 2, 3]);
arr.push(4);

在获取返回值:

function get(target, key, receiver) {
    // ...
    
    // 获取 target 的 key 属性值
    const res = Reflect.get(target, key, receiver);
    
    // ...
};

到这一步,就需要target的key属性值,这里使用了Reflect.get。这是ES6新增的访问对象属性的方法。和target[key]等价,但Reflect.get可以传入receiver。

对特殊属性不进行依赖收集:

function get(target, key, receiver) {
    // ...
    
    // 如果是内置的 Symbol,或者是不可追踪的 key,那么直接返回 res
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
        return res;
    }
    
    // ...
};

这里是过滤掉一些内置属性,因为它们不会改变就不需要收集。

依赖收集:

function get(target, key, receiver) {
    // ...
    
    // 如果不是只读的,那么进行依赖收集
    if (!isReadonly) {
        track(target, "get" /* TrackOpTypes.GET */, key);
    }
    
    // ...
};

这里调用了track方法来收集依赖。

浅的不进行递归:

function get(target, key, receiver) {
    // ...
    
    // 如果是浅的,那么直接返回 res
    if (shallow) {
        return res;
    }
    
    // ...
};

这一步是为了处理shallow的情况,如果是shallow,就不进行递归代理,直接返回res。

对返回值解包:

function get(target, key, receiver) {
    // ...
    
    // 如果 res 是 ref,对返回的值进行解包
    if (isRef(res)) {
        // 对于数组和整数类型的 key,不进行解包
        return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    
    // ...
};

这里是处理ref的情况,如果res是ref,那就对res进行解包。这里还有一个判断,如果是数组,且key是整数类型,就不进行解包。

代理返回值:

function get(target, key, receiver) {
    // ...
    
    // 如果 res 是对象,那么对返回的值进行代理
    if (isObject(res)) {
        return isReadonly ? readonly(res) : reactive(res);
    }
    
    // ...
};

这里添加了一个判断,如果是readonly,就用readonly方法,否则就使用reactive方法来代理。

以上就是对get方法的拆分讲解,接下来再看下set方法。

set

set方法比get方法实现起来比较复杂,但代码相对较少,拆分一下有以下几步。

获取旧值:

function set(target, key, value, receiver) {
    // 获取旧值
    let oldValue = target[key];
    
    // ...
};

这里的旧值指的是target[ket]的值。

判断是否只读:

function set(target, key, value, receiver) {
    // ...
    
    // 如果旧值是只读的,并且是 ref,并且新值不是 ref,那么直接返回 false,代表设置失败
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
        return false;
    }
    
    // ...
};

如果只读的话ref的值是不能修改的,所以直接返回false。

判断是否是浅的:

function set(target, key, value, receiver) {
    // ...
    
    // 如果不是浅的
    if (!shallow) {
        // ...
    }
    
    // ...
};

在判断是否是浅的时,参数是通过闭包下来的。如果不是浅层响应,那么就会获取旧值的原始值和新值的原始值:

function set(target, key, value, receiver) {
    // ...
    
    // 如果不是浅的
    if (!shallow) {
        // 如果新值不是浅的,并且不是只读的
        if (!isShallow(value) && !isReadonly(value)) {
            // 获取旧值的原始值
            oldValue = toRaw(oldValue);
            // 获取新值的原始值
            value = toRaw(value);
        }
    }
    
    // ...
};

在这里需要先判断新值是不是浅层响应的且不是只读。如果是,就不需要获取原始值,因为这时的新值就是原始值。

如果是浅层响应的,那就说明这个响应式对象的元素只有一层响应式,只关心当前对象的响应式。所以不用获取原始值,会直接覆盖。

deleteProperty
function deleteProperty(target, key) {
    // 当前对象是否有这个 key
    const hadKey = hasOwn(target, key);
    
    // 旧值
    const oldValue = target[key];
    
    // 通过 Reflect.deleteProperty 删除属性
    const result = Reflect.deleteProperty(target, key);
    
    // 如果删除成功,并且当前对象有这个 key,那么就触发 delete 事件
    if (result && hadKey) {
        trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
    }
    
    // 返回结果,这个结果为 boolean 类型,代表是否删除成功
    return result;
}

deleteProperty方法就是通过Reflect.deleteProperty来删除属性,再通过trigger触发delete事件,最后返回是否删除成功。

has
function has$1(target, key) {
    // 通过 Reflect.has 判断当前对象是否有这个 key
    const result = Reflect.has(target, key);
    
    // 如果当前对象不是 Symbol 类型,或者当前对象不是内置的 Symbol 类型,那么就触发 has 事件
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, "has" /* TrackOpTypes.HAS */, key);
    }
    
    // 返回结果,这个结果为 boolean 类型,代表当前对象是否有这个 key
    return result;
}

has直接通过Reflect.has判断当前对象是否有key,然后通过track触发has事件,最后返回是否有key的结果。

ownKeys
function ownKeys(target) {
    // 直接触发 iterate 事件
    track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    
    // 通过 Reflect.ownKeys 获取当前对象的所有 key
    return Reflect.ownKeys(target);
}

ownKeys方法的实现也是比较简单的,直接触发iterate事件,然后通过Reflect.ownKeys获取当前对象的所有 key,最后返回这些 key;

注意点在于对数组的特殊处理,如果当前对象是数组的话,那么就会触发lengthiterate事件,如果不是数组的话,那么就会触发ITERATE_KEYiterate事件。

Effect

在了解了reactive方法后,再来看下effect方法。effect实际上就是创建一个副作用函数,这个函数会在依赖的数据发生变化时执行。来看下它是怎么实现的:

function effect(fn, options) {
    // 如果 fn 对象上有 effect 属性
    if (fn.effect) {
        // 那么就将 fn 替换为 fn.effect.fn
        fn = fn.effect.fn;
    }
    
    // 创建一个响应式副作用函数
    const _effect = new ReactiveEffect(fn);
    
    // 如果有配置项
    if (options) {
        // 将配置项合并到响应式副作用函数上
        extend(_effect, options);
        
        // 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)
        if (options.scope)
            // 那么就将 scope 属性记录到响应式副作用函数上(类似一个作用域链)
            recordEffectScope(_effect, options.scope);
    }
    
    // 如果没有配置项,或者配置项中没有 lazy 属性,或者配置项中的 lazy 属性为 false
    if (!options || !options.lazy) {
        // 那么就执行响应式副作用函数
        _effect.run();
    }
    
    // 将 _effect.run 的 this 指向 _effect
    const runner = _effect.run.bind(_effect);
    
    // 将响应式副作用函数赋值给 runner.effect
    runner.effect = _effect;
    
    // 返回 runner
    return runner;
}

在这段代码中有两个重点:

  1. 创建一个响应式副作用函数const_effect=new ReactiveEffect(fn)
  2. 返回一个runner函数,可以通过这个函数来执行副作用函数。

收集依赖

在了解了reactiveeffect之后,再来看下收集依赖。

在学习响应式系统时经常能听到在getter中收集依赖,在setter中触发依赖。现在看下如何收集依赖。

track

getter中有这样一段代码:

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        // ...
        
        // 如果不是只读的,就会收集依赖
        if (!isReadonly) {
            track(target, "get" /* TrackOpTypes.GET */, key);
        }
        
        // ...
        
        return res;
    };
}

这里调用了track方法来收集依赖,该方法实现如下:

const targetMap = new WeakMap();

/**
 * 收集依赖
 * @param target 指向的对象
 * @param type 操作类型
 * @param key 指向对象的 key
 */
function track(target, type, key) {
    // 如果 shouldTrack 为 false,并且 activeEffect 没有值的话,就不会收集依赖
    if (shouldTrack && activeEffect) {
        
        // 如果 targetMap 中没有 target,就会创建一个 Map
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        
        // 如果 depsMap 中没有 key,就会创建一个 Set
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = createDep()));
        }
        
        // 将当前的 ReactiveEffect 对象添加到 dep 中
        const eventInfo = {
            effect: activeEffect,
            target,
            type,
            key
        };
        
        // 如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去
        trackEffects(dep, eventInfo);
    }
}

这里targetMap是一个weakMap,它的键是target,值是Map,而这个Map的键是key,值是Set

这里使用weakMap是因为它是一个弱引用的键值对集合,这意味着如果一个对象作为weakMap的键,且没有其他引用指向该对象,那么这个对象可以被垃圾回收机制自动回收。可以有效的提升性能,释放内存。

这里还有两个熟人:shouldTrackactiveEffectshouldTrack是用来控制是否收集依赖的;activeEffect指向当前正在执行的副作用函数。

track方法是利用targetMap记录下targetkey。这意味着在操作targetkey时,就会收集依赖,targetkey就会被记录到targetMap中:

const obj = {
    a: 1,
    b: 2
};

const targetMap = new WeakMap();

// 我在操作 obj.a 的时候,就会收集依赖
obj.a;

// 这个时候,targetMap 中就会记录下 obj 和 a
let depsMap = targetMap.get(obj);
if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
}

// createDep 实现很简单,就不在讲解的代码里面单独写出来了,具体就是一个 Set,多了两个属性,w 和 n
const createDep = (effects) => {
    const dep = new Set(effects);
    dep.w = 0; // 指向的是 watcher 对象的唯一标识
    dep.n = 0; // 指向的是不同的 dep 的唯一标识
    return dep;
};


let dep = depsMap.get("a");
if (!dep) {
    depsMap.set("a", (dep = createDep()));
}

// dep 就是一个 Set,里面存放的就是当前的 ReactiveEffect 对象
dep.add(activeEffect);

trigger

trigger方法也很简单,就是触发依赖,实现如下:

/**
 * 触发依赖
 * @param target 指向的对象
 * @param type 操作类型
 * @param key 指向对象的 key
 * @param newValue 新值
 * @param oldValue 旧值
 * @param oldTarget 旧的 target
 */
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    // 获取 targetMap 中的 depsMap
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        // never been tracked
        return;
    }
    
    // 创建一个数组,用来存放需要执行的 ReactiveEffect 对象
    let deps = [];
    
    // 如果 type 为 clear,就会将 depsMap 中的所有 ReactiveEffect 对象都添加到 deps 中
    if (type === "clear" /* TriggerOpTypes.CLEAR */) {
        // 执行所有的 副作用函数
        deps = [...depsMap.values()];
    }
    
    // 如果 key 为 length ,并且 target 是一个数组
    else if (key === 'length' && isArray(target)) {
        // 修改数组的长度,会导致数组的索引发生变化
        // 但是只有两种情况,一种是数组的长度变大,一种是数组的长度变小
        // 如果数组的长度变大,那么执行所有的副作用函数就可以了
        // 如果数组的长度变小,那么就需要执行索引大于等于新数组长度的副作用函数
        const newLength = Number(newValue);
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= newLength) {
                deps.push(dep);
            }
        });
    }
    
    // 其他情况
    else {
        // key 不是 undefined,就会将 depsMap 中 key 对应的 ReactiveEffect 对象添加到 deps 中
        // void 0 就是 undefined
        if (key !== void 0) {
            deps.push(depsMap.get(key));
        }
        
        
        // 执行 add、delete、set 操作时,就会触发的依赖变更
        switch (type) {
            // 如果 type 为 add,就会触发的依赖变更
            case "add" /* TriggerOpTypes.ADD */:
                // 如果 target 不是数组,就会触发迭代器
                if (!isArray(target)) {
                    // ITERATE_KEY 再上面介绍过,用来标识迭代属性
                    // 例如:for...in、for...of,这个时候依赖会收集到 ITERATE_KEY 上
                    // 而不是收集到具体的 key 上
                    deps.push(depsMap.get(ITERATE_KEY));
                    
                    // 如果 target 是一个 Map,就会触发 MAP_KEY_ITERATE_KEY
                    if (isMap(target)) {
                        // MAP_KEY_ITERATE_KEY 同上面的 ITERATE_KEY 一样
                        // 不同的是,它是用来标识 Map 的迭代器
                        // 例如:Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries()
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                
                // 如果 key 是一个数字,就会触发 length 依赖
                else if (isIntegerKey(key)) {
                    // 因为数组的索引是可以通过 arr[0] 这种方式来访问的
                    // 也可以通过这种方式来修改数组的值,所以会触发 length 依赖
                    deps.push(depsMap.get('length'));
                }
                break;
                
            // 如果 type 为 delete,就会触发的依赖变更
            case "delete" /* TriggerOpTypes.DELETE */:
                // 如果 target 不是数组,就会触发迭代器,同上面的 add 操作
                if (!isArray(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                    if (isMap(target)) {
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                break;
                
            // 如果 type 为 set,就会触发的依赖变更
            case "set" /* TriggerOpTypes.SET */:
                // 如果 target 是一个 Map,就会触发迭代器,同上面的 add 操作
                if (isMap(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                }
                break;
        }
    }
    
    // 创建一个 eventInfo 对象,主要是调试的时候会用到
    const eventInfo = {
        target,
        type,
        key,
        newValue,
        oldValue,
        oldTarget
    };
    
    // 如果 deps 的长度为 1,就会直接执行
    if (deps.length === 1) {
        if (deps[0]) {
            {
                triggerEffects(deps[0], eventInfo);
            }
        }
    }
    else {
        // 如果 deps 的长度大于 1,这个时候会组装成一个数组,然后再执行
        // 这个时候调用就类似一个调用栈
        const effects = [];
        for (const dep of deps) {
            if (dep) {
                effects.push(...dep);
            }
        }
        {
            triggerEffects(createDep(effects), eventInfo);
        }
    }
}

当我们修改数据时,就会触发依赖,再执行依赖中的副作用函数。在这里主要是收集需要执行的副作用函数,再到triggerEffects函数中执行。

来看下triggerEffects

function triggerEffects(dep, debuggerEventExtraInfo) {
    // 如果 dep 不是数组,就会将 dep 转换成数组,因为这里的 dep 可能是一个 Set 对象
    const effects = isArray(dep) ? dep : [...dep];
    
    // 执行 computed 依赖
    for (const effect of effects) {
        if (effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
    
    // 执行其他依赖
    for (const effect of effects) {
        if (!effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
}

这里只是转换dep,再执行computed依赖和其他依赖,主要是看triggerEffect函数:

function triggerEffect(effect, debuggerEventExtraInfo) {
    // 如果 effect 不是 activeEffect,或者 effect 允许递归,就会执行
    if (effect !== activeEffect || effect.allowRecurse) {
        
        // 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行
        if (effect.onTrigger) {
            effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
        }
        
        // 如果 effect 是一个调度器,就会执行 scheduler
        if (effect.scheduler) {
            effect.scheduler();
        }
        
        // 否则直接执行 effect.run()
        else {
            effect.run();
        }
    }
}

这里的effect.scheduler是一个调度器,允许用户自定义调用副作用函数的实际。effect.run就是调用副作用函数。

总结

讲了这么多,实际上响应式原理就四步:

  1. 创建一个响应式对象。
  2. 创建一个副作用函数。
  3. 访问响应式对象,触发依赖收集。
  4. 修改响应式对象,出发依赖执行。

这四步都是围绕effect函数,reactive函数,track函数,trigger函数这四个函数。

这样的设计让整个响应式系统变得简单且容易维护。