Vue3响应系统系统实现

94 阅读4分钟

什么是副作用函数

指会产生副作用的函数。一个函数(这里我们叫effect)的执行会直接或简介的影响其他函数的执行,这时effect函数就产生了副作用。其实副作用是非常容易产生的,比如一个函数修改了全局变量,这也是一个副作用

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

effect函数的执行会设置body的值,但除了它,任何函数都可以读取或设置body的文本内容,也就是说effect执行会直接影响或间接影响其他函数的执行,那么effect就产生了副作用,他就是副作用函数

简易实现

  • 当副作用函数effect执行时,触发字段obj.text的读取操作
  • 当修改obj.text的值时,会触发字段obj.text的设置操作 如果我们能拦截一个对象的读取和设置操作,就可以实现,代码如下
// 存储副作用的容器
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj= new Proxy(data, {
  // 拦截读取数据操作
  get(target, key) {
    // 将副作用函数存储到容器中
    bucket.add(effect)
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数取出来并执行
    bucket.forEach(fn => fn())
    return true
  }
})

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

effect()
document.body.innerText = obj.text
setTimeout(() => {
  obj.text = 'hello lee'
}, 1000);

我们定义了一个Set类型的容器(桶),在读取obj.text的值时,我们就把effect函数放到桶里,即bucket.add(effect),然后返回属性值,在设置obj.text的effect函数取出并执行。而我们的拦截操作是通过代理对象Proxy来实现的。这样看来是不是很简单,只通过Proxy进行拦截就可以实现了。

从上述代码中,我们可以看出响应系统的工作流程如下:

  • 当读取操作时,将副作用函数收集到容器中
  • 当设置操作时,从容器中去除副作用函数并执行

完善的响应系统

存在的问题

  • 当副作用函数名称改变,代码即失效
  • 为obj添加新的属性时,理论上不执行,但函数确重新执行了 根本原因:使用了Set数据结构作为存储副作用函数的容器,没有在副作用函数与备操作的目标字段之间建立明确的联系。

解决方式

  1. 提供注册副作用函数的机制 定义全局变量activeEffect,他的作用是存储被注册的副作用函数,接着重新定义了effect函数,它变成了一个用来注册副作用函数的函数,effect函数接受一个参数fn,即要注册的函数
// 全局变量存储被注册的副作用函数
let activeEffect
// effct函数用于注册副作用函数
function effect(fn) {
  // 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn() // 执行副作用函数
}

使用一个匿名的副作用函数作为effect函数的参数,当effect函数执行时,首先会将匿名函数的副作用函数fn赋值给全局变量,接着执行被注册的匿名函数fn,这样厨房obj.text的读取操作,进而厨房Proxy的get拦截

const obj= new Proxy(data, {
  // 拦截读取数据操作
  get(target, key) {
    if(activeEffect) {
       // 将副作用函数存储到容器中
       bucket.add(effect)
    }
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数取出来并执行
    bucket.forEach(fn => fn())
    return true
  }
})
  1. 建立副作用函数与被操作的字段之间联系 首先,需要使用WeakMap代替Set,然后修改get/set拦截器
const obj = new Proxy(data, {
  get(target, p, receiver) {
    if (!activeEffect) return target[key]// 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
      bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去

    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    effects && effects.forEach(fn => fn())
  }
})

完整代码

const data = { text: 'hello world' }
// 全局变量存储被注册的副作用函数
let activeEffect
// effct函数用于注册副作用函数
function effect(fn) {
  // 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn() // 执行副作用函数
}
// 存储副作用函数的桶
const bucket = new WeakMap();
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    track(target, key);
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key)
  }
})

// 在 get中拦截函数内调用track函数追踪变化
function track(target, key) {
  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())
}

effect(() => {
  console.log('effect run')
  document.body.innerHTML = obj.text
})

setTimeout(() => {
  console.log(222)
  obj.text = 'hello lee'
}, 1000)