船长的航行日记
首页
  • 原生能力

    • JavaScript
    • Node
  • 框架能力

    • Vue
    • React
  • 学习笔记

    • 《TypeScript》学习笔记
    • 《JavaScript高程4》学习笔记
  • HTML
  • CSS
  • 最近在读
  • 奇思妙想
  • 读书收获
历史足迹
飞鸽传书
GitHub (opens new window)

captain

心之所向,海的彼岸
首页
  • 原生能力

    • JavaScript
    • Node
  • 框架能力

    • Vue
    • React
  • 学习笔记

    • 《TypeScript》学习笔记
    • 《JavaScript高程4》学习笔记
  • HTML
  • CSS
  • 最近在读
  • 奇思妙想
  • 读书收获
历史足迹
飞鸽传书
GitHub (opens new window)
  • JavaScript

  • node

  • vue组件库开发实践

  • react

  • 学习笔记

  • vue3源码

    • 如何阅读Vue3源码
    • 如何理解响应式
      • 实现手动执行的响应式
      • 实现自动执行的响应式
      • 实现对象多个属性对应的依赖的收集
      • 实现多个对象对应的依赖的收集
      • 实现ref
      • 优化
      • 总结
    • 前端乱炖
    • vue3源码
    masongsong
    2021-10-16
    时间 5分钟
    阅读量 0

    如何理解响应式

    # 需求背景

    我们先看这样的一个场景:我们已知商品的单价 price,数量quantity,求商品的总价total。这是个再简单不过的需求,于是我们自信的写出代码:

    let price = 10
    let quantity = 2
    let total = 0
    
    total = price * quantity // 20
    
    1
    2
    3
    4
    5

    但是,有一天我们需要进行促销活动,商品的价格在某个时间段内不断的波动,此时我们上面的代码还满足需求吗?

    let price = 10
    let quantity = 2
    let total = 0
    
    total = price * quantity // 20
    
    // 促销修改价格
    price = 20
    
    console.log(total) // 20
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    我们打印 total,仍然是 20, 因为这是JavaScript正常的行为,total 和 price并没有必然的联系,price的改变并不能使得total再次被计算,有聪明的同学可能就想到了,那我们把total的计算逻辑封装成函数,在price改变的时候,再次调用不就好了吗?是的,我们来试试。那我们暂且将这个函数命名为 effect: 也就是说,total的计算需要依赖price的改变,所以称之为依赖函数。

    let price = 10
    let quantity = 2
    let total = 0
    
    let effect = () => {
      total = price * quantity
    }
    
    // 首次获取total
    effect() // 20
    
    // 促销修改价格 获取total
    price = 20
    effect()
    
    console.log(total) // 40
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这样做只能说满足了部分需求。但是价格是不断波动的,每调整一次价格就需要手动调用一次effect函数,不高效,而且也显得程序很'笨'。有没有别的更智慧一点的方案呢?这个时候有同学可能就提出了这样的设想:

    • 那我们能不能做到当price值改变的时候,自动去执行effect函数呢?这样当price的值被修改,total的值也就自动计算好了。有点像Excel表格中的计算函数,比如求和函数,输入a,b两个数字,和就自动帮你计算出来了。

    想法很好,我们如何实现呢?首先需要解决第一个问题,我们需要侦测price的改变,进一步说,我们需要侦测在JavaScript中对象的属性被修改的时机。JavaScript提供了这样的能力吗?你别说,还真有。

    • Object.defineProperty
    • proxy

    这两种方式都能拦截属性的获取和设置,比如:

    const person = {};
    let age = 18
    Object.defineProperty(person, 'age', {
      get: function () {
        console.log('属性被获取')
        return age
      },
      set: function (newValue) {
        console.log('属性被设置')
        age = newValue
      }
    });
    
    console.log(person.age)
    // 18 
    // 属性被获取
    
    person.age = 23
    // 属性被设置
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    如果不懂Object.defineProperty的基本概念和用法,可以去Object.defineProperty (opens new window)了解学习。而对于 proxy就更强大了,Object.defineProperty只能对对象的属性一个一个拦截,proxy则能完成对整个对象的拦截,我们来看例子:

    const person = {
      age: 18
    }
    const handler = {
      get: function(target ,key) {
        console.log(`${key}属性被获取`)
        return target[key]
      },
      set: function(terget, key, value) {
        console.log(`${key}属性被设置为${value}`)
        person[key] = value
      }
    }
    
    const proxyPerson = new Proxy(person, handler)
    
    console.log(proxyPerson.age)
    // 18 
    // age 属性被获取
    
    proxyPerson.age = 23
    // age 属性被设置
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    同样的,关于proxy的更多的用法,你可以去proxy (opens new window)学习。那么回到我们的需求,我们现在已经能侦测属性修改的时机,接下来,我们需要做的就是侦测到属性修改的时候,自动执行我们的 effect 函数。

    
    let total = 0
    
    const product = {
      price: 10,
      quantity: 2
    }
    let effect = () => {
      total = product.price * product.quantity
    }
    
    const handler = {
      get: function(target ,key) {
        return target[key]
      },
      set: function(target, key, value) {
        target[key] = value
        // 这里调用 efect函数
        effect()
      }
    }
    
    const proxyProduct = new Proxy(product, handler)
    
    // 第一次需要手动调用 effect
    effect()
    console.log(total) // 20
    
    // 会自动执行
    proxyProduct.price = 13
    console.log(total) // 26
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    这样似乎完成了我们上述的需求。但是此时我们会发现,不管是修改price还是quantity,都会触发我们的effect函数,但是实际场景中,我们可能希望属性和effect对应起来,也就是:

    • 修改price,我们只想触发 price 对应的effect
    • 修改quantity, 我们只想触发quantity对应的effect
    • 或者说,修改price和quantity其中一个,执行相同的effect

    基于上述的场景,我们最好是有一个依赖的收集过程,进行统一的管理,这样方便我们更好的控制依赖的执行。那么如何进行依赖的收集呢?我们又在什么时候进行依赖收集呢?通过上面的例子,我们会很快发现,这两个问题很好解决:

    • 使用数组这样的数据结构用来收集依赖
    • 在属性被读取的时候收集依赖
    const effects = []
    let effect = () => {
      total = product.price * product.quantity
    }
    let track = () => {
      effects.push(effect)
    }
    
    const handler = {
      get: function(target ,key) {
        track()
        return target[key]
      },
      set: function(target, key, value) {
        target[key] = value
        // 这里调用 efect函数
        effect()
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    这样我们就实现了依赖的收集,这时候有同学就提出疑问了,那我们执行effect的时候是不是就不能直接effect()了,因为是多个依赖,我们需要将数组里面的所有effect都执行一遍。确实是这样的,我们把之前的代码完善一下:

    let total = 0
    const effects = []
    
    const product = {
      price: 10,
      quantity: 2
    }
    
    let track = () => {
      effects.push(effect)
    }
    
    let effect = () => {
      total = product.price * product.quantity
    }
    
    const handler = {
      get: function(target ,key) {
        track()
        return target[key]
      },
      set: function(target, key, value) {
        target[key] = value
        // 这里调用 efect函数
        effect()
      }
    }
    
    const proxyProduct = new Proxy(product, handler)
    
    
    // 会自动执行
    proxyProduct.price = 13
    console.log(total) // 26
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    这样就解决了一半上述的依赖收集问题,但是呢?细心的同学又发现问题了,我们并没有解决peice和quantity的依赖对应问题啊。是这样的,接着往下看,我们后面的文章将解决这个问题。

    # 总结

    我们上述实现的需求,其实就是Vue3响应式实现的思想。通过使用proxy进行对象属性获取和设置的拦截器,进行依赖的收集和执行,看完本篇文章,你应该明白了下述的概念:

    • effect 依赖函数
    • track 用来收集依赖
    • trigger 触发依赖

    在Vue3中也存在effect、track、trigger函数,理解这这三个函数的概念,对你后面理解Vue3的reactivity的源码有着至关重要的作用。下一篇我们将会按照Vue3中响应式实现的思路实现一个最简单的响应式。

    编辑 (opens new window)
    上次更新: 2021/10/30, 16:48:40
    如何阅读Vue3源码
    实现手动执行的响应式

    ← 如何阅读Vue3源码 实现手动执行的响应式→

    最近更新
    01
    总结
    10-19
    02
    优化
    10-16
    03
    实现ref
    10-16
    更多文章>
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 生命绿
    • 收获黄
    • 天空蓝
    • 激情红
    • 高贵紫