【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析

1,032 阅读12分钟

Vue的响应式系统很让人着迷,Vue2使用的是Object.definePropertyVue3使用的是Proxy,这个是大家都知道的技术点;

但是知道了这些个技术点就能写出一个响应式系统吗?答案是肯定是NOVue的响应式系统是一个非常复杂的系统,技术只是实现的手段,今天我们就来看看背后实现的思想。

本章内容有点多,如果不能耐着性子看的话,建议先看看我在线实现的小 demo 去理解核心思想,不明白再来在文章中寻找答案:codesandbox.io/s/magical-k…

reactive 和 effect

Vue3的响应式系统通过官网的API可以看到有很多,例如refcomputedreactivereadonlywatchEffectwatch等等,这些都是Vue3的响应式系统的一部分;

reactive

reactive根据官网的介绍,有如下特点:

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

上面的这些特点都是可以在官网中有介绍,如果我说的不是很好理解建议去官网看看,官网对这些特点都有详细的介绍,并且还有示例代码。

对于reactive的作用其实使用Vue3的同学都知道是干嘛的,就不多说了。

effect

effect在官网上是没有提到这个API的,但是在源码中是有的,并且我们也是可以直接使用,如下代码所示:

import { reactive, effect } from "vue";

const data = reactive({
  foo: 1,
  bar: 2
});

effect(() => {
  console.log(data.foo);
});

data.foo = 10;

通常情况下我们是不会直接使用effect的,因为effect是一个底层的API,在我们使用Vue3的时候Vue默认会帮我们调用effect,所以我们的关注点通常都是在reactive上。

但是reactive需要和effect配合使用才会有响应式的效果,所以我们需要了解一下effect的作用。

effect直接翻译为作用,意思是使其发生作用,这个使其就是我们传入的函数,所以effect的作用就是让我们传入的函数发生作用,也就是执行这个函数。

但是effect是怎么知道我们传入的函数需要执行呢?这些答案都在源码中,现在来进入正式的源码阅读环节。

源码

Vue的响应式系统的源码在packages/reactivity目录下,Vue3将其单独抽离出来为一个独立的系统,我们可以看看这个工程的README文件;

根据README文件中的介绍,响应系统被内联到面向用户的生产和开发构建的包中,但是也可以单独使用。

如果你想单独使用的话,建议不要和Vue混合使用,因为独立使用的话,和Vue的响应式系统内部的数据并不互通,这样就会有两个响应式系统发挥作用,这样可能会有产生一些不可预知的问题。

响应式系统出了对ArrayMapWeakMapSetWeakSet这些原生类型进行了响应式处理,对其他的原生类型,例如DateRegExpError等等,都没有进行响应式处理。

reactive

reactive的源码在packages/reactivity/src/reactive.ts文件中,还是老样子,我们不看原始的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;
}

createReactiveObject方法的源码也很简单,最开始的一些代码都是对需要代理的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));
}

这里主要看的是Vue写的代码注释,这里的注释是Vuets源码中的枚举类型,最后返回的值枚举类型的值:

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来判断使用哪个handlertargetType2的时候使用collectionHandlers,否则使用baseHandlers

其实这个targetType根据枚举值也就只有3个值,最后走向代理的也就只有两种情况:

  • targetType1的时候,这个时候target是一个普通的对象或者数组,这个时候使用baseHandlers
  • targetType2的时候,这个时候target是一个集合类型,这个时候使用collectionHandlers

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

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

可以看到的是mutableHandlersmutableCollectionHandlers分别对应baseHandlerscollectionHandlers

而这两个handler的定义在reactivity/src/baseHandlers.tsreactivity/src/collectionHandlers.ts中;

感兴趣的可以去翻看一下这两个文件的源码,这里还是贴出打包之后的代码,先从baseHandlers开始;

baseHandlers

注意这里的baseHandlers指向的是mutableHandlersmutableHandlersbaseHandlers的一个export

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

这里分别定义了getsetdeletePropertyhasownKeys这几个方法拦截器,简单介绍一下作用:

  • get:拦截对象的getter操作,比如obj.name
  • set:拦截对象的setter操作,比如obj.name = '田八'
  • deleteProperty:拦截delete操作,比如delete obj.name
  • has:拦截in操作,比如'name' in obj
  • ownKeys:拦截Object.getOwnPropertyNamesObject.getOwnPropertySymbolsObject.keys等操作;

