?本篇博文可能学到的知识点
更好的理解 Vue 响应式工作原理
学习 Vue 的设计模式
学习 Proxy API
使用 Proxy 实现观察者模式
现代前端开发必不可少会用到的 Vue、React 等框架,这些框架的共同之处在于都提供了响应式(Reactive)和组件化(Composable)的视图组件,组件化开发重新定义了前端开发技术栈。结合前端构建工具以及基于框架出现的各种经过精心设计的UI组件库,让前端也进入到了一个工程化的时代。构建页面变得从未有过的简洁高效。
如果你是一名经验丰富(nian ling da)的程序员,或多或少都会接触到没有这些框架之前的状态,那时候我们还手持 jQuery 利器,操纵着一手好 dom,当你初次接触到响应式框架的时候或许会被它的好用所惊艳到。我们只需要改变数据,dom就更新了。本篇博文主要是来讨论被惊艳到的响应式框架是如何实现的。我们首先来看看 Vue 是如何实现响应式系统的?
Vue 是如何实现响应式系统的?
看一个简单的小例子
假设我们购物车中有一个商品,单价 price,数量 quantity,总价为 total。
有这样一个简单的功能,点击按钮让 price 发生变化,那么 total 为计算属性,也随之发生变化。
考虑一下 Vue是如何实现这样的功能的,其实我们是更改了数据,而依赖于它的计算属性也发生了变化。
Vue 是如何实现追踪数据的变化的
在官方文档,深入响应式原理里有讲到
当你把一个普通的 JavaScript 对象传入 Vue 实例做为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
reactivity
一些弊端
受现代 JavaScript 的限制
对于已创建的实例,Vue **无法检测到对象属性的添加和删除。**Vue 允许使用 Vue.set(object, propertyName, value) 的方法进行动态添加响应式属性,或者使用别名写成 this.$set(object, propertyName, value)
对于数组,Vue 无法检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
第一条的解决方式同样是使用 this.$set(vm.items, indexOfItem, newValue)
第二条的解决方式是用 vm.items.splice(indexOfItem, 1, newValue)
对于以上的弊端,其实官方已经给出了解决方法,就是使用 Proxy 代替 Object.defineProperty API,这个改动会伴随着万众期待的 Vue 3.0 的发布而应用。大家都知道 Vue 2.x 也就是现在的版本,响应式是通过 Object.defineProperty API来实现的,那么既然知道了解决方法,我们不妨提前学习一下 Proxy 是如何做到响应式的。
使用 Proxy 实现响应式系统
Proxy 简介
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
基本用法
let p = new Proxy(target, hanlder)
参数:
target 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数。
MDN 上的基础示例:
let handler = {
get: function(target, name){
return name in target ? target[name] : 37;
}
};
let p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log(‘c’ in p, p.c); // false, 37
复制代码
上述例子使用了 get ,当对象中不存在属性名时,缺省返回数为37。我们知道了对于操作对象,可以在使用 handler 处理一些事务。关于 handler 则有十三种。
实现一个响应式系统
重新回到我们的小例子,我们有一些变量,单价 price,数量 quantity,总价为 total。我们来看下 JavaScript 是如何工作的
let price = 6
let quantity = 2
let total = 0
total = price * quantity
console.log(total) // 12
price = 10
console.log(total) // 12
复制代码
这好像不是我们的期望,我们期望的是,改变了 price,total的值也会更新。如何做到这一点,并不难。
let price = 6
let quantity = 2
let total = 0
total = price * quantity
console.log(total) // 12
price = 10
console.log(total) // 12
const updateTotal = () => {
total = price * quantity
}
updateTotal()
console.log(total) // 20
复制代码
我们定义了一个方法 updateTotal,让这个方法执行了我们需要更新 total 的业务逻辑,再重新执行这个方法那么 total 的值就改变了。
我们可以考虑下我们想到达到什么样的目的,其实很简单,就是当改变 price 或者 quantity 的时候 total 的值会跟着改变。学过设计模式的话,我们很容易想到这个场景符合观察者模式。
使用观察者模式
观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
observer_pattern
现在我们使用观察者模式定义了观察变量 price 与 quantity,如果它们的值发生变化,那么依赖于它的计算属性 total 将会得到一个 notify,这个 notify 即是我们的目标,去执行 updateTotal。
创建依赖类
把观察者模式抽象为一个依赖类
// 代表依赖类
class Dep {
constructor () {
this.subscribers = [] // 把所有目标收集到订阅里
}
addSub (sub) { // 当有可观察目标时,添加到订阅里
if (sub && !this.subscribers.includes(sub)) {
// 只添加未添加过的订阅
this.subscribers.push(sub)
}
}
notify () { // 当被观察的属性发生变动时通知所有依赖于它的对象
this.subscribers.forEach(fn => fn()) // 重新执行所有订阅过的目标方法
}
}
复制代码
那么如何使变量 price 和 quantify 变得可观察,在 Vue 2.x 中使用的是 Object.defineProperty ,本文会使用 Proxy 来实现。
// 使变量变为一个可观察的对象的属性
const dataObj = {
price: 6,
quantity: 2
}
let total = 0
let target = null
class Dep { // 代表依赖类
…
}
const dep = new Dep()
data = new Proxy(dataObj, {
get (obj, key) {
dep.addSub(target) // 将目标添加到订阅中
return obj[key]
},
set (obj, key, newVal) {
obj[key] = newVal // 将新的值赋值给旧的值,引起值的变化
dep.notify() // 被观察的属性值发生变化,即通知所有依赖于它的对象
}
})
total = data.price * data.quantity
console.log(total) // 12
data.price = 10
console.log(total) // 12
target = () => {
total = data.price * data.quantity
}
target()
target = null
console.log(total) // 20
复制代码
上面代码稍微有些凌乱,我们重构一下,观察者模式结合 Proxy 最终目的就是输出被观察的对象。我们可以抽象为一个 observer
使用 Proxy 实现观察者模式
// 将依赖类与 Proxy 封装为 observer,输入一个普通对象,输出为被观察的对象
const observer = dataObj => {
const dep = new Dep()
return new Proxy(dataObj, {
get (obj, key) {
dep.addSub(target) // 将目标添加到订阅中
return obj[key]
},
set (obj, key, newVal) {
obj[key] = newVal // 将新的值赋值给旧的值,引起值的变化
dep.notify() // 被观察的属性值发生变化,即通知所有依赖于它的对象
}
})
}
const data = observer(dataObj)
复制代码
我们注意到,其实每次我们还要重新执行我们的目标 target ,让 total 值发生变化。这块儿逻辑我们可以抽象为一个 watcher ,让它帮我们做一些重复做的业务逻辑。
创建 watcher
const watcher = fn => {
target = fn
target()
target = null
}
watcher(() => {
total = data.price * data.quantity
})
复制代码
我们最终代码优化为:
// 使变量变为一个可观察的对象的属性
const dataObj = {
price: 6,
quantity: 2
}
let total = 0
let target = null
// 代表依赖类
class Dep {
…
}
// 使用 Proxy 实现了观察者模式
const observer = dataObj => {
…
}
const data = observer(dataObj)
// 高阶函数,重复执行订阅方法
const watcher = fn => {
…
}
watcher(() => {
total = data.price * data.quantity
})
console.log(total) // 12
data.price = 30
console.log(total) // 60
复制代码
我们最终实现了开始的想法,total 会根据 price 值的改变而改变。实现了简单的响应式系统。
为了缩小篇幅,上面的方法同时也有讲过,即折叠(简化)起来,我们再回到 Vue 是如何追踪数据的依赖的那张图。再看看我们是如何实现的。
vue-reactive-proxy
总结一下
通过小例子我们学习到的内容
我们学习到了通过创建一个 Dep 依赖类,来收集依赖关系,在订阅者属性被改变时,所有依赖于它的对象得以得到一个通知。
结合 Dep 依赖类,我们使用 Proxy 实现了观察者模式的 observer 方法
我们创建了一个 watcher 观察者方便管理我们要重新执行的业务逻辑,即我们添加到订阅里需要执行的方法