关于前端:源码库跟着-Vue3-的源码学习-reactive-背后的实现原理

35次阅读

共计 23906 个字符,预计需要花费 60 分钟才能阅读完成。

reactive作为 Vue3 中的外围 API 之一,其背地的实现原理是十分值得咱们学习以及借鉴的;

上一篇文章只是初略的过了一遍 Vue3 的响应式流程,就那么初略的一瞥就有上万字,而且还没讲到具体的解说实现原理;

所以这一篇将具体的解析 reactive 的实现原理,后续还会补上 effect 的原理和思维,以及响应式的整体流程都将从新梳理,谢谢大家的反对;

因为上一篇文章曾经解说过源码,所以这一篇文章的节奏会放慢,尽管会放慢节奏,然而内容还是很多,万字正告,耐下性子能力继续成长。

reactive可代理的类型

跟着上篇咱们晓得了 reactive 可代理的数据类型有ObjectArrayMapSetWeakMapWeakSet

这代表着咱们能够创立响应式的数据类型有这些,先用代码看看咱们到底能够创立响应式的数据类型有哪些:

import {reactive, effect} from "vue";

// Object
const obj = reactive({
    foo: "foo",
    bar: "bar",
    baz: "baz"
});
effect(() => {console.log("object", obj.foo);
});
obj.foo = "foo1";

// Array
const arr = reactive([1, 2, 3]);
effect(() => {console.log("array", arr[0]);
});
arr[0] = 4;

// Map
const map = reactive(new Map());
effect(() => {console.log("map", map.get("foo"));
});
map.set("foo", "foo");

// Set
const set = reactive(new Set());
effect(() => {console.log("set", set.has("foo"));
});
set.add("foo");

// WeakMap
const weakMap = reactive(new WeakMap());
effect(() => {console.log("weakMap", weakMap.get(reactive));
});
weakMap.set(reactive, "foo");

// WeakSet
const weakSet = reactive(new WeakSet());
effect(() => {console.log("weakSet", weakSet.has(reactive));
});
weakSet.add(reactive);

// 除了上述的数据类型,还有一些内置的数据类型,比方 `Date`、`RegExp`、`Symbol` 等;// 这些内置的数据类型都是不可变的,所以不须要响应式,所以 `Vue3` 中没有对这些数据类型进行响应式解决;// 尽管它们 typeof 的后果都是 object,然而它们都是不可变的,所以不须要响应式;// Date
const date = reactive(new Date());
effect(() => {console.log("date", date.foo);
});
date.foo = "foo";

// RegExp
const regExp = reactive(new RegExp());
effect(() => {console.log("regExp", regExp.foo);
});
regExp.foo = "foo";

// Symbol 是只读的
// const symbol = reactive(Symbol());
// effect(() => {//   console.log("symbol", symbol.foo);
// });
// symbol.foo = "foo";

// function
const fn = reactive(function() {});
effect(() => {console.log("function", fn.foo);
});
fn.foo = "foo";

能够看到的是,咱们创立响应式的数据只有ObjectArrayMapSetWeakMapWeakSet

它们都打印了两次,而且第二次打印的值都是批改后的值,然而 DateRegExpfunction 都没有打印进去,并且 function 还给出了一个正告;

Symbol 是不可批改的,在代码的层面曾经给屏蔽了,所以不在思考范畴内;

reactive的实现原理

reactive的实现原理其实就是应用 Proxy 对数据进行代理,而后在 Proxygetset 钩子中进行依赖收集和派发更新;

getset钩子只能应答 ObjectArray,并且不能笼罩所有的利用场景,因为不论是Object 还是 Array 都是能够迭代的;

对于 MapSetWeakMapWeakSet 这些数据类型,它们并不是间接操作 keyvalue,而是通过 setget办法来操作的;

接下来咱们就来详细分析这些利用场景,看看 Vue3 是如何解决的;

代理Object

对于 ObjectVue3 是间接应用 Proxy 对数据进行代理,而后在 getset钩子中进行依赖收集和派发更新;

get钩子

跟着上一章咱们晓得是 get 钩子是通过 createGetter 函数来创立的,而 set 钩子是通过 createSetter 函数来创立的;

抛开一些边界条件,咱们只关怀响应式的外围逻辑,其实 get 钩子非常简单,如下:

function createGetter(isReadonly = false, shallow = false) {return function get(target, key, receiver) {
        
        // 判断是否是数组
        const targetIsArray = isArray(target);

        // 对数组原型上的办法进行特地看待
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
        }

        // 获取后果
        const res = Reflect.get(target, key, receiver);
        
        // 收集依赖
        track(target, "get" /* TrackOpTypes.GET */, key);
        
        // 返回后果
        return res;
    };
}

这里毁坏了源码的构造,把 get 钩子的外围逻辑提取进去,咱们能够看到,它最次要做的只有三件事:

  1. 对于数组,如果调用它的原型上的办法,比方 pushpop 等,那么返回的是通过代理的办法,这个前面会讲到;
  2. 获取对象的后果,最初返回这个后果;
  3. 收集依赖 (这里的收集依赖是能够放到后面去的,因为在源码中, 这个期间还做了其余事,所以当初是放在这里的);

