响应式系统无疑是vue最富有魅力的特性之一,它让开发应用变得更简单更快速,就好像伐木工开上了电锯车,哼着曲儿,面向树根,树木就温柔的倒下了。
当我们在data
对象中定义了属性,当属性被访问和改变的时候,视图即刻被渲染和更新,它让我们的工作如此简单,它是如何让一个普通的对象变为可响应的?我想我们是时候以更深刻的方式去拥抱它,去理解它是如何做到的。
非常幸运的是,JS里有一个叫Object.defineProperty()
的API,它能够将一个对象的属性改写为getter
和setter
,使对象变成可响应的,它的实现大概有2步:
- 递归遍历对象的每个属性,使用
Object.defineProperty()
重写属性的getter
和setter
,在getter
中额外添加一个追踪器,在setter
中额外添加一个触发器。而且还会为其创建一个dep数组,把使用到的属性记录为依赖,这些依赖其实就是一个个跟属性相关的函数。 - 每次访问属性的时候,getter会调用追踪函数,把属性作为依赖添加到相关的dep中,当改变属性的值的时候,setter更新属性值的同时,会调用触发函数,会执行相关的dep中的每一个依赖。
这样就实现了对数据的观察,下面的例子是对响应式系统的实现:
// data对象
const data = { x: 1, y: 2 }
// 依赖dep
let realX = data.x
let realY = data.y
const realDepsX = []
const realDepsY = []
// 使它变为可响应的
Object.defineProperty(data, 'x', {
get() {
trackX()
return realX
},
set(v) {
realX = v
triggerX()
},
})
Object.defineProperty(data, 'y', {
get() {
trackY()
return realY
},
set(v) {
realY = v
triggerY()
},
})
// 属性的追踪和触发
const trackX = () => {
if (isDryRun && currentDep) {
realDepsX.push(currentDep)
}
}
const trackY = () => {
if (isDryRun && currentDep) {
realDepsY.push(currentDep)
}
}
const triggerX = () => {
realDepsX.forEach((dep) => dep())
}
const triggerY = () => {
realDepsY.forEach((dep) => dep())
}
// 观察函数
let isDryRun = false
let currentDep = null
const observe = (fn) => {
isDryRun = true
currentDep = fn
fn()
currentDep = null
isDryRun = false
}
// 定义3个函数
const depA = () => console.log(`x = ${data.x}`)
const depB = () => console.log(`y = ${data.y}`)
const depC = () => console.log(`x + y = ${data.x + data.y}`)
// 运行所有依赖项
observe(depA)
observe(depB)
observe(depC)
// 输出: x = 1, y = 2, x + y = 3
// 变更data
data.x = 3
// 输出: x = 3, x + y = 5
data.y = 4
// 输出: y = 4, x + y = 7
vue实际的响应式代码更复杂,但基本原理跟上面的例子相同。
总结:使用Object.defineProperty()
能让对象变成可观察的,它把组件接触到的属性记录为依赖,所以当属性被访问和修改的时候,让我们能够从依赖中找出相关的计算重新运行从而得到自动更新后的结果,然后通知视图更新。
最后要说的是Object.defineProperty()
它的能力是有限的,比如对对象新增的属性、数组(vue2通过拦截Array.prototype实现)、es6的Map和Set是无法进行侦测的,vue3里面的响应式机制使用的是ES6的proxy API,它能更优雅的解决vue2.x中存在的这些问题,请关注我的下一篇文章吧。祝你学习愉快。