更具体的可以看看MDN的介绍;

再来看看这些个拦截器的具体实现。

get
const get$1 = /*#__PURE__*/ createGetter();
function createGetter(isReadonly = false, shallow = false) {
    // 闭包返回 get 拦截器方法
    return 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;
        }
        
        // 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;
            }
        }
        
        // 获取 target 的 key 属性值
        const res = Reflect.get(target, key, receiver);
        
        // 如果是内置的 Symbol,或者是不可追踪的 key,那么直接返回 res
        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
            return res;
        }
        
        // 如果不是只读的,那么进行依赖收集
        if (!isReadonly) {
            track(target, "get" /* TrackOpTypes.GET */, key);
        }
        
        // 如果是浅的,那么直接返回 res
        if (shallow) {
            return res;
        }
        
        // 如果 res 是 ref,对返回的值进行解包
        if (isRef(res)) {
            // 对于数组和整数类型的 key,不进行解包
            return targetIsArray && isIntegerKey(key) ? res : res.value;
        }
        
        // 如果 res 是对象,递归代理
        if (isObject(res)) {
            // 将返回的值也转换为代理。我们在这里进行 isObject 检查,以避免无效的值警告。
            // 还需要延迟访问 readonly 和 reactive,以避免循环依赖。
            return isReadonly ? readonly(res) : reactive(res);
        }
        
        // 返回 res
        return res;
    };
}

稍微有点复杂,但是也不难理解,我来拆解一下:

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内部定义好的,就是上面提到过的枚举值,用于判断是否是reactivereadonlyshallow等等。

这一段代码对于我们理解源码并不重要,重要的是下面一段:

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;
        }
    }
    
    // 获取 target 的 key 属性值
    const res = Reflect.get(target, key, receiver);
    
    // 如果是内置的 Symbol,或者是不可追踪的 key,那么直接返回 res
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
        return res;
    }
    
    // 如果不是只读的,那么进行依赖收集
    if (!isReadonly) {
        track(target, "get" /* TrackOpTypes.GET */, key);
    }
    
    // 如果是浅的,那么直接返回 res
    if (shallow) {
        return res;
    }
    
    // 如果 res 是 ref,对返回的值进行解包
    if (isRef(res)) {
        // 对于数组和整数类型的 key,不进行解包
        return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    
    // 如果 res 是对象,递归代理
    if (isObject(res)) {
        // 将返回的值也转换为代理。我们在这里进行 isObject 检查,以避免无效的值警告。
        // 还需要延迟访问 readonly 和 reactive,以避免循环依赖。
        return isReadonly ? readonly(res) : reactive(res);
    }
    
    // 返回 res
    return res;
};

这一段还是太多了,但是其实每段代码都是为了完成一个独立的需求,我们再来拆解一下:

  • 对数组的方法访问处理
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;
        }
    }
    
    // ...
};

这一段代码是为了处理数组的一些方法,比如pushpop等等,如果我们在调用这些方法的时候,就会进入这一段代码,然后返回对应的方法,例如:

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

arr.push(4);

这些方法都在arrayInstrumentations中,这次不做重点分析,后面会专门讲解。

  • 获取返回值,返回值的特别对待
function get(target, key, receiver) {
    // ...
    
    // 获取 target 的 key 属性值
    const res = Reflect.get(target, key, receiver);
    
    // ...
};

走到这里,就需要获取targetkey属性值了,这里使用了Reflect.get

这个方法是ES6中新增的,用于访问对象的属性,和target[key]是等价的,但是Reflect.get可以传入receiver,这个参数是用来绑定this的;

这是为了解决Proxythis指向问题,这里不做过多的解释,后面会专门讲解,Reflect不了解的看:MDN Reflect

  • 特殊属性的不进行依赖收集
function get(target, key, receiver) {
    // ...
    
    // 如果是内置的 Symbol,或者是不可追踪的 key,那么直接返回 res
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
        return res;
    }
    
    // ...
};

这一步是为了过滤一些特殊的属性,例如原生的Symbol类型的属性,如:Symbol.iteratorSymbol.toStringTag等等,这些属性不需要进行依赖收集,因为它们是内置的,不会改变;

还有一些不可追踪的属性,如:__proto____v_isRef__isVue这些属性也不需要进行依赖收集;

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