咱们一个一个的剖析,先放下对于数组的解决,咱们先来看看 get 钩子是如何获取对象的后果的,当初咱们有如下的代码:

// 对象取值
const obj = {foo: "foo",};
console.log(obj.foo);

// 数组取值
const arr = [1, 2, 3];
console.log(arr[0]);

不论是对象还是数组,咱们都是能够间接通过拜访 key 来获取后果的,数组的下标也是 key,它们都是能够进入到get 钩子中的,如下:

// 省略下面对象的创立代码
const proxyObj = new Proxy(obj, {get(target, key, receiver) {console.log("get", key);
        return Reflect.get(target, key, receiver);
    },
});
proxyObj.foo

// 省略下面数组的创立代码
const proxyArr = new Proxy(arr, {get(target, key, receiver) {console.log("get", key);
        return Reflect.get(target, key, receiver);
    },
});
proxyArr[0]

能够看到都是能够进入到 get 钩子中的,而且 key 都是咱们想要的,而且代理的代码长得都一样,所以能够封装成一个函数,例如 reactive 函数:

function reactive(target) {
    return new Proxy(target, {get(target, key, receiver) {console.log("get", key);
            return Reflect.get(target, key, receiver);
        },
    });
}

const proxyObj = reactive(obj);
proxyObj.foo

const proxyArr = reactive(arr);
proxyArr[0]
Reflect

然而这里有一个问题是,明明能够间接通过 targe[key] 来获取后果,为什么要应用 Reflect.get 呢?

这里不解说Reflect,能够去看看 MDN Reflect;

这是为了解决 this 指向的问题,这是一个很有意思的事件,因为对象能够设置 gettersetter 函数,间接看上面的代码:

const obj = {
    foo: "foo",
    get bar() {return this.foo;},
};

const proxyObj = new Proxy(obj, {get(target, key, receiver) {console.log("get", key);
        return target[key];
    },
});
proxyObj.bar;

getter 和 setter 函数不懂间接点这里:

  • MDN getter;
  • MDN setter;

这里的最初返回的 this 指向的都是obj,这样会造成什么问题呢?看执行的成果截图:

这里能够看到,在代理的 get 钩子中只走了一次,而实在应用 obj 对象的属性有两次;

这是因为单纯的应用 target[key] 来获取后果,在 getter 函数中的 this 指向的是仍然是obj,而不是proxyObj,所以会造成这个问题;

而应用 Reflect.get 来获取后果,就不会有这个问题,因为 Reflect.get 的第三个参数就是 receiver,它的作用就是用来指定this 指向的,所以咱们能够这样写:

const obj = {
    foo: "foo",
    get bar() {return this.foo;},
};

const proxyObj = new Proxy(obj, {get(target, key, receiver) {console.log("get", key);
        return Reflect.get(target, key, receiver);
    },
});
proxyObj.bar;

能够看到,这样就能够解决这个问题了;

Reflect 还有其余的办法,都是和 Proxy 配合应用的,这里就不一一介绍了,包含在 set 钩子中也是应用 Reflect.set 来设置值的,都是为了解决这个问题;

数组的非凡解决

咱们晓得,数组是能够间接调用原型上的办法的,应用这些办法实质上也是拜访 key,所以也是能够进入到get 钩子中的,例如:

const arr = [1, 2, 3];

const proxyArr = new Proxy(arr, {get(target, key, receiver) {console.log("get", key);
        return Reflect.get(target, key, receiver);
    },
});
proxyArr.push(4);

能够看到这里胜利的进入了 get 钩子中,key就是调用的原型办法的名称;

push办法执行实现之后会返回数组的长度,所以这里还会有一个 get 钩子,key就是length,其余的办法也是一样的;

然而这里会有一个问题就是,数组的原型办法会扭转数组自身,然而这个时候并不会告诉到 Proxy,所以Vue3get钩子中对数组的原型办法进行了非凡解决,例如:

function createGetter(isReadonly = false, shallow = false) {return function get(target, key, receiver) {
        
        // 判断是否是数组
        const targetIsArray = isArray(target);

        // 对数组原型上的办法进行特地看待
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
        }

    };
}

这里的要害就是arrayInstrumentations,它是一个对象,外面寄存的是数组的原型办法,源码实现如下:

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations();

function createArrayInstrumentations() {const instrumentations = {};
    
    // 对数组的查询方法进行非凡解决
    ['includes', 'indexOf', 'lastIndexOf'].forEach(key => {instrumentations[key] = function (...args) {const arr = toRaw(this);
            for (let i = 0, l = this.length; i < l; i++) {track(arr, "get" /* TrackOpTypes.GET */, i + '');
            }
            // we run the method using the original args first (which may be reactive)
            const res = arr[key](...args);
            if (res === -1 || res === false) {
                // if that didn't work, run it again using raw values.
                return arr[key](...args.map(toRaw));
            }
            else {return res;}
        };
    });
    
    // 对会批改数组自身的办法进行非凡解决
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {instrumentations[key] = function (...args) {pauseTracking();
            const res = toRaw(this)[key].apply(this, args);
            resetTracking();
            return res;
        };
    });
    return instrumentations;
}

数组的查询方法,例如includesindexOflastIndexOf,这些办法的应用如下:

