Vue 最独特的特性之一,是其非侵入性的响应性系统(Vue 3 Reactivity System)。我们有必要研究一下它的底层原理。从它是如何构建的、Vue 内部使用的设计模式、以及利用它提高 Vue 调试技能,来确保掌握新的 Vue 3 模块化响应式库。
理解响应式(Understanding Reactivity)
当你第一次看到 Vue 的响应式式系统时,它看起来很神奇。
就拿这个简单的 app 来说:
<div id="app">
<div>Price: ${{ product.price }}</div>
<div>Total: ${{ product.price * product.quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
product: {
price: 5.00,
quantity: 2
}
},
computed: {
totalPriceWithTax() {
return this.product.price * this.product.quantity * 1.03
}
}
})
</script>
不知何故,Vue 的 Reactivity 系统知道一旦price
发生变化,它就应该做三件事:
● 更新我们网页上的price
值。
● 重新计算乘以price
*quantity
的表达式,并更新页面。
● 再次调用totalPriceWithTax
函数并更新页面。
但是 Vue 的 Reactivity 系统如何知道price
变化时要更新什么,以及它如何跟踪一切?
这并不是 JavaScript 编程通常的工作方式。
例如,如果运行下面👇的代码:
let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity // 10?
product.price = 20
console.log(`total is ${total}`)
你认为它会打印什么?由于我们没有使用 Vue,它将打印 10。
>> total is 10
在 Vue 中,我们希望在price
或quantity
更新时更新total
。我们想要:
>> total is 40
可惜的是,JavaScript 是过程式的,而不是响应式的,所以这在现实生活中是行不通的。为了使total
变成响应式,我们必须使用 JavaScript 使事情的行为模式变得不同。
我们将使用与 Vue 3 相同的方法 (与 Vue 2 极其不同) 从头开始构建一个简单的响应式系统。然后再查看 Vue 3 源码以发现我们编写的这些模式。
构建简易版响应式系统
首先,我们需要通过某种方式来告诉我们的应用程序,“存储我即将运行的代码(effect
),我可能需要你在其他时间运行它。”然后我们运行代码,后面如果price
或quantity
变量更新了,再次运行存储的代码。

我们可以通过记录函数(effect
)来做到这一点,以便我们可以再次运行它。
let product = { price: 5, quantity: 2 }
let total = 0
let effect = function () {
total = product.price * product.quantity
})
track() // 记住这段 effect 代码
effect() // 同时运行它
我们在effect
变量中存储了一个匿名函数,然后调用了一个track
函数。还可以使用 ES6 箭头语法将其改为👇,使得代码更为简洁:
let effect = () => { total = product.price * product.quantity }
为了存储我们的effects
,我们将创建一个dep
变量,它是一个 new Set 集,代表依赖关系。通常在观察者设计模式中,依赖项会有订阅者(在本例中是effects
),当对象(本例是product object
)改变状态时订阅者会得到通知。
let dep = new Set() // 我们的 object 追踪的 effects 列表
为了跟踪我们的依赖,我们通过track
函数将effects
添加到这个集合中:
function track () {
dep.add(effect) // 存储当前的 effect
}
两点通过对比 Vue 2 对存储依赖模式的展开
(可以先把整体看完,回过头再来看)
1.和 Vue 2 用
depend
和notify
记录和执行effects
,为什么 Vue 3 更改成了track
和trigger
?
![]()
depend
和notify
是动词,和它们的所有者(Dep 类的实例)相关,可以说成一个 Dep 实例正在被依赖、或者正在通知它的订阅者。
Vue 3从技术上来讲已经没有 Dep 类(Class)了。Dep 类里的depend
和notify
现在被抽离到两个独立的函数(track
和trigger
)里。所以当调用track
和trigger
更像是跟踪 sth,而不是 sth 正在被依赖。只是一种形式上的转变,就像从a.b=> b.call(a)
2.为什么在 Vue 2 中 Dep 是一个有
subscribers
(订阅者)的类,而 Vue 3 只是一个简单的 Set 集?
![]()
因为两个方法都被抽出了,Dep 类本身只剩下一个订阅者集合,这样依赖用一个类仅仅去封装一个Set是没有意义的,因此直接声明一个Set,而不是创建一个对象去做这件事。
JavaScript Array 和 Set 之间的区别在于,Set 不能有重复的值,并且不像数组那样使用索引。
我们正在存储effect
(在我们的例子中是 { total = price *quantity }
),以便我们可以稍后运行它。这是这个 dep Set 的可视化:

让我们编写一个触发器函数(trigger
)来运行我们记录的所有内容。
function trigger() {
dep.forEach(effect => effect())
}
它会遍历我们存储在 dep Set 中的所有匿名函数并执行它们中的每一个。然后我们只需在product
的状态改变后触发trigger()
:
product.price = 20
console.log(total) // => 10
trigger()
console.log(total) // => 40
是不是很简单?这里是完整的代码:
let product = { price: 5, quantity: 2 }
let total = 0
let dep = new Set()
function track() {
dep.add(effect)
}
function trigger() {
dep.forEach(effect => effect())
}
let effect = () => {
total = product.price * product.quantity
}
track()
effect()
product.price = 20
console.log(total) // => 10
trigger() // 调用 trigger 才会再次运行 dep 的 effect
console.log(total) // => 40
存在问题一:多个属性
我们可以根据需要继续跟踪trigger
,但是我们的响应式对象具有不同的属性,且每个属性都需要自己的 dep (一组effects
set 集),这个 dep 里的effects
会在该属性值改变时重新运行。
比如我们的product
对象:let product = { price: 5, quantity: 2 }
,
price
属性需要它自己的 dep (effects set集),quantity
也需要它自己的 dep (effects Set集)。让我们构建一个解决方案来正确记录这些。
当现在调用track
或trigger
时,我们需要知道对象中的哪个属性(price
或quantity
)是追踪目标。为此,我们将创建一个depsMap
,它的类型为 Map。以下是它的可视化:

