美文网首页
Vue 3 响应式原理一 - Vue 3 Reactivity

Vue 3 响应式原理一 - Vue 3 Reactivity

作者: AizawaSayo | 来源:发表于2021-08-24 18:11 被阅读0次

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 中,我们希望在pricequantity更新时更新total。我们想要:
>> total is 40
可惜的是,JavaScript 是过程式的,而不是响应式的,所以这在现实生活中是行不通的。为了使total变成响应式,我们必须使用 JavaScript 使事情的行为模式变得不同。
我们将使用与 Vue 3 相同的方法 (与 Vue 2 极其不同) 从头开始构建一个简单的响应式系统。然后再查看 Vue 3 源码以发现我们编写的这些模式。

构建简易版响应式系统

首先,我们需要通过某种方式来告诉我们的应用程序,“存储我即将运行的代码(effect),我可能需要你在其他时间运行它。”然后我们运行代码,后面如果pricequantity变量更新了,再次运行存储的代码。

我们可以通过记录函数(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 用dependnotify记录和执行effects,为什么 Vue 3 更改成了tracktrigger


dependnotify是动词,和它们的所有者(Dep 类的实例)相关,可以说成一个 Dep 实例正在被依赖、或者正在通知它的订阅者。
Vue 3从技术上来讲已经没有 Dep 类(Class)了。Dep 类里的dependnotify现在被抽离到两个独立的函数(tracktrigger)里。所以当调用tracktrigger更像是跟踪 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集)。让我们构建一个解决方案来正确记录这些。

当现在调用tracktrigger时,我们需要知道对象中的哪个属性(pricequantity)是追踪目标。为此,我们将创建一个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 就是我们的响应式对象(productuser)。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存储每个响应式对象关联的depsMapdepsMap存储每个属性的dep依赖,每个dep存储一组 effect set 集,这些 effects 会在值发生变化时重新运行。

之所以这样嵌套式设计是因为:
在 Vue2 中,我们使用 ES5 进行gettersetter转换,当我们用 for Each 遍历对象上的key时,自然有一个小闭包为其属性(key)存储关联的 Dep。
但是在 Vue 3,我们使用了 Proxy,proxy 的handler接收targetkey,你并不能得到为每个属性存储关联 dependency 的一个个闭包。
所以我们需要给定一个target对象和一个target对象上的key,来保证我们始终能找对对应的 dependency 实例。唯一的方法就是把它们(targetkey)分到两个等级不同的嵌套 maps。

当我们现在调用tracktrigger时,需要知道目标是哪个对象。因此,当我们会在调用它时发送targetkey

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 代理自动调用tracktrigger

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

相关文章

网友评论

      本文标题:Vue 3 响应式原理一 - Vue 3 Reactivity

      本文链接:https://www.haomeiwen.com/subject/dlydiltx.html