const arr = [1, 2, 3];
arr.includes(1);
arr.indexOf(1);
arr.lastIndexOf(1);

它们的参数都是原始值,匹配的是数组中的每一项,如果应用代理对象调用这些办法,那么永远返回的都是匹配不到;

所以 Vue3 在这里对这些办法进行了非凡解决,它会先应用原始值去匹配,如果匹配不到,再应用代理对象去匹配,这样就能够解决这个问题;

简化的实现如下,这里不关怀依赖收集的逻辑:

['includes', 'indexOf', 'lastIndexOf'].forEach(key => {instrumentations[key] = function (...args) {
        // 这里的 this 就是代理对象,toRaw 就是将代理对象转换为原始对象
        const arr = toRaw(this);
        
        // 先间接应用 用户传入的参数 去匹配
        const res = arr[key](...args);
        
        // 如果没有匹配到
        if (res === -1 || res === false) {
            // 再将参数转换为原始值,再去匹配
            return arr[key](...args.map(toRaw));
        }

        // 如果匹配到了,间接返回
        return res;
    };
});

总体来说这里就是会匹配两次后果,第一次是应用用户传入的参数去与用户传入的参数匹配,如何没有匹配到,再将用户传入的参数转换为原始值,再去匹配;

而对于会批改数组自身的办法,例如pushpopshiftunshiftsplice,这些办法的应用如下:

const arr = [1, 2, 3];
arr.push(4);
arr.pop();
arr.shift();
arr.unshift(0);
arr.splice(0, 1, 0);

这些办法都是会扭转数组自身的,然而扭转了之后 Proxy 中的 get 钩子并不会被触发,所以 Vue3 对这些办法也进行了非凡解决,它会在执行这些办法之前暂停依赖收集,执行完之后再复原依赖收集,源码实现如下:

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {instrumentations[key] = function (...args) {
        // 暂停依赖收集
        pauseTracking();
        
        // 这里的 this 就是代理对象,toRaw 就是将代理对象转换为原始对象
        // 这里还是执行的原始对象的办法,只是在执行之前暂停了依赖收集
        const res = toRaw(this)[key].apply(this, args);
        
        // 复原依赖收集
        resetTracking();
        return res;
    };
});

这里的实现是非常简单的,并没有做其余的解决,只是简略的暂停和复原依赖收集,简略的看一下 pauseTrackingresetTracking的实现:

let shouldTrack = true;
const trackStack = [];
function pauseTracking() {trackStack.push(shouldTrack);
    shouldTrack = false;
}

function resetTracking() {const last = trackStack.pop();
    shouldTrack = last === undefined ? true : last;
}

这里有两个全局变量,在上一篇的流程中是有讲到过的,在每次 tack 的时候都会判断 shouldTrack 是否为 true,如果为true 才会进行依赖收集;

所以这里的 pauseTrackingresetTracking就是通过扭转 shouldTrack 的值来暂停和复原依赖收集的;

为什么要这样解决呢?还记得我下面说到的调用 push 函数会拜访 length 属性吗?

如果不暂停依赖收集,那么在执行 push 函数的时候,会拜访 length 属性,这个时候就会触发 get 钩子,而 get 钩子中又会进行依赖收集,这样就会导致死循环;

来试试看:

const arr = [1, 2, 3];

const proxy = new Proxy(arr, {get(target, key) {console.log('get');
        return target[key];
    },
    set(target, key, value) {console.log('set');
        // set 会触发依赖
        effect();
        
        target[key] = value;
        return true;
    }
});

// 假如这个是一个 effect
function effect() {proxy.push(4);
}
effect();

能够本人在浏览器中运行一下,最初会报错:Uncaught RangeError: Maximum call stack size exceeded

这里就是因为在执行 push 函数的时候,会扭转原数组,同时原数组的 length 属性也会发生变化,这个时候就会触发 set 钩子;

set 钩子有是依赖触发的中央,所以会再次执行 effect,这样就会导致死循环,所以Vue3 在这里就是通过暂停依赖收集来解决这个问题的;

当初 get 钩子外面的内容以及差不多了,解决了对象的 gettersetter办法的 this 问题,解决了数组的原型办法的问题,接下来就是解决 set 钩子了;

set钩子

set钩子的源码比拟与 get 钩子相比代码量少,然而流程会比 get 钩子略微简单一些,这里我会尽量简略的介绍一下 set 钩子的实现;

get次要是解决边界状况,而 set 关注的是以后的值能不能设置到指标对象上,设置胜利之后需不需要触发依赖;

上面是 createSetter 函数的实现,简化实现如下:

const set$1 = /*#__PURE__*/ createSetter();
function createSetter(shallow = false) {return function set(target, key, value, receiver) {
        // 这里的 target 是原始对象,获取原始对象的值
        let oldValue = toRaw(oldValue);
        value = toRaw(value);

        // 判断以后拜访的属性是否存在与原始对象中
        const hadKey = isArray(target) && isIntegerKey(key)
            ? Number(key) < target.length
            : hasOwn(target, key);

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

        // 如果以后操作的对象就是原始对象,那么就会触发依赖
        if (target === toRaw(receiver)) {
            
            // 如果以后操作的属性不存在与原始对象中,那么就会触发 add 依赖
            if (!hadKey) {trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
            }
            
            // 如果以后操作的值和旧值雷同,那么就不会触发依赖
            else if (hasChanged(value, oldValue)) {trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
            }
        }
        return result;
    };
}

