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 和 alet depsMap = targetMap.get(obj);if (!depsMap) {    targetMap.set(target, (depsMap = new Map()));}// createDep 实现很简略,就不在解说的代码外面独自写进去了,具体就是一个 Set,多了两个属性,w 和 nconst 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 魔法背地的原理