刚刚开始未完待续 。。。
响应系统是 Vuejs 的重要组成部分,在学习响应系统之前要搞明响应式数据和副作用函数具体是什么。然后通过一个基础的响应式数据实现来开启本篇的学习。期间会面临着解决硬编码副作用函数、代码分支切换导致遗留副作用函数、属性自增导致无限递归等问题,还有如何实现副作用函数调度执行,以及计算属性 Computed 和 Watch 函数的实现原理。
4.1 响应式数据与副作用函数
副作用函数是指函数执行过程中产生的除其预期输出以外的效果。副作用可以包括但不限于以下几种情况:修改输入参数(引用类型)、修改全局变量、I/O 操作等。
在下面的代码显示,在 effect
函数中通过全局的 document
对象提供的 API 修改了 body
的内容文本。但这样的修改会直接影响其它读取 body 内容文本函数的结果。这种现象就是典型的副作用,effect
函数就被称为副作用函数:
function effect() {
document.body.innerText = 'hello vue3'
}
在下面的代码显示,当第 7 行的 effect
函数执行时会将 body
的内容文本设置为 data.text
的值,当第 9 行执行改变 data.text
的属性时,我们希望 effect
函数可以重新执行:
const data = { text: 'hello world' }
function effect() {
document.body.innerText = data.text
}
effect()
data.text = 'hello vuejs'
如果一个数据的属性发生改变时可以驱动该属性相关的副作用函数自动重新执行,那么这个数据被称为响应式数据。
4.2 响应式数据的基本实现
将普通数据变成响应式数据的底层基础是要实现对数据读取和设置操作的拦截,正如下图所示,当 data.text
被读取时将副作用函数存储到“桶”里,当 data.text
被设置/更新时,在将“桶”里的副作用函数取出并执行。
如何拦截一个对象属性的读取和设置操作,这在 ES2015 之前,只能使用 Object.defineProperty
函数实现,这也是 Vue.js 2 采用的原因和方式。在 ES2015 中,可以通过代理对象 Proxy
来实现,Vue.js 3 也是基于此实现了响应系统的重构。
在下面的代码中显示,在一个将普通数据转换为响应式数据的 reactive
函数中返回一个 Proxy
对象,在这个对象的 getter
属性中通过硬编码的方式向“桶”中存储全局中名为 effect
的副作用函数,并在 setter
属性中通过遍历“桶”中的副作用函数并执行。
const bucket = new Set() // 定义用来存储副作用函数的桶,利用 Set 结构去重
// 将一个数据转换为响应式数据
function reactive(data) {
return new Proxy(data, {
get: (target, prop) => {
// 此处通过硬编码形式存储全局中名为 effect 的副作用函数
bucket.add(effect)
// 完成 getter 的基础功能,返回属性值
return target[prop]
},
set: (target, prop, newVal) => {
// 完成 setter 的基础功能,更新属性值
target[prop] = newVal
bucket.forEach(fn => fn())
},
})
}
在下面的代码中显示,在上一节的代码案例中使用 reactive
函数将普通数据转换为响应式数据,在 1 秒钟后 data.text 属性被修改,观察到 effect
函数重新执行,页面同时渲染为最新的 hello vuejs
内容文本。完成了基本的响应式数据:
const data = reactive({ text: 'hello world' })
function effect() {
document.body.innerText = data.text
}
effect()
setTimeout(() => {
data.text = 'hello vuejs'
}, 1000)
补充 :Vue.js 3 中响应系统的一大改进就是从 Object.defineProperty
转向使用 ES6 的 Proxy
对象来实现数据的响应性。其升级优点包括以下几个方面:
- 更全面的拦截:
Proxy
可以拦截更多的操作类型,如删除属性(deleteProperty
)、验证属性是否存在(has
)、获取属性(get
)、设置属性(set
)等。使得 Vue.js 可以更全面的控制响应式数据的变化。 - 更简洁的代码:使用
Proxy
可以让代码变得更加简洁易读,不用向使用Object.defineProperty
为每一个属性都单独添加getter
和setter
属性。 - 更好的性能:在创建响应式对象时 Proxy 可以做到非侵入式且完整的代理,不需要递归遍历对象的每一个属性来将它们转换为可响应的状态。这将大大减少初始创建响应式对象时的工作量,也避免了在对象在新增属性后需要重新转换的问题。
- 数组的变更检测:
Object.defineProperty
在处理数组时存在一定的限制,如无法检测到splice
、push
等方法引起的数组变化。而Proxy
可以通过拦截set
操作更好的监听数组内部的变化。