构建简单的 Vue 3 响应式系统

634 阅读16分钟

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 中,我们希望只要pricequantity得到更新,total就会得到更新。 我们想要的结果:

>> total is 40

可惜 JavaScript 是过程式的,不是响应式的,因此在实际上会不起作用。 为了使total是响应式的,我们必须使用JavaScript 使其行为有所不同。

在后面的其余部分中,我们将使用与 Vue 3 相同的方法(与 Vue 2 截然不同)从头开始构建 Reactive System。 然后,我们将研究 Vue 3源代码,来查找阅读我们从头开始编写的这些模式。

保存代码以便以后运行

问题

如您在上面的代码中所看到的,为了建立响应式,我们需要保存我们计算total的方式,以便我们可以在pricequantity发生变化时重新运行它。

解决方案

首先,我们需要以某种方式告诉我们的应用程序:“存储我将要运行的影响(effect),可能需要您在其他时间运行它。” 然后,我们要运行代码,如果pricequantity变量得到更新,需要再次运行存储的代码。

我们可以通过记录影响(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

现在,当我们调用跟踪或触发器时,我们需要知道我们要定位的对象是哪个属性(pricequantity)。 为此,我们将创建一个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,是因为我们将考虑将目标对象定位为目标。 还有一个原因,在下一课中会讲得更清楚。 这是我们可视化展示:

当我们调用tracktrigger,我们现在需要知道要定位的对象。因此,我们将在调用目标时同时发送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 自动调用tracktrigger

Proxy 与 Reflect

我们以及学习了 Vue 3 如何跟踪 effects 以在需要时重新运行它们。 但是,我们仍然必须手动调用tracktrigger。 在本节中,我们将学习如何使用 ReflectProxy 来自动调用它们。

解决方案:挂钩获取和设置

我们需要一种方法来挂钩(或监听)响应式对象上的 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,可用于定义代理对象上的自定义行为,例如拦截getset调用。 这些拦截器方法称为 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)

开始想象一下,需要在上面的代码中调用tracktrigger的地方:

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)

请注意,我们不再需要调用triggertrack,因为在我们的getset方法中已经正确调用了它们。 运行这段代码可以给我们:

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}`)

getset获取 fullName并相应地 设置 fullName的对象访问器。 这是纯 JavaScript 功能,不是 Vue 的函数。

2. 用对象访问器定义 Ref

使用对象访问器以及我们的tracktrigger操作,现在可以使用以下方法定义引用:

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 进行编码,我会将salePricetotal都写为计算属性。 如果您熟悉 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 - 包含诸如getset之类的Proxy处理程序,它们调用tracktrigger(来自 effect.ts)。
  • react.ts - 包含使用getset(来自 basehandlers.ts)创建 ES6 代理的响应式语法的功能。
  • ref.ts - 定义我们如何使用对象访问器创建响应 Ref 引用(就像我们所做的那样)。 还包含toRefs,它将响应式对象转换为一系列访问原始代理的响应式引用。
  • compute.ts - 使用effect和对象访问器定义computed函数(与我们所完成的稍有不同)。

当然还有一些其他文件,这些文件具有核心功能。 这像是一个挑战,你可能会想深入研究源代码

文章来自:www.kancloud.cn/chandler/vu… 翻译自:Vuemastery - Vue 3 Reactivity 视频讲解 本示例 Reactivity 代码