设计一个完善的响应式系统

本文探讨了如何设计一个完善的响应式系统,包括设置注册副作用函数、建立副作用函数与依赖对象的关系,以及使用weakMap和Map数据结构来管理这些关系。文章重点介绍了如何通过track和trigger函数实现副作用函数的自动收集和执行。
摘要由CSDN通过智能技术生成

设计一个完善的响应式系统

从简单的响应式系统中我们知道,响应式系统工作的流程需要有以下两个步骤:

  • 当读取操作发生时,将副作用函数收集到“桶”中
  • 当设置操作发生时,从“桶”取出副作用函数执行

设置注册副作用函数机制

上次的副作用函数采用的是硬编码,我们希望哪怕副作用函数哪怕是个匿名函数也可以被正确收集到桶里。

// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

在执行中,activeEffect会将传进来的副作用函数存储到全局,在get操作中可以将其收集到对应的依赖函数集合中。

建立副作用函数和依赖之间的关系

简单的响应式系统中存在的问题

在简单的响应式系统中,我们只是单纯拿一个set来作为收集函数的桶:

const obj = new Proxy(data, {
  get(target, key) {
    // 将 activeEffect 中存储的副作用函数收集到“桶”中
    if (activeEffect) {  // 新增
      bucket.add(activeEffect)  // 新增
    }  // 新增
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn())
    return true
  }
})

但是测试上述代码可以发现,当我们开启了一个定时器,一秒钟后为对象 obj 添加新的 notExist 属性。我们知道,在匿名副作用函数内并没有读取obj.notExist 属性的值,所以理论上,字段 obj.notExist 并没有与副作用建立响应联系,因此,定时器内语句的执行不应该触发匿名副作用函数重新执行。但是在定时器触发之后,仍然会触发副作用函数。显而易见,设置副作用函数和依赖的联系是必要的。

数据结构的选取

观察下面代码:

effect(function effectFn() {
  document.body.innerText = obj.text
})

在这段代码中存在三个角色:

  • 被操作(读取)的代理对象 obj;
  • 被操作(读取)的字段名 text;
  • 使用 effect 函数注册的副作用函数 effectFn。

如果用 target 来表示一个代理对象所代理的原始对象,用 key 来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数,那么这三个变量会有以下关系:

target
    └── key
        └── effectFn
举几个例子来说明这种树形结构:
1.如果有两个副作用函数同时读取同一个对象的属性值:
01 effect(function effectFn1() {
02   obj.text
03 })
04 effect(function effectFn2() {
05   obj.text
06 })

那么关系如下:

target
    └── text
        └── effectFn1
        └── effectFn2
2.如果一个副作用函数中读取了同一个对象的两个不同属性:
effect(function effectFn() {
  obj.text1
  obj.text2
})

那么关系如下:

target
    └── text1
        └── effectFn
    └── text2
        └── effectFn
3.如果在不同的副作用函数中读取了两个不同对象的不同属性:
effect(function effectFn1() {
  obj1.text1
})
effect(function effectFn2() {
  obj2.text2
})

那么关系如下:

target1
    └── text1
        └── effectFn1
target2
    └── text2
        └── effectFn2
用Map模拟这种树形结构

可以看到:
在这里插入图片描述

每个红色正方形内,都可以以key-value的形式建立联系,那么在第一层,使用weakMap作为键值对,第二层使用Map建立属性值和依赖函数之间的关系,那么Map的value就是存储这个依赖函数的桶。

那么图形化表示这种关系:

在这里插入图片描述

weakMap和Map的区别

其实上边存在一个问题:为什么第一层联系使用weakMap而不是用Map?

首先weakMap的key只能是对象,而Map的key可以是任何数据类型。

最重要的是,weakMap的key对对象的引用是弱引用,它不影响垃圾回收器的工作,那么不会触发内存泄露。

综合以上逻辑:

那么obj的拦截器,可以表述为:

const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return target[key]
    // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
    let depsMap = bucket.get(target)
    // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
    // 里面存储着所有与当前 key 相关联的副作用函数:effects
    let deps = depsMap.get(key)
    // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    // 最后将当前激活的副作用函数添加到“桶”里
    deps.add(activeEffect)

    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    effects && effects.forEach(fn => fn())
  }
})
抽象一部分逻辑

我们把get中收集依赖的部分抽象为track:

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}

把set中通知依赖的逻辑设置为trigger:

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

那么完整的响应式系统如下:

const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值