什么是响应式 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 才会变化.
- a = 4;
- console.log(a); // 4
- console.log(b); // 30
- 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>
- document
- .querySelector('.cell.b')
- .textContent = state.a * 10
现在建立数据 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 文档里的流程图就更加清晰了.
Job Done!!!
本文内容参考自 VUE 作者尤大的付费视频
来源: https://juejin.im/post/5bce6a26e51d4579e9711f1d