设计一个完善的响应系统
在vue响应系统的作用和实现一中,简单书写一个用new proxy的响应式数据,但是里面还很多没有考虑到的细节。
命名问题
并不是所有的副作用函数都叫做effect,甚至匿名函数的副作用函数需要储存近桶里面,那么我们就需要有一个用来注册副作用函数的机制。注册指的是将某个函数(即副作用函数)添加到某个机制或列表中,以便在特定的时间点或条件下执行这些函数。
为什么要注册副作用函数呢?
在组件或应用的生命周期中,我们经常需要在某些阶段执行这些副作用操作,但又不想将这些操作直接嵌入到组件的渲染逻辑中,因为这可能会导致性能问题(如无限渲染循环)或使组件的逻辑变得复杂且难以维护。通过注册副作用函数,我们可以:
- 控制执行时机:确保副作用函数在合适的时机执行,例如在组件挂载后、更新后或卸载前。
- 避免不必要的重复执行:通过管理副作用函数的注册和注销,我们可以确保在组件不需要时停止执行这些函数,从而避免浪费资源。
- 提高代码的可读性和可维护性:将副作用逻辑与组件的其他逻辑分离,使得代码更加清晰和模块化。
注册副作用函数,并执行响应式数据的代码如下所示:
//用一个全局变量来存储被注册的副作用函数
// 使用null而不是undefined,因为null表示没有活动的副作用函数
let activeEffect = null
//不是所有的副作用函数都叫effect,那我们就写一个effect用来注册副作用函数
//effect函数用于注册副作用函数,传入的fn就是副作用函数
function effect(fn){
//当调用effect时,将副作用函数赋值给全局变量activeEffect
activeEffect = fn
//执行副作用函数
fn()
}
const bucket = new Set()
const data = { text:'hello,world' }
//我们需要在原来的代理中,添加一个判断
const obj = new Proxy(data,{
get(target,key){
//将全局变量收集到的副作用函数收集到桶中
if(activeEffect){ //新增
bucket.add(activeEffect) //新增
} //新增
// 使用Reflect来确保正确的行为
return Reflect.get(target, key) //新增
},
//拦截set操作
set(target,key,newVal){
target[key] = newVal
bucket.forEach(fn=>fn())
return true
}
})
//调用effect
effect(
//一个匿名的副作用函数
()=>{
document.body.innerText = obj.text
}
)
setTimeout(()=>{
obj.text = 'hello,vue'
},1000)
不存在的属性
如果设置一个不存在的属性时,这个effect副作用函数还是会执行。理论上,effect中的副作用函数并没有读取obj.notExist属性,它们并没有联系,但是还是执行了副作用函数。执行代码如下:
let activeEffect = null
function effect(fn){
activeEffect = fn
fn()
}
const data = { text:'hello,world' }
const bucket = new Set()
const obj = new Proxy(data,{
get(target,key){
if(activeEffect){
bucket.add(activeEffect)
}
return Reflect.get(target, key)
},
set(target,key,newVal){
target[key] = newVal
bucket.forEach(fn=>fn())
return true
}
})
//调用effect
effect(
()=>{
//执行两次,第一次是在effect函数中调用,第二次是在set中调用
console.log('effect run')
document.body.innerText = obj.text
}
)
setTimeout(()=>{
obj.notExist = 'hello,vue'
},1000)
这个问题是由于我们直接把副作用函数放进去桶里面。不管是哪个属性get了,都会把副作用函数放进桶里,set了就直接拿出来执行。根本原因就是没有在副作用函数与被操作的目标字段之间建立明确的关系。
因此这个桶结构需要换一个数据结构来建立明确的关系,我们选择WeakMap数据结构来当桶。其中WeakMap由target-->Map构成,Map由key-->Set构成。
let activeEffect = null
function effect(fn){
activeEffect = fn
fn()
}
const data = { text:'hello,world' }
//使用WeakMap来新建桶
const bucket = new WeakMap() //修改
const obj = new Proxy(data,{
get(target,key){
track(target,key) //新增
return Reflect.get(target, key)
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key) //新增
}
})
//封装track函数
function track(target,key){ //新增
//没有副作用函数就直接返回
if(!activeEffect) return
//根据target从桶中取得depsMap
let depsMap = bucket.get(target)
//如果不存在,就新建一个Map关联
if(!depsMap){
bucket.set(target,(depsMap = new Map()))
}
//在根据depsMap获取key
//里面储存所有当前key相关的副作用函数:effects
let deps = depsMap.get(key)
//如果不存在,就新建一个Set关联
if(!deps){
depsMap.set(key,(deps = new Set()))
}
//将副作用函数添加到桶里
deps.add(activeEffect)
}
//封装trigger函数
function trigger(target,key){ //新增
const depsMap = bucket.get(target)
if(!depsMap) return
//根据key获取相关的副作用函数
const effects = depsMap.get(key)
//执行副作用函数
effects && effects.forEach(fn=>fn())
}
//调用effect
effect(
()=>{
//执行一次
console.log('effect run')
document.body.innerText = obj.text
}
)
setTimeout(()=>{
obj.notExist = 'hello,vue'
},1000)