这里没有太多边界解决的代码,大体的流程如下:

  1. 将旧值和新值都转换为原始对象,简化的代码只是为了做差别比照,判断新值和旧值是否雷同;
  2. 判断以后操作的属性是否存在与原始对象中;
  3. 判断以后操作的对象是否就是原始对象,如果是,那么就会触发依赖;
  4. 如果以后操作的属性不存在与原始对象中,那么就会触发 add 依赖;
  5. 如果以后操作的值和旧值不同,那么就会触发 set 依赖;

将旧值和新值都转换为原始对象,在源码中还会解决 ref 对象,这里就只解说简略的状况,所以这里只是为了解决差别比照;

对于数组的下标拜访,通过判断下标是否小于数组的 length 来判断以后操作的属性是否存在与原始对象中;

对于对象的属性拜访,通过 hasOwn 来判断以后操作的属性是否存在与原始对象中,hasOwn就是Object.prototype.hasOwnProperty

至于为什么要判断以后操作的对象是否就是原始对象,这里是为了解决 proxy 的异常情况,比方上面这种状况:

function reavtive(obj) {
    return new Proxy(obj, {get(target, key) {return target[key];
        },
        set(target, key, value, receiver) {console.log('set', target, receiver);
            target[key] = value;
            return true;
        }
    });
}

const obj = {a: 1};

obj.__proto__ = reavtive({});

obj.a = 2; // 不会触发代理对象的 set 钩子
obj.b = 1; // 会触发代理对象的 set 钩子

如果操作的对象不是一个代理对象,并且操作的属性在操作的对象中不存在,并且操作的对象的原型链上存在代理对象,那么就会触发代理对象的 set 钩子;

这里的 receiver 就是代理对象,而 target 就是操作的对象,这里的判断就是为了解决这个问题;

前面就是判断是否新增或者批改属性,而后触发对应的依赖;

deleteProperty钩子

deleteProperty钩子的实现比较简单,就是在删除属性的时候触发依赖,代码如下:

function deleteProperty(target, key) {
    // 判断以后操作的属性是否存在与原始对象中
    const hadKey = hasOwn(target, key);
    
    // 旧值
    const oldValue = target[key];
    
    // 删除属性
    const result = Reflect.deleteProperty(target, key);
    
    // 是否胜利删除
    if (result && hadKey) {
        // 触发 delete 依赖
        trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
    }
    
    // 返回后果
    return result;
}

这里的 deleteProperty 钩子就是在删除属性的时候触发依赖,这里并没有什么特地的中央,就是简略的删除属性;

细节点在于如果删除的属性不存在原始对象中,那么就不会触发依赖,也没必要触发依赖;

has钩子

has钩子的实现也比较简单,就是在判断属性是否存在的时候触发依赖,代码如下:

function has$1(target, key) {
    // 应用 Reflect.has 判断属性是否存在
    const result = Reflect.has(target, key);
    
    // 如果以后操作的属性不是内置的 Symbol,那么就会触发 has 依赖
    if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, "has" /* TrackOpTypes.HAS */, key);
    }
    
    // 返回后果
    return result;
}

这里的 has 钩子就是在判断属性是否存在的时候触发依赖,该钩子是针对 in 操作符的;

须要留神的是这里的 in 并不是 for...infor...in 是遍历对象的属性,而 in 是判断属性是否存在;

ownKeys钩子

ownKeys钩子的实现也比较简单,就是在迭代对象的时候触发依赖,代码如下:

function ownKeys(target) {
    // 触发 iterate 依赖
    track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    
    // 返回后果
    return Reflect.ownKeys(target);
}

这里并没有什么特地的中央,就是在迭代对象的时候触发依赖;

这里的细节是对于数组的迭代,会触发 length 属性的依赖,因为对于数组的迭代是能够通过 length 属性来迭代的,例如上面的代码:

const arr = [1, 2, 3];
const proxy = new Proxy(arr, {get(target, key) {console.log('get', key);
        return Reflect.get(target, key);
    },
    ownKeys(target) {console.log('ownKeys');
        return Reflect.ownKeys(target);
    }
});

// 不会进入 ownKeys 钩子
for (let i = 0; i < proxy.length; i++) {console.log(arr[i]);
}

// 会进入 ownKeys 钩子
for (let i in proxy) {console.log(arr[i]);
}

// 不会进入 ownKeys 钩子
for (let i of proxy) {console.log(i);
}

这里迭代数组的形式有这么多种,Vue3通过应用 length 作为 key 来触发依赖,这样就能够保障对于数组的迭代都能触发依赖;

而对于对象的迭代,会将 ITERATE_KEY 作为 key 来触发依赖,这里的 ITERATE_KEY 是一个 Symbol 类型的值,这样就能够保障对于对象的迭代也能触发依赖;

这个时候可能还会蛊惑,我下面说的这些个 key 是什么,为什么要这样做?这些都是依赖收集和依赖触发的逻辑,前面会独自写一篇文章来解说;

