前言
我们都知道Vue3响应系统采用的是Proxy代理的方式实现的,对比于Vue2这种方式更智能,解决了很多Vue2响应系统的一些小问题,比如增加属性或者删除属性无法触发响应。本篇文章主要针对Vue3如何用Proxy来实现响应,最后实现一个简单的响应式系统。
响应式数据的基本实现
在讨论响应式数据实现之前,我们需要先来讨论一下副作用函数。
1.什么是副作用函数
所谓副作用函数指的是会产生副作用的函数:
函数的执行会直接或间接影响其他函数的执行,换句话说就是当函数调用的时候会对外部产生影响
比如:
let val=1//全局变量
function effect(){
val=2//修改全局变量,产生副作用
}
上面的代码中,在effect函数里对全局变量val进行了修改,如果之后有其他函数也用到了该全局变量,那么用的是修改后的值。通过这种方式就影响了其他函数执行的结果
比如,一个函数foo返回值是val的平方,那么foo函数在effect函数执行前执行得到的结果是1,而在effect函数执行后执行的结果为4
副作用函数很容易产生,由于JavaScript是动态语言,函数调用到底会不会产生副作用,只有代码真正运行的时候才能知道,因此在rollup这种打包工具就很难静态的分析哪些代码是dead code,所以我们经常看见代码中有/*#__PURE__#*/这种代码注释,其作用就是告诉打包器该函数调用不会产生副作用,可以放心进行Tree-Shaking
2.什么是响应式数据
理解了什么是副作用函数,再来说说什么是响应式数据,例如:
let obj = {text:"hello world"}
function effect(){
//effect函数执行会读取obj.text属性
document.body.innerText=obj.text;
}
上面的代码中,副作用函数effect执行会设置body元素的innerText属性值为obj.text,在未来obj.text的值变化时我们希望副作用函数effect会重新执行,如:
obj.text = "你好 世界";
这段代码修改了obj.text的值,我们希望当值变化后副作用函数自动重新执行,实现了这个目标那么我们就可以说对象obj就是响应式数据。
3.响应式数据的简单实现
如何让obj变成响应式数据?
通过观察思考,我们可以发现了两条线索:
- 当副作用函数 effect 执行时,会触发字段 obj.text 的 读取 操作
- 当修改 obj.text 时,会触发字段 obj.text 的 设置 操作
我们可以通过拦截对象的读取和设置操作,如何进行相应的额外处理:存储副作用函数,并在需要的时候取出来执行,具体如下:
- 当读取字段 obj.text 时,我们把副作用函数effect存储到一个“桶”中;
- 当设置 obj.text 时,我们再把副作用函数从“桶”里取出来并执行
如图:
那么我们怎么实现拦截一个对象属性的读取和设置操作呢?在ES2015之前我们只能通过 Object.defineProperty函数实现,这也是Vue2采用的方式;现在我们可以使用新特性代理对象Proxy来实现,这也是Vue3采用的方式
实现代码如下:
//存储副作用函数的桶
const bucket = new Set()
//原始数据
let data = {text:"hello world"}
//对原始数据的代理
let obj = new Proxy(data,{
//拦截读取操作
get(target,key){
//将副作用函数effect添加到存储副作用函数的桶中
bucket.add(effect)
//返回属性值
return target[key]
},
//拦截设置操作
set(target,key,newVal){
//设置属性值
target[key]=newVal
//把副作用函数从桶中取出来并执行
bucket.forEach(fn=>fn())
//返回true代表设置操作成功
return true
}
})
可以写一段简单的测试代码来测试一下,比如:
function effect(){
document.body.innerText=obj.text
}
effect()
setTimeout(()=>{
obj.text="你好 世界"
},1000)
在浏览器中运行上段代码会得到预期的结果
至此,简单的响应式数据就已经实现了,但是目前的实现还有很多缺陷,
比如我们直接通过名字(effect)来获取副作用函数,这种硬编码的方式很不灵活,副作用函数的名字应该可以任意取,甚至可以是一个匿名函数
关于如何解决这些缺陷以致于实现一个更完善的响应式系统,我们可以先思考思考然后继续读我的下一篇文章