Skip to content

4. 响应系统的作用与实现

4.1 响应式数据与副作用函数

什么是副作用函数

函数的执行会直接或间接影响其他函数的执行,这时我们说函数产生了副作用。比如修改了全局变量

js
// 全局变量
let val = 1
function effect() {
   val = 2 // 修改全局变量,产生副作用
}

4.2 响应式数据的基本实现

现在我们有个effect函数,我们希望obj.text改变的时候,会自动重新运行effect函数,达到响应式数据更新的目标

js
// 原始数据
const data = { text: 'hello world' }
function effect() {
  document.body.innerText = data.text
}

现在的两个重点操作就是

  1. 读取 data.text的值的时候,把effect函数收集起来放在一个桶里面
  2. 设置 data.text的值的时候,即值被重新赋值了,执行桶里面的effect函数

转换成代码就是

js
// 存储副作用函数的桶
const bucket = new Set()
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    bucket.add(effect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    bucket.forEach(fn => fn())
  }
})

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

从上面不难看出,目前有很多缺陷

  1. 代理中的get,硬编码取了全局的effect函数,不够灵活

    改造一下effect函数,新增一个参数接收副作用函数,新建一个全局变量activeEffect来记录当前活动的副作用函数

    js
    // 用一个全局变量存储当前激活的 effect 函数
    let activeEffect
    function effect(fn) {
      // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
      activeEffect = fn
      // 执行副作用函数
      fn()
    }
  2. 副作用函数没有和具体的key所关联,只要改变了obj的任意一个key,都会触发副作用函数的执行

    假设原始对象为targettarget对象有多个key,然后每一个key对应有副作用函数effect

    • 先把bucket的类型更改为WeakMap,以target作为WeakMapkey,而他的value是一个map结构

      使用WeakMap,因为利于垃圾回收,WeakMap对于key的引用是弱引用,当target不存在时候,就会进行垃圾回收

    • 上面的Map结构我们命名为depsMap,他的key是原始对象target的属性,而他的value就是和这个key所关联的副作用函数集合**(Set结构,目的是去重)**,命名为deps

    image-20230131170927023

    1. 为了优化代码,应该封装两个函数tracktrigger。在get中追踪(track)依赖;在set中触发(trigger)依赖

    代码如下

    js
    // 原始数据
    const data = { text: 'hello world' }
    // 对原始数据的代理
    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)
      }
    })
    
    function track(target, key) {
      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)
    }
    
    function trigger(target, key) {
      const depsMap = bucket.get(target)
      if (!depsMap) return
      const effects = depsMap.get(key)
      effects && effects.forEach(fn => fn())
    }
    
    // 用一个全局变量存储当前激活的 effect 函数
    let activeEffect
    function effect(fn) {
      // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
      activeEffect = fn
      // 执行副作用函数
      fn()
    }
    
    effect(() => {
      console.log('effect run')
      document.body.innerText = obj.text
    })

4.4 分支切换与 cleanup

所谓分支切换,是指有条件影响哪部分代码的执行,比如一个三元表达式

js
effect(() => {
  console.log('effect run')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

可以看到,副作用函数 分别被字段 data.ok 和字段 data.text 所对应的依赖集合收集。

但当字段 obj.ok 的值修改为 false,并触发副作用函数重新执行后,不应该被字段 obj.text 所对应的依赖集合收集

目前我们没有做到这一点,解决方案也很简单:每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除

cleanup

  1. 副作用函数要明确知道哪些依赖集合关联到它

    重构一下effect函数,新建effectFn函数来做之前的事情,在effectFn函数上新增一个数组,来储存当前副作用函数的依赖集合deps

    因为函数本质上也是一个对象

    js
    function effect(fn) {
      const effectFn = () => {
        // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
        activeEffect = effectFn
        fn()
      }
      // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
      effectFn.deps = []
      // 执行副作用函数
      effectFn()
    }
  2. 上面只是初始化了数组,具体要在track的时候收集

    js
    function track(target, key) {
      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()))
      }
      // 当前key所关联的副作用函数
      deps.add(activeEffect)
      // 收集当前副作用函数关联的依赖集合  
      activeEffect.deps.push(deps)
    }