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] 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
钩子进行处理就可以了;