《Vue.js设计与实现》笔记 第5章:非原始值的响应式方案

Vue.js设计与实现 第5章:非原始值的响应式方案

本章导读

第4章实现了最简单版本:

const obj = reactive({
  foo: 1
})

实现:

读取属性
↓
track()

修改属性
↓
trigger()

但真实项目中还会遇到:

obj.bar = 2

delete obj.foo

for (const key in obj)

Object.keys(obj)

arr.push()

map.set()

set.add()

这些操作。

因此需要更完整的响应式方案。


一、Proxy与Reflect

为什么引入Reflect

Proxy负责:

拦截

Reflect负责:

执行默认行为

示例

不使用Reflect:

get(target,key){
  return target[key]
}

使用Reflect:

get(target,key,receiver){
  return Reflect.get(
    target,
    key,
    receiver
  )
}

为什么推荐Reflect

保持默认行为一致

例如:

Reflect.get()
Reflect.set()
Reflect.deleteProperty()

对应:

obj[key]
obj[key] = value
delete obj[key]

正确处理this

例如:

const obj = {
  foo:1,
  get bar(){
    return this.foo
  }
}

使用Reflect:

Reflect.get(
  target,
  key,
  receiver
)

能够保证:

this指向代理对象

二、对象属性新增与删除

问题

之前只考虑:

obj.foo = 2

但还存在:

obj.bar = 100

新增属性。


以及:

delete obj.foo

删除属性。


如何区分新增与修改

判断:

const hadKey =
  Object.prototype.hasOwnProperty.call(
    target,
    key
  )

设置后:

if(!hadKey){
  TriggerType.ADD
}else{
  TriggerType.SET
}

TriggerType

const TriggerType = {
  SET:'SET',
  ADD:'ADD',
  DELETE:'DELETE'
}

三、合理触发响应

删除属性

实现:

deleteProperty(
  target,
  key
)

删除成功:

trigger(
  target,
  key,
  TriggerType.DELETE
)

为什么要区分操作类型

例如:

for(const key in obj){
}

依赖的是:

对象结构

不是:

具体属性值

修改值:

obj.foo = 2

不需要重新执行。


新增属性:

obj.bar = 3

必须重新执行。


四、for...in响应式

问题

effect(() => {

  for(const key in obj){
    console.log(key)
  }

})

新增属性:

obj.bar = 2

应该触发更新。


解决方案

创建特殊Key:

const ITERATE_KEY = Symbol()

收集依赖

track(
  target,
  ITERATE_KEY
)

触发更新

trigger(
  target,
  ITERATE_KEY
)

工作流程

for...in
↓
ITERATE_KEY

新增属性
↓
trigger
↓
重新遍历

五、数组响应式

数组特点

数组本质:

const arr = [1,2,3]

也是对象。


但存在特殊操作:

arr.length

arr.push()

arr.pop()

arr.shift()

arr.unshift()

arr.splice()

六、数组索引响应

示例

effect(() => {
  console.log(arr[0])
})

修改:

arr[0] = 100

应触发更新。


实际上:

与对象完全一致

无需额外处理。


七、数组长度响应

示例

effect(() => {
  console.log(arr.length)
})

修改:

arr.push(4)

长度变化。

必须更新。


触发逻辑

trigger(
  target,
  'length'
)

八、数组长度截断

示例

arr.length = 1

原数组:

[1,2,3]

变成:

[1]

影响:

索引1失效
索引2失效

因此需要:

触发length依赖

触发被删除索引依赖

九、数组遍历响应

for...of

effect(() => {

  for(const item of arr){
    console.log(item)
  }

})

依赖:

数组内容

修改:

arr.push()

需要更新。


实现

仍然使用:

ITERATE_KEY

进行依赖收集。


十、避免数组方法死循环

问题

例如:

effect(() => {
  arr.push(1)
})

执行:

push
↓
修改length
↓
trigger
↓
effect
↓
push
↓
无限循环

Vue解决方案

重写数组方法。


创建:

const arrayInstrumentations={}

重写:

push
pop
shift
unshift
splice

