《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
核心新增能力:
- Reflect
- TriggerType
- ITERATE_KEY
- MAP_KEY_ITERATE_KEY
- 数组方法重写
- Map/Set响应式
- Reactive缓存
最终构建出:
完整的非原始值响应式系统
这是 Vue3 响应式源码中最复杂、最重要的一部分,也是高级前端面试中出现频率最高的源码知识点之一。

浙公网安备 33010602011771号