边界问题处理
判断传入的参数是否是对象
Section titled “判断传入的参数是否是对象”虽然有TS帮我们做参数限定,但是这只是在编译时,编译之后的js还是需要我们判断传入的参数是否是一个对象
export function reactive<T extends object>(target: T): Texport function reactive(target: object) { // 判断传入的是否是对象 if (!isObject(target)) { console.log('传入的必须是一个对象') return target }
// ......}如果对象已经被代理过,无需再次代理
Section titled “如果对象已经被代理过,无需再次代理”如果再次被代理,同一个原始对象,就生成了两个不同的Proxy对象,而且两个Proxy对象如果去比较的话,还是不相等的。
const obj = { count: 1 }
const state1 = reactive(obj)const state2 = reactive(obj)console.log('state1 === state2', state1 === state2)
此时可以使用 weakMap 来存储代理对象,如果有,直接取出就行了,没有就 set 到 weakMap 中。
const targetMap = new WeakMap()
export function reactive<T extends object>(target: T): Texport function reactive(target: object) { // 判断传入的是否是对象 if (!isObject(target)) { console.log('传入的必须是一个对象') return target }
// 判断是否已经被代理过 if (targetMap.has(target)) { return targetMap.get(target) }
const proxy = new Proxy(target, { get(target, key) { // todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key) return result }, set(target, key, value) { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value) return result }, })
// 存储代理对象 targetMap.set(target, proxy)
return proxy}
这样处理后,state1 和 state2 就相等了
如果是已经被 proxy 处理过的代理对象,无需再次代理
Section titled “如果是已经被 proxy 处理过的代理对象,无需再次代理”const obj = { count: 1 }const state1 = reactive(obj)const state2 = reactive(state1)console.log('state1 === state2', state1 === state2)
const obj = { count: 1 }const state1 = reactive(obj)const state2 = reactive(state1)console.log('state1 === state2', state1 === state2)
const proxy = new Proxy(obj, { get(target, key) { console.log('get 方法被命中', key) return target[key] },})
obj.count // 不会触发 getproxy.count // 会触发 getproxy.xxx // 会触发 get,就算没有 xxx 属性
因此,利用这个特性,在创建 reactive 对象的时候,访问一下这个对象的一个特定值 __v_isReactive,访问的时候如果触发了 get 方法,那么就说明肯定是一个 proxy 代理对象,没有触发,就是一个普通对象
const proxy = new Proxy(target, { get(target, key, receiver) { // 如果访问的是ReactiveFlags.IS_REACTIVE,返回true,因为能进入到这里的肯定是代理对象 if (key === '__v_isReactive') { return true }
track(target, key) const result = Reflect.get(target, key) console.log('result---', result) return result }, // ......})import { isObject } from '@vue/shared'
import { track, trigger } from './effect'
const targetMap = new WeakMap()
export function reactive<T extends object>(target: T): Texport function reactive(target: object) { // 判断传入的是否是对象 if (!isObject(target)) { console.log('传入的必须是一个对象') return target }
// 判断是否已经被代理过 if (targetMap.has(target)) { return targetMap.get(target) }
// 只要读到了__v_isReactive,就返回target // 因为Proxy对象直接拦截了这个属性 if (target['__v_isReactive']) { return target }
const proxy = new Proxy(target, { get(target, key) { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === '__v_isReactive') { return true }
// todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key) return result }, set(target, key, value) { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value) return result }, })
// 存储代理对象 targetMap.set(target, proxy)
return proxy}
这样修改后,就搞定了,然后处理下 ts 的问题,和硬编码的问题,这里直接把 vue 源码的类型拿过来就行了

import { isObject } from '@vue/shared'
import { track, trigger } from './effect'
export const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', RAW = '__v_raw',}
export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any}
export const targetMap = new WeakMap<Target, any>()
export function reactive<T extends object>(target: T): Texport function reactive(target: object) { // 判断传入的是否是对象 if (!isObject(target)) { console.log('传入的必须是一个对象') return target }
// 判断是否已经被代理过 if (targetMap.has(target)) { return targetMap.get(target) }
// 只要读到了__v_isReactive,就返回target // 因为Proxy对象直接拦截了这个属性 if (target[ReactiveFlags.IS_REACTIVE]) { return target }
const proxy = new Proxy(target, { get(target, key) { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === ReactiveFlags.IS_REACTIVE) { return true }
// todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key) return result }, set(target, key, value) { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value) return result }, })
// 存储代理对象 targetMap.set(target, proxy)
return proxy}如果原始对象中有 get,set 访问器属性处理
Section titled “如果原始对象中有 get,set 访问器属性处理”const obj = { a: 1, b: 2, get c() { console.log('this', this) return this.a + this.b },}
const state1 = reactive(obj)
state1.c
这里访问了属性 c,是通过代理对象访问的 c 属性,按道理来说,这里应该会触发 a 和 b 属性的 get,但是从上面图可以看到并没有触发,而且 this 也还是原始对象,并不是 proxy 代理的对象。
处理这个很简单,只需要把 receiver 传进去就行了。receiver 就是代表当前被代理的 proxy 对象
// ...const proxy = new Proxy(target, { get(target, key, receiver) { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === ReactiveFlags.IS_REACTIVE) { return true }
// todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key, receiver) return result }, set(target, key, value, receiver) { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value, receiver) return result },})// ...原始对象属性嵌套处理
Section titled “原始对象属性嵌套处理”const obj = { a: 1, b: 2, c: { d: 3, },}
const state1 = reactive(obj)
state1.c.d
可以看到只收集了 c 的依赖,并没有触发 d 的 get,
const obj = { a: 1, b: 2, c: { d: 3, },}
const state1 = reactive(obj)
// state1.c.d
console.log(state1.c)console.log(state1.c.d)
可以看到 c 对应的对象并没有被 proxy 代理,所以收集不到依赖。
解决方法很简单,只需要在 proxy 中读取属性的时候,加个判断,判断是不是对象,是对象就递归调用就行了
const proxy = new Proxy(target, { get(target, key, receiver) { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === ReactiveFlags.IS_REACTIVE) { return true }
// todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key, receiver)
// 判断是不是对象,是对象就递归代理 if (isObject(result)) { return reactive(result) }
return result }, set(target, key, value, receiver) { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value, receiver) return result },})
这个时候就可以看到 c 对应值的是一个代理对象了。
对于 in 关键字的处理
Section titled “对于 in 关键字的处理”const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
function fn() { console.log('a' in state1)}
fn()对于这种情况也是需要处理的,比如刚开始 a 在,后面又把 a 删了,那么 a 就不在了,那么也是需要收集依赖的
in关键字,在JS内部,触发的是[[HasProperty]]的内部方法,而这个内部方法刚好对应Proxy代理对象的has方法
const proxy = new Proxy(target, { // ... has(target, key) { // 收集依赖 track(target, key) const result = Reflect.has(target, key) return result },})
对 proxy 代理的配置项拆分
Section titled “对 proxy 代理的配置项拆分”将 Proxy 中的配置对象拆分到 baseHandlers.ts中。
import { isObject } from '@vue/shared'
import { track, trigger } from './effect'import { reactive, ReactiveFlags } from './reactive'
function get(target: object, key: string | symbol, receiver: object): any { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === ReactiveFlags.IS_REACTIVE) { return true }
// todo: 收集依赖 track(target, key) // 返回对象的相应属性值 const result = Reflect.get(target, key, receiver)
// 判断是不是对象,是对象就递归代理 if (isObject(result)) { return reactive(result) }
return result}
function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean { // todo: 触发更新 trigger(target, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value, receiver) return result}
function has(target: object, key: string | symbol): boolean { // 收集依赖 track(target, key) const result = Reflect.has(target, key) return result}
export const mutableHandlers: ProxyHandler<object> = { get, set, has,}import { isObject } from '@vue/shared'
import { mutableHandlers } from './baseHandlers'
export const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', RAW = '__v_raw',}
export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any}
export const targetMap = new WeakMap<Target, any>()
export function reactive<T extends object>(target: T): Texport function reactive(target: object) { // 判断传入的是否是对象 if (!isObject(target)) { console.log('传入的必须是一个对象') return target }
// 判断是否已经被代理过 if (targetMap.has(target)) { return targetMap.get(target) }
// 只要读到了__v_isReactive,就返回target // 因为Proxy对象直接拦截了这个属性 if (target[ReactiveFlags.IS_REACTIVE]) { return target }
const proxy = new Proxy(target, mutableHandlers)
// 存储代理对象 targetMap.set(target, proxy)
return proxy}细化依赖收集和触发更新动作
Section titled “细化依赖收集和触发更新动作”当在函数中执行某些动作的时候,需要更细致的区分读取操作:
比如,当下面的情况出现
const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
function fn() { 'a' in state1}state1.a = 123fn()函数中关心的是属性a在state1中是否存在。如果'a' in state1是存在的。那么下面的state1.a = 123这个修改操作,并不会改变'a' in state1有还是没有的状况。换句话说,如果函数中仅仅只是有这么一个判断语句,这样的操作并不会对界面有任何的影响。当然,如果是有一句显示语句那结果又不一样了。
也就是说,在这种情况下,就算是a的值修改了,但是对函数没有影响,那就不应该再次去执行fn()函数,也就不需要再次进行依赖收集
但是如果是下面这种代码
const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
function fn() { 'c' in state1}state1.c = 123fn()如果c本身就是不存在的,那么fn函数中的'c' in state1这个语句,就会影响函数的结果,从而可能会影响视图显示的结果。
也就是说,在这种情况下,c的值修改了,对函数是有影响,那就应该再次去执行fn()函数,进行依赖收集
其实也就是说,我们之前在依赖收集和派发更新的时候,执行行为的划分不太细致,我们应该对执行的行为划分的更加细致,当触发某种动作的时候,才会触发相应的行为。track和trigger函数,我们需要为其添加对应的动作标志。
有了对应的标志,才可以更好的表示,当前我正在读取某个对象的某个属性,或者我正在判断某个对象是否存在。
还是直接把 vue 源码的值先拿过来

export const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate',}
export const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', CLEAR = 'clear',}继续修改 effect 中的 track 和 trigger 方法
import { TrackOpTypes, TriggerOpTypes } from './operations'
export function track(target: object, type: TrackOpTypes, key: unknown) { console.log(`%c依赖收集: target ${JSON.stringify(target)}【${type}】${String(key)}`, 'color: #f40')}
export function trigger(target: object, type: TriggerOpTypes, key: unknown) { console.log(`%c派发更新: target ${JSON.stringify(target)}【${type}】${String(key)}`, 'color: #0f0')}继续修改 baseHandlers.ts 中调用
function get(target: object, key: string | symbol, receiver: object): any { // 如果进入到get方法,说明肯定是一个proxy代理对象 // 如果访问的是__v_isReactive,返回true if (key === ReactiveFlags.IS_REACTIVE) { return true }
// todo: 收集依赖 track(target, TrackOpTypes.GET, key) // 返回对象的相应属性值 const result = Reflect.get(target, key, receiver)
// 判断是不是对象,是对象就递归代理 if (isObject(result)) { return reactive(result) }
return result}
function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean { // todo: 触发更新 trigger(target, TriggerOpTypes.SET, key) // 设置对象的相应属性值 const result = Reflect.set(target, key, value, receiver) return result}
function has(target: object, key: string | symbol): boolean { // 收集依赖 track(target, TrackOpTypes.HAS, key) const result = Reflect.has(target, key) return result}改好后再来看看刚刚两种情况分别的打印,先看第二种情况
const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
function fn() { 'c' in state1}state1.c = 123fn()
可以看到先派发更新,触发 set 操作,然后触发依赖收集 has 操作是没问题的。
再来看看第一种情况
const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
function fn() { 'a' in state1}state1.a = 123fn()
根据上面的分析,其实是不对的,如果已经有相关的属性了,修改属性,如果仅仅只是判断'a' in state1 是不应该在触发读取操作的重新执行的,不需要再次进行依赖收集。只是进一步操作,后续再处理这个问题。
处理遍历操作
Section titled “处理遍历操作”const obj = { a: 1, b: 2,}
const state1 = reactive(obj)
// function fn() {// for (const key in state1) {// }// }
// 或者function fn() { Object.keys(state1)}
fn()可以在ecma262中找到对应的内部方法,发现for-in调用了方法EnumerateObjectProperties,而EnumerateObjectProperties中其实就是调用了反射方法Reflect.ownKeys()。
而Object.keys其实差不多,只是调用了方法EnumerableOwnProperties ,而这个方法直接就调用了内部方法[[OwnPropertyKeys]],代理Proxy中就有对应的处理方法Proxy.ownKeys()
之前已经直接把 vue 源码复制过来了
export const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate',}但是需要注意的是,对象迭代依赖并没有指定的具体键key
因此,当迭代一个对象的属性时,Vue 需要追踪这个对象的所有属性(因为任何属性的增删都可能影响迭代的结果)。所以 Vue 使用 ITERATE_KEY 作为一个特殊的键,表示“这个 effect 依赖于对象的所有属性”
所以这个键,原则上可以使用任意的常量进行表示,比如const ITERATE_KEY = "iterate",但是常量容易造成冲突,因此可以使用Symbol来表示唯一性,const ITERATE_KEY = Symbol("iterate"),既然使用Symbol了,有没有iterate这个名字来表示并不重要,因此也可以直接使用const ITERATE_KEY = Symbol("")来表示,其实在Vue3中,还有专门判断生产环境还是开发环境,ITERATE_KEY的赋值并不一样,暂时只需要用Symbol来表示对象迭代依赖的标识即可

// 用来表示对对象的“迭代依赖”的标识export const ITERATE_KEY = Symbol('')// ...function ownKeys(target: object): (string | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target)}
export const mutableHandlers: ProxyHandler<object> = { get, set, has, ownKeys,}
新增,修改,删除不同动作的处理
Section titled “新增,修改,删除不同动作的处理”新增和修改之前只是做了简单的处理,那么到底是新增还是修改的动作,需要区分出来,因为对于读的操作影响是不一样的。
同时,如果修改的值确实有变化的时候,才触发trigger函数,不然触发的就毫无意义,这个其实就判断一下新旧值是否一样就行了。
// 通过Object.is比较可以避免出现一些特殊情况// 比如NaN和NaN是相等的,+0和-0是不相等的export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)
const hasOwnProperty = Object.prototype.hasOwnPropertyexport const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => hasOwnProperty.call(val, key)这里还添加了 hasOwn,用于判断是新增还是修改
import { hasChanged, hasOwn, isObject } from '@vue/shared'
function set(target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object): boolean { // 判断对象是否有这个属性 const hadKey = hasOwn(target, key)
const oldValue = target[key]
// 原来就没这个属性,那说明是新增 if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key) }
// 设置对象的相应属性值 const result = Reflect.set(target, key, value, receiver) return result}
function deleteProperty(target: Record<string | symbol, unknown>, key: string | symbol) { // 判断对象是否有这个属性,不然删除就没有意义 const hadKey = hasOwn(target, key) // 删除是否成功的结果 const result = Reflect.deleteProperty(target, key) // 对象有这个属性,并且删除成功才会触发更新 if (hadKey && result) { trigger(target, TriggerOpTypes.DELETE, key) } return result}
export const mutableHandlers: ProxyHandler<object> = { get, set, has, ownKeys, deleteProperty,}接下来测试一下这几种情况
const obj = { a: 1, b: 2, c: { d: 3, },}
const state1 = reactive(obj)
function fn() { Object.keys(state1)}
fn()state1.a = 2state1.a = 2state1.e = 2
delete state1.a
delete state1.f
可以看到迭代正常收集依赖,新增 e 触发 add,删除 a 触发 delete,删除 f 由于 f 根本就不存在所以不触发任何操作
- 可以看到
迭代正常收集依赖 - 第一次修改 a 触发 set
- 第二次修改 a 没有触发,因为修改后的值是一样的
- 第三次修改 e 是触发的 add 操作,因为 e 一开始不存在
- 第四次删除 a 正常触发 delete 操作
- 第五次删除 f 没有触发 delete,因为 f 本来就不存在