执行期间:

shouldTrack = false

结束后:

shouldTrack = true

避免重复收集依赖。


十一、查找方法优化

问题

const obj={}

const arr=[
  reactive(obj)
]

arr.includes(obj)

结果:

false

原因:

数组存储代理对象
查找原对象

Vue解决方案

重写:

includes
indexOf
lastIndexOf

查找失败:

先查代理对象

失败再查原对象

十二、Map与Set响应式

为什么特殊处理

Map:

map.get()
map.set()

Set:

set.add()
set.delete()

不是:

map.key=value

无法通过普通Proxy拦截。


十三、Map响应式

get

map.get(key)

需要:

track(target,key)

set

map.set(key,value)

需要:

trigger(
  target,
  key
)

size属性

map.size

依赖:

集合结构

使用:

ITERATE_KEY

收集依赖。


十四、Set响应式

add

set.add(value)

类型:

TriggerType.ADD

delete

set.delete(value)

类型:

TriggerType.DELETE

size

set.size

依赖:

集合元素数量

同样:

ITERATE_KEY

处理。


十五、Map遍历响应

示例

effect(() => {

  for(const [k,v] of map){

  }

})

依赖:

Map结构

修改:

map.set()

需要重新执行。


解决方案

使用:

ITERATE_KEY

建立依赖。


十六、keys()遍历问题

示例

effect(() => {

  for(const key of map.keys()){

  }

})

只依赖:

key

修改:

map.set(existingKey,newValue)

不应该更新。


新增:

map.set(newKey,newValue)

必须更新。


Vue方案

新增:

MAP_KEY_ITERATE_KEY

作用:

专门追踪key变化

十七、响应式对象缓存

问题

const p1 = reactive(obj)

const p2 = reactive(obj)

希望:

p1 === p2

实现

使用:

const reactiveMap =
  new Map()

缓存代理对象。


流程:

原对象
↓
reactive()
↓
缓存代理
↓
重复调用直接返回

十八、避免代理污染

问题

const obj = reactive(data)

再次代理:

reactive(obj)

不应创建新代理。


解决方案

增加标识:

__v_isReactive

检测:

if(target.__v_isReactive){
  return target
}

第五章核心知识图谱

Reactive

├── Object
│
├── Array
│   ├── length
│   ├── push
│   ├── pop
│   ├── includes
│   └── for...of
│
├── Map
│   ├── get
│   ├── set
│   ├── size
│   ├── keys
│   └── forEach
│
├── Set
│   ├── add
│   ├── delete
│   └── size
│
├── ITERATE_KEY
│
├── MAP_KEY_ITERATE_KEY
│
├── TriggerType
│   ├── SET
│   ├── ADD
│   └── DELETE
│
└── Reactive Cache

高频面试题

为什么Vue3使用Reflect?

保证默认行为一致。

正确处理:

getter中的this

问题。


为什么需要ITERATE_KEY?

解决:

for...in
Object.keys()
Map遍历
Set遍历

依赖收集问题。


为什么数组需要特殊处理?

因为:

length
push
pop
splice

会影响多个依赖。


为什么重写数组includes?

解决:

原对象
代理对象

比较失败问题。


Map和Set为什么不能直接用Proxy解决?

因为操作方式是:

map.set()
set.add()

不是属性访问。

需要额外封装。


为什么需要Reactive缓存?

保证:

reactive(obj)
===
reactive(obj)

成立。


本章总结

第5章扩展了第4章的响应式系统。

支持:

Object
Array
Map
Set
WeakMap
WeakSet

核心新增能力:

  1. Reflect
  2. TriggerType
  3. ITERATE_KEY
  4. MAP_KEY_ITERATE_KEY
  5. 数组方法重写
  6. Map/Set响应式
  7. Reactive缓存

最终构建出:

完整的非原始值响应式系统

这是 Vue3 响应式源码中最复杂、最重要的一部分,也是高级前端面试中出现频率最高的源码知识点之一。

posted @ 2025-03-20 15:42  Li_pk  阅读(5)  评论(0)    收藏  举报