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

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

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

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

reactive可代理的类型

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

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

import { reactive, effect } from "vue";// Objectconst obj = reactive({    foo: "foo",    bar: "bar",    baz: "baz"});effect(() => {    console.log("object", obj.foo);});obj.foo = "foo1";// Arrayconst arr = reactive([1, 2, 3]);effect(() => {    console.log("array", arr[0]);});arr[0] = 4;// Mapconst map = reactive(new Map());effect(() => {    console.log("map", map.get("foo"));});map.set("foo", "foo");// Setconst set = reactive(new Set());effect(() => {    console.log("set", set.has("foo"));});set.add("foo");// WeakMapconst weakMap = reactive(new WeakMap());effect(() => {    console.log("weakMap", weakMap.get(reactive));});weakMap.set(reactive, "foo");// WeakSetconst weakSet = reactive(new WeakSet());effect(() => {    console.log("weakSet", weakSet.has(reactive));});weakSet.add(reactive);// 除了上述的数据类型,还有一些内置的数据类型,比方`Date`、`RegExp`、`Symbol`等;// 这些内置的数据类型都是不可变的,所以不须要响应式,所以`Vue3`中没有对这些数据类型进行响应式解决;// 尽管它们 typeof 的后果都是 object,然而它们都是不可变的,所以不须要响应式;// Dateconst date = reactive(new Date());effect(() => {    console.log("date", date.foo);});date.foo = "foo";// RegExpconst 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";// functionconst 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.fooconst 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;    }});// 假如这个是一个 effectfunction 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 的代理 handlerconst 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 实现原理以及源码剖析