美文网首页
Vue 3 响应式原理三 - activeEffect & re

Vue 3 响应式原理三 - activeEffect & re

作者: AizawaSayo | 来源:发表于2021-08-25 12:48 被阅读0次

    在本篇我们将修复一个小 bug 来继续构建我们的响应式代码,然后实现响应式引用。

    继续之前的代码:

    ...
    let product = reactive({ price: 5, quantity: 2 })
    let total = 0
    let effect = () => {
      total = product.price * product.quantity
    }
    effect() // 活跃 effect
    console.log(total)
    product.quantity = 3
    
    // 添加了一段获取响应式对象的属性的代码
    console.log('Updated quantity to = ' + product.quantity)
    console.log(total)
    

    当我们从响应式对象中获取属性时,问题就出现了:

    在新增的console.log访问product.quantity时,track及它里面的所有方法都会被调用,即使这段代码不在effect(就是我们常说的副作用)中。我们只想查找并记录 内部调用了get property (访问属性) 的活跃 effect

    activeEffect

    为了解决这个问题,我们首先创建一个activeEffect全局变量,用于存储当前运行的effect。然后我们将在一个名为effect的新函数中设置它。

    let activeEffect = null // 运行的 active effect
    ...
    function effect(eff) {
      activeEffect = eff  // 把要运行的匿名函数赋给 activeEffect
      activeEffect()      // 运行它
      activeEffect = null // 再把 activeEffect 设置为 null
    }
    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 上获取并尝试所有代码: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
      }
    }
    

    现在运行我们的代码会输出:

    Ref

    我们发现使用salePrice而不是price来计算总数应该更准确,于是把第一个effect修改如下:

    effect(() => {
      total = salePrice * product.quantity
    })
    

    如果我们正在创建一个真实的 store,我们可能会根据salePrice来计算 total。然而,这句代码不会响应式工作。当product.price更新时,它会响应式地重新计算salePrice,因为有这个副作用:

    effect(() => {
      salePrice = product.price * 0.9
    })
    

    但是由于salePrice不是响应式的,所以它的变更不会重新计算 total的影响。我们上面的第一个副作用不会重新运行。我们需要一些方法来使salePrice具有响应性,如果你熟悉 Composition API,你可能认为应该使用ref来创建一个响应式引用,那就这样做吧:

    let product = reactive({ price: 5, quantity: 2 })
    let salePrice = ref(0)
    let total = 0
    

    根据 Vue 文档,响应性引用采用内部值并返回一个具有响应性和可维护的ref对象。ref对象有一个指向内部值的属性.value。所以我们需要稍微修改一下我们的effect

    effect(() => {
      total = salePrice.value * product.quantity
    })
    effect(() => {
      salePrice.value = product.price * 0.9
    })
    

    我们的代码现在应该起效了,当salePrice更新时能正确更新total。但是我们仍然需要通过ref定义。这个ref又是怎么实现的呢?我们有两种方式。

    1. 通过 Reactive 定义 Ref

    简单地通过reactive包装

    function ref(intialValue) {
      return reactive({ value: initialValue })
    }
    

    然而,这不是 Vue 3 用真正原始定义 ref 的方式

    理解 JavaScript Object Accessors - 对象访问器

    首先需要确保先熟悉对象访问器(object accessors),有时也称为 JavaScript 的 computed 属性(不要和 Vue 的计算属性混淆)。
    下面👇是 Object Accessors 的一个简单示例:

    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 fullNameset fullName这两个获取/设置fullName值的函数就是对象访问器。这是纯 JavaScript,不是 Vue 的特性。

    2. 通过 Object Accessors 定义 Ref

    在对象访问器内配合使用我们的tracktrigger操作,我们可以这样定义 ref:

    function ref(raw) {
      const r = {
        get value() {
          track(r, 'value')
          return raw
        },
        set value(newVal) {
          raw = newVal
          trigger(r, 'value')
        },
      }
      return r
    }
    

    这就是全部了。

    这样做是因为:ref设计的初衷就是为包装一个内部值而服务,如果用reactive包裹的方式封装它,这样的“ref”就允许额外添加属性,违背了最初的目的。所以ref不应该被当作一个reactive对象。另外还有出于性能的考虑,用对象字面量创建ref会更节省性能。

    当我们运行下面👇的代码:

    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 响应式原理一 - Vue 3 Reactivity
    Vue 3 响应式原理二 - Proxy and Reflect
    Vue 3 响应式原理三 - activeEffect & ref
    Vue 3 响应式原理四 - Computed Values & Vue 3 源码

    相关文章

      网友评论

          本文标题:Vue 3 响应式原理三 - activeEffect & re

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