所以看到这里,没有讲 tracktrigger不要焦急,当相熟 Vue3 对数据拦挡的解决流程,前面再来看 tracktrigger就会比拟容易了解;

代理 MapSetWeakMapWeakSet

Vue3对于 MapSetWeakMapWeakSet 的解决是不同于 Object 的;

因为 MapSetWeakMapWeakSet 的设置值和获取值的形式和 Object 不一样,所以 Vue3 对于这些类型的数据的解决也是不一样的;

然而绝对于 Object 来说要简略的很多,在 createReactiveObject 函数中,有这样的一段代码:

// 对 Map、Set、WeakMap、WeakSet 的代理 handler
const mutableCollectionHandlers = {get: /*#__PURE__*/ createInstrumentationGetter(false, false)
};

// reactive 是通过 createReactiveObject 函数来创立代理对象的
function reactive(target) {return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
   // 省略其余代码...
    
    // 这里的 targetType 在上一篇文章中曾经讲过了,值为 2 代表着 target 的类型为 Map、Set、WeakMap、WeakSet
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    
    // 省略其余代码...
}

这里的关注点就在 targetType 的判断上,如果 targetType 的值为 2,那么就会应用collectionHandlers 作为 handler,否则就会应用baseHandlers 作为handler

baseHandlers就是我下面讲的,对 Object 的代理 handler,而collectionHandlers 就是对 MapSetWeakMapWeakSet 的代理handler

baseHandlerscollectionHandlers 都是通过 reactive 传入的,而指向的都是全局的 mutableHandlersmutableCollectionHandlers

mutableCollectionHandlers

mutableCollectionHandlers看下面的定义,只有一个 get 钩子,依据下面的解说,咱们也晓得 get 钩子的作用;

对于 MapSetWeakMapWeakSet 来说,不论是设置值还是获取值,都是通过调用对应的办法来实现的,所以它们的依赖收集和依赖触发都是通过 get 钩子来实现的;

get钩子通过 createInstrumentationGetter 函数来创立,代码如下:

function createInstrumentationGetter(isReadonly, shallow) {
    const instrumentations = shallow
        ? isReadonly
            ? shallowReadonlyInstrumentations
            : shallowInstrumentations
        : isReadonly
            ? readonlyInstrumentations
            : mutableInstrumentations;
    return (target, key, receiver) => {if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {return !isReadonly;}
        else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {return isReadonly;}
        else if (key === "__v_raw" /* ReactiveFlags.RAW */) {return target;}
        return Reflect.get(hasOwn(instrumentations, key) && key in target
            ? instrumentations
            : target, key, receiver);
    };
}

而依据 mutableCollectionHandlers 创立的时候传入的参数,而后再去掉边界状况,咱们将代码能够简化成如下:

function createInstrumentationGetter(isReadonly, shallow) {
    // 这里获取的都是对 Map、Set、WeakMap、WeakSet 的操作方法
    const instrumentations = mutableInstrumentations;
    
    return (target, key, receiver) => {
        // 获取 target,这里并不是应用原始的 target,而是依据操作方法的不同来获取不同的 target
        const _target = hasOwn(instrumentations, key) && key in target ? instrumentations : target
        
        // 返回对应的值,这里返回的值可能是 instrumentations 中的办法,也可能是 target 中的值
        return Reflect.get(_target, key, receiver);
    };
}

这里的要害是 mutableInstrumentations 是什么,这个是一个全局的对象,它的定义如下:

