1

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";

image.png

可以看到的是,我们创建响应式的数据只有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]

image.png

可以看到都是可以进入到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 函数不懂直接点这里:

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

image.png

这里可以看到,在代理的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;

image.png

可以看到,这样就可以解决这个问题了;

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

image.png

可以看到这里成功的进入了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的源码阅读计划不出意外每周一更,欢迎大家关注。

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

系列章节:


田八
357 声望313 粉丝