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

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

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

上篇文章实现手动执行的响应式 (opens new window)实现了自动收集依赖和自动执行依赖,但是很显然, 还存在问题: 我们如何将数据的属性和依赖对应起来。举个例子:

const product = {
  price: 10,
  quantity: 20
}
1
2
3
4

product有两个属性: price和quantity。然后使用前面实现的reactive的函数将product转换成响应式对象:

const proxyProduct = reactive(product)
1

然后分别定义price和quantity的依赖函数:

// price 的依赖函数
let pEffect1 = () => {
  // ....
}

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

// quantity的依赖函数
let qEffect1 = () => {
  // ...
}

let qEffect2 = () => {
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后我们试图去收集依赖函数的时候,就出现文首的问题了。我们之前的实现并没有将对象的属性和依赖对应的收集起来。也就是说,我们期望的是:

  • 读取 price 属性,只收集price相关联的依赖,可能是多个
  • 读取 quantity属性,只收集quantity相关联的依赖,可能是多个
  • 设置price为新值的时候,只触发price相关联的依赖
  • 设置quantity为新值的时候,只触发quantity相关联的依赖

前文我们已经提到了,会使用Set集合保存收集的effect,那么再结合我们本文的需求,我们可以很容易画出下面的示意图:

其实说到这里,想必大家都已经知道使用什么数据结构维护这个关系了。是的,其实就是用Map。key其实就是对象的属性,value就是 Set,Set里面保存的就是收集的effect。正如上面的图所示。明确了思路,那我们现在就来用代码实现一下(为了更直观,之前实现的代码没有展示出来):

// 使用map描述一个对象多个属性对应多个依赖的对应关系
const depsMap = new Map()

1
2
3

然后在track函数收集依赖的时候,将依赖收集进depsMap:


// 收集依赖函数
function track(key) {
  // 新增 需要根据 key 收集对应的依赖
  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

同时,由于我们收集依赖的时候需要对象的属性作为key,调用track函数的地方也需要传入key:

function reactive(target) {
  const handler = {
    get: function(target, key, receiver) {
     // ... 
     // 传入key值
    track(key)
     //...
    },
  }
}
1
2
3
4
5
6
7
8
9
10

同样的,我们之前调用trigger函数的时候,是直接调用依赖函数的,现在也需要修改下逻辑,我们需要传入需要调用的依赖的key:


// 引用类型的代理函数
function reactive(target) {
  const handler = {
    // ...
    set: function(target, key, value) {
      // ...
    }
    // 这里触发依赖
    trigger(key)
    // ...
    }
    // ...
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后在trigger函数中需要根据传入的key,取出对应的依赖函数并执行:

function trigger(key) {
  let dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}
1
2
3
4
5
6

这样我们就解决了属性和依赖之间的对应关系的问题,完整代码如下:

// 使用map描述一个对象多个属性对应多个依赖的对应关系
const depsMap = new Map()

// 引用类型的代理函数
function reactive(target) {
  const handler = {
    get: function(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      // 收集依赖
      track(key)
      return res
    },
    set: function(target, key, value) {
      Reflect.set(target, key, value)
      // 这里触发依赖
      trigger(key)
      return true
    }
  }

  const proxy = new Proxy(target, handler)
  return proxy
}

// 收集依赖函数
function track(key) {
  // 新增 需要根据 key 收集对应的依赖
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set())
  }
  // 收集依赖
  dep.add(effect)
}

// 触发依赖函数
function trigger(key) {
  let dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}
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

最后我们写个测试例子测试一下:

let total = 0
let product = {
  price: 10,
  quantity: 2
}

const proxyObj = reactive(product)

// 这个就是副作用函数
let effect = () => {
  total = proxyObj.quantity * proxyObj.price
}
console.log(proxyObj.price) // 10
// console.log(proxyObj.quantity)

proxyObj.price = 20

console.log(total) // 40
console.log(depsMap) 
// Map(2) {'price' => Set(1) { [Function: effect] },  'quantity' => Set(1) { [Function: effect] } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

是不是很完美?其实并不是,这里我们已经明确知道了某个属性的所有依赖,因为我们使用Map结构对应好了这种关系。但是我们在实际应用中 并不只是会定义一个响应式对象,而是多个,这是再正常不过的场景了。说到这里,可能有的同学会说,根据我们目前的实现,也可以啊,比如:


const proxyProduct1 = reactive({name: '张三', age: 11})
const proxyProduct2 = reactive({name: '李四', age: 12})
const proxyProduct3 = reactive({name: '王五', age: 13})
// ...
1
2
3
4
5

我们每需要使用一个对象就用reactive代理一下不就行了吗?理论是这样的,但是这样做代码维护性并不是很好,我们还是希望使用某种数据结构将这种关系也维护起来,进行统一的依赖管理。好了,明确了为什么需要这样做,接下来就看如何实现了。

编辑 (opens new window)
上次更新: 2021/10/30, 16:48:40
实现自动执行的响应式
实现多个对象对应的依赖的收集

← 实现自动执行的响应式 实现多个对象对应的依赖的收集→

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