reactive
作为Vue3
中的核心API
之一,其背后的实现原理是非常值得我们学习以及借鉴的;
上一篇文章只是初略的过了一遍Vue3
的响应式流程,就那么初略的一瞥就有上万字,而且还没讲到详细的讲解实现原理;
所以这一篇将详细的解析reactive
的实现原理,后续还会补上effect
的原理和思想,以及响应式的整体流程都将重新梳理,谢谢大家的支持;
由于上一篇文章已经讲解过源码,所以这一篇文章的节奏会加快,虽然会加快节奏,但是内容还是很多,万字警告,耐下性子才能持续成长。
reactive
可代理的类型
跟着上篇我们知道了reactive
可代理的数据类型有Object
、Array
、Map
、Set
、WeakMap
、WeakSet
;
这代表着我们可以创建响应式的数据类型有这些,先用代码看看我们到底可以创建响应式的数据类型有哪些:
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";
可以看到的是,我们创建响应式的数据只有Object
、Array
、Map
、Set
、WeakMap
、WeakSet
;
它们都打印了两次,而且第二次打印的值都是修改后的值,但是Date
、RegExp
、function
都没有打印出来,并且function
还给出了一个警告;
而Symbol
是不可修改的,在代码的层面已经给屏蔽了,所以不在考虑范围内;
reactive
的实现原理
reactive
的实现原理其实就是使用Proxy
对数据进行代理,然后在Proxy
的get
和set
钩子中进行依赖收集和派发更新;
而get
和set
钩子只能应对Object
、Array
,并且不能覆盖所有的应用场景,因为不管是Object
还是Array
都是可以迭代的;
对于Map
、Set
、WeakMap
、WeakSet
这些数据类型,它们并不是直接操作key
和value
,而是通过set
和get
方法来操作的;
接下来我们就来详细分析这些应用场景,看看Vue3
是如何处理的;
代理Object
对于Object
,Vue3
是直接使用Proxy
对数据进行代理,然后在get
和set
钩子中进行依赖收集和派发更新;
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
钩子的核心逻辑提取出来,我们可以看到,它最主要做的只有三件事:
- 对于数组,如果调用它的原型上的方法,比如
push
、pop
等,那么返回的是经过代理的方法,这个后面会讲到; - 获取对象的结果,最后返回这个结果;
- 收集依赖 (这里的收集依赖是可以放到前面去的,因为在源码中,这个期间还做了其他事,所以现在是放在这里的);
我们一个一个的分析,先放下对于数组的处理,我们先来看看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
指向的问题,这是一个很有意思的事情,因为对象可以设置getter
、setter
函数,直接看下面的代码:
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
,这样会造成什么问题呢?看执行的效果截图:
这里可以看到,在代理的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
,所以Vue3
在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);
}
};
}
这里的关键就是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;
}
数组的查询方法,例如includes
、indexOf
、lastIndexOf
,这些方法的使用如下:
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;
};
});
总体来说这里就是会匹配两次结果,第一次是使用用户传入的参数去与用户传入的参数匹配,如何没有匹配到,再将用户传入的参数转换为原始值,再去匹配;
而对于会修改数组本身的方法,例如push
、pop
、shift
、unshift
、splice
,这些方法的使用如下:
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;
};
});
这里的实现是非常简单的,并没有做其他的处理,只是简单的暂停和恢复依赖收集,简单的看一下pauseTracking
和resetTracking
的实现:
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
才会进行依赖收集;
所以这里的pauseTracking
和resetTracking
就是通过改变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
钩子里面的内容以及差不多了,处理了对象的getter
和setter
方法的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;
};
}
这里没有太多边界处理的代码,大体的流程如下:
- 将旧值和新值都转换为原始对象,简化的代码只是为了做差异对比,判断新值和旧值是否相同;
- 判断当前操作的属性是否存在与原始对象中;
- 判断当前操作的对象是否就是原始对象,如果是,那么就会触发依赖;
- 如果当前操作的属性不存在与原始对象中,那么就会触发
add
依赖; - 如果当前操作的值和旧值不同,那么就会触发
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...in
,for...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
是什么,为什么要这样做?这些都是依赖收集和依赖触发的逻辑,后面会单独写一篇文章来讲解;
所以看到这里,没有讲track
和trigger
不要着急,当熟悉Vue3
对数据拦截的处理流程,后面再来看track
和trigger
就会比较容易理解;
代理 Map
、Set
、WeakMap
、WeakSet
Vue3
对于Map
、Set
、WeakMap
、WeakSet
的处理是不同于Object
的;
因为Map
、Set
、WeakMap
、WeakSet
的设置值和获取值的方式和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
就是对Map
、Set
、WeakMap
、WeakSet
的代理handler
;
baseHandlers
和collectionHandlers
都是通过reactive
传入的,而指向的都是全局的mutableHandlers
和mutableCollectionHandlers
;
mutableCollectionHandlers
mutableCollectionHandlers
看上面的定义,只有一个get
钩子,根据上面的讲解,我们也知道get
钩子的作用;
对于Map
、Set
、WeakMap
、WeakSet
来说,不管是设置值还是获取值,都是通过调用对应的方法来实现的,所以它们的依赖收集和依赖触发都是通过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();
可以看到的这里对Map
、Set
、WeakMap
、WeakSet
的操作方法都进行了拦截,使用自定义的方法来代替原生的方法,这样就可以在自定义的方法中进行一些额外的操作,比如收集依赖、触发更新等。
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
依赖;
但是看了上面的set
和get
方法,感觉像是两个人写的,手动狗头;
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
方法主要是返回target
的size
,实现很简单,就是通过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
方法创建的keys
、values
、entries
方法,这个方法的定义如下:
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;
}
};
};
}
这些都是用来处理keys
、values
、entries
方法的,同时还包括了Symbol.iterator
方法;
这些都是可迭代的方法,所以返回的都是一个迭代器,细节是Map
的Symbol.iterator
方法的数据结构是[key, value]
,而Set
的Symbol.iterator
方法的数据结构是value
;
所以前面判断了一下,后面返回值的时候就可以根据不同的数据结构进行包装;
总结
这一章是对上一章的补充,主要补充reavtive
方法的实现,reavtive
对Object
、Array
、Map
、Set
、WeakMap
、WeakSet
进行了不同的处理;
reavtive
方法的实现主要是通过createReactiveObject
方法实现的,这个方法主要是通过Proxy
对target
进行代理,然后对target
中的每一个属性进行响应式处理;
Vue3
考虑到了各种情况下的响应式处理,所以对代理的handler
完善程度很高,对于Object
类型有get
、set
、delete
、has
、ownKeys
等等钩子,覆盖到了所有的情况;
对于Array
类型补充了对·原型方法的处理以及对length
属性的处理;
对于Map
、Set
、WeakMap
、WeakSet
类型,只有一个get
钩子,因为这里对象通常都是通过对应的操作方法进行操作的,所以只需要对get
钩子进行处理就可以了;
大家好,这里是田八的【源码&库】系列,
Vue3
的源码阅读计划,Vue3
的源码阅读计划不出意外每周一更,欢迎大家关注。如果想一起交流的话,可以点击这里一起共同交流成长
系列章节:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。