跳转到内容

边界问题处理

虽然有TS帮我们做参数限定,但是这只是在编译时,编译之后的js还是需要我们判断传入的参数是否是一个对象

vue-source/packages/reactivity/src/reactive.ts
export function reactive<T extends object>(target: T): T
export 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)

20260330135306

此时可以使用 weakMap 来存储代理对象,如果有,直接取出就行了,没有就 set 到 weakMap 中。

vue-source/packages/reactivity/src/reactive.ts
const targetMap = new WeakMap()
export function reactive<T extends object>(target: T): T
export 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
}

20260330142235

这样处理后,state1state2 就相等了

如果是已经被 proxy 处理过的代理对象,无需再次代理

Section titled “如果是已经被 proxy 处理过的代理对象,无需再次代理”
const obj = { count: 1 }
const state1 = reactive(obj)
const state2 = reactive(state1)
console.log('state1 === state2', state1 === state2)

20260330143445

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 // 不会触发 get
proxy.count // 会触发 get
proxy.xxx // 会触发 get,就算没有 xxx 属性

20260330144254

因此,利用这个特性,在创建 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
},
// ......
})
vue-source/packages/reactivity/src/reactive.ts
import { isObject } from '@vue/shared'
import { track, trigger } from './effect'
const targetMap = new WeakMap()
export function reactive<T extends object>(target: T): T
export 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
}

20260330150025

这样修改后,就搞定了,然后处理下 ts 的问题,和硬编码的问题,这里直接把 vue 源码的类型拿过来就行了

20260330150146

vue-source/packages/reactivity/src/reactive.ts
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): T
export 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

20260330153425

这里访问了属性 c,是通过代理对象访问的 c 属性,按道理来说,这里应该会触发 ab 属性的 get,但是从上面图可以看到并没有触发,而且 this 也还是原始对象,并不是 proxy 代理的对象。

处理这个很简单,只需要把 receiver 传进去就行了。receiver 就是代表当前被代理的 proxy 对象

vue-source/packages/reactivity/src/reactive.ts
// ...
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
},
})
// ...
const obj = {
a: 1,
b: 2,
c: {
d: 3,
},
}
const state1 = reactive(obj)
state1.c.d

20260330155600

可以看到只收集了 c 的依赖,并没有触发 dget

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)

20260330155656

可以看到 c 对应的对象并没有被 proxy 代理,所以收集不到依赖。

解决方法很简单,只需要在 proxy 中读取属性的时候,加个判断,判断是不是对象,是对象就递归调用就行了

vue-source/packages/reactivity/src/reactive.ts
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
},
})

20260330160241

这个时候就可以看到 c 对应值的是一个代理对象了。

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
},
})

20260330161528

将 Proxy 中的配置对象拆分到 baseHandlers.ts中。

vue-source/packages/reactivity/src/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,
}
vue-source/packages/reactivity/src/reactive.ts
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): T
export 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
}

当在函数中执行某些动作的时候,需要更细致的区分读取操作:

比如,当下面的情况出现

const obj = {
a: 1,
b: 2,
}
const state1 = reactive(obj)
function fn() {
'a' in state1
}
state1.a = 123
fn()

函数中关心的是属性astate1中是否存在。如果'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 = 123
fn()

如果c本身就是不存在的,那么fn函数中的'c' in state1这个语句,就会影响函数的结果,从而可能会影响视图显示的结果。

也就是说,在这种情况下,c的值修改了,对函数是有影响,那就应该再次去执行fn()函数,进行依赖收集

其实也就是说,我们之前在依赖收集和派发更新的时候,执行行为的划分不太细致,我们应该对执行的行为划分的更加细致,当触发某种动作的时候,才会触发相应的行为。tracktrigger函数,我们需要为其添加对应的动作标志。

有了对应的标志,才可以更好的表示,当前我正在读取某个对象的某个属性,或者我正在判断某个对象是否存在。

还是直接把 vue 源码的值先拿过来

20260330163552

vue-source/packages/reactivity/src/operations.ts
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate',
}
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear',
}

继续修改 effect 中的 tracktrigger 方法

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 = 123
fn()

20260330164857

可以看到先派发更新,触发 set 操作,然后触发依赖收集 has 操作是没问题的。

再来看看第一种情况

const obj = {
a: 1,
b: 2,
}
const state1 = reactive(obj)
function fn() {
'a' in state1
}
state1.a = 123
fn()

20260330165056

根据上面的分析,其实是不对的,如果已经有相关的属性了,修改属性,如果仅仅只是判断'a' in state1 是不应该在触发读取操作的重新执行的,不需要再次进行依赖收集。只是进一步操作,后续再处理这个问题。

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来表示对象迭代依赖的标识即可

20260330170853

vue-source/packages/reactivity/src/effect.ts
// 用来表示对对象的“迭代依赖”的标识
export const ITERATE_KEY = Symbol('')
vue-source/packages/reactivity/src/baseHandlers.ts
// ...
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,
}

20260330171147

新增,修改,删除不同动作的处理

Section titled “新增,修改,删除不同动作的处理”

新增修改之前只是做了简单的处理,那么到底是新增还是修改的动作,需要区分出来,因为对于读的操作影响是不一样的。

同时,如果修改的值确实有变化的时候,才触发trigger函数,不然触发的就毫无意义,这个其实就判断一下新旧值是否一样就行了。

vue-source/packages/shared/src/general.ts
// 通过Object.is比较可以避免出现一些特殊情况
// 比如NaN和NaN是相等的,+0和-0是不相等的
export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => hasOwnProperty.call(val, key)

这里还添加了 hasOwn,用于判断是新增还是修改

vue-source/packages/reactivity/src/baseHandlers.ts
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 = 2
state1.a = 2
state1.e = 2
delete state1.a
delete state1.f

20260330173706

可以看到迭代正常收集依赖,新增 e 触发 add,删除 a 触发 delete,删除 f 由于 f 根本就不存在所以不触发任何操作

  • 可以看到迭代正常收集依赖
  • 第一次修改 a 触发 set
  • 第二次修改 a 没有触发,因为修改后的值是一样的
  • 第三次修改 e 是触发的 add 操作,因为 e 一开始不存在
  • 第四次删除 a 正常触发 delete 操作
  • 第五次删除 f 没有触发 delete,因为 f 本来就不存在