响应式数据与副作用函数

副作用函数就是会产生副作用的函数, 例如:

function effect () {
  document.body.innerText = 'hello vue3'
}

当函数effect运行后, 会设置body的文本使其改变, 而其他函数也可以改变与获取body中的文本, 也就是说effect函数的执行直接或间接影响了其他其他函数的执行结果, 这是effect的副作用. 副作用函数有很多也非常常见比如改变某一个全局变量的值.

响应式的基本思想

const obj = { text: 'hello world' }
function effect () {
  document.body.innerText = obj.text
}

obj.text = 'hello no world'

如上代码, 副作用函数effect执行时会触发obj.text的读取[[GET]]操作, 而对obj.text赋值时会触发设置[[SET]]操作, 因此读取操作时, 可以把副作用函数effect存储到某一数据结构中, 当设置时可以把副作用函数effect取出在运行, 这就实现了最基本的响应式如下图:

image.png

image.png

对于监听读取与设置操作, 可以使用代理对象Prosy实现:

  // 存储副作用函数
  const bucket = new Set()
  
  // 原始数据
  const data = {
    text: 'hello world'
  }
  // 处理原函数数据
  const obj = new Proxy(data, {
    // 拦截读取操作
    get (target, key) {
      // 存储副作用函数effect
      bucket.add(effect)
      // 返回属性值
      return target[key]
    },
  
    // 拦截设置操作
    set (target, key, newVal) {
      console.log(newVal);
      // 设置属性值
      target[key] = newVal
      // 把副作用函数取出并执行
      bucket.forEach(fn => fn())
      // 返回 true 代表操作成功
      return true
    }
  })

目前虽然可以实现, 但是还有许多缺陷, 比如这里直接通过名字获取副作用函数, 这种硬编码的方式不灵活, 并且没有在副作用函数与被操作的目标对象的属性没有建立明确的关系, 此时若读取obj中的属性, 都会把副作用函数存储, 若设置某一个属性都会把副作用函数取出并执行, 因此可以使用某一全局变量储存副作用属性:

// 用于储存被注册的副作用的函数
let activeEffect = undefined

function effect (fn) {
  activeEffect = fn
  fn()
}

并使用WeakMap, Map, Set分别储存:

image.png
之所以使用WeakMap作为储存副作用函数的容器, 是因为当WeakMapkey是弱引用, 所引用的对象不存在时, 会自动被垃圾回收机制回收防止内存溢出:

// 储存副作用函数的桶
const bucket = new WeakMap()

// 用于储存被注册的副作用的函数
let activeEffect = undefined

function effect (fn) {
  activeEffect = fn
  fn()
}

const data = {
  text: 'hello world'
}


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 关联0
    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())
  }
  })

读取操作流程

读取操作.png

设置操作流程

设置操作.png


玛拉_以琳
8.7k 声望6.2k 粉丝