企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] > [Reactivity Source Code](https://github.com/Code-Pop/vue-3-reactivity) # 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 编程的工作方式。 例如,如果我运行以下代码: ```js 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`变量得到更新,需要再次运行存储的代码。 ![](https://img.kancloud.cn/89/e6/89e667beef446b4f3957f39cf3e6c216_856x272.png) 我们可以通过记录影响(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 不能有重复的值,并且不使用数组之类的索引。 如果您不熟悉,请在[此处](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)详细了解 。 我们要存储 effect(在我们的示例中是`{ total = price * quantity }`),以便可以稍后运行它。 这是dep 的 Set 的可视化展示: ![](https://img.kancloud.cn/1e/e3/1ee31da6de91f3eca7a8936f07b2ae55_846x252.png) 让我们编写一个触发函数来运行我们记录的所有内容。 ``` 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 ``` ![](https://img.kancloud.cn/b2/aa/b2aafbf755c0b80423c73ef424e31a05_518x108.png) ## 问题:多个属性 我们可以根据需要继续跟踪 effects,但是我们的响应式对象将具有不同的属性,而且每个属性都需要它们自己的dep(一组 effects)。看看下面的对象: ``` let product = { price: 5, quantity: 2 } ``` 我们的`price`属性需要自己的 dep(影响集),而我们的`quantity`需要自己的 dep(影响集)。 让我们再想办法以来正确记录这些内容。 ## 解决方案:depsMap 现在,当我们调用跟踪或触发器时,我们需要知道我们要定位的对象是哪个属性(`price`或`quantity`)。 为此,我们将创建一个`depsMap`,它是 **Map** 类型(请考虑键和值)。 可以将其可视化展示: ![](https://img.kancloud.cn/ad/26/ad266424974a772556f4104d5c8203d7_849x314.png) 请注意,`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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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`,是因为我们将考虑将目标对象定位为目标。 还有一个原因,在下一课中会讲得更清楚。 这是我们可视化展示: ![](https://img.kancloud.cn/a7/82/a782b670b3a0cd572f92d4964f6e2edd_853x361.png) 当我们调用`track`或`trigger`,我们现在需要知道要定位的对象。因此,我们将在调用目标时同时发送`target`和键(the key)。 ```js 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的方法: ```js 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) ``` 在控制台中,我会看到: ```shell 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 并向其添加额外参数的地方。 ~~~js 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 方法,这里应该没有什么大的惊喜: ```js 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`。我们预期的输出是: ```shell 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`的地方: ```js 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) } ``` 现在,将这两段代码放在一起: ```js 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 中看到的那样。前面代码的底部如下所示: ```js ... 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`的新函数中进行设置: ```js 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](https://github.com/Code-Pop/vue-3-reactivity) 上获取所有代码。 目前一切都很好,但是我们还需要在`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 } } ``` 现在来运行代码,如下: ```shell 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 的计算属性混淆)。 在下面,您可以看到一个使用对象访问器的简单示例: ```js 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}` ) ``` 我们得到了预期的结果: ```shell 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](https://github.com/Code-Pop/vue-3-reactivity/blob/d497a3fc874c0e856c1315df12994ff0f04b9bb1/07-computed.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 ``` ## 没有注意事项的 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](https://github.com/vuejs/vue-next) 库(当前为alpha 5),运行 `yarn install`,然后`yarn build reactivity`。在`package/reactivity/dist/`中产生来很多文件。 然后,在那里找到`reactivity.cjs.js`文件,把它移到我的示例文件([github 上的示例文件](https://github.com/Code-Pop/vue-3-reactivity)),并测试使用 Vue 的 Reactivity,而编写代码: ```js 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 的 响应式系统的源文件,以开始熟悉。 ## 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`函数(与我们所完成的稍有不同)。 还有一些其他文件,但列出的这些文件具有响应式核心功能。 如果您觉得这是一个挑战,你可能想深入研究[源代码](https://github.com/vuejs/vue-next)。 # 参考 [Vuemastery - Vue 3 Reactivity](https://coursehunters.online/t/vuemastery-vue-3-reactivity/2960)