function createInstrumentations() {
    const mutableInstrumentations = {get(key) {return get(this, key);
        },
        get size() {return size(this);
        },
        has,
        add,
        set,
        delete: deleteEntry,
        clear,
        forEach: createForEach(false, false)
    };
    const shallowInstrumentations = {get(key) {return get(this, key, false, true);
        },
        get size() {return size(this);
        },
        has,
        add,
        set,
        delete: deleteEntry,
        clear,
        forEach: createForEach(false, true)
    };
    const readonlyInstrumentations = {get(key) {return get(this, key, true);
        },
        get size() {return size(this, true);
        },
        has(key) {return has.call(this, key, true);
        },
        add: createReadonlyMethod("add" /* TriggerOpTypes.ADD */),
        set: createReadonlyMethod("set" /* TriggerOpTypes.SET */),
        delete: createReadonlyMethod("delete" /* TriggerOpTypes.DELETE */),
        clear: createReadonlyMethod("clear" /* TriggerOpTypes.CLEAR */),
        forEach: createForEach(true, false)
    };
    const shallowReadonlyInstrumentations = {get(key) {return get(this, key, true, true);
        },
        get size() {return size(this, true);
        },
        has(key) {return has.call(this, key, true);
        },
        add: createReadonlyMethod("add" /* TriggerOpTypes.ADD */),
        set: createReadonlyMethod("set" /* TriggerOpTypes.SET */),
        delete: createReadonlyMethod("delete" /* TriggerOpTypes.DELETE */),
        clear: createReadonlyMethod("clear" /* TriggerOpTypes.CLEAR */),
        forEach: createForEach(true, true)
    };
    const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
    iteratorMethods.forEach(method => {mutableInstrumentations[method] = createIterableMethod(method, false, false);
        readonlyInstrumentations[method] = createIterableMethod(method, true, false);
        shallowInstrumentations[method] = createIterableMethod(method, false, true);
        shallowReadonlyInstrumentations[method] = createIterableMethod(method, true, true);
    });
    return [
        mutableInstrumentations,
        readonlyInstrumentations,
        shallowInstrumentations,
        shallowReadonlyInstrumentations
    ];
}
const [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* #__PURE__*/ createInstrumentations();

太多了不想看,咱们只关注 mutableInstrumentations 就好了,简化如下:


function createInstrumentationGetter(isReadonly, shallow) {
    // 这里获取的都是对 Map、Set、WeakMap、WeakSet 的操作方法
    const instrumentations = mutableInstrumentations;
    
    return (target, key, receiver) => {
        // 获取 target,这里并不是应用原始的 target,而是依据操作方法的不同来获取不同的 target
        const _target = hasOwn(instrumentations, key) && key in target ? instrumentations : target
        
        // 返回对应的值,这里返回的值可能是 instrumentations 中的办法,也可能是 target 中的值
        return Reflect.get(_target, key, receiver);
    };
}

这里的要害是 mutableInstrumentations 是什么,这个是一个全局的对象,它的定义如下:

function createInstrumentations() {
    // 须要代理的办法
    const mutableInstrumentations = {get(key) {return get(this, key);
        },
        get size() {return size(this);
        },
        has,
        add,
        set,
        delete: deleteEntry,
        clear,
        forEach: createForEach(false, false)
    };
    
    // 遍历对象的迭代办法
    const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
    iteratorMethods.forEach(method => {mutableInstrumentations[method] = createIterableMethod(method, false, false);
    });
    
    // 返回
    return [mutableInstrumentations,];
}

const [mutableInstrumentations] = /* #__PURE__*/ createInstrumentations();

能够看到的这里对 MapSetWeakMapWeakSet 的操作方法都进行了拦挡,应用自定义的办法来代替原生的办法,这样就能够在自定义的办法中进行一些额定的操作,比方收集依赖、触发更新等。

set办法

咱们先来看 set 办法,这个办法的定义如下:

function set(key, value) {
    // 将存入的值装换为原始值
    value = toRaw(value);
    
    // 获取 target,这个时候 this 是代理对象
    const target = toRaw(this);
    
    // 获取 target 的 has、get 办法
    const {has, get} = getProto(target);
    
    // 调用 has 判断是否有这个键值
    let hadKey = has.call(target, key);
    
    // 如果没有就将 key 装换为原始值再查问一次
    if (!hadKey) {key = toRaw(key);
        hadKey = has.call(target, key);
    }
    
    // 如果没有就查看这个 key 是否还存在一份原始值正本在 target 中
    // 意思是 key 响应式对象,存了一份数据在 target 中
    // 又将 key 的原始值作为 key,再存一份数据在 target 中
    // 这样可能导致代码凌乱,是不举荐的做法,所以会有提醒音讯
    else {checkIdentityKeys(target, has, key);
    }
    
    // 获取旧值
    const oldValue = get.call(target, key);
    
    // 设置新值
    target.set(key, value);
    
    // 如果之前没有这个键值,就触发 add 依赖
    if (!hadKey) {trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
    }
    
    // 如果值产生扭转就触发 set 依赖
    else if (hasChanged(value, oldValue)) {trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
    }
    return this;
}

set办法的后半局部和 set 钩子是相似的,重点是在前半部分,对于 key 的解决;

这些个办法都是能够存任意值的,key也能够是任意类型,然而在响应式零碎中,一个数据会有两个版本;

一个是响应式对象,就是咱们通过 reactive 创立的对象,还有一个是原始对象,这个没什么好说的;

他们两个是不一样的,如果都作为 key 可能导致一些问题,Vue很贴心的将这一块揭示进去了;

get办法

咱们再来看 get 办法,这个办法的定义如下,去掉边界状况,简化的代码如下:

前面的源码剖析都将会去掉边界解决的状况,也不再贴出原始代码,如果想看源码写什么样能够本人去查看,前面不会在独自强调。

function get(target, key, isReadonly = false, isShallow = false) {
    // 获取原始值,因为在调用 get 办法时,target 传入的值是 this,也就是代理对象
    target = target["__v_raw" /* ReactiveFlags.RAW */];

    // 多重代理的状况,通常这里和 target 是一样的
    const rawTarget = toRaw(target);

    // 获取原始值的 key,还记得 set 办法中对 key 的解决吗?const rawKey = toRaw(key);

    // 还是和 set 办法一样,如果 key 是响应式对象,就可能会有两份数据
    // 所以 key 是响应式对象会触发两次依赖收集
    if (key !== rawKey) {track(rawTarget, "get" /* TrackOpTypes.GET */, key);
    }
    track(rawTarget, "get" /* TrackOpTypes.GET */, rawKey);

    // 原始对象的 has 办法
    const {has} = getProto(rawTarget);

    // toReactive 是一个工具函数,用来将值转换为响应式对象,前提是值是对象
    const wrap = toReactive;

    // 如果原始对象中有这个 key,就间接返回,这个 key 可能是响应式对象
    if (has.call(rawTarget, key)) {return wrap(target.get(key));
    }

    // 如果原始对象中没有这个 key,就应用装换后的 key 来查问
    else if (has.call(rawTarget, rawKey)) {return wrap(target.get(rawKey));
    }

    // 如果还是没有,这里是 readonly(reactive(Map)) 这种嵌套的状况解决
    // 这里确保了嵌套的 reactive(Map) 也能够进行依赖收集
    else if (target !== rawTarget) {target.get(key);
    }
}

Vue3为了确保使用者可能获取到值,并且值也是响应式的,所以在 get 办法中应用了 toReactive 办法将值转换为响应式对象;

同时也为了让使用者肯定能获取到值,所以会对 key 进行两次查问,一次用户传入的 key,一次是key 的原始值,然而这样可能会导致数据的不统一;

set办法重点是对 key 的解决,而 get 办法重点是对 value 的解决;

add办法

咱们再来看 add 办法,这个办法的定义如下:

function add(value) {
    // 将存入的值装换为原始值
    value = toRaw(value);
    
    // 获取 target,这个时候 this 是代理对象
    const target = toRaw(this);
    
    // 获取 target 的原型
    const proto = getProto(target);
    
    // 应用原型的 has 办法判断是否有这个值
    const hadKey = proto.has.call(target, value);
    
    // 如果没有就将 value 存入 target,并触发 add 依赖
    if (!hadKey) {target.add(value);
        trigger(target, "add" /* TriggerOpTypes.ADD */, value, value);
    }
    
    // 返回 this
    return this;
}

add办法次要针对 Set 类型的数据,Set类型的数据是不容许反复的,所以在 add 办法中会判断是否曾经存在这个值;

这里并没有什么非凡的,就是将值转换为原始值,而后判断是否曾经存在,如果不存在就存入,而后触发 add 依赖;

然而看了下面的 setget办法,感觉像是两个人写的,手动狗头;

has办法

接下来看 has 办法,这个办法的定义如下:

function has(key, isReadonly = false) {
    // 获取原始值,和 get 办法一样
    const target = this["__v_raw" /* ReactiveFlags.RAW */];
    const rawTarget = toRaw(target);
    const rawKey = toRaw(key);
    
    // 和 get 办法一样,如果 key 是响应式对象,就可能会有两份数据
    // 所以这里也一样会有两次依赖收集
    if (key !== rawKey) {track(rawTarget, "has" /* TrackOpTypes.HAS */, key);
    }
    track(rawTarget, "has" /* TrackOpTypes.HAS */, rawKey);
    
    // 如果 key 不是响应式对象,就间接返回 target.has(key) 的后果
    // 如果 key 是响应式对象,检测两次
    return key === rawKey
        ? target.has(key)
        : target.has(key) || target.has(rawKey);
}

has办法次要是判断 target 中是否有key,如果有就返回true,否则返回false

这里的逻辑和 get 雷同,都是对 key 进行两次查问,一次是用户传入的 key,一次是key 的原始值;

delete办法

接下来看 delete 办法,delete办法是通过 deleteEntry 办法实现的,这个办法的定义如下:

function deleteEntry(key) {
    // 获取原始值
    const target = toRaw(this);
    
    // 获取原型的 has 和 get 办法
    const {has, get} = getProto(target);
    
    // 判断是否有这个 key
    let hadKey = has.call(target, key);
    
    // 如果没有这个 key,就将 key 转换为原始值再获取一次后果
    if (!hadKey) {key = toRaw(key);
        hadKey = has.call(target, key);
    }
    
    // 如果有证实这个 key 存在,有可能是响应式对象
    // 这里和 set 办法一样,响应式对象作为 key 会提醒正告信息
    else {checkIdentityKeys(target, has, key);
    }
    
    // 获取旧值
    const oldValue = get ? get.call(target, key) : undefined;
    
    // 删除
    const result = target.delete(key);
    
    // 如果 key 在 target 中存在,就触发 delete 依赖
    if (hadKey) {trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
    }
    
    // 返回删除后果
    return result;
}

delete办法次要是删除 target 中的key,如果删除胜利就返回true,否则返回false

这个办法和 set 办法很像,都是对 key 进行了两次查问,一次是用户传入的 key,一次是key 的原始值;

如果将响应式对象作为 key,并且key 的原始值也作为 target 中的key,那么就会提醒正告信息;

clear办法

接下来看 clear 办法,这个办法的定义如下:

function clear() {
    // 获取原始值
    const target = toRaw(this);
    
    // 获取指标的 size
    const hadItems = target.size !== 0;
    
    // 获取旧值
    const oldTarget = isMap(target)
            ? new Map(target)
            : new Set(target)
        ;
    
    // 清空
    const result = target.clear();
    
    // 如果 size 不为 0,就触发 clear 依赖
    if (hadItems) {trigger(target, "clear" /* TriggerOpTypes.CLEAR */, undefined, undefined, oldTarget);
    }
    
    // 返回清空后果
    return result;
}

clear办法次要是清空 target 中的所有值,如果清空胜利就返回true,否则返回false

这个办法很简略,就是清空 target,而后触发clear 依赖;

size属性

size属性是通过 getter 实现的,外部是通过 size 办法返回的后果;

const mutableInstrumentations = {get size() {return size(this);
    },
}

size办法的定义如下:

function size(target, isReadonly = false) {
    // 获取原始值
    target = target["__v_raw" /* ReactiveFlags.RAW */];
    
    // 不是只读的,就收集依赖
    !isReadonly && track(toRaw(target), "iterate" /* TrackOpTypes.ITERATE */, ITERATE_KEY);
    
    // 返回 size
    return Reflect.get(target, 'size', target);
}

size办法次要是返回 targetsize,实现很简略,就是通过 Reflect.get 获取 size 属性的值;

这里收集的依赖是 iterate 类型,因为能够通过 size 属性来迭代指标对象。

forEach办法

接下来看 forEach 办法,这个办法通过 createForEach 办法实现,这个办法的定义如下:

function createForEach(isReadonly, isShallow) {return function forEach(callback, thisArg) {
        // 以后实例,指向的是响应式对象
        const observed = this;
        
        // 获取原始值
        const target = observed["__v_raw" /* ReactiveFlags.RAW */];
        const rawTarget = toRaw(target);
        const wrap = toReactive;
        
        // 不是只读的,就收集依赖
        !isReadonly && track(rawTarget, "iterate" /* TrackOpTypes.ITERATE */, ITERATE_KEY);
        
        // 应用原始值调用 forEach 办法
        return target.forEach((value, key) => {
            // 重要:确保回调函数
            // 1. 以响应式 map 作为 this 和第三个参数调用
            // 2. 接管到的值应该是相应的响应式 / 只读的
            return callback.call(thisArg, wrap(value), wrap(key), observed);
        });
    };
}

forEach办法次要是遍历 target 中的所有值,而后调用 callback 办法;

这里并没有什么非凡的,重要的是须要将回调函数中的所有参数都转换为响应式对象,依赖收集须要在这个之前进行;

createIterableMethod办法

最初就是通过 createIterableMethod 办法创立的 keysvaluesentries 办法,这个办法的定义如下:

function createIterableMethod(method, isReadonly, isShallow) {return function (...args) {
        // 获取原始值
        const target = this["__v_raw" /* ReactiveFlags.RAW */];
        const rawTarget = toRaw(target);
        
        // 判断指标对象是否是 Map
        const targetIsMap = isMap(rawTarget);
        
        // 判断是否是 entries 或者是迭代器
        const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap);
        
        // 判断是否是 keys
        const isKeyOnly = method === 'keys' && targetIsMap;
        
        // 获取外部的迭代器,这一块能够参考 Map、Set 的相应的 API
        const innerIterator = target[method](...args);
        
        // 包装器
        const wrap = toReactive;
        
        // 不是只读的,就收集依赖
        !isReadonly && track(rawTarget, "iterate" /* TrackOpTypes.ITERATE */, isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY);
        
        // 返回一个包装的迭代器,从原始迭代器获取到的值进行响应式包装后返回
        return {
            // 迭代器协定,能够通过 for...of 遍历
            next() {
                // 获取原始迭代器的值
                const {value, done} = innerIterator.next();
                
                // 如果是 done,就间接返回
                // 否则就将 value 进行包装后返回
                return done
                    ? {value, done}
                    : {value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
                        done
                    };
            },
            
            // 迭代器协定
            [Symbol.iterator]() {return this;}
        };
    };
}

