1. 准备工作
现在github上下载vue3的源码 Vue3源代码地址。下载完成后简单看一眼目录结构,我们就直接进入 packages=>reactivity=>reactive.ts
文件,深入了解一下vue3中的响应式,reactive
是如何实现的。
接下来跟随我的思路,手写一个简易的reactive
,看看vue在实现过程中都做了哪些考虑和涉及到哪些知识点。
2.手写简易reactive
我们都知道,vue3响应式的实现是基于proxy,并且在 读取数据属性时 为其添加依赖,在设置属性值时 触发更新。基于这点知识,我们可以写出如下代码
reactive.ts
这个文件中实现reactive函数
ts
import { track, trigger } from "./effect";
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
const proxy = new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key)
return res
},
set(target, key, val) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val)
return res
}
})
return proxy
}
可能你对Proxy
和Reflect
不太熟悉?或者不知道为什么返回值
effect.ts
这个文件中涉及到依赖收集,派发更新和监听数据变化的一系列函数
ts
// 暂时不需要知道 track和 trigger中具体做了什么事情
export const track = (target: object, key: unknown) => {
console.log("在收集依赖中", target, `${key}属性被读取`);
};
export const trigger = (target: object, key: unknown, val: unkown) => {
console.log("在派发更新中", target, `${key}属性更改,触发更新`);
};
index.ts
这个文件为主入口文件,用于测试代码返回结果
ts
import { reactive } from "./reactive";
// 定义普通对象,测试是否代理成功
const obj = {
name: 'labmen',
age: 18,
grade: {
math: 90,
english: 15,
get totalScore() {
return this.math + this.english
}
}
}
const proxyObj = reactive(obj)
console.log(proxyObj.name) // 在收集依赖中 name 属性被读取
proxyObj.name = 'xiaoMing' // 在派发更新中 name 属性被更改 触发更新
3.边界问题处理
现在响应式最最最基础的内容就写完了,但是还存在很多问题,传入的参数类型不可能是任意的,所以要先进行类型判断。
3.1 参数是否为对象检测
也就是检测传入值是否是 对象
,如果不是,那么直接返回原值就可以了。
我们新建一个文件夹utils
来写一些工具函数
ts
export function isObject(val: unkown) :val is Record<any,any> {
return val !== null && typeof val === 'object'
}
在reactive
函数中就多了一步处理
ts
import { track, trigger } from "./effect";
import { isObject } from "./utils";
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
const proxy = new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key)
return res
},
set(target, key, val) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val)
return res
}
})
return proxy
}
我们在vue3中如果一个对象 被代理了多次,每次的返回值都是全等的,这样做的好处是:
- 避免重复代理消耗内存
- 防止依赖收集混乱
- 不同proxy的状态可能会不一致
ts
const proxyObj1 = reactive(obj)
const proxyObj2 = reactive(obj)
console.log(proxyObj1 === proxyObj2) // true
所以在我们写的代码中也要加上这个判断
3.2 检测对象是否被代理过,如果代理过就直接返回代理过的数据
我们可以用一个Map
来维护已经代理过数据的状态,为了性能问题可以使用WeakMap
在reactive
函数中就又多了一步处理
ts
import { track, trigger } from "./effect";
import { isObject } from "./utils";
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
// 如果同一个对象已经被代理过了 就不需要再重复代理了
if(proxyMap.get(target)) {
retrun proxyMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key)
return res
},
set(target, key, val) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val)
return res
}
})
// 收集代理过的对象
proxyMap.set(target, proxy)
return proxy
}
3.3 判断传入的是否是代理过的proxy对象
如果传入的是代理过的proxy对象
,那么就不需要再嵌套代理了,直接返回这个对象就可以。我们可以在get
读取属性时,为代理过的对象加一个标识__v_isReactive
如果有这个属性,那么表明现在传入的就是响应式对象了。我们用一个enum来维护这个值,并且加上判断
在reactive
函数中就又又多了一步处理
ts
import { track, trigger } from "./effect";
import { isObject } from "./utils";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
// 如果同一个对象已经被代理过了 就不需要再重复代理了
if(proxyMap.get(target)) {
retrun proxyMap.get(target)
}
// 判断是不是 响应式对象 如果是直接返回
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key)
return res
},
set(target, key, val) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val)
return res
}
})
// 收集代理过的对象
proxyMap.set(target, proxy)
return proxy
}
打印测试
ts
const proxyObj1 = reactive(obj)
const proxyObj2 = reactive(proxyObj1)
console.log(proxyObj1 === proxyObj2) // true
3.4 递归代理对象
我们能看到 原始对象中又有一个对象属性 grade
那么这个对象里的属性 能不能进行依赖收集呢,我们来尝试一下
ts
console.log(proxyObj.grade.math) // 在收集依赖中 grade 属性被读取
我们发现,只有第一层的对象属性可以进行依赖收集,所以我们需要再修改一下reactive
函数
ts
import { track, trigger } from "./effect";
import { isObject } from "./utils";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
// 如果同一个对象已经被代理过了 就不需要再重复代理了
if(proxyMap.get(target)) {
retrun proxyMap.get(target)
}
// 判断是不是 响应式对象 如果是直接返回
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key)
// 如果读取到的属性是对象 那么就进行递归代理
if(isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, val) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val)
return res
}
})
// 收集代理过的对象
proxyMap.set(target, proxy)
return proxy
}
3.5 原始对象中getter中用this读取本身值无法被依赖收集问题
我们发现,grade
中有一项 totalSorce
他的成绩是 math
和english
的加和,按理说当我们读取totalSorce
属性时,由于也用到了math
和english
这两个属性也应该被依赖收集。但是结果却并没有获取到。
其实proxy 的get和set中都有一个receiver
参数,这个参数就相当于是代理后的对象,可以理解为当前对象的this
,我们只需要把这个参数传递下去就可以了
ts
import { track, trigger } from "./effect";
import { isObject } from "./utils";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
// 如果同一个对象已经被代理过了 就不需要再重复代理了
if(proxyMap.get(target)) {
retrun proxyMap.get(target)
}
// 判断是不是 响应式对象 如果是直接返回
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key, receiver)
// 如果读取到的属性是对象 那么就进行递归代理
if(isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, val, receiver) {
// 派发更新
tigger(target, key, val)
const res = Reflect.set(target, key, val, receiver)
return res
}
})
// 收集代理过的对象
proxyMap.set(target, proxy)
return proxy
}
此时再打印 totalSorce
的值我们会发现,依赖收集成功了!
3.6 对于其他读取属性值动作的监听
如果我们想判断一个属性是不是在响应式对象里,那是不是也是读取了响应式对象的属性呢?但是仅仅靠我们上面的代码 还是不足以劫持这种 in
操作
ts
const proxyObj1 = reactive(obj)
console.log('name' in proxyObj1) // 没有触发依赖收集
不过在proxy中 提供了多种数据劫持的方法,比如has就可以获取到我们上面的操作。
那么只需要在处理函数中加一个has
就可以了,但是如果每增加一个处理函数,就要在reactive函数中加一段代码,整个文件会显得十分冗杂,我们把这些函数的定义抽离出来,新建一个文件 baseHandlers.ts
ts
import { reactive } from "./reactive";
import { ReactiveFlags } from "./reactive";
import { isObject } from "./utils";
import { track, trigger } from "./effect";
function get(target: object, key: string | symbol, receiver: object): any {
// 用于判断是不是代理后的对象 打标识
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
track(target, key);
const res = Reflect.get(target, key, receiver);
// 如果读取的值是对象,在递归代理
if (isObject(res)) {
return reactive(res);
}
return res;
}
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
trigger(target, key);
return Reflect.set(target, key, value, receiver);
}
function has(target: object, key: string | symbol): boolean {
track(target, key);
return Reflect.has(target, key);
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
has,
};
再修改一下reactive.ts
ts
import { isObject } from "./utils";
import { mutableHandlers } from "./baseHandlers";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
// 传入类型判断是否为对象,如果不是则直接返回原值
if(!isObject) {
return target
}
// 如果同一个对象已经被代理过了 就不需要再重复代理了
if(proxyMap.get(target)) {
retrun proxyMap.get(target)
}
// 判断是不是 响应式对象 如果是直接返回
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// vue3的核心 通过 proxy 代理对象
const proxy = new Proxy(target, mutableHandlers);
// 收集代理过的对象
proxyMap.set(target, proxy)
return proxy
}
现在我们也可以收集 in
操作符的依赖了
ts
const proxyObj = reactive(obj);
console.log("age" in proxyObj);
// 在收集依赖中 age属性被读取
// true
3.7 完善和区分其他数据劫持的方法
对于对象的操作,还有新增属性,删除属性以及遍历属性等。对于这些繁多的操作,我们需要在依赖收集和派发更新中 具体到是哪个操作触发了 我们这两个函数 新增一个operations.ts
ts
export const enum TrackOpTypes {
GET = "get",
HAS = "has",
ITERATE = "iterate",
}
export const enum TriggerOpTypes {
SET = "set",
ADD = "add",
DELETE = "delete",
}
同时需要对trigger
的条件做一些判断,比如是SET
值还是ADD
值?如果修改的值和原来的值相等我们就不需要触发更新了?
utils.ts
中新增 hasChanged函数用来判断新旧值是否相同
ts
export const hasChanged = (oldVal: any, val: any) => {
// 不用 === 要考虑到 NaN === NaN 和 +0 === -0 的问题
return !Object.is(oldVal, val);
};
baseHandlers.ts
ts
import { reactive } from "./reactive";
import { ReactiveFlags } from "./reactive";
import { isObject, hasChanged } from "./utils";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operations";
// 迭代的时候获取不到具体的key值,我们自定义一个symbol来标识迭代器
const ITERATE_KEY = Symbol("iterate");
function get(target: object, key: string | symbol, receiver: object): any {
// 用于判断是不是代理后的对象 打标识
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
track(target, TrackOpTypes.GET, key);
const res = Reflect.get(target, key, receiver);
// 如果读取的值是对象,在递归代理
if (isObject(res)) {
return reactive(res);
}
return res;
}
// 要判断是否有这个属性,区别修改还是新增,并且要判断前后更改的值是否相同
function set(
target: Record<string | symbol, any>,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 判断是否有这个属性
const hadKey = target.hasOwnProperty(key);
const oldVal = target[key];
// 如果没有这个属性,说明是新增操作
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key);
// 如果有这个属性,并且确实改变了前后的值 那么派发更新
} else if (hasChanged(oldVal, value)) {
trigger(target, TriggerOpTypes.SET, key);
}
return Reflect.set(target, key, value, receiver);
}
function has(target: object, key: string | symbol): boolean {
track(target, TrackOpTypes.HAS, key);
return Reflect.has(target, key);
}
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY);
return Reflect.ownKeys(target);
}
// 删除的时候要判断本身对象中有没有这个属性,有才触发更新
function deleteProperty(target: object, key: string | symbol) {
// 判断对象是否有这个属性
const hadKey = target.hasOwnProperty(key);
// 并且判断删除是否成功
const res = Reflect.deleteProperty(target, key);
if (hadKey && res) {
trigger(target, TriggerOpTypes.DELETE, key);
}
return res;
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
has,
ownKeys,
deleteProperty,
};
让我们来测试一下,大功告成
小结
以上关于对象相关的操作的依赖收集,已经定义好了。接下来是对于数组的处理 和 依赖收集以及派发更新具体做了什么事情