什么是响应式Reactivity
Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。
需求
现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?
简单尝试1:
let a = 3;
let b = a * 10;
console.log(b); // 30
乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。
let a = 4;console.log(a); // 4
console.log(b); // 30
let b = a * 10;
console.log(b); // 40
简单尝试2:
将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。
onAChanged(() => {
b = a * 10;
})
所以现在的问题就变成了实现onAChanged函数,当a改变之后自动执行onAChanged
,请看后续.
结合view层
现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。
<span class="cell b"></span>
onStateChanged(() => {
document.querySelector('.cell.b').textContent = state.a * 10
})
再次抽象之后如下所示。
<span class="cell b">
{{ state.a * 10 }}
</span>
onStateChanged(() => {
view = render(state)
})
view = render(state)
是所有的页面渲染的高级抽象。这里暂时不考虑view = render(state)
的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是onStateChanged
的实现
实现
实现方式是通过Object.defineProperty
中的getter
和setter
方法。具体使用方法参考如下链接。
MDN之Object.defineProperty
需要注意的是get和set函数是存取描述符,value和writable函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。
实例1:实现 convert() 函数
要求如下:
- 1、传入对象obj作为参数
- 2、使用
Object.defineProperty
转换对象的所有属性 - 3、转换后的对象保留原始行为,但在get或者set操作中输出日志
示例:
const obj = { foo: 123 }
convert(obj)
obj.foo //输出 getting key "foo": 123
obj.foo = 234 // 输出 setting key "foo" to 234
obj.foo // 输出 getting key "foo": 234
在了解Object.defineProperty
中getter
和setter
的使用方法之后,通过修改get
和set
函数就可以实现onAChanged
和onStateChanged
。
实现:
function convert (obj) { //迭代对象的所有属性
//并使用Object.defineProperty()转换成getter/setters
Object.keys(obj).forEach(key => {
//保存原始值
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
console.log(`getting key "${key}": ${internalValue}`)
return internalValue
},
set(newValue) {
console.log(`setting key "${key}" to: ${newValue}`)
internalValue = newValue
}
})
})
}
实例2:实现Dep类
要求如下:
- 1、创建一个Dep类,包含两个方法:
depend
和notify
- 2、创建一个
autorun
函数,传入一个update
函数作为参数 - 3、在
update
函数中调用dep.depend()
,显式依赖于Dep
实例 - 4、调用
dep.notify()
触发update
函数重新运行
示例:
const dep = new Dep()
autorun(() => {
dep.depend()
console.log('updated')
})// 注册订阅者,输出 updated
dep.notify()// 通知改变,输出 updated
首先需要定义autorun
函数,接收update
函数作为参数。因为调用autorun
时要在Dep
中注册订阅者,同时调用dep.notify()
时要重新执行update
函数,所以Dep
中必须持有update
引用,这里使用变量activeUpdate
表示包裹update
的函数。
实现代码如下:
let activeUpdate = null
function autorun(update) {
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate // 引用赋值给activeUpdate
update() // 调用update,即调用内部的dep.depend
activeUpdate = null // 绑定成功之后清除引用
}
wrappedUpdate() // 调用
}
wrappedUpdate
本质是一个闭包,update
函数内部可以获取到activeUpdate
变量,同理dep.depend()
内部也可以获取到activeUpdate
变量,所以Dep
的实现就很简单了。
实现代码如下:
class Dep{ //初始化
constructor () {
this.subscribers = new Set()
} //订阅update函数列表
depend () {
if(activeUpdate) {
this.subscribers.add(activeUpdate)
}
} //所有update函数重新运行
notify () {
this.subscribers.forEach(sub => sub())
}
}
结合上面两部分就是完整实现。
实例3:实现响应式系统
要求如下:
- 1、结合上述两个实例,
convert()
重命名为观察者observe()
- 2、
observe()
转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个Dep
实例,该实例跟踪订阅update
函数列表,并在调用setter
时触发它们重新运行 - 3、
autorun()
接收update
函数作为参数,并在update
函数订阅的属性发生变化时重新运行。
示例:
const state = { count: 0 }
observe(state)
autorun(() => {
console.log(state.count)
}) //输出 count is:0
state.count++ //输出 count is: 1
结合实例1和实例2之后就可以实现上述要求,observe
中修改obj
属性的同时分配Dep
的实例,并在get
中注册订阅者,在set
中通知改变。autorun
函数保存不变。
实现如下:
class Dep {
// 初始化
constructor() {
this.subscribers = new Set()
}
// 订阅update函数列表
depend() {
if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
}
// 所有update函数重新运行
notify() {
this.subscribers.forEach(sub => sub())
}
}
function observe(obj) {
// 迭代对象的所有属性
// 并使用Object.defineProperty()转换成getter/setters
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
// 每个属性分配一个Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
// getter负责注册订阅者
get() {
dep.depend()
return internalValue
},
// setter负责通知改变
set(newVal) {
const changed = internalValue !== newVal
internalValue = newVal
// 触发后重新计算
if (changed) {
dep.notify()
}
}
})
})
return obj
}
let activeUpdate = null
function autorun(update) {
// 包裹update函数到"wrappedUpdate"函数中,
// "wrappedUpdate"函数执行时注册和注销自身
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}
结合Vue文档里的流程图就更加清晰了。
本文内容参考自VUE作者尤大的付费视频