乐趣区

关于前端:源码库Vue3-的响应式核心-reactive-和-effect-实现原理以及源码分析

Vue的响应式零碎很让人着迷,Vue2应用的是 Object.definePropertyVue3 应用的是Proxy,这个是大家都晓得的技术点;

然而晓得了这些个技术点就能写出一个响应式零碎吗?答案是必定是 NOVue 的响应式零碎是一个非常复杂的零碎,技术只是实现的伎俩,明天咱们就来看看背地实现的思维。

本章内容有点多,如果不能耐着性子看的话,倡议先看看我在线实现的小 demo 去了解核心思想,不明确再来在文章中寻找答案:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js

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函数。

在线地址:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js

总结

这一篇文章,次要是解说了 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的源码浏览打算不出意外每周一更,欢送大家关注。

如果想一起交换的话,能够点击这里一起独特交换成长

首发在掘金,无任何引流的意思,后续文章不再强调。

系列章节:

  • 【源码 & 库】跟着 Vue3 学习前端模块化
  • 【源码 & 库】在调用 createApp 时,Vue 为咱们做了那些工作?
  • 【源码 & 库】Vue3 中的 nextTick 魔法背地的原理
退出移动版