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

    • 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
    时间 8分钟
    阅读量 0

    优化

    # 前言

    在前面的文章中,我们一步一步实现了reactive和ref两个响应式的API,基本功能是实现了。但其实还有很多需要优化的地方,Vue3性能的大幅提升,其实也是在体现在这些细节的优化中。我们先来看下,就我们目前的实现来说,有哪些需要优化的地方。

    # 避免重复代理

    这个场景应该很容易想到,已经被代理过的对象再次使用ref和reactive代理时不应该再走代理的逻辑,因为该对象已经被代理了。举个例子:

    const product = {
      price: 10,
      quantity: 20
    }
    
    // 第一次代理
    const proxyProduct = reactive(product)
    
    // 再次代理
    const proxyProduct1 = reactive(product)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    此时我们的proxyProduct和proxyProduct2不应该是不同的代理对象,我们希望相同的对象只被代理一次。我们可以使用这样的策略避免重复代理:

    • 将之前已经代理过的对象缓存起来,再次代理时判断该对象是否在缓存中,如果是,就直接从缓存中取出代理,返回。反之,则走正常代理的逻辑。

    明确了思路,我们用代码来一步步实现。 首先使用一个容器缓存代理对象,这里我们选择使用WeakMap:

    // 缓存已经代理过的对象
    const proxtMap = new WeakMap()
    
    1
    2

    然后在代理函数reactive中进行判断:

    function reactive(target) {
    
      // 新增 已经代理的直接返回 proxy
      const existProxy = proxtMap.get(target)
      if (existProxy) {
        return existProxy
      }
    
      const handler = {
        get: function(target, key, receiver) {
          const res = Reflect.get(target, key, receiver)
          // 收集依赖
          track(target, key)
          return res
        },
        set: function(target, key, value) {
          let oldValue = target[key]
          let res = Reflect.set(target, key, value)
          // 这里触发依赖
          if (res && oldValue !== value) {
            trigger(target, key)
          }
          return res
        }
      }
      const proxy = new Proxy(target, handler)
      
      // 新增 之前没有代理过 将新代理的对象缓存起来
      proxtMap.set(target, proxy)
      return proxy
    }
    
    
    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

    上述新增的逻辑很简单,其实就是在走代理逻辑之前,查看proxyMap中是否缓存了该对象的代理对象,有就直接取出返回。否则,说明没有被代理过,走正常的代理逻辑,然后将代理对象缓存到proxyMap中。

    # effect 嵌套使用

    这种场景在实际应用场景中也是存在的,举个例子:

    effect(function fn1(){
      effect(function fn2() { // activeEffect = effect2
        total = product.price * product.quantity
      })
      total1 = goods.price * goods.quantity
    })
    
    1
    2
    3
    4
    5
    6

    我们使用了effect嵌套了另一个effect,即函数fn1嵌套了函数fn2。我们可以分析下这个函数是如何执行的,分析之前,我们再回顾一下effect函数和track函数的实现:

    function effect(eff) {
      activeEffect = eff
      activeEffect()
      activeEffect = null
    }
    
    // 依赖收集函数
    function track(target, key) {
      if (!activeEffect) return
      let depsMap = targrtMap.get(target)
      if (!depsMap) {
        targrtMap.set(target, depsMap = new Map())
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, dep = new Set())
      }
      // 注意这里收集了当前的 activeEffect
      dep.add(activeEffect)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    好,我们分析一下执行的过程:首先是执行fn1函数,此时activeEffect等于fn1,然后由于fn2函数并没有异步操作,所以会按照顺序执行。也就是紧接着执行fn2函数,此时activeEffect等于fn2,接着fn2函数内计算了total变量,此时会读取product的price属性和quantity属性,从而触发依赖收集函数track,product对象的price和quantity的依赖都是fn2,接着内部的effect执行完毕,此时activeEffect为null。接着继续执行外部effect的逻辑,也就是执行:

    total1 = goods.price * goods.quantity
    
    1

    此时,想必大家都发现问题了。我们此时的activeEffect已经是null,这里即使读取了属性,也收集不到依赖。这就是问题的所在。那我们的期望是什么呢? 我们希望执行到外部effect的时候,activeEffect等于fn1,这样依赖才能被正确的收集到。如何解决这样的问题呢?其实,大家仔细想想,这像不像我们函数的调用栈,首先入栈的函数先执行,执行完就出栈,栈顶的元素永远是我们当前需要执行的函数。是的,Vue3中就是这样解决这个问题的。我们来看下具体如何实现。 首先定义一个栈:

    // 使用一个栈结构处理effect嵌套的情况
    const effectStack = []
    
    1
    2

    然后,在effect函数中将当前执行的activeEffect推入栈,默认执行一次后,将栈顶元素出栈,并将activeEffect始终指向为我们栈顶的元素。

    function effect(fn) {
      activeEffect = fn
      if (!effectStack.includes(activeEffect)) {
        effectStack.push(activeEffect)
        try {
          return activeEffect()
        } finally {
          effectStack.pop()
          activeEffect = effectStack[effectStack.length - 1]
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    再回到我们刚才嵌套effect的例子,我们再分析一下,看是否符合我们的期望:

    effect(function fn1(){
      effect(function fn2() {
        total = product.price * product.quantity
      })
      total1 = goods.price * goods.quantity
    })
    
    1
    2
    3
    4
    5
    6

    首先执行fn1函数,此时activeEffect等于fn1然后将fn1推入effectStack。接着执行内部的effect函数,此时activeEffect等于fn2,同时会将fn2推入effectStack,此时effectStack的元素如图:

    接着fn2函数内计算了total变量,此时会读取product的price属性和quantity属性,从而触发依赖收集函数track,product对象的price和quantity的依赖都是fn2,接着内部的effect执行完毕。注意,内部effect函数执行完毕后,会删除栈顶的元素,也就是删除栈顶的fn2,并将activeEffect指向栈顶的元素,因为我们此时栈顶的元素是fn1,也就是说此时的activeEffect等于fn1:

    这样我们就解决了依赖嵌套的问题,完整代码如下:

    
    // 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
    const targrtMap = new WeakMap()
    
    // 缓存已经代理过的对象
    const proxtMap = new WeakMap()
    
    // 使用一个栈结构处理effect嵌套的情况
    const effectStack = []
    
    // 使用 activeEffect 变量保存当前激活的 effect
    let activeEffect = null
    
    // 引用类型的代理函数
    function reactive(target) {
    
      // 已经代理的直接返回 proxy
      const existProxy = proxtMap.get(target)
      if (existProxy) {
        return existProxy
      }
    
      const handler = {
        get: function(target, key, receiver) {
          const res = Reflect.get(target, key, receiver)
          // 收集依赖
          track(target, key)
          return res
        },
        set: function(target, key, value) {
          let oldValue = target[key]
          let res = Reflect.set(target, key, value)
          // 这里触发依赖
          if (res && oldValue !== value) {
            trigger(target, key)
          }
          return res
        }
      }
      const proxy = new Proxy(target, handler)
      proxtMap.set(target, proxy)
      return proxy
    }
    
    // 基本类型的响应式API 类似于 Object.defineProperty
    function ref(raw) {
      const target =  {
        get value() {
          console.log('基本类型----收集依赖')
          track(target, 'value')
          return raw
        },
        set value(newValue) {
          console.log('基本类型----触发依赖')
          raw = newValue
          trigger(target, 'value')
        }
      }
      return target
    }
    
    // 依赖收集函数
    function track(target, key) {
      if (!activeEffect) return
      let depsMap = targrtMap.get(target)
      if (!depsMap) {
        targrtMap.set(target, depsMap = new Map())
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, dep = new Set())
      }
      // 收集依赖
      dep.add(activeEffect)
    }
    
    // 触发依赖函数
    function trigger(target, key) {
      let depsMap = targrtMap.get(target)
      if (!depsMap) return
      let dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => effect())
      }
    }
    
    // 副作用函数
    function effect(fn) {
      activeEffect = fn
      if (!effectStack.includes(activeEffect)) {
        effectStack.push(activeEffect)
        try {
          return activeEffect()
        } finally {
          effectStack.pop()
          activeEffect = effectStack[effectStack.length - 1]
        }
      }
    }
    
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99

    然后写个测试例子测试下:

    // 测试例子
    let total = 0
    let total1 = 0
    let discount = 0.8
    let product = {
      price: 10,
      quantity: 2
    }
    
    let goods = {
      price: 10,
      quantity: 2
    }
    
    const proxyProduct = reactive(product)
    const proxyProduct1 = reactive(goods)
    
    effect(() => {
      effect(() => {
        total = proxyProduct.price * proxyProduct.quantity
      })
      total1 = proxyProduct1.price * proxyProduct1.quantity
    })
    
    // 所以此时total的值应该是400
    proxyProduct.price = 200
    console.log(total) // 400
    
    // 所以此时total1的值应该是800
    proxyProduct1.price = 400
    console.log(total1) // 800
    
    
    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

    # 值改变时,才触发依赖函数

    我们先回顾下,我们实现的reactive函数,我们只保留了需要关注的代码部分:

    
    function reactive(target) {
      // ...
      const handler = {
        set: function(target, key, value) {
          let res = Reflect.set(target, key, value)
            // 这里触发依赖
            trigger(target, key)
          return res
        }
      }
      // ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    请注意我们执行trigger函数的时机,其实是只要有值改变,我们就会执行当前的trigger函数,当是如果是下面这种场景呢?

    const p = {
      price: 20,
      quantity: 2
    }
    
    const product = reactive(p)
    
    effect(() => {
      console.log(product.price)
    })
    
    product.price = 10
    product.price = 10
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    按照我们当前的实现,我们修改了两次price的值,就会打印两次10。但这符合我们的期望吗?并不符合。当前price的值相比于第一次修改后并没有改变,此时并不应该再次执行依赖函数。所以,这里我们可以做个优化:只有当值真正改变后,我们才执行trigger函数,我们来实现下:

    
    function reactive(target) {
      // ...
      const handler = {
        set: function(target, key, value) {
          let oldValue = target[key]
          let res = Reflect.set(target, key, value)
          if (res && oldValue !== value) {
            trigger(target, key)
          }
          return res
        }
      }
      // ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    我们会把之前的值和即将要修改的值进行对比,如果不相等,我们才会去执行依赖函数,这样就避免了重复执行依赖函数的问题。 最终实现的完整代码如下:

    
    
    // 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
    const targrtMap = new WeakMap()
    
    // 缓存已经代理过的对象
    const proxtMap = new WeakMap()
    
    // 使用一个栈结构处理effect嵌套的情况
    const effectStack = []
    
    // 使用 activeEffect 变量保存当前激活的 effect
    let activeEffect = null
    
    // 引用类型的代理函数
    function reactive(target) {
      // 已经代理的直接返回 proxy
      const existProxy = proxtMap.get(target)
      if (existProxy) {
        return existProxy
      }
      const handler = {
        get: function(target, key, receiver) {
          const res = Reflect.get(target, key, receiver)
          // 收集依赖
          track(target, key)
          return res
        },
        set: function(target, key, value) {
          let oldValue = target[key]
          let res = Reflect.set(target, key, value)
          // 这里触发依赖
          if (res && oldValue !== value) {
            trigger(target, key)
          }
          return res
        }
      }
      const proxy = new Proxy(target, handler)
      proxtMap.set(target, proxy)
      return proxy
    }
    
    // 基本类型的响应式API 类似于 Object.defineProperty
    function ref(raw) {
      const target =  {
        get value() {
          console.log('基本类型----收集依赖')
          track(target, 'value')
          return raw
        },
        set value(newValue) {
          console.log('基本类型----触发依赖')
          raw = newValue
          trigger(target, 'value')
        }
      }
      return target
    }
    
    // 依赖收集函数
    function track(target, key) {
      if (!activeEffect) return
      let depsMap = targrtMap.get(target)
      if (!depsMap) {
        targrtMap.set(target, depsMap = new Map())
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, dep = new Set())
      }
      // 收集依赖
      dep.add(activeEffect)
    }
    
    // 触发依赖函数
    function trigger(target, key) {
      let depsMap = targrtMap.get(target)
      if (!depsMap) return
      let dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => effect())
      }
    }
    
    // 副作用函数
    function effect(fn) {
      activeEffect = fn
      if (!effectStack.includes(activeEffect)) {
        effectStack.push(activeEffect)
        try {
          return activeEffect()
        } finally {
          effectStack.pop()
          activeEffect = effectStack[effectStack.length - 1]
        }
      }
    }
    
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    编辑 (opens new window)
    上次更新: 2021/10/30, 16:48:40
    实现ref
    总结

    ← 实现ref 总结→

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