响应式编程(reactive programming)是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程范式
以下使用js代码示例:
let price = 5 let quantity = 2 let total = price * quantity // 10 ? price = 20 console.log(`total is ${total}`) // 10 // 我们想要的 total 值希望是更新后的 40
问题
我们需要将 total
的计算过程存起来,这样我们就能够在 price
或者 quantity
变化时运行计算过程。
方案
首先,我们需要告诉应用程序,“这里有一个关于计算的方法,存起来,我会在数据更新的时候去运行它。“
我们创建一个记录函数并运行它:
let price = 5 let quantity = 2 let total = 0 let target = null target = () => { total = price * quantity } record() // 记录我们想要运行的实例 target() // 运行 total 计算过程
简单定义一个 record
:
let storage = [] // 将 target 函数存在这里 function record () { // target = () => { total = price * quantity } storage.push(target) }
我们存储了 target
函数,我们需要运行它,需要顶一个 replay
函数来运行我们记录的函数:
function replay () { storage.forEach( run => run() ) }
在代码中我们运行:
price = 20 console.log(total) // => 10 replay() console.log(total) // => 40
这样代码可读性好,并且可以运行多次。FYI,这里用了最基础的方式进行编码,先扫盲
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] target = () => { total = price * quantity } function record () { storage.push(target) } function replay () { storage.forEach( run => run() ) } record() target() price = 20 console.log(total) // => 10 replay() console.log(total) // => 40
对象化
问题
我们可以不断记录我们需要的 target
, 并进行 record
,但是我们需要更健壮的模式去扩展我们的应用。也许面向对象的方式可以维护一个 targe 列表,我们用通知的方式进行回调。
方案 Dependency Class
我们通过一个属于自己的类进行行为的封装,一个标准的依赖类 Dependency Class ,实现观察者模式。
如果我们想要创建一个管理依赖的类,标准模式如下:
class Dep { // Stands for dependency constructor () { this.subscribers = [] // 依赖数组,当 notify() 调用时运行 } depend () { if (target && !this.subscribers.includes(target)) { // target 存在并且不存在于依赖数组中,进行依赖注入 this.subscribers.push(target) } } notify () { // 替代之前的 replay 函数 this.subscribers.forEach(sub => sub()) // 运行我们的 targets,或者观察者函数 } }
注意之前替换的方法, storage
替换成了构造函数中的 subscribers
。 recod
函数替换为 depend
。 replay
函数替换为 notify
。
现在在运行:
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend() // 依赖注入 target() // 计算 total console.log(total) // => 10 price = 20 console.log(total) // => 10 dep.notify() console.log(total) // => 40
工作正常,到这一步感觉奇怪的地方,配置 和 运行 target
的地方。
观察者
问题
之后我们希望能够将 Dep 类应用在每一个变量中,然后优雅地通过匿名函数去观察更新。可能需要一个观察者 watcher
函数去满足这样的行为。
我们需要替换的代码:
target = () => { total = price * quantity }
dep.depend()
target()
替换为:
watcher( () => { total = price * quantit })
方案 A Watcher Function
我们先定义一个简单的 watcher 函数:
function watcher (myFunc) { target = myFunc // 动态配置 target dep.depend() // 依赖注入 target() // 回调 target 方法 target = null // 重置 target } 复制代码
正如你所见, watcher
函数传入一个 myFunc
的形参,配置全局变量 target
,调用 dep.depend()
进行依赖注入,回调 target
方法,最后,重置 target
。
运行一下:
price = 20 console.log(total) // => 10 dep.notify() console.log(total) // => 40 复制代码
你可能会质疑,为什么我们要对一个全局变量的 target
进行操作,这显得很傻,为什么不用参数传递进行操作呢?文章的最后将揭晓答案,答案也是显而易见的。
数据抽象
:warning: 问题
我们现在有一个单一的 Dep class
,但是我们真正想要的是我们每一个变量都拥有自己的 Dep
。让我们先将数据抽象到 properties
。
let data = { price: 5, quantity: 2 } 复制代码
将设每个属性都有自己的内置 Dep 类
然我我们运行:
watcher( () => { total = data.price * data.quantit }) 复制代码
当 data.price
的 value 开始存取时,我想让关于 price
属性的 Dep 类 push 我们的匿名函数(存储在 target 中)进入 subscriber 数组(通过调用 dep.depend())。而当 quantity
的 value 开始存取时,我们也做同样的事情。
如果我们有其他的匿名函数,假设存取了 data.price
,同样的在 price
的 Dep 类中 push 此匿名函数。
当我们想通过 dep.notify()
进行 price
的依赖回调时候。我们想 在 price
set 时候让回调执行。在最后我们要达到的效果是:
$ total 10 $ price = 20 // 回调 notify() 函数 $ total 40 复制代码
:white_check_mark: 方案 Object.defineProperty()
我们需要学习一下关于 Object.defineProperty() – JavaScript | MDN 。它允许我们在 property 上定义 getter 和 setter 函数。我们展示一下最基本的用法:
let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { // 仅定义 price 属性 get () { // 创建一个 get 方法 console.log(`I was accessed`) }, set (newVal) { // 创建一个 set 方法 console.log(`I was changed`) } }) data.price // 回调 get() // => I was accessed data.price = 20 // 回调 set() // => I was changed 复制代码
正如你所见,打印两行 log。然而,这并不能推翻既有功能, get
或者 set
任意的 value
。 get()
期望返回一个 value, set()
需要持续更新一个值,所以我们加入 internalValue
变量用于存储 price
的值。
let data = { price: 5, quantity: 2 } let internalValue = data.price // 初始值 Object.defineProperty(data, 'price', { // 仅定义 price 属性 get () { // 创建一个 get 方法 console.log(`Getting price: ${internalValue}`) return internalValue }, set (newVal) { // 创建一个 set 方法 console.log(`Setting price: ${newVal}`) internalValue = newVal } }) total = data.price * data.quantity // 回调 get() // => Getting price: 5 data.price = 20 // 回调 set() // => Setting price: 20 复制代码
至此,我们有了一个当 get 或者 set 值的时候的通知方法。我们也可以用某种递归可以将此运行在我们的数据队列中?
FYI, Object.keys(data)
返回对象的 key 值列表。
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { let internalValue = data[key] Object.defineProperty(data, key, { get () { console.log(`Getting ${key}: ${internalValue}`) return internalValue }, set (newVal) { console.log(`Setting ${key}: ${newVal}`) internalValue = newVal } }) }) total = data.price * data.quantity // => Getting price: 5 // => Getting quantity: 2 data.price = 20 // => Setting price: 20 复制代码
CI 集成
将所有的理念集成起来
total = data.price * data.quantity 复制代码
当代码碎片比如 get 函数的运行并且 get 到 price
的值,我们需要 price
记录在匿名函数 function(target) 中,如果 price
变化了,或者 set 了一个新的 value,会触发这个匿名函数并且 get return,它能够知道这里更新了一样。所以我们可以做如下抽象:
-
Get=> 记录这个匿名函数,如果值更新了,会运行此匿名函数
-
Set=> 运行保存的匿名函数,仅仅改变保存的值。
在我们的 Dep 类的实例中,抽象如下:
-
Price accessed (get)=> 回调
dep.depend()
去注入当前的target
-
Price set=> 回调 price 绑定的
dep.notify()
,重新计算所有的targets
让我们合并这两个理念,生成最终代码:
let data = { price: 5, quantity: 2 } let target = null class Dep { // Stands for dependency constructor () { this.subscribers = [] // 依赖数组,当 notify() 调用时运行 } depend () { if (target && !this.subscribers.includes(target)) { // target 存在并且不存在于依赖数组中,进行依赖注入 this.subscribers.push(target) } } notify () { // 替代之前的 replay 函数 this.subscribers.forEach(sub => sub()) // 运行我们的 targets,或者观察者函数 } } // 遍历数据的属性 Object.keys(data).forEach(key => { let internalValue = data[key] // 每个属性都有一个依赖类的实例 const dep = new Dep() Object.defineProperty(data, key, { get () { dep.depend() return internalValue }, set (newVal) { internalValue = newVal dep.notify() } }) }) // watcher 不再调用 dep.depend // 在数据 get 方法中运行 function watcher (myFunc) { target = myFunce target() target = null } watcher(()=> { data.total = data.price * data.quantity }) data.total // => 10 data.price = 20 // => 20 data.total // => 40 data.quantity = 3 // => 3 data.total // => 60 复制代码
这已经达到了我们的期望, price
和 quantity
都成为响应式的数据了。
Vue 的数据响应图如下:
看到紫色的 Data 数据,里面的 getter 和 setter?是不是很熟悉了。每个组件的实例会有一个 watcher
的实例(蓝色圆圈), 从 getter 中收集依赖。然后 setter 会被回调,这里 notifies 通知 watcher 让组件重新渲染。下图是本例抽象的情况:
显然,Vue 的内部转换相对于本例更复杂,但我们已经知道最基本的了。