共计 23906 个字符,预计需要花费 60 分钟才能阅读完成。
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 函数不懂间接点这里:
- 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
,所以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
的源码浏览打算不出意外每周一更,欢送大家关注。如果想一起交换的话,能够点击这里一起独特交换成长
系列章节:
- 【源码 & 库】跟着 Vue3 学习前端模块化
- 【源码 & 库】在调用 createApp 时,Vue 为咱们做了那些工作?
- 【源码 & 库】细数 Vue3 的实例办法和属性背地的故事
- 【源码 & 库】Vue3 中的 nextTick 魔法背地的原理
- 【源码 & 库】Vue3 的响应式外围 reactive 和 effect 实现原理以及源码剖析