今天看了一个尤大神介绍Vue中实现响应式基本原理的视频, 把视频内容总结一下。
首先是问题的提出: 什么响应式(Reactivity) ?
举一个简单的例子
let a = 2
let b = 3
let c = a * b
console.log(c) // 6
a = 3
console.log(c) // 6 or 9 ?
从上面的代码可以看出,c的值显而易见仍然还是6, 因为JS是顺序执行的。当a变成3之后,c = a * b 并没有再次执行, 当然c的值也就不会发生变化。而所谓的响应式,就是希望c的值能够根据a, b的变化而自动变化,这就很像我们在Excel表格中做计算一样, 不管哪一个变量发生变化, 结果都会自动更新。
用公式总结一下
正常情况下, 结果就是依赖项的函数运算值, 这个函数只运行一次就结束了:
结果 = fx(依赖项1, 依赖项2, 依赖项3,...)
而所谓响应式的结果就是时刻观察着这些依赖项,无论哪一个发生变化, 都重新运行一次函数,从而的得到更新后的值:
响应式的结果 = watch(fx(依赖项1, 依赖项2, 依赖项3,...))
Vue中实现响应式的利器: Object.defineProperty API
有关这个API的具体用法, 这里就不做详细介绍了, 大家可以参看MDN。这里只给出一个简单的例子, 说明一下Object.defineProperty API 的 get / set方法, 相信大家应该都看的懂。
const convert = (obj) => {
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
console.log(`Get value ${internalValue}`)
return internalValue
},
set (newValue) {
internalValue = newValue
console.log(`Set value ${internalValue}`)
}
})
})
}
let test = {
'a': 2
}
convert(test)
test.a = 3 // 'Set value 3'
test.a // 'Get value 3'
// get () 方法在读取对象属性时被调用
// set () 方法在设置对象属性时被调用
实现响应式基本思路: 如何在依赖项发生变化时, 重新触发计算
无论在什么样的计算中,我们总是会有多个依赖项,那么我们首先把依赖项抽象成一个类,每个依赖项都是这个类的实例。
每个实例需要有两个方法,分别是depend() 和 notify()。depend()的作用是将该依赖项所在的所有函数记录下来, notify()的作用是在该依赖项变化时, 将之前存储的函数全部运行一次。
说到这里可能有点抽象, 还是用代码解释一下:
let dep = new Dependency()
watch(() => {
dep.depend()
console.log('运行一次计算')
}) // 打印:'运行一次计算'
dep.notify() // 打印:'运行一次计算'
我们首先创建了一个依赖项实例dep,而后我们监听一个函数运算, 这是一个箭头函数。dep是它的依赖项,或者从数学角度理解,dep就是这个函数的一个变量。当这个函数运行时,它做了两件事,一个运行dep的depend()方法, 此时我们希望将这个箭头函数存储在某个地方,以便日后使用,而后打印出'运行一次计算‘。
() => {
dep.depend()
console.log('运行一次计算')
}
接下来,运行dep的notify()方法,我们希望刚才存储起来的箭头函数运行一次, 进而又打印出'运行一次计算‘。
这些其实就是实现响应式的大致思路,接下来我们开始用代码具体实现一下:
let global = null // 用来临时存储函数的全局变量
// 实现 watch 方法
const watch = (func) => {
global = func // 将这个函数临时存储在全局变量中
func() // 运行watch的时候, 首先运行一次函数
global = null // 清空全局变量
}
// 注意: 这个global并不是最终存储函数的地方, 因为一个依赖项可能用在多个函数中
// 我们真正要实现的是将这些函数和这个依赖项关联起来, 那么很容易想到, 将这些函数存在这个依赖项实例中
// 因此我们可以这样定义Dependency类
class Dependency {
constructor () {
this.subscribers = new Set() // 用于存储所有相关的func
}
depend () {
if (global) {
this.subscribers.add(global) // 添加func
}
}
notify () {
this.subscribers.forEach(func => func()) // 运行一遍所有func
}
}
此时我们已经大体实现了响应式,因为只要运行dep.notify,就会打印出’运行一次计算‘
接下来的问题是,在上面的代码中,我们是通过手动触发depend()和notify()实现的响应式。这显然离我们的要求还差一点, 我们希望可以自动触发depend()和notify(), 一个典型的使用场景如下:
// 一个名为state的依赖项
let state = {
'a': 1,
'b': 2
}
let sum
// 监听求和运算
watch(() => {
sum = state.a + state.b // 此处应该触发了depend()
})
console.log(sum) // 3
state.a = 4 // 此处应该触发了notify()
console.log(sum) // 6
state.b = 4 // 此处应该触发了notify()
console.log(sum) //8
该怎么实现自动触发depend() 和 notify()呢? 这里我们就想到了前面提到过的Object.defineProperty() API。思路就是: 当读取state.a或者state.b时,在get() 方法中触发depend(); 当给state.a 或 state.b 赋值时,在set()中触发notify()。 由此, 我们将前文提到的convert()函数修改一下:
// 创建一个dep实例与obj绑定
const convert = (obj) => {
let dep = new Dependency()
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
dep.depend(global)
return internalValue
},
set (newValue) {
internalValue = newValue
dep.notify()
}
})
})
}
完整的示例如下:
let global = null // 用来临时存储函数的全局变量
const convert = (obj) => {
let dep = new Dependency()
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
dep.depend(global)
return internalValue
},
set (newValue) {
internalValue = newValue
dep.notify()
}
})
})
}
const watch = (func) => {
global = func // 将这个函数临时存储在全局变量中
func() // 运行watch的时候, 首先运行一次函数
global = null // 清空全局变量
}
class Dependency {
constructor () {
this.subscribers = new Set()
}
depend () {
if (global) {
this.subscribers.add(global)
}
}
notify () {
this.subscribers.forEach(func => func())
}
}
// 响应式测试
let state = {
'a': 1,
'b': 2
}
convert(state) // 重新定义state的get / set 方法
let sum
watch(() => {
sum = state.a + state.b
})
console.log(sum) // 3
state.a = 4
console.log(sum) // 6
state.b = 4
console.log(sum) //8
我又对完整示例中的方法和类做了一下简单的处理, 全部都封装在一个名为Vue的类中, 最终效果如下:
class Vue {
constructor () {
this.global = null
this.Dep = class Dependency {
constructor () {
this.subscribers = new Set()
}
depend (global) {
if (global) {
this.subscribers.add(global)
}
}
notify () {
this.subscribers.forEach(sub => sub())
}
}
}
convert (obj) {
let dep = new this.Dep()
Object.keys(obj).forEach(key => {
let _this = this
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
dep.depend(_this.global)
return internalValue
},
set (value) {
internalValue = value
dep.notify()
}
})
})
}
watch (update) {
this.global = update
update()
this.global = null
}
}
// 响应式测试
let A = {
'a': 1,
}
let B = {
'b': 1,
}
let vue = new Vue()
vue.convert(A)
vue.convert(B)
let sum
let minus
vue.watch(() => {
sum = A.a + B.b
})
vue.watch(() => {
minus = A.a - B.b
})
console.log(sum, minus) // 2, 0
A.a = 10
console.log(sum, minus) // 11, 9
B.b = 20
console.log(sum, minus) // 30, -10
到此为止有关Vue响应式的基本原理就介绍完了。当然这些与Vue中的真正实现方法还有很大不同,但是希望能帮助大家对Vue响应式的基本实现有个宏观的认识。
网友评论