Vue
的响应式零碎很让人着迷,Vue2
应用的是 Object.defineProperty
,Vue3
应用的是Proxy
,这个是大家都晓得的技术点;
然而晓得了这些个技术点就能写出一个响应式零碎吗?答案是必定是 NO
,Vue
的响应式零碎是一个非常复杂的零碎,技术只是实现的伎俩,明天咱们就来看看背地实现的思维。
本章内容有点多,如果不能耐着性子看的话,倡议先看看我在线实现的小 demo 去了解核心思想,不明确再来在文章中寻找答案:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js
reactive 和 effect
Vue3
的响应式零碎通过官网的 API
能够看到有很多,例如 ref
、computed
、reactive
、readonly
、watchEffect
、watch
等等,这些都是 Vue3
的响应式零碎的一部分;
reactive
reactive
依据官网的介绍,有如下特点:
- 接管一个一般对象,返回一个响应式的代理对象;
- 响应式的对象是深层的,会影响对象外部所有嵌套的属性;
- 会主动对
ref
对象进行解包; - 对于数组、对象、
Map
、Set
等原生类型中的元素,如果是ref
对象不会主动解包; - 返回的对象会通过
Proxy
进行包装,所以不等于原始对象;
下面的这些特点都是能够在官网中有介绍,如果我说的不是很好了解倡议去官网看看,官网对这些特点都有具体的介绍,并且还有示例代码。
对于 reactive
的作用其实应用 Vue3
的同学都晓得是干嘛的,就不多说了。
effect
effect
在官网上是没有提到这个 API
的,然而在源码中是有的,并且咱们也是能够间接应用,如下代码所示:
import {reactive, effect} from "vue";
const data = reactive({
foo: 1,
bar: 2
});
effect(() => {console.log(data.foo);
});
data.foo = 10;
通常状况下咱们是不会间接应用 effect
的,因为 effect
是一个底层的 API
,在咱们应用Vue3
的时候 Vue
默认会帮咱们调用 effect
,所以咱们的关注点通常都是在reactive
上。
然而 reactive
须要和 effect
配合应用才会有响应式的成果,所以咱们须要理解一下 effect
的作用。
effect
间接翻译为 作用
,意思是 使其产生作用
,这个 使其
的其
就是咱们传入的函数,所以 effect
的作用就是让咱们传入的函数产生作用,也就是执行这个函数。
然而 effect
是怎么晓得咱们传入的函数须要执行呢?这些答案都在源码中,当初来进入正式的源码浏览环节。
源码
Vue
的响应式零碎的源码在 packages/reactivity
目录下,Vue3
将其独自抽离进去为一个独立的零碎,咱们能够看看这个工程的 README
文件;
依据 README
文件中的介绍,响应零碎被内联到面向用户的生产和开发构建的包中,然而也能够独自应用。
如果你想独自应用的话,倡议不要和 Vue
混合应用,因为独立应用的话,和 Vue
的响应式零碎外部的数据并不互通,这样就会有两个响应式零碎发挥作用,这样可能会有产生一些不可预知的问题。
响应式零碎出了对 Array
、Map
、WeakMap
、Set
和WeakSet
这些原生类型进行了响应式解决,对其余的原生类型,例如 Date
、RegExp
、Error
等等,都没有进行响应式解决。
reactive
reactive
的源码在 packages/reactivity/src/reactive.ts
文件中,还是老样子,咱们不看原始的 ts
代码,间接看编译后的 js
代码,这样更容易了解。
function reactive(target) {
// 如果对只读的代理对象进行再次代理,那么应该返回原始的只读代理对象
if (isReadonly(target)) {return target;}
// 通过 createReactiveObject 办法创立响应式对象
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
reactive
的源码很简略,就是调用了 createReactiveObject
办法,这个办法是一个工厂办法,用来创立响应式对象的,咱们来看看这个办法的源码。
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 如果 target 不是对象,那么间接返回 target
if (!isObject(target)) {
{console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// 如果 target 曾经是一个代理对象了,那么间接返回 target
// 异样:如果对一个响应式对象调用 readonly() 办法
if (target["__v_raw" /* ReactiveFlags.RAW */] &&
!(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {return target;}
// 如果 target 曾经有对应的代理对象了,那么间接返回代理对象
const existingProxy = proxyMap.get(target);
if (existingProxy) {return existingProxy;}
// 对于不能被察看的类型,间接返回 target
const targetType = getTargetType(target);
if (targetType === 0 /* TargetType.INVALID */) {return target;}
// 创立一个响应式对象
const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
// 将 target 和 proxy 保留到 proxyMap 中
proxyMap.set(target, proxy);
// 返回 proxy
return proxy;
}
createReactiveObject
办法的源码也很简略,最开始的一些代码都是对须要代理的 target
进行一些判断,判断的边界都是 target
不是对象的状况和 target
曾经是一个代理对象的状况;
其中的外围的代码次要是最初七行代码:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 对于不能被察看的类型,间接返回 target
const targetType = getTargetType(target);
if (targetType === 0 /* TargetType.INVALID */) {return target;}
// 创立一个响应式对象
const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
// 将 target 和 proxy 保留到 proxyMap 中
proxyMap.set(target, proxy);
// 返回 proxy
return proxy;
}
这里有一个 targetType
的判断,那么这个 targetType
是什么呢?咱们来看看 getTargetType
办法的源码:
// 获取原始数据类型
const toRawType = (value) => {// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1);
};
// 获取数据类型
function targetTypeMap(rawType) {switch (rawType) {
case 'Object':
case 'Array':
return 1 /* TargetType.COMMON */;
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return 2 /* TargetType.COLLECTION */;
default:
return 0 /* TargetType.INVALID */;
}
}
// 获取 target 的类型
function getTargetType(value) {return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
? 0 /* TargetType.INVALID */
: targetTypeMap(toRawType(value));
}
这里次要看的是 Vue
写的代码正文,这里的正文是 Vue
的ts
源码中的枚举类型,最初返回的值枚举类型的值:
const enum TargetType {
// 有效的数据类型,对应的值是 0,示意 Vue 不会对这种类型的数据进行响应式解决
INVALID = 0,
// 一般的数据类型,对应的值是 1,示意 Vue 会对这种类型的数据进行响应式解决
COMMON = 1,
// 汇合类型,对应的值是 2,示意 Vue 会对这种类型的数据进行响应式解决
COLLECTION = 2
}
export const enum ReactiveFlags {
// 用于标识一个对象是否不可被转为代理对象,对应的值是 __v_skip
SKIP = '__v_skip',
// 用于标识一个对象是否是响应式的代理,对应的值是 __v_isReactive
IS_REACTIVE = '__v_isReactive',
// 用于标识一个对象是否是只读的代理,对应的值是 __v_isReadonly
IS_READONLY = '__v_isReadonly',
// 用于标识一个对象是否是浅层代理,对应的值是 __v_isShallow
IS_SHALLOW = '__v_isShallow',
// 用于保留原始对象的 key,对应的值是 __v_raw
RAW = '__v_raw'
}
这里的枚举值以及含意都列出来了,而后联合源码,咱们就能够更清晰的了解每段的代码的含意了。
collectionHandlers 和 baseHandlers
其实代理依据这几年的推广,早就不是什么陈腐事物了,createReactiveObject
办法最初返回的就是一个代理对象;
关键点就在于这个代理对象的 handler
,而这个handler
就是 collectionHandlers
和baseHandlers
这两个对象;
源码中通过 targetType
来判断应用哪个 handler
,targetType
为2
的时候应用collectionHandlers
,否则应用baseHandlers
;
其实这个 targetType
依据枚举值也就只有 3
个值,最初走向代理的也就只有两种状况:
targetType
为1
的时候,这个时候target
是一个一般的对象或者数组,这个时候应用baseHandlers
;targetType
为2
的时候,这个时候target
是一个汇合类型,这个时候应用collectionHandlers
;
而这两个 handler
的是通过内部传入的,也就是 createReactiveObject
办法的第三个和第四个参数,而传入这两个参数的中央就是 reactive
办法:
function reactive(target) {
// ...
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
能够看到的是 mutableHandlers
和mutableCollectionHandlers
别离对应 baseHandlers
和collectionHandlers
;
而这两个 handler
的定义在 reactivity/src/baseHandlers.ts
和reactivity/src/collectionHandlers.ts
中;
感兴趣的能够去翻看一下这两个文件的源码,这里还是贴出打包之后的代码,先从 baseHandlers
开始;
baseHandlers
留神这里的 baseHandlers
指向的是 mutableHandlers
,mutableHandlers
是baseHandlers
的一个export
;
const mutableHandlers = {
get: get$1,
set: set$1,
deleteProperty,
has: has$1,
ownKeys
};
这里别离定义了 get
、set
、deleteProperty
、has
、ownKeys
这几个办法拦截器,简略介绍一下作用:
get
:拦挡对象的getter
操作,比方obj.name
;set
:拦挡对象的setter
操作,比方obj.name = '田八'
;deleteProperty
:拦挡delete
操作,比方delete obj.name
;has
:拦挡in
操作,比方'name' in obj
;ownKeys
:拦挡Object.getOwnPropertyNames
、Object.getOwnPropertySymbols
、Object.keys
等操作;
更具体的能够看看 MDN 的介绍;
再来看看这些个拦截器的具体实现。
get
const get$1 = /*#__PURE__*/ createGetter();
function createGetter(isReadonly = false, shallow = false) {
// 闭包返回 get 拦截器办法
return function get(target, key, receiver) {
// 如果拜访的是 __v_isReactive 属性,那么返回 isReadonly 的取反值
if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {return !isReadonly;}
// 如果拜访的是 __v_isReadonly 属性,那么返回 isReadonly 的值
else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {return isReadonly;}
// 如果拜访的是 __v_isShallow 属性,那么返回 shallow 的值
else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {return shallow;}
// 如果拜访的是 __v_raw 属性,并且有一堆条件满足,那么返回 target
else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {return target;}
// target 是否是数组
const targetIsArray = isArray(target);
// 如果不是只读的
if (!isReadonly) {
// 如果是数组,并且拜访的是数组的一些办法,那么返回对应的办法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果拜访的是 hasOwnProperty 办法,那么返回 hasOwnProperty 办法
if (key === 'hasOwnProperty') {return hasOwnProperty;}
}
// 获取 target 的 key 属性值
const res = Reflect.get(target, key, receiver);
// 如果是内置的 Symbol,或者是不可追踪的 key,那么间接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res;}
// 如果不是只读的,那么进行依赖收集
if (!isReadonly) {track(target, "get" /* TrackOpTypes.GET */, key);
}
// 如果是浅的,那么间接返回 res
if (shallow) {return res;}
// 如果 res 是 ref,对返回的值进行解包
if (isRef(res)) {
// 对于数组和整数类型的 key,不进行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// 如果 res 是对象,递归代理
if (isObject(res)) {
// 将返回的值也转换为代理。咱们在这里进行 isObject 查看,以防止有效的值正告。// 还须要提早拜访 readonly 和 reactive,以防止循环依赖。return isReadonly ? readonly(res) : reactive(res);
}
// 返回 res
return res;
};
}
略微有点简单,然而也不难理解,我来拆解一下:
function get(target, key, receiver) {
// 如果拜访的是 __v_isReactive 属性,那么返回 isReadonly 的取反值
if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {return !isReadonly;}
// 如果拜访的是 __v_isReadonly 属性,那么返回 isReadonly 的值
else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {return isReadonly;}
// 如果拜访的是 __v_isShallow 属性,那么返回 shallow 的值
else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {return shallow;}
// 如果拜访的是 __v_raw 属性,并且有一堆条件满足,那么返回 target
else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {return target;}
// ...
};
这一段代码是为了解决一些非凡的属性,这些都是 Vue
外部定义好的,就是下面提到过的枚举值,用于判断是否是 reactive
、readonly
、shallow
等等。
这一段代码对于咱们了解源码并不重要,重要的是上面一段:
function get(target, key, receiver) {
// ...
// target 是否是数组
const targetIsArray = isArray(target);
// 如果不是只读的
if (!isReadonly) {
// 如果是数组,并且拜访的是数组的一些办法,那么返回对应的办法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果拜访的是 hasOwnProperty 办法,那么返回 hasOwnProperty 办法
if (key === 'hasOwnProperty') {return hasOwnProperty;}
}
// 获取 target 的 key 属性值
const res = Reflect.get(target, key, receiver);
// 如果是内置的 Symbol,或者是不可追踪的 key,那么间接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res;}
// 如果不是只读的,那么进行依赖收集
if (!isReadonly) {track(target, "get" /* TrackOpTypes.GET */, key);
}
// 如果是浅的,那么间接返回 res
if (shallow) {return res;}
// 如果 res 是 ref,对返回的值进行解包
if (isRef(res)) {
// 对于数组和整数类型的 key,不进行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// 如果 res 是对象,递归代理
if (isObject(res)) {
// 将返回的值也转换为代理。咱们在这里进行 isObject 查看,以防止有效的值正告。// 还须要提早拜访 readonly 和 reactive,以防止循环依赖。return isReadonly ? readonly(res) : reactive(res);
}
// 返回 res
return res;
};
这一段还是太多了,然而其实每段代码都是为了实现一个独立的需要,咱们再来拆解一下:
- 对数组的办法拜访解决
function get(target, key, receiver) {
// ...
// target 是否是数组
const targetIsArray = isArray(target);
// 如果不是只读的
if (!isReadonly) {
// 如果是数组,并且拜访的是数组的一些办法,那么返回对应的办法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果拜访的是 hasOwnProperty 办法,那么返回 hasOwnProperty 办法
if (key === 'hasOwnProperty') {return hasOwnProperty;}
}
// ...
};
这一段代码是为了解决数组的一些办法,比方 push
、pop
等等,如果咱们在调用这些办法的时候,就会进入这一段代码,而后返回对应的办法,例如:
const arr = reactive([1, 2, 3]);
arr.push(4);
这些办法都在 arrayInstrumentations
中,这次不做重点剖析,前面会专门解说。
- 获取返回值,返回值的特地看待
function get(target, key, receiver) {
// ...
// 获取 target 的 key 属性值
const res = Reflect.get(target, key, receiver);
// ...
};
走到这里,就须要获取 target
的key
属性值了,这里应用了Reflect.get
;
这个办法是 ES6
中新增的,用于拜访对象的属性,和 target[key]
是等价的,然而 Reflect.get
能够传入 receiver
,这个参数是用来绑定this
的;
这是为了解决 Proxy
的this
指向问题,这里不做过多的解释,前面会专门解说,Reflect
不理解的看:MDN Reflect
- 非凡属性的不进行依赖收集
function get(target, key, receiver) {
// ...
// 如果是内置的 Symbol,或者是不可追踪的 key,那么间接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res;}
// ...
};
这一步是为了过滤一些非凡的属性,例如原生的 Symbol
类型的属性,如:Symbol.iterator
、Symbol.toStringTag
等等,这些属性不须要进行依赖收集,因为它们是内置的,不会扭转;
还有一些不可追踪的属性,如:__proto__
、__v_isRef
、__isVue
这些属性也不须要进行依赖收集;
- 进行依赖收集
function get(target, key, receiver) {
// ...
// 如果不是只读的,那么进行依赖收集
if (!isReadonly) {track(target, "get" /* TrackOpTypes.GET */, key);
}
// ...
};
这一步是为了进行依赖收集,这里调用了 track
办法,这个办法在 effect
中会用到,稍后会解说;
- 浅的不进行递归代理
function get(target, key, receiver) {
// ...
// 如果是浅的,那么间接返回 res
if (shallow) {return res;}
// ...
};
这一步是为了解决 shallow
的状况,如果是 shallow
的,那么就不须要进行递归代理了,间接返回 res
即可;
- 对返回值进行解包
function get(target, key, receiver) {
// ...
// 如果 res 是 ref,对返回的值进行解包
if (isRef(res)) {
// 对于数组和整数类型的 key,不进行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// ...
};
这一步是为了解决 ref
的状况,如果 res
是ref
,那么就对 res
进行解包,这里有一个判断,如果是数组,并且 key
是整数类型,那么就不进行解包;
- 对返回值进行代理
function get(target, key, receiver) {
// ...
// 如果 res 是对象,那么对返回的值进行代理
if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res);
}
// ...
};
如果是对象,那么就对 res
进行代理,这里有一个判断,如果是 readonly
的,那么就应用 readonly
办法进行代理,否则就应用 reactive
办法进行代理;
最初就是返回 res
了,这里就是 Vue3
的get
办法的全部内容了,其实拆分下来就容易了解多了,上面咱们来看看 Vue3
的set
办法;
set
const set$1 = /*#__PURE__*/ createSetter();
function createSetter(shallow = false) {
// 闭包返回一个 set 办法
return function set(target, key, value, receiver) {
// 获取旧值
let oldValue = target[key];
// 如果旧值是只读的,并且是 ref,并且新值不是 ref,那么间接返回 false,代表设置失败
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false;}
// 如果不是浅的
if (!shallow) {
// 如果新值不是浅的,并且不是只读的
if (!isShallow(value) && !isReadonly(value)) {
// 获取旧值的原始值
oldValue = toRaw(oldValue);
// 获取新值的原始值
value = toRaw(value);
}
// 如果指标对象不是数组,并且旧值是 ref,并且新值不是 ref,那么设置旧值的 value 为新值,并且返回 true,代表设置胜利
// ref 的值是在 value 属性上的,这里判断了旧值的代理类型,所以设置到了旧值的 value 上
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
// 如果是数组,并且 key 是整数类型
const hadKey = isArray(target) && isIntegerKey(key)
// 如果 key 小于数组的长度,那么就是有这个 key
? Number(key) < target.length
// 如果不是数组,那么就是一般对象,直接判断是否有这个 key
: hasOwn(target, key);
// 通过 Reflect.set 设置值
const result = Reflect.set(target, key, value, receiver);
// 如果指标对象是原始数据的原型链中的某个元素,则不会触发依赖收集
if (target === toRaw(receiver)) {
// 如果没有这个 key,那么就是新增了一个属性,触发 add 事件
if (!hadKey) {trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
}
// 如果有这个 key,那么就是批改了一个属性,触发 set 事件
else if (hasChanged(value, oldValue)) {trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
}
}
// 返回后果,这个后果为 boolean 类型,代表是否设置胜利
// 只是代理相干,,和业务无关,必须要返回是否设置胜利的后果
return result;
};
}
set
办法的实现其实整体要比 get
办法的实现要简单一些,尽管代码比 get
要少一些,不过整体梳理下来,大体分为上面几个步骤:
- 获取旧值
function set(target, key, value, receiver) {
// 获取旧值
let oldValue = target[key];
// ...
};
这里的旧值就是 target[key]
的值,旧值在 Vue3
中有很多用途,会贯通整个流程,这里先不开展讲,前面会讲到;
- 判断旧值是否是只读的
function set(target, key, value, receiver) {
// ...
// 如果旧值是只读的,并且是 ref,并且新值不是 ref,那么间接返回 false,代表设置失败
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false;}
// ...
};
只读的 ref
是不能被批改的,所以这里就间接返回 false
了,代表设置失败;
然而这里须要有很多的条件,首先旧值必须是只读的,其次旧值必须是ref
,最初新值不能是ref
,如上面的例子:
const refObj = ref({
a: 1,
b: 2
});
const readonlyObj = readonly(refObj);
const obj = reactive({readonlyObj})
obj.readonlyObj = 10;
console.log(obj.readonlyObj); // 设置失败
obj.readonlyObj = ref(10);
console.log(obj.readonlyObj); // 设置胜利
很奇怪的断定,集体的常识储备量还不够,没想明确为什么要有这么样一个的断定才会设置失败。
- 判断是否是浅的
function set(target, key, value, receiver) {
// ...
// 如果不是浅的
if (!shallow) {// ...}
// ...
};
在判断是否不是浅层响应的时候,这个参数是通过闭包保留下来的,不是浅层响应的时候,这个外部会做两件事件:
- 获取旧值的原始值和新值的原始值
function set(target, key, value, receiver) {
// ...
// 如果不是浅的
if (!shallow) {
// 如果新值不是浅的,并且不是只读的
if (!isShallow(value) && !isReadonly(value)) {
// 获取旧值的原始值
oldValue = toRaw(oldValue);
// 获取新值的原始值
value = toRaw(value);
}
}
// ...
};
这里须要先判断新值是否不是浅层响应的,并且不是只读的,如果是的话,那么就不须要获取原始值了,因为这个时候新值就是原始值了;
这里因为如果新值是浅层响应的,那就阐明这个响应式对象的元素只有一层响应式,只会关怀以后对象的响应式,以后对象的元素是否是响应式的就不关怀了,所以不必获取原始值,间接笼罩准则就能够了;
deleteProperty
function deleteProperty(target, key) {
// 以后对象是否有这个 key
const hadKey = hasOwn(target, key);
// 旧值
const oldValue = target[key];
// 通过 Reflect.deleteProperty 删除属性
const result = Reflect.deleteProperty(target, key);
// 如果删除胜利,并且以后对象有这个 key,那么就触发 delete 事件
if (result && hadKey) {trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
}
// 返回后果,这个后果为 boolean 类型,代表是否删除胜利
return result;
}
deleteProperty
办法的实现比照 get
和set
办法的实现都要简略很多,也没有什么特地的中央,就是通过 Reflect.deleteProperty
删除属性,而后通过 trigger
触发 delete
事件,最初返回删除是否胜利的后果;
has
function has$1(target, key) {
// 通过 Reflect.has 判断以后对象是否有这个 key
const result = Reflect.has(target, key);
// 如果以后对象不是 Symbol 类型,或者以后对象不是内置的 Symbol 类型,那么就触发 has 事件
if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, "has" /* TrackOpTypes.HAS */, key);
}
// 返回后果,这个后果为 boolean 类型,代表以后对象是否有这个 key
return result;
}
has
办法的实现也是比较简单的,就是通过 Reflect.has
判断以后对象是否有这个 key,而后通过 track
触发 has
事件,最初返回是否有这个 key
的后果;
ownKeys
function ownKeys(target) {
// 间接触发 iterate 事件
track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
// 通过 Reflect.ownKeys 获取以后对象的所有 key
return Reflect.ownKeys(target);
}
ownKeys
办法的实现也是比较简单的,间接触发 iterate
事件,而后通过 Reflect.ownKeys
获取以后对象的所有 key,最初返回这些 key;
留神点在于对数组的非凡解决,如果以后对象是数组的话,那么就会触发 length
的iterate
事件,如果不是数组的话,那么就会触发 ITERATE_KEY
的iterate
事件;
这一块的区别都是在 track
办法中才会有体现,这个就是响应式的外围思路,前面会具体解说;
effect
下面讲完了 reactive
办法,接下来就是 effect
办法,effect
办法的作用是创立一个副作用函数,这个函数会在依赖的数据发生变化的时候执行;
依赖收集和触发更新的过程先不要焦急,等讲完 effect
办法之后,再来剖析这个过程,先看看 effect
办法的实现:
function effect(fn, options) {
// 如果 fn 对象上有 effect 属性
if (fn.effect) {
// 那么就将 fn 替换为 fn.effect.fn
fn = fn.effect.fn;
}
// 创立一个响应式副作用函数
const _effect = new ReactiveEffect(fn);
// 如果有配置项
if (options) {
// 将配置项合并到响应式副作用函数上
extend(_effect, options);
// 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)if (options.scope)
// 那么就将 scope 属性记录到响应式副作用函数上(相似一个作用域链)recordEffectScope(_effect, options.scope);
}
// 如果没有配置项,或者配置项中没有 lazy 属性,或者配置项中的 lazy 属性为 false
if (!options || !options.lazy) {
// 那么就执行响应式副作用函数
_effect.run();}
// 将 _effect.run 的 this 指向 _effect
const runner = _effect.run.bind(_effect);
// 将响应式副作用函数赋值给 runner.effect
runner.effect = _effect;
// 返回 runner
return runner;
}
其实这里的源码一下并不能看明确具体想要干嘛,而且外部的调用,或者说数据的指向也比较复杂;
然而梳理下来,这里的关键点有两个局部:
- 创立一个响应式副作用函数
const _effect = new ReactiveEffect(fn)
; - 返回一个
runner
函数,能够通过这个函数来执行响应式副作用函数;
ReactiveEffect
先来剖析下 ReactiveEffect
这个类,这个类的作用是创立一个响应式副作用函数,这个函数会在依赖的数据发生变化的时候执行;
class ReactiveEffect {constructor(fn, scheduler = null, scope) {
// 副作用函数
this.fn = fn;
// 调度器,用于管制副作用函数何时执行
this.scheduler = scheduler;
// 标记位,用于标识以后 ReactiveEffect 对象是否处于活动状态
this.active = true;
// 响应式依赖项的汇合
this.deps = [];
// 父级作用域
this.parent = undefined;
// 记录以后 ReactiveEffect 对象的作用域
recordEffectScope(this, scope);
}
run() {// ...}
stop() {// ...}
}
ReactiveEffect
这个类的实现次要体现在两个办法上,一个是 run
办法,一个是 stop
办法;
其余的属性都是用来记录一些数据的,比方 fn
属性就是用来记录副作用函数的,scheduler
属性就是用来记录调度器的,active
属性就是用来记录以后 ReactiveEffect
对象是否处于活动状态的;
这些属性的具体作用将在上面的剖析中解说,先来看看 run
办法的实现;
run
function run() {
// 如果以后 ReactiveEffect 对象不处于活动状态,间接返回 fn 的执行后果
if (!this.active) {return this.fn();
}
// 寻找以后 ReactiveEffect 对象的最顶层的父级作用域
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {if (parent === this) {return;}
parent = parent.parent;
}
try {
// 记录父级作用域为以后流动的 ReactiveEffect 对象
this.parent = activeEffect;
// 将以后流动的 ReactiveEffect 对象设置为“本人”activeEffect = this;
// 将 shouldTrack 设置为 true(示意是否须要收集依赖)shouldTrack = true;
// effectTrackDepth 用于标识以后的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
trackOpBit = 1 << ++effectTrackDepth;
// 这里是用于管制 "effect 调用栈的深度" 在一个阈值之内
if (effectTrackDepth <= maxMarkerBits) {
// 初始依赖追踪标记
initDepMarkers(this);
}
else {
// 革除所有的依赖追踪标记
cleanupEffect(this);
}
// 执行副作用函数,并返回执行后果
return this.fn();}
finally {
// 如果 effect 调用栈的深度 没有超过阈值
if (effectTrackDepth <= maxMarkerBits) {
// 确定最终的依赖追踪标记
finalizeDepMarkers(this);
}
// 执行结束会将 effectTrackDepth 减 1
trackOpBit = 1 << --effectTrackDepth;
// 执行结束,将以后流动的 ReactiveEffect 对象设置为“父级作用域”activeEffect = this.parent;
// 将 shouldTrack 设置为上一个值
shouldTrack = lastShouldTrack;
// 将父级作用域设置为 undefined
this.parent = undefined;
// 延时进行,这个标记是在 stop 办法中设置的
if (this.deferStop) {this.stop();
}
}
}
整体梳理下来,run
办法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖;
整体的流程还是非常复杂的,然而这里的核心思想是各种标识位的设置,以及在执行副作用函数的过程中,会收集依赖;
这里的流程没必要一下就全都理解,当初只须要记住上面这样的流程就能够了:
stop
function stop() {
// 如果以后 流动的 ReactiveEffect 对象是“本人”// 提早进行,须要执行完以后的副作用函数之后再进行
if (activeEffect === this) {
// 在 run 办法中会判断 deferStop 的值,如果为 true,就会执行 stop 办法
this.deferStop = true;
}
// 如果以后 ReactiveEffect 对象处于活动状态
else if (this.active) {
// 革除所有的依赖追踪标记
cleanupEffect(this);
// 如果有 onStop 回调函数,就执行
if (this.onStop) {this.onStop();
}
// 将 active 设置为 false
this.active = false;
}
}
stop
办法的作用就是进行以后的 ReactiveEffect
对象,进行之后,就不会再收集依赖了;
这里的 activeEffect
和this
并不是每次都相等的,因为 activeEffect
会跟着调用栈的深度而变动,而 this
则是固定的;
this.active
标识的本身是否处在活动状态,因为嵌套的 ReactiveEffect
对象,activeEffect
并不一定指向本人,而 this.active
则是本身的状态;
依赖收集
讲了 reactive
和effect
之后,咱们就能够来讲讲依赖收集了;
下面讲了这么多,他们两个如同还没有分割起来,如同是互相独立的,而他们的分割的纽带就是activeEffect
;
常听人说响应式零碎在 getter
中收集依赖,在 setter
中触发依赖,当初回头看看 getter
是怎么收集依赖的;
track
当初回顾一下 getter
的实现,外面有这样的一段代码:
function createGetter(isReadonly = false, shallow = false) {return function get(target, key, receiver) {
// ...
// 如果不是只读的,就会收集依赖
if (!isReadonly) {track(target, "get" /* TrackOpTypes.GET */, key);
}
// ...
return res;
};
}
track
办法的作用就是收集依赖,它的实现如下:
const targetMap = new WeakMap();
/**
* 收集依赖
* @param target 指向的对象
* @param type 操作类型
* @param key 指向对象的 key
*/
function track(target, type, key) {
// 如果 shouldTrack 为 false,并且 activeEffect 没有值的话,就不会收集依赖
if (shouldTrack && activeEffect) {
// 如果 targetMap 中没有 target,就会创立一个 Map
let depsMap = targetMap.get(target);
if (!depsMap) {targetMap.set(target, (depsMap = new Map()));
}
// 如果 depsMap 中没有 key,就会创立一个 Set
let dep = depsMap.get(key);
if (!dep) {depsMap.set(key, (dep = createDep()));
}
// 将以后的 ReactiveEffect 对象增加到 dep 中
const eventInfo = {
effect: activeEffect,
target,
type,
key
};
// 如果 dep 中没有以后的 ReactiveEffect 对象,就会增加进去
trackEffects(dep, eventInfo);
}
}
在这里咱们发现了两个老熟人,一个是 shouldTrack
,一个是activeEffect
,这两个变量都是在effect
办法中呈现过的;
shouldTrack
在下面也讲过,它的作用就是管制是否收集依赖,临时不必深刻;
activeEffect
就是咱们刚刚讲的 ReactiveEffect
对象,它指向的就是以后正在执行的副作用函数;
track
办法的作用就是收集依赖,它的实现非常简单,就是在 targetMap
中记录下 target
和key
;
targetMap
是一个 WeakMap
,它的键是target
,值是一个Map
,这个Map
的键是key
,值是一个Set
;
这意味着,如果咱们在操作 target
的key
时,就会收集依赖,这个时候,target
和 key
就会被记录到 targetMap
中,用代码示意就是:
const obj = {
a: 1,
b: 2
};
const targetMap = new WeakMap();
// 我在操作 obj.a 的时候,就会收集依赖
obj.a;
// 这个时候,targetMap 中就会记录下 obj 和 a
let depsMap = targetMap.get(obj);
if (!depsMap) {targetMap.set(target, (depsMap = new Map()));
}
// createDep 实现很简略,就不在解说的代码外面独自写进去了,具体就是一个 Set,多了两个属性,w 和 n
const createDep = (effects) => {const dep = new Set(effects);
dep.w = 0; // 指向的是 watcher 对象的惟一标识
dep.n = 0; // 指向的是不同的 dep 的惟一标识
return dep;
};
let dep = depsMap.get("a");
if (!dep) {depsMap.set("a", (dep = createDep()));
}
// dep 就是一个 Set,外面寄存的就是以后的 ReactiveEffect 对象
dep.add(activeEffect);
下面就是一个收集依赖的过程,咱们能够看到,targetMap
中记录的是 target
和key
,而 dep
中记录的是 ReactiveEffect
对象;
trigger
当初咱们来看看 trigger
办法,它的作用就是触发依赖,它的实现如下:
/**
* 触发依赖
* @param target 指向的对象
* @param type 操作类型
* @param key 指向对象的 key
* @param newValue 新值
* @param oldValue 旧值
* @param oldTarget 旧的 target
*/
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 获取 targetMap 中的 depsMap
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}
// 创立一个数组,用来寄存须要执行的 ReactiveEffect 对象
let deps = [];
// 如果 type 为 clear,就会将 depsMap 中的所有 ReactiveEffect 对象都增加到 deps 中
if (type === "clear" /* TriggerOpTypes.CLEAR */) {
// 执行所有的 副作用函数
deps = [...depsMap.values()];
}
// 如果 key 为 length,并且 target 是一个数组
else if (key === 'length' && isArray(target)) {
// 批改数组的长度,会导致数组的索引发生变化
// 然而只有两种状况,一种是数组的长度变大,一种是数组的长度变小
// 如果数组的长度变大,那么执行所有的副作用函数就能够了
// 如果数组的长度变小,那么就须要执行索引大于等于新数组长度的副作用函数
const newLength = Number(newValue);
depsMap.forEach((dep, key) => {if (key === 'length' || key >= newLength) {deps.push(dep);
}
});
}
// 其余状况
else {
// key 不是 undefined,就会将 depsMap 中 key 对应的 ReactiveEffect 对象增加到 deps 中
// void 0 就是 undefined
if (key !== void 0) {deps.push(depsMap.get(key));
}
// 执行 add、delete、set 操作时,就会触发的依赖变更
switch (type) {
// 如果 type 为 add,就会触发的依赖变更
case "add" /* TriggerOpTypes.ADD */:
// 如果 target 不是数组,就会触发迭代器
if (!isArray(target)) {
// ITERATE_KEY 再下面介绍过,用来标识迭代属性
// 例如:for...in、for...of,这个时候依赖会收集到 ITERATE_KEY 上
// 而不是收集到具体的 key 上
deps.push(depsMap.get(ITERATE_KEY));
// 如果 target 是一个 Map,就会触发 MAP_KEY_ITERATE_KEY
if (isMap(target)) {
// MAP_KEY_ITERATE_KEY 同下面的 ITERATE_KEY 一样
// 不同的是,它是用来标识 Map 的迭代器
// 例如:Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries()
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
// 如果 key 是一个数字,就会触发 length 依赖
else if (isIntegerKey(key)) {// 因为数组的索引是能够通过 arr[0] 这种形式来拜访的
// 也能够通过这种形式来批改数组的值,所以会触发 length 依赖
deps.push(depsMap.get('length'));
}
break;
// 如果 type 为 delete,就会触发的依赖变更
case "delete" /* TriggerOpTypes.DELETE */:
// 如果 target 不是数组,就会触发迭代器,同下面的 add 操作
if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY));
if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
// 如果 type 为 set,就会触发的依赖变更
case "set" /* TriggerOpTypes.SET */:
// 如果 target 是一个 Map,就会触发迭代器,同下面的 add 操作
if (isMap(target)) {deps.push(depsMap.get(ITERATE_KEY));
}
break;
}
}
// 创立一个 eventInfo 对象,次要是调试的时候会用到
const eventInfo = {
target,
type,
key,
newValue,
oldValue,
oldTarget
};
// 如果 deps 的长度为 1,就会间接执行
if (deps.length === 1) {if (deps[0]) {
{triggerEffects(deps[0], eventInfo);
}
}
}
else {
// 如果 deps 的长度大于 1,这个时候会组装成一个数组,而后再执行
// 这个时候调用就相似一个调用栈
const effects = [];
for (const dep of deps) {if (dep) {effects.push(...dep);
}
}
{triggerEffects(createDep(effects), eventInfo);
}
}
}
tigger
函数的作用就是触发依赖,当咱们批改数据的时候,就会触发依赖,而后执行依赖中的副作用函数。
在这里的实现其实并没有执行,次要是收集一些须要执行的副作用函数,而后在丢给 triggerEffects
函数去执行。
这里的难点在于辨别不同的操作类型,而后收集不同的副作用函数,并且须要了解为什么要这样辨别;
次要是这节写的有点多,所以这一块临时不在这里开展,前面会独自写一篇文章来解说。
当初咱们来看看 triggerEffects
函数:
function triggerEffects(dep, debuggerEventExtraInfo) {
// 如果 dep 不是数组,就会将 dep 转换成数组,因为这里的 dep 可能是一个 Set 对象
const effects = isArray(dep) ? dep : [...dep];
// 执行 computed 依赖
for (const effect of effects) {if (effect.computed) {triggerEffect(effect, debuggerEventExtraInfo);
}
}
// 执行其余依赖
for (const effect of effects) {if (!effect.computed) {triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
这里没什么非凡的,就是转换一下 dep
,而后执行computed
依赖和其余依赖,次要还是在 triggerEffect
函数:
function triggerEffect(effect, debuggerEventExtraInfo) {
// 如果 effect 不是 activeEffect,或者 effect 容许递归,就会执行
if (effect !== activeEffect || effect.allowRecurse) {
// 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行
if (effect.onTrigger) {effect.onTrigger(extend({ effect}, debuggerEventExtraInfo));
}
// 如果 effect 是一个调度器,就会执行 scheduler
if (effect.scheduler) {effect.scheduler();
}
// 否则间接执行 effect.run()
else {effect.run();
}
}
}
这里的逻辑也很简略,然而如果联合 effect
函数,就会发现这里的实现十分的奇妙。
这里的 effect.scheduler
和effect.run
,在咱们看 effect
函数的时候,就曾经呈现过了;
run
就是调用副作用函数,scheduler
是调度器,容许用户自定义调用副作用函数的机会。
还是因为这一篇写的太多了,所以这里就不开展了,前面会独自写一篇文章来解说。
入手工夫
下面讲了那么多,还不如本人动一下手来实现这一整套流程,这样能力更好的了解。
首先咱们梳理一下整个流程:
- 创立一个响应式对象
- 创立一个副作用函数
- 拜访响应式对象,触发依赖收集
- 批改响应式对象,触发依赖执行
// 1. 创立一个响应式对象
const state = reactive({
name: '田八',
age: 18
});
// 2. 创立一个副作用函数
effect(() => {
// 3. 拜访响应式对象,触发依赖收集
console.log(state.name);
});
// 4. 批改响应式对象,触发依赖执行
state.age = 19;
创立一个响应式对象
function reactive(obj) {
return new Proxy(obj, {get(target, key) {
// 依赖收集
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {const res = Reflect.set(target, key, value);
// 依赖触发
trigger(target, key);
return res;
}
});
}
这里只做最简略的实现,所以没有做深度监听,只是简略的监听了一层,并且只有 get
和set
两个钩子,只对 Object
类型的数据做了监听。
创立一个副作用函数
let activeEffect = null;
function effect(fn) {const _effect = new ReactiveEffect(fn);
_effect.run();}
class ReactiveEffect {constructor(fn) {
this.fn = fn;
this.deps = [];}
run() {
activeEffect = this;
this.fn();
activeEffect = null;
}
}
这里的 ReactiveEffect
类,次要是用来存储副作用函数的,而后在 run
函数中,将 activeEffect
设置为以后的 ReactiveEffect
实例,这样在 track
函数中,就能够拿到以后的 ReactiveEffect
实例。
依赖收集
const targetMap = new WeakMap();
function track(target, key) {if (activeEffect) {let depsMap = targetMap.get(target);
if (!depsMap) {targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
}
这里的主流程和 Vue3
的源码是一样的,并没有做什么改变,的确是十分的奇妙。
依赖触发
function trigger(target, key) {const depsMap = targetMap.get(target);
if (!depsMap) {return;}
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {effect.run();
});
}
}
这里简化了流程,间接遍历 dep
,而后执行effect
的run
函数。
在线地址:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js
总结
这一篇文章,次要是解说了 Vue3
的响应式原理,以及如何手动实现一个简略的响应式零碎。
整个响应式零碎的实现,次要是围绕的 effect
函数,reactive
函数,track
函数,trigger
函数这四个函数。
每个函数都只做本人的事件,各司其职:
effect
函数:创立一个副作用函数,次要的作用是来运行副作用函数reactive
函数:创立一个响应式对象,次要的作用是来监听对象的变动track
函数:依赖收集,次要收集的就是effect
函数trigger
函数:依赖触发,次要的作用是来触发track
函数收集的effect
函数
这样的设计,让整个响应式零碎的实现变得十分的简略,也让整个零碎的可维护性变得十分的高。
这里的奇妙点在于依赖收集,当调用副作用函数时,副作用函数外面的响应式对象在调用时,会触发 get
钩子;
get
中调用 track
函数收集 activeEffect
,这个时候activeEffect
是肯定存在的,并且 activeEffect
中的副作用函数是肯定援用了这个响应式对象的,所以这个时候就能够将这个响应式对象和 activeEffect
关联起来。
将以后的对象作为 key
,将activeEffect
作为 value
,存储到targetMap
中,这样就实现了依赖收集。
在响应式对象的 set
钩子中,调用 trigger
函数,将 targetMap
中的 activeEffect
取出来,而后执行 activeEffect
的run
函数,这样就实现了依赖触发。
明天就到了这里,如有不对的中央,欢送大家斧正。
大家好,这里是田八的【源码 & 库】系列,
Vue3
的源码浏览打算,Vue3
的源码浏览打算不出意外每周一更,欢送大家关注。如果想一起交换的话,能够点击这里一起独特交换成长
首发在掘金,无任何引流的意思,后续文章不再强调。
系列章节:
- 【源码 & 库】跟着 Vue3 学习前端模块化
- 【源码 & 库】在调用 createApp 时,Vue 为咱们做了那些工作?
- 【源码 & 库】Vue3 中的 nextTick 魔法背地的原理