这一步是为了进行依赖收集,这里调用了track方法,这个方法在effect中会用到,稍后会讲解;

  • 浅的不进行递归代理
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的情况,如果resref,那么就对res进行解包,这里有一个判断,如果是数组,并且key是整数类型,那么就不进行解包;

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

如果是对象,那么就对res进行代理,这里有一个判断,如果是readonly的,那么就使用readonly方法进行代理,否则就使用reactive方法进行代理;

最后就是返回res了,这里就是Vue3get方法的全部内容了,其实拆分下来就容易理解多了,下面我们来看看Vue3set方法;

set
const set$1 = /*#__PURE__*/ createSetter();

function createSetter(shallow = false) {
    // 闭包返回一个 set 方法
    return function set(target, key, value, receiver) {
        // 获取旧值
        let oldValue = target[key];

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

        // 如果不是浅的
        if (!shallow) {

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

            // 如果目标对象不是数组,并且旧值是 ref,并且新值不是 ref,那么设置旧值的 value 为新值,并且返回 true,代表设置成功
            // ref 的值是在 value 属性上的,这里判断了旧值的代理类型,所以设置到了旧值的 value 上
            if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                oldValue.value = value;
                return true;
            }
        }

        // 如果是数组,并且 key 是整数类型
        const hadKey = isArray(target) && isIntegerKey(key)
            // 如果 key 小于数组的长度,那么就是有这个 key
            ? Number(key) < target.length
            // 如果不是数组,那么就是普通对象,直接判断是否有这个 key
            : hasOwn(target, key);

        // 通过 Reflect.set 设置值
        const result = Reflect.set(target, key, value, receiver);

        // 如果目标对象是原始数据的原型链中的某个元素,则不会触发依赖收集
        if (target === toRaw(receiver)) {
            // 如果没有这个 key,那么就是新增了一个属性,触发 add 事件
            if (!hadKey) {
                trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
            }

            // 如果有这个 key,那么就是修改了一个属性,触发 set 事件
            else if (hasChanged(value, oldValue)) {
                trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
            }
        }

        // 返回结果,这个结果为 boolean 类型,代表是否设置成功
        // 只是代理相关,,和业务无关,必须要返回是否设置成功的结果
        return result;
    };
}

set方法的实现其实整体要比get方法的实现要复杂一些,虽然代码比get要少一些,不过整体梳理下来,大体分为下面几个步骤:

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

这里的旧值就是target[key]的值,旧值在Vue3中有很多用处,会贯穿整个流程,这里先不展开讲,后面会讲到;

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

只读的ref是不能被修改的,所以这里就直接返回false了,代表设置失败;

但是这里需要有很多的条件,首先旧值必须是只读的,其次旧值必须是ref,最后新值不能是ref,如下面的例子:

    const refObj = ref({
    a: 1,
    b: 2
});
const readonlyObj = readonly(refObj);

const obj = reactive({
    readonlyObj
})

obj.readonlyObj = 10;
console.log(obj.readonlyObj); // 设置失败

obj.readonlyObj = ref(10);
console.log(obj.readonlyObj); // 设置成功

很奇怪的判定,个人的知识储备量还不够,没想明白为什么要有这么样一个的判定才会设置失败。

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

在判断是否不是浅层响应的时候,这个参数是通过闭包保存下来的,不是浅层响应的时候,这个内部会做两件事情:

  1. 获取旧值的原始值和新值的原始值
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方法的实现对比getset方法的实现都要简单很多,也没有什么特别的地方,就是通过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事件;

这一块的区别都是在track方法中才会有体现,这个就是响应式的核心思路,后面会详细讲解;

effect

上面讲完了reactive方法,接下来就是effect方法,effect方法的作用是创建一个副作用函数,这个函数会在依赖的数据发生变化的时候执行;

依赖收集和触发更新的过程先不要着急,等讲完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

先来分析下ReactiveEffect这个类,这个类的作用是创建一个响应式副作用函数,这个函数会在依赖的数据发生变化的时候执行;

class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        // 副作用函数
        this.fn = fn;
        // 调度器,用于控制副作用函数何时执行
        this.scheduler = scheduler;
        // 标志位,用于标识当前 ReactiveEffect 对象是否处于活动状态
        this.active = true;
        // 响应式依赖项的集合
        this.deps = [];
        // 父级作用域
        this.parent = undefined;
        
        // 记录当前 ReactiveEffect 对象的作用域
        recordEffectScope(this, scope);
    }
    run() {
        // ...
    }
    stop() {
        // ...
    }
}

