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

    • 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

实现多个对象对应的依赖的收集

上篇文章实现对象多个属性对应的依赖的收集 (opens new window)我们将 属性 和 依赖 的关系维护了起来,并且读取该属性的时候只会收集该属性所对应的依赖,设置该属性的时候只会触发该属性所对应的依赖。同时,我们也发现了一个问题,我们需要将响应式对象和该对象的属性对应的所有的依赖关系维护起来。那么如何做呢?我们来看。

首先我们可以明确一点,这肯定也是一个Map结构,因为很明确的key和value的关系。只不过此时我们的key是一个对象,value是一个Map。什么样的数据结构符合这样的对应关系呢?你别说,还真有,这就是WeakMap (opens new window),使用WeakMap不只是因为它很适合这种场景,还有别的优点:

  • WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。

  • 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。 也就是说,对于WeakMap保存的对象的引用,垃圾回收器会监听这个对象是否是在使用,如果没有,就会自动回收这个对象。避免内存溢出和不必要的空间占用。这对于Vue3提高性能也是很有帮助的。可能还有同学不太明白,我们可以画个简单的图表示目前我们依赖收集的对应关系:

我们略微解释下这个图:

  • targetMap 表示多个对象的依赖对应关系
  • depsMap 表示多个属性的依赖对应关系
  • dep 当前属性收集的依赖集合

首先我们需要在track函数里面将depsMap收集进我们定义的WeakMap容器:

// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()

// 收集依赖函数
function track(target, key) {
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(effect)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

同时,reactive函数中调用的时候,就需要传入当前需要进行响应式包装的对象:

function reactive(target) {
  // ...
  const handles = {
    get: function(target, key, receiver) {
      // ...
    }
    // 新增 传入响应式对象 target
      track(target, key)
    },
    // ...
}
1
2
3
4
5
6
7
8
9
10
11

这样我们就将对象和依赖收集容器的关系维护好了,同样的我们去将trigger函数也进行改进:

function trigger(target, key) {
  // 新增 先根据对象取出所有属性对应的依赖容器
  let depsMap = targrtMap.get(target)
  let dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}
1
2
3
4
5
6
7
8

trigger函数执行的时候,也需要传入该响应式对象:

function reactive(target) {
  const handles = {
    // ...
    set: function(target, key, value) {
      // 新增 传入 target
      trigger(target, key)
    }
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10

好了,这样我们就解决对象对应依赖收集容器的对应关系。但这就完美了吗?相信有的同学很早就发现了一个问题,依赖收集和执行的依赖关系倒是维护好了,但是收集的依赖我们并没有做区分啊,按照我们当前的实现,我们是直接使用effect定义依赖函数:

let effect1 = () => {
  // ...
}

let effect2 = () => {
  // ...
}
1
2
3
4
5
6
7

但是我们收集的时候,是暴力收集,没有做任何区分:

function track(target, key) {
  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(effect)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们只能收集一个effect函数,那我们就得思考一下了,这里肯定不能直接收集effect,那我们需要改造下effect函数了,我们需要使用一个全局变量activeEffect标识当前正在收集的依赖,然后需要改造下effect函数,如下:

// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null

// 副作用函数
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
1
2
3
4
5
6
7
8
9

可以看到,我们改造了effect函数,我们此时并不是使用effect去定义依赖函数,而是采用高级函数的思想,将依赖函数作为参数eff传入effect, 并且使用全局变量activeEffect保存起来,这里为啥使用activeEffect这个全局变量呢?我们分析下:

function a () {
  // ...
}

function b () {
  // ...
}

1
2
3
4
5
6
7
8

可以看到我们定义了两个函数a,b,那我们我们如何在a函数和b函数中共享变量呢?它们属于不同的作用域,却要共享变量,那只能在他们的父级作用域定义该变量了:

let count = 0
function a () {
  // 这里能拿到
  console.log(count)
}

function b () {
  // 这里也能拿到
  console.log(count)
}

1
2
3
4
5
6
7
8
9
10
11

现在想必大家明白activeEffect的作用了吧,实际就是方便在track和trigger之间共享依赖函数。再回到effect函数的实现:

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
1
2
3
4
5

我们使用activeEffect保存了依赖函数后,立即调用了一次,为什么会立即调用一次呢?答案很简单:为了兼顾大多数大应用场景。对于Vue3来说,reactivity模块来说,已经没有和Vue强耦合了,你可以在任何框架或者库中使用reactivity模块,比如说我们只是在一个单独的js文件中引用reactivity,就正如我们目前做的这样,我们没有渲染任何视图,仅仅是验证输出是否符合我们的期望。比如没有立即调用activeEffect:

function effect(eff) {
  activeEffect = eff
  
  // 不进行手动调用
  // activeEffect()
  activeEffect = null
}
1
2
3
4
5
6
7

当你修改值的时候,大家可以想一下,我们收集的依赖函数会执行吗?比如:

let total = 0
const proxyProduct = reactive({ price: 10, quantity: 20 })
effect(() => {
  total = price * quantity
})

// 设置新值
proxyProduct.price = 30
console.log(total) // 是200 而不是正确的300
1
2
3
4
5
6
7
8
9

可以看到,我们的依赖函数并不会执行,看到这里大家可以思考下,为什么修改了值,但是依赖函数并没有执行。其实,细心的同学可能很快就发现了问题的关键,我们并没有触发收集依赖的动作啊。也就是说,上述例子的依赖并没有被收集。原理很简单,我们没有读取响应式对象proxyProduct的属性。所以我们需要在依赖被收集的时候,手动触发一次依赖函数。这样就会触发属性的读取,依赖也就被正常收集了。再回到effect的代码:

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
1
2
3
4
5

最后我们将activeEffect重置为null,方便下一次的依赖收集。这样我们看到,实际上我们要收集的依赖就是activeEffect。所以在track函数中 需要收集的就是activeEffect:

function track(target, key) {
   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

这样我们完美了收集了依赖函数了,完整代码如下:

// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()

// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null

// 引用类型的代理函数
function reactive(target) {
  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)
  return proxy
}

// 依赖收集函数
function track(target, key) {
   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(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

// 测试例子
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 proxyGoods = reactive(goods)

effect(() => {
  total = proxyProduct.quantity * proxyProduct.price * discount
})

effect(() => {
  total1 = proxyGoods.price * proxyGoods.quantity * discount
})

proxyProduct.price = 500
proxyGoods.quantity = 500

console.log('product的total的值为:', total) // 800
console.log('goods的total1的值为:', total1) // 4000
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

完美,我们修改了不同对象的不同属性,执行的依赖函数是该属性对应的依赖函数,所以最后打印800和4000,这符合我们的预期。其实说到这里,已经去Vue3尝过鲜的同学,立马就能想到,这不正是Vue3给我们提供的响应式API reactive (opens new window)吗?是的,这正是Vue3实现reactive API的思路。

但是,当我们修改了discount的值,并不会重新执行依赖函数,测试一下:

discount = 0.5

console.log('product的total的值为:', total) // 800
console.log('goods的total1的值为:', total1) // 4000
1
2
3
4

我们会发现,并没有重新计算值。因为目前我们的代理函数reactive只能代理对象,我们不能代理基本类型。最本质的原因其实是proxy本身的限制,只能代理引用类型。所以我们也需要提供一个代理基本类型的响应式API。大家估计也想到我说的啥了,对,就是Vue3提供的另一个响应式APIref。

编辑 (opens new window)
上次更新: 2021/10/30, 16:48:40
实现对象多个属性对应的依赖的收集
实现ref

← 实现对象多个属性对应的依赖的收集 实现ref→

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