前言
所谓的前端响应系统,简单的理解可以认为,浏览器根据特定变量显示的数据,在这个变量的值发生变化后,能够自动的刷新页面从而展现出变化后的新值。这也是响应前端框架实现双向绑定的前提。
示例
如下示例:通过setTimeout定时器在2秒后更改变量的值,从而实现界面在2秒后能够自动从“Hello world”变化为“你好,世界”
原理分析
提起响应系统,耳熟能详的就是数据劫持(数据代理),所谓的数据劫持就是指,在访问或者修改对象某个属性时,通过一段代码拦截这个行为,并进行相应的操作返回结果。而前端响应系统,主要就是根据数据劫持这个原理实现的,具体的逻辑可以拆分为以下几步
- 初始化读取对象属性值
let obj1 = { text: "Hello world" }
function effect() {
document.getElementById("toshow").innerHTML = obj1.text
}
- 劫持读取行为添加相应操作(将该读取行为暂存起来)
const buckset = new Set()
buckset.add(effect)
- 设置对象属性的值
obj1.text = '你好 世界'
- 劫持设置行为添加相应操作(设置完成后并调用之前暂存的读取操作)
buckset.forEach(fn => fn())
上述步骤就是响应系统最简易的基础逻辑
代码示例
在 ES2015 以前主要是通过Object.defineProperty()方法修改对象属性的get()方法和set()方法实现劫持读取对象属性值和设置属性值的行为,进而添加额外的逻辑实现主动响应,这也是VUE2的实现方式。在 ES2015版本及以后新增了Proxy代理对象,可以在代理的对象种新增属性的读取和设置行为,从而实现主动响应,VUE3采取的正式这种方式。Object.defineProperty()和Proxy的实现方式分别如下,也可以认为下面两个是两个简易的响应系统:
数据劫持实现方式
<!DOCTYPE html>
<html>
<body>
<h3>响应系统基础原理</h3>
<br />
<span>
2秒后自动更换:
<h4 id="toshow"></h4>
</span>
<script>
// 初始化数据
let obj1 = { text: 'Hello world' }
// 读取操作
function effect() {
document.getElementById('toshow').innerHTML = data.text
}
// 创建“桶” 用于暂存读取操作
const buckset = new Set()
// 劫持读取和操作行为
function newObj(obj = {}) {
// 遍历对象键, 根据键劫持对象get和set方法
const keys = Object.keys(obj)
keys.forEach(k => {
// 获取初始值
const val = obj[k]
Object.defineProperty(obj, k, {
get: function() {
// 暂存读取行为
buckset.add(effect)
// 读取行为正常返回
return this.k ? this.k : val
},
set: function(value) {
// 设置行为正常设置
this.k = value
// 执行读取行为
buckset.forEach(fn => fn())
}
})
})
return obj
}
// 执行劫持行为
let data = new newObj(obj1)
// 初始化读取对象
effect()
// 修改对象值
setTimeout(() => {
data.text = '你好,世界'
}, 2000)
</script>
</body>
</html>
数据代理实现方式
<!DOCTYPE html>
<html>
<body>
<h3>响应系统基础原理</h3>
<br />
<span>
2秒后自动更换:
<h4 id="toshow"></h4>
</span>
<script>
// 初始化数据
let obj = { text: 'Hello world' }
// 读取操作
function effect() {
document.getElementById('toshow').innerHTML = data.text
}
// 创建“桶” 用于暂存读取操作
const buckset = new Set()
// 代理读取和操作行为
const data = new Proxy(obj, {
get(target, key) {
// 暂存读取行为
buckset.add(effect)
// 读取行为正常返回
return target[key]
},
set(target, key, newVal) {
// 设置行为正常设置
target[key] = newVal
// 执行读取行为
buckset.forEach(fn => fn())
return true
}
})
// 初始化读取对象
effect()
// 修改对象值
setTimeout(() => {
data.text = '你好,世界'
}, 2000)
</script>
</body>
</html>
最后
当然,这仅仅只是最基础的响应系统原理,对于前端框架来说,其功能还是远远不够的,比如如何避免陷入无限递归、如何避免重复调用、如何实现嵌套响应等等,要优化的空间还是非常大。但是通过本文,我们至少有了一个清晰的印象,知道了所谓响应系统的大概实现逻辑,不至于以后在面对响应、数据劫持和数据代理时一头雾水。
如果想对VUE的响应系统有跟深的理解和学习推荐阅读霍春阳编写的《Vue.js设计与实现》第四章响应系统的作用与实现。
如果本文对你有帮助,请动动鼠标点赞加收藏,谢谢😊!