ReactiveEffect这个类的实现主要体现在两个方法上,一个是run方法,一个是stop方法;

其他的属性都是用来记录一些数据的,比如fn属性就是用来记录副作用函数的,scheduler属性就是用来记录调度器的,active属性就是用来记录当前ReactiveEffect对象是否处于活动状态的;

这些属性的具体作用将在下面的分析中讲解,先来看看run方法的实现;

run

function run() {
    // 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果
    if (!this.active) {
        return this.fn();
    }
    
    // 寻找当前 ReactiveEffect 对象的最顶层的父级作用域
    let parent = activeEffect;
    let lastShouldTrack = shouldTrack;
    while (parent) {
        if (parent === this) {
            return;
        }
        parent = parent.parent;
    }
    
    try {
        // 记录父级作用域为当前活动的 ReactiveEffect 对象
        this.parent = activeEffect;
        
        // 将当前活动的 ReactiveEffect 对象设置为 “自己”
        activeEffect = this;
        
        // 将 shouldTrack 设置为 true (表示是否需要收集依赖)
        shouldTrack = true;
        
        // effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
        trackOpBit = 1 << ++effectTrackDepth;
        
        // 这里是用于控制 "effect调用栈的深度" 在一个阈值之内
        if (effectTrackDepth <= maxMarkerBits) {
            // 初始依赖追踪标记
            initDepMarkers(this);
        }
        else {
            // 清除所有的依赖追踪标记
            cleanupEffect(this);
        }
        
        // 执行副作用函数,并返回执行结果
        return this.fn();
    }
    finally {
        // 如果 effect调用栈的深度 没有超过阈值
        if (effectTrackDepth <= maxMarkerBits) {
            // 确定最终的依赖追踪标记
            finalizeDepMarkers(this);
        }
        
        // 执行完毕会将 effectTrackDepth 减 1
        trackOpBit = 1 << --effectTrackDepth;
        
        // 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”
        activeEffect = this.parent;
        
        // 将 shouldTrack 设置为上一个值
        shouldTrack = lastShouldTrack;
        
        // 将父级作用域设置为 undefined
        this.parent = undefined;
        
        // 延时停止,这个标志是在 stop 方法中设置的
        if (this.deferStop) {
            this.stop();
        }
    }
}

整体梳理下来,run方法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖;

整体的流程还是非常复杂的,但是这里的核心思想是各种标识位的设置,以及在执行副作用函数的过程中,会收集依赖;

这里的流程没必要一下就全都了解,现在只需要记住下面这样的流程就可以了:

stop

function stop() {
    // 如果当前 活动的 ReactiveEffect 对象是 “自己”
    // 延迟停止,需要执行完当前的副作用函数之后再停止
    if (activeEffect === this) {
        // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
        this.deferStop = true;
    }
    
    // 如果当前 ReactiveEffect 对象处于活动状态
    else if (this.active) {
        // 清除所有的依赖追踪标记
        cleanupEffect(this);
        
        // 如果有 onStop 回调函数,就执行
        if (this.onStop) {
            this.onStop();
        }
        
        // 将 active 设置为 false
        this.active = false;
    }
}

stop方法的作用就是停止当前的ReactiveEffect对象,停止之后,就不会再收集依赖了;

这里的activeEffectthis并不是每次都相等的,因为activeEffect会跟着调用栈的深度而变化,而this则是固定的;

this.active标识的自身是否处在活动状态,因为嵌套的ReactiveEffect对象,activeEffect并不一定指向自己,而this.active则是自身的状态;

依赖收集

讲了reactiveeffect之后,我们就可以来讲讲依赖收集了;

上面讲了这么多,他们两个好像还没有联系起来,好像是相互独立的,而他们的联系的纽带就是activeEffect

常听人说响应式系统在getter中收集依赖,在setter中触发依赖,现在回头看看getter是怎么收集依赖的;

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

在这里我们发现了两个老熟人,一个是shouldTrack,一个是activeEffect,这两个变量都是在effect方法中出现过的;

shouldTrack在上面也讲过,它的作用就是控制是否收集依赖,暂时不用深入;

activeEffect就是我们刚刚讲的ReactiveEffect对象,它指向的就是当前正在执行的副作用函数;

track方法的作用就是收集依赖,它的实现非常简单,就是在targetMap中记录下targetkey