这些都是用来解决 keysvaluesentries 办法的,同时还包含了 Symbol.iterator 办法;

这些都是可迭代的办法,所以返回的都是一个迭代器,细节是 MapSymbol.iterator办法的数据结构是 [key, value],而SetSymbol.iterator办法的数据结构是value

所以后面判断了一下,前面返回值的时候就能够依据不同的数据结构进行包装;

总结

这一章是对上一章的补充,次要补充 reavtive 办法的实现,reavtiveObjectArrayMapSetWeakMapWeakSet 进行了不同的解决;

reavtive办法的实现次要是通过 createReactiveObject 办法实现的,这个办法次要是通过 Proxytarget进行代理,而后对 target 中的每一个属性进行响应式解决;

Vue3思考到了各种状况下的响应式解决,所以对代理的 handler 欠缺水平很高,对于 Object 类型有 getsetdeletehasownKeys 等等钩子,笼罩到了所有的状况;

对于 Array 类型补充了对·原型办法的解决以及对 length 属性的解决;

对于 MapSetWeakMapWeakSet 类型,只有一个 get 钩子,因为这里对象通常都是通过对应的操作方法进行操作的,所以只须要对 get 钩子进行解决就能够了;

大家好,这里是田八的【源码 & 库】系列,Vue3的源码浏览打算,Vue3的源码浏览打算不出意外每周一更,欢送大家关注。

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

系列章节:

  • 【源码 & 库】跟着 Vue3 学习前端模块化
  • 【源码 & 库】在调用 createApp 时,Vue 为咱们做了那些工作?
  • 【源码 & 库】细数 Vue3 的实例办法和属性背地的故事
  • 【源码 & 库】Vue3 中的 nextTick 魔法背地的原理
  • 【源码 & 库】Vue3 的响应式外围 reactive 和 effect 实现原理以及源码剖析

正文完
 0