注意 depsMap 的每一个key
,是我们想要添加(或追踪)新effect
的属性名。因此,我们需要将 key 发送到 track 函数。每个key
对应的值是一个 dep (effects set集)
const depsMap = new Map()
function track(key) { // 确保这个 effect 被追踪了
let dep = depsMap.get(key) // 获取 key(属性) set 时需要运行的 dep (effects set 集)
if (!dep) {
// 目前还没有依赖这个 key 的 effects
depsMap.set(key, (dep = new Set())) // 创建一个 new Set
}
dep.add(effect) // Add effect to dep
}
}
function trigger(key) {
let dep = depsMap.get(key) // 获取此 key 的 dep (effects set)
if (dep) { // 如果存在
dep.forEach(effect => {
// 运行所有 effects
effect()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track('quantity') // 存储 quantity 的 effect
effect() // 运行 effect
console.log(total) // --> 10
product.quantity = 3
trigger('quantity') // 触发 quantity 的 dep (effects set)
console.log(total) // --> 40
存在问题二:多个响应式对象
这个方案看起来不错,直到我们有多个需要track effects
的响应式对象,比如新增了一个let user = { id: 213, name: 'Joe Smith' }
。
现在我们需要一种为每个对象存储 depsMap 的方法。我们需要另一种 Map (WeakMap 类型),key 就是我们的响应式对象(product
、user
)。WeakMap 是一个 JavaScript Map,它只使用对象作为键。它工作起来就像这样:
let product = { price: 5, quantity: 2 }
const targetMap = new WeakMap()
targetMap.set(product, "example code to test")
console.log(targetMap.get(product)) // ---> "example code to test"
显然这不是我们要使用的代码。Vue 3 将用来存储每个响应式对象的属性关联的依赖的 Map 对象称之为targetMap
,因为我们会考虑 target 我们正在跟踪的对象。这是我们可视化的targetMap
:

targetMap
存储每个响应式对象关联的depsMap
,depsMap
存储每个属性的dep
依赖,每个dep
存储一组 effect set 集,这些 effects 会在值发生变化时重新运行。
之所以这样嵌套式设计是因为:
在 Vue2 中,我们使用 ES5 进行getter
和setter
转换,当我们用 for Each 遍历对象上的key
时,自然有一个小闭包为其属性(key
)存储关联的 Dep。
但是在 Vue 3,我们使用了 Proxy,proxy 的handler
接收target
和key
,你并不能得到为每个属性存储关联 dependency 的一个个闭包。
所以我们需要给定一个target
对象和一个target
对象上的key
,来保证我们始终能找对对应的 dependency 实例。唯一的方法就是把它们(target
和key
)分到两个等级不同的嵌套 maps。
当我们现在调用track
或trigger
时,需要知道目标是哪个对象。因此,当我们会在调用它时发送target
和key
。
const targetMap = new WeakMap() // targetMap 存储每个 object 更新时应重新运行的 effects
function track(target, key) { // 我们需要确保这个 effect 被追踪了
let depsMap = targetMap.get(target) // 获取此 target (响应式对象) 当前的 depsMap
if (!depsMap) {
// 不存在的话,新建一个 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // 获取 key (属性) 被 set 时需要运行的当前 dependencies (effects)
if (!dep) {
// 如果 dependencies 不存在,新建一个 new Set
depsMap.set(key, (dep = new Set()))
}
dep.add(effect) // 把需要的 effect 添加到 dependency
}
function trigger(target, key) {
const depsMap = targetMap.get(target) // 此对象是否有任何具有dependencies (effects) 的属性
if (!depsMap) {
return
}
let dep = depsMap.get(key) // 获取与此属性关联的 dependencies (effects)
if (dep) {
dep.forEach(effect => {
// 遍历 dep 运行每个 effect
effect()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track(product, 'quantity') // 添加 effect 的时候需传入对象和属性
effect() // 执行 effect
console.log(total) // --> 10
product.quantity = 3
trigger(product, 'quantity') // 触发也需传入对象和属性
console.log(total) // --> 15
所以现在我们有一种非常有效的方法来跟踪多个对象的 dependencies,这是构建我们的响应式系统时的一大难题。战斗已经结束了一半。在下一篇,我们将了解如何使用 ES6 代理自动调用track
和trigger
。
Vue 3 响应式原理一 - Vue 3 Reactivity
Vue 3 响应式原理二 - Proxy and Reflect
Vue 3 响应式原理三 - activeEffect & ref
Vue 3 响应式原理四 - Computed Values & Vue 3 源码
课程链接:https://www.vuemastery.com/courses/vue-3-reactivity/vue3-reactivity
网友评论