targetMap是一个WeakMap,它的键是target,值是一个Map,这个Map的键是key,值是一个Set

这意味着,如果我们在操作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);

上面就是一个收集依赖的过程,我们可以看到,targetMap中记录的是targetkey,而dep中记录的是ReactiveEffect对象;

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

tigger函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。

在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给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函数,就会发现这里的实现非常的巧妙。

这里的 effect.schedulereffect.run,在我们看effect函数的时候,就已经出现过了;

run就是调用副作用函数,scheduler是调度器,允许用户自定义调用副作用函数的时机。

还是因为这一篇写的太多了,所以这里就不展开了,后面会单独写一篇文章来讲解。

动手时间

上面讲了那么多,还不如自己动一下手来实现这一整套流程,这样才能更好的理解。

首先我们梳理一下整个流程:

  1. 创建一个响应式对象
  2. 创建一个副作用函数
  3. 访问响应式对象,触发依赖收集
  4. 修改响应式对象,触发依赖执行
// 1. 创建一个响应式对象
const state = reactive({
    name: '田八',
    age: 18
});

// 2. 创建一个副作用函数
effect(() => {
    // 3. 访问响应式对象,触发依赖收集
    console.log(state.name);
});

// 4. 修改响应式对象,触发依赖执行
state.age = 19;

创建一个响应式对象

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            // 依赖收集
            track(target, key);
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            const res = Reflect.set(target, key, value);
            // 依赖触发
            trigger(target, key);

            return res;
        }
    });
}

这里只做最简单的实现,所以没有做深度监听,只是简单的监听了一层,并且只有getset两个钩子,只对Object类型的数据做了监听。

创建一个副作用函数

let activeEffect = null;
function effect(fn) {
    const _effect = new ReactiveEffect(fn);
    _effect.run();
}

class ReactiveEffect {
    constructor(fn) {
        this.fn = fn;
        this.deps = [];
    }
    
    run() {
        activeEffect = this;
        this.fn();
        activeEffect = null;
    }
}

这里的ReactiveEffect类,主要是用来存储副作用函数的,然后在run函数中,将activeEffect设置为当前的ReactiveEffect实例,这样在track函数中,就可以拿到当前的ReactiveEffect实例。

依赖收集

const targetMap = new WeakMap();
function track(target, key) {
    if (activeEffect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set()));
        }
        if (!dep.has(activeEffect)) {
            dep.add(activeEffect);
            activeEffect.deps.push(dep);
        }
    }
}

这里的主流程和Vue3的源码是一样的,并没有做什么改动,确实是非常的巧妙。

依赖触发

function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => {
            effect.run();
        });
    }
}

这里简化了流程,直接遍历dep,然后执行effectrun函数。

在线地址:codesandbox.io/s/magical-k…

总结

这一篇文章,主要是讲解了Vue3的响应式原理,以及如何手动实现一个简单的响应式系统。

整个响应式系统的实现,主要是围绕的effect函数,reactive函数,track函数,trigger函数这四个函数。

每个函数都只做自己的事情,各司其职:

  • effect函数:创建一个副作用函数,主要的作用是来运行副作用函数
  • reactive函数:创建一个响应式对象,主要的作用是来监听对象的变化
  • track函数:依赖收集,主要收集的就是effect函数
  • trigger函数:依赖触发,主要的作用是来触发track函数收集的effect函数

这样的设计,让整个响应式系统的实现变得非常的简单,也让整个系统的可维护性变得非常的高。

这里的巧妙点在于依赖收集,当调用副作用函数时,副作用函数里面的响应式对象在调用时,会触发get钩子;

get中调用track函数收集activeEffect,这个时候activeEffect是一定存在的,并且activeEffect中的副作用函数是一定引用了这个响应式对象的,所以这个时候就可以将这个响应式对象和activeEffect关联起来。

将当前的对象作为key,将activeEffect作为value,存储到targetMap中,这样就完成了依赖收集。

在响应式对象的set钩子中,调用trigger函数,将targetMap中的activeEffect取出来,然后执行activeEffectrun函数,这样就完成了依赖触发。

今天就到了这里,如有不对的地方,欢迎大家指正。

大家好,这里是田八的【源码&库】系列,Vue3的源码阅读计划,Vue3的源码阅读计划不出意外每周一更,欢迎大家关注。

如果想一起交流的话,可以点击这里一起共同交流成长

系列章节: