reactive
作为Vue3
中的外围API
之一,其背地的实现原理是十分值得咱们学习以及借鉴的;
上一篇文章只是初略的过了一遍Vue3
的响应式流程,就那么初略的一瞥就有上万字,而且还没讲到具体的解说实现原理;
所以这一篇将具体的解析reactive
的实现原理,后续还会补上effect
的原理和思维,以及响应式的整体流程都将从新梳理,谢谢大家的反对;
因为上一篇文章曾经解说过源码,所以这一篇文章的节奏会放慢,尽管会放慢节奏,然而内容还是很多,万字正告,耐下性子能力继续成长。
reactive
可代理的类型
跟着上篇咱们晓得了reactive
可代理的数据类型有Object
、Array
、Map
、Set
、WeakMap
、WeakSet
;
这代表着咱们能够创立响应式的数据类型有这些,先用代码看看咱们到底能够创立响应式的数据类型有哪些:
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";
能够看到的是,咱们创立响应式的数据只有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.fooconst 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; }});// 假如这个是一个 effectfunction 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 的代理 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
就是对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 实现原理以及源码剖析