Vue 3 响应式
我们将了解新的 Vue 3 响应式系统(reactivity system)。了解它是如何从头构建的,将帮助您理解 Vue 中使用的设计模式,提高您的 Vue 调试技能,使您能够使用新的 Vue 3 模块化响应式库,甚至自己编写 Vue 3的源代码。 在本节课中,我们将使用与 Vue 3 源代码中相同的技术开始构建一个简单的响应式系统。
理解响应式系统
先以这个简单的应用程序为例 现看一下下面简单的的程序:
<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 的响应式系统以某种方式知道,如果price
发生变化,它应该做三件事:
- 在更新网页上
price
值。 - 重新计算
price * quantity
的表达式,然后更新页面。 - 再次调用
totalPriceWithTax
函数并更新页面。
所以,Vue的响应式系统是如何知道price
变化时要更新什么,以及如何跟踪所有情况?
这不是 JavaScript 编程的工作方式。 例如,如果我运行以下代码:
let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity // 10 right?
product.price = 20
console.log(`total is ${total}`)
会输出什么呢?如果我们不借助 Vue,会输出:
>> total is 10
Vue 中,我们希望只要price
或quantity
得到更新,total
就会得到更新。 我们想要的结果:
>> total is 40
可惜 JavaScript 是过程式的,不是响应式的,因此在实际上会不起作用。 为了使total
是响应式的,我们必须使用JavaScript 使其行为有所不同。
在后面的其余部分中,我们将使用与 Vue 3 相同的方法(与 Vue 2 截然不同)从头开始构建 Reactive System。 然后,我们将研究 Vue 3源代码,来查找阅读我们从头开始编写的这些模式。
保存代码以便以后运行
问题
如您在上面的代码中所看到的,为了建立响应式,我们需要保存我们计算total
的方式,以便我们可以在price
或quantity
发生变化时重新运行它。
解决方案
首先,我们需要以某种方式告诉我们的应用程序:“存储我将要运行的影响(effect),可能需要您在其他时间运行它。” 然后,我们要运行代码,如果price
或quantity
变量得到更新,需要再次运行存储的代码。
我们可以通过记录影响(effect)来实现这一点,这样我们就可以再次运行它。
let product = { price: 5, quantity: 2 }
let total = 0
let effect = function () {
total = product.price * product.quantity
})
track() // Remember this in case we want to run it later
effect() // Also go ahead and run it
请注意,我们将匿名函数存储在effect
变量内,然后调用track
函数。 使用 ES6 箭头语法,我也可以这样写:
let effect = () => { total = product.price * product.quantity }
为了定义track
,我们需要一个存放影响(effects)的地方,我们可能有很多。我们将创建一个名为dep
的变量。 之所以称为依赖,是因为通常在观察者设计模式下,依赖具有订阅者(在我们的情况下为effects),这些订阅者将在对象更改状态时得到通知。 就像我们在 Vue 2 版本中所做的那样,我们可以使依赖项成为具有订阅者数组的类。 但是,由于它需要存储的只是一组效果,我们可以简单地创建一个 Set。
let dep = new Set() // Our object tracking a list of effects
然后我们的track
函数可以简单地将我们的影响(effects)添加到这个集合中。
function track () {
dep.add(effect) // Store the current effect
}
如果您不熟悉 JavaScript 数组和 Set之间的区别,则是 Set 不能有重复的值,并且不使用数组之类的索引。 如果您不熟悉,请在此处详细了解 。
我们要存储effect(在我们的示例中是{ total = price * quantity }
),以便我们可以稍后运行它。 这是dep 的 Set 的可视化展示:
让我们编写一个触发函数来运行我们记录的所有内容。
function trigger() {
dep.forEach(effect => effect())
}
这将遍历我们存储在dep
集中的所有匿名函数,并执行每个匿名函数。 然后在我们的代码中,我们可以:
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()
console.log(total) // => 40
问题:多个属性
我们可以根据需要继续跟踪 effects,但是我们的响应式对象将具有不同的属性,而且每个属性都需要它们自己的dep(一组 effects)。看看下面的对象:
let product = { price: 5, quantity: 2 }
我们的price
属性需要自己的 dep(效果集),而我们的quantity
需要自己的 dep(效果集)。 让我们再想办法以来正确记录这些内容。
解决方案:depsMap
现在,当我们调用跟踪或触发器时,我们需要知道我们要定位的对象是哪个属性(price
或quantity
)。 为此,我们将创建一个depsMap
,它是 Map 类型(请考虑键和值)。 可以将其可视化展示:
请注意,depsMap
有一个键,这个键将是我们想要添加(或跟踪)新 effect 的属性名。所以我们需要将这个键值发送给track
函数。
const depsMap = new Map()
function track(key) {
// Make sure this effect is being tracked.
let dep = depsMap.get(key) // Get the current dep (effects) that need to be run when this key (property) is set
if (!dep) {
// There is no dep (effects) on this key yet
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dep
}
}
function trigger(key) {
let dep = depsMap.get(key) // Get the dep (effects) associated with this key
if (dep) { // If they exist
dep.forEach(effect => {
// run them all
effect()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track('quantity')
effect()
console.log(total) // --> 10
product.quantity = 3
trigger('quantity')
console.log(total) // --> 40
问题:多个响应对象
现在,我们需要一种为每个对象(例如product
)存储depsMap
的方法。 我们需要另一个Map
,每个对象一个Map
,但是关键是什么? 如果我们使用 *WeakMap`,则可以将对象本身用作键。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"
显然,这不是我们要使用的代码,但我想向您展示我们的targetMap
如何将我们的product
对象用作键。 之所以将其称为targetMap
,是因为我们将考虑将目标对象定位为目标。 还有一个原因,在下一课中会讲得更清楚。 这是我们可视化展示:
当我们调用track
或trigger
,我们现在需要知道要定位的对象。因此,我们将在调用目标时同时发送target
和键(the key)。
const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
// We need to make sure this effect is being tracked.
let depsMap = targetMap.get(target) // Get the current depsMap for this target
if (!depsMap) {
// There is no map.
targetMap.set(target, (depsMap = new Map())) // Create one
}
let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
if (!dep) {
// There is no dependencies (effects)
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
if (!depsMap) {
return
}
let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
if (dep) {
dep.forEach(effect => {
// run them all
effect()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track(product, 'quantity')
effect()
console.log(total) // --> 10
product.quantity = 3
trigger(product, 'quantity')
console.log(total) // --> 15
现在我们有了一种非常有效的方法来跟踪对多个对象的依赖关系,这是构建响应式系统时的一大难题。战斗已经结束了一半。 在下面,我们将发现如何使用 ES6 proxy 自动调用track
和trigger
。
Proxy 与 Reflect
我们以及学习了 Vue 3 如何跟踪 effects 以在需要时重新运行它们。 但是,我们仍然必须手动调用track
和trigger
。 在本节中,我们将学习如何使用 Reflect 和 Proxy 来自动调用它们。
解决方案:挂钩获取和设置
我们需要一种方法来挂钩(或监听)响应式对象上的 get 和 set 方法。 GET 属性 => 我们需要跟踪当前 effect SET 属性 => 我们需要触发此属性的所有跟踪依赖项(effects)
了解如何执行此操作的第一步是了解在带有 ES6 Reflect 和 Proxy 的 Vue 3 中,我们如何拦截 GET 和 SET 调用。 在 Vue 2 中,我们使用 ES5 的Object.defineProperty
进行了此操作。
了解ES6 Reflect
要打印出对象属性,我可以这样做:
let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or
console.log('quantity is ' + product['quantity'])
但是,我也可以通过使用Reflect
获取对象上的值。Reflect
允许您获取对象的属性。 这只是我上面写的另一种方式:
console.log('quantity is ' + Reflect.get(product, 'quantity'))
为什么要使用reflect
? 因为它具有我们稍后需要的功能,后面会说道。
了解 ES6 Proxy
代理是另一个对象的占位符,默认情况下,被代理给了该对象。 因此,如果我运行以下代码:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)
proxiedProduct
代理了product
,该product
返回 2 作为数量。 注意Proxy {}
中的第二个参数吗? 称为handler
,可用于定义代理对象上的自定义行为,例如拦截get
和set
调用。 这些拦截器方法称为 traps(捕捉器),这是我们如何在handler
上设置get
trap的方法:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get() {
console.log('Get was called')
return 'Not the value'
}
})
console.log(proxiedProduct.quantity)
在控制台中,我会看到:
Get was called
Not the value
我们已经重写了属性值被访问时get
的返回值。我们应该返回实际的值,我们可以这样做:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key) { // <--- The target (our object) and key (the property name)
console.log('Get was called with key = ' + key)
return target[key]
}
})
console.log(proxiedProduct.quantity)
这里get
函数具有两个参数,即target(即我们的对象(product
))和我们尝试获取的key
(为quantity
)。 现在我们看到:
Get was called with key = quantity*
2
这也是我们可以使用 Reflect 并向其添加额外参数的地方。
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) { // <--- notice the receiver
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver) // <----
}
})
请注意,我们的get
还有一个称为receiver
的附加参数,我们将其作为参数发送到Reflect.get
中。 这样可以确保当我们的对象从另一个对象继承了值 / 函数时,将使用正确的this
值。 这就是为什么我们总是在Proxy
内部使用Reflect
的原因,这样我们可以保留我们自定义的原始行为。
现在,我们添加一个 setter 方法,这里应该没有什么大的惊喜:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
}
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
})
proxiedProduct.quantity = 4
console.log(proxiedProduct.quantity)
注意,set
看起来和get
非常相似,除了使用了Reflect.set
,该函数接收设置target
(product)的value
。我们预期的输出是:
Set was called with key = quantity and value = 4
Get was called with key = quantity
4
还有另一种方法可以封装此代码,即您在 Vue 3 源代码中看到的内容。首先,我们将把代理代码包装在一个返回代理的reactive
函数中,如果您使用过 Vue 3 Composition API,您应该会很熟悉这个函数。然后我们将分别声明我们的handler
和 traps,并将它们发送到代理中。
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)
返回的结果与上面相同,但是现在我们可以轻松地创建多个响应式对象。
结合 Proxy + Effect 存储
如果我们使用创建响应式对象的代码,请记住: GET 属性 => 我们需要跟踪当前 effect SET 属性 => 我们需要触发此属性的所有跟踪依赖项(effects)
开始想象一下,需要在上面的代码中调用track
和trigger
的地方:
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// Track
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue != result) { // Only if the value changes
// Trigger
}
return result
}
}
return new Proxy(target, handler)
}
现在,将这两段代码放在一起:
const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
// We need to make sure this effect is being tracked.
let depsMap = targetMap.get(target) // Get the current depsMap for this target
if (!depsMap) {
// There is no map.
targetMap.set(target, (depsMap = new Map())) // Create one
}
let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
if (!dep) {
// There is no dependencies (effects)
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
if (!depsMap) {
return
}
let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
if (dep) {
dep.forEach(effect => {
// run them all
effect()
})
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue != result) {
trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
}
return result
}
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect()
console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)
请注意,我们不再需要调用trigger
和track
,因为在我们的get
和set
方法中已经正确调用了它们。 运行这段代码可以给我们:
before updated quantity total = 10
after updated quantity total = 15
哇,我们已经走了很长一段路! 在此代码稳定之前,只有一个错误要修复。 具体来说,我们只希望在会被effect
函数影响到的响应式对象上调用track
。 而现在,只要获得响应式对象属性,就会调用track
。
activeEffect & ref
我们将通过修复一个小错误然后实现响应式引用来继续构建我们的响应式代码,就像您在 Vue 3 中看到的那样。前面代码的底部如下所示:
...
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect()
console.log(total)
product.quantity = 3
console.log(total)
当我们添加从响应式对象(reactive object)获取属性的代码时,问题就来了,就像这样:
console.log('Updated quantity to = ' + product.quantity)
这里的问题是即使我们不在effect
内,也将调用track
及其所有功能。 我们只希望在活动的影响(the active effect) 中调用get
,查找并记录 effect。
解决方案:activeEffect
为了解决这个问题,先创建一个activeEffect
,这是一个全局变量,用于存储当前正在运行的 effect 。然后在一个名为effect
的新函数中进行设置:
let activeEffect = null // The active effect running
...
function effect(eff) {
activeEffect = eff // Set this as the activeEffect
activeEffect() // Run it
activeEffect = null // Unset it
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
effect(() => {
total = product.price * product.quantity
})
effect(() => {
salePrice = product.price * 0.9
})
console.log(
`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`
)
product.quantity = 3
console.log(
`After updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`
)
product.price = 10
console.log(
`After updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`
)
这里不再需要手动调用effect
。 它会在我们的新 effect 函数中自动调用。 而且还添加了第二个 effect。 我还更新了console.log
,使其看起来更像是测试,可以验证正确的输出。 您可以从 github 上获取所有代码。
目前一切都很好,但是我们还需要在track
函数中进行另一项更改。 它需要使用我们新的activeEffect
。
function track(target, key) {
if (activeEffect) { // <------ Check to see if we have an activeEffect
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(activeEffect) // <----- Add activeEffect to dependency map
}
}
现在来运行代码,如下:
Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 15) = 15 salePrice (should be 4.5) = 4.5
After updated total (should be 30) = 30 salePrice (should be 9) = 9
Ref 的必需需求
我意识到,如果使用salePrice
而不是price
,那么我计算total
的方式可能会更有意义,就像这样:
effect(() => {
total = salePrice * product.quantity
})
如果要创建一家真实的商店,则会基于salePrice
来计算总数。 但是,此代码不会响应式地起工作。 具体来说,当product.price
更新时,它将以这种方式响应式地重新计算salePrice
:
effect(() => {
salePrice = product.price * 0.9
})
但是,由于salePrice
不是响应式地,因此不会重新运行技术total
的 effect。 上面的第一个 effect 不会重新运行。 我们需要某种方法来使salePrice
具有响应式,如果我们不必将其包装在另一个响应式对象中,那就太好了。 如果您熟悉Vue 3 Composition API,您可能会认为我应该使用ref
创建一个 Reactive Reference。 我们开工吧:
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
根据 Vue 文档,响应式引用接受一个内部值,并返回一个反应性的、可变的ref
对象。
ref对象具有指向内部值的单个属性.value
。 因此,我们需要使用.value
稍微改变一下 effect。
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
代码应该可以正常工作,并在更新salePrice
时正确地更新total
。但是我们仍然需要定义ref
。 我们有两种方法可以做到。
1. 用响应式定义引用
首先,我们可以简单地使用已定义的reactive
:
function ref(intialValue) {
return reactive({ value: initialValue })
}
但是,这不是 Vue 3 用基本值(primitives)定义ref
的方式,因此让我们以不同的方式实现它。
理解 JavaScript 对象访问器
为了理解 Vue 3 如何定义ref
,首先需要确保理解对象访问器。 这些有时也称为 JavaScript 计算属性(computed properties)(不要与 Vue 的计算属性混淆)。 在下面,您可以看到一个使用对象访问器的简单示例:
let user = {
firstName: 'Gregg',
lastName: 'Pollack',
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ')
},
}
console.log(`Name is ${user.fullName}`)
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`)
get
和set
是 获取 fullName
并相应地 设置 fullName
的对象访问器。 这是纯 JavaScript 功能,不是 Vue 的函数。
2. 用对象访问器定义 Ref
使用对象访问器以及我们的track
和trigger
操作,现在可以使用以下方法定义引用:
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value')
},
}
return r
}
这就是它的全部。现在,当我们运行以下代码时:
...
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value')
},
}
return r
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
console.log(
`Before updated quantity total (should be 9) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
`After updated quantity total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
`After updated price total (should be 27) = ${total} salePrice (should be 9) = ${salePrice.value}`
)
我们得到了预期的结果:
Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated total (should be 27) = 27 salePrice (should be 9) = 9
现在salePrice
是响应式的,并且total
会在它发生变化时进行更新!
计算值和 Vue 3 源码
在构建响应式示例时,您可能想知道“为什么在使用effect
的地方没有使用computed
来表示值” :
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
salePrice.value = product.price * 0.9
})
effect(() => {
total = salePrice.value * product.quantity
})
显然,如果我要对 Vue 进行编码,我会将salePrice
和total
都写为计算属性。 如果您熟悉 Vue 3 composition API,则可能熟悉计算的语法。 们可能会像这样使用计算后的语法(即使我们尚未定义):
let product = reactive({ price: 5, quantity: 2 })
let salePrice = computed(() => {
return product.price * 0.9
})
let total = computed(() => {
return salePrice.value * product.quantity
})
有道理吧? 注意salePrice
计算属性是如何包含在total
计算属性中的,并且使用.value
进行访问。 这是我们实现的第一个线索。 看来我们正在创建另一个响应式引用。 下面是创建计算函数的方式:
function computed(getter) {
let result = ref() // Create a new reactive reference
effect(() => (result.value = getter())) // Set this value equal to the return value of the getter
return result // return the reactive reference
}
您可以在 Github 上完整查看/运行代码。 我们的代码打印出来:
Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9
几乎完美的 Vue 响应式
值得一提的是,我们可以对响应式对象进行某些 Vue 2 无法实现的工作。具体地说,我们可以添加新的响应式属性。 像这样:
...
let product = reactive({ price: 5, quantity: 2 })
...
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'
它将输出:
Product name is now Shoes
Product name is now Socks
在 Vue 2 中,这是不可能的,因为它使用Object.defineProperty
将 getter 和 setter 添加到单个对象属性中实现响应式的。 现在,借助Proxy
,可以毫无问题地添加新属性,并且它们可以立即响应的。
针对 Vue 3 源代码测试我们的代码
您可能想知道,此代码是否可针对 Vue 3 源工作? 为此,我克隆了 vue-next 库,运行 yarn install
,然后yarn build reactivity
。在package/reactivity/dist/
中产生来很多文件。 然后,在那里找到reactivity.cjs.js
文件,把它移到我的示例文件(github 上的示例文件),进行测试,代码:
var { reactive, computed, effect } = require('./reactivity.cjs')
// Exactly the same code here from before, without the definitions
let product = reactive({ price: 5, quantity: 2 })
let salePrice = computed(() => {
return product.price * 0.9
})
let total = computed(() => {
return salePrice.value * product.quantity
})
console.log(
`Before updated quantity total (should be 9) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
`After updated quantity total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
`After updated price total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`
)
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'
运行 node 08-vue-reactivity.js
,正如所料,得到了所有相同的结果:
Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9
Product name is now Shoes
Product name is now Socks
所以我们的响应式系统和 Vue 一样好! 好吧,从基本的角度来看……是的,但实际上 Vue 的版本要复杂得多。
Vue 3 响应式文件
如果在/packages/reactivity/src/
中查看 Vue 3 源代码,就会发现以下文件。它们是 TypeScript(ts)文件,我们还是能够阅读它们(即使您不知道 TypeScript)。
- effect.ts - 定义
effect
函数以封装可能包含响应式引用和对象的代码(reactive references and objects)。 包含从get
请求调用的track
和从set
请求调用的trigger
。 - baseHandlers.ts - 包含诸如
get
和set
之类的Proxy
处理程序,它们调用track
和trigger
(来自 effect.ts)。 - react.ts - 包含使用
get
和set
(来自 basehandlers.ts)创建 ES6 代理的响应式语法的功能。 - ref.ts - 定义我们如何使用对象访问器创建响应 Ref 引用(就像我们所做的那样)。 还包含
toRefs
,它将响应式对象转换为一系列访问原始代理的响应式引用。 - compute.ts - 使用
effect
和对象访问器定义computed
函数(与我们所完成的稍有不同)。
当然还有一些其他文件,这些文件具有核心功能。 这像是一个挑战,你可能会想深入研究源代码。
文章来自:www.kancloud.cn/chandler/vu… 翻译自:Vuemastery - Vue 3 Reactivity 视频讲解 本示例 Reactivity 代码