vue3 响应式原理 | 青训营笔记

86 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天

vue2的不足

  • 对象只能劫持设置好的数据,新增的数据需要Vue. Set(xxx)数组只能操作七种方法,修改某一项值无法劫持。
  • 对象新增的属性没有响应式
  • 基于Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
  • Object.defineProperty 无法检测到对象属性的添加和删除 。
  • 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
  • 深度监听需要一次性递归,对性能影响比较大。

什么是副作用函数

举个例子,这里是两个完全不同的函数,第一个函数的入参有个默认值,主要作用是把传入的值渲染到body中,第二个函数的作用则是打印body中的值,如果我们改变第一个函数的值,则第二个函数的输出也会收到影响,这里称为副作用

// 设置 body 中文本内容 
function setTextForBody(text = 'hello vue3'){
  document.body.innerText = text
}
// 获取 body 中的文本并输出
function getTextFromBody(){
  console.log("document.body = ", document.body.innerText)
}
​

什么是响应式数据

举个例子,当我们修改某一个值时,副作用函数可以自动执行,不用我们人为去设置,这就是响应式数据

// 初始数据
const data = { text: 'hello world' }
​
// 副作用函数
function effect(){
  document.body.innerText = data.text
}
​
// 修改数据
setTimeout(() => {
  data.text = 'hello vue3'
}, 3000);
​

用最少的代码实现响应式系统

思路

  • 将原始数据进行代理,实现 gettersetter 函数
  • 当执行副作用 effect 函数时,会触发对应数据的 getter 函数,此时将这个 effect 函数保存到容器 activeEffect 中,等待在未来某时刻执行
  • 当执行 data.text = xxx 操作时,会触发对应数据的 setter 函数,此时从容器 activeEffect 中取出所有 effect 函数并执行它们

副作用函数Effect

  • 首先创建一个容器来装我们的依赖,一开始要做的是将我们要触发的函数保存起来,所以才创建了_effect以便于我们后面执行
  • 然后就是收集依赖,首先创建一个track函数,参数为target和key,为什么是这两个,因为后面的Proxy的get方法有这两个参数,方面我们做后续的操作。
  • 接着创建一个WeakMap,为什么不是Map呢? 问就是性能优化。我们在这个WeakMap中的key主要存储传过来的响应式对象数据源,value则存储另外一个map
  • 这里放WeakMap的用法,注意这里的key必须是对象,这刚好对应我们的响应式对象数据源
  • developer.mozilla.org/zh-CN/docs/…
  • 而另外一个map的key则是存储响应数据源的key,value则是一个Set,里面存放着我们的依赖函数
  • --------------------分割线--------------------------
  • trigger的实现就简单了,我们先根据数据源(对象)来通过WeakMap获取到map,然后通过key来获取到对应的函数,然后执行,
  • 这就是为什么map的key存的是响应式对象数据源的key,方便我们执行执行对应的依赖
  • 这里有个疑问🤔️,那这样随便修改一个值都是执行同一个依赖了,这引发了我的思考
/**
 * 当依赖发生变换,去执行对应的副作用函数
 */
let activeEffect;
​
export const effect = (fn) => {
  const _effect = () => {
    activeEffect = _effect
    fn()
  }
  _effect()
}
​
/**
 * 实现依赖的收集
 */
const targetMap = new WeakMap()
export const track = (target, key) => {
  // console.log(target,key)
  let depsMap = targetMap.get(target)
  // console.log(depsMap)
  // 如果没有响应式数据,我们就创建一个空的
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  deps.add(activeEffect)
}
​
export const trigger = (target, key) => {
  // 触发依赖
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  deps.forEach((effect) => {
    effect()
  })
}
​

这里放一张流程图

无标题-2022-12-07-2018.excalidraw.png

reactive

import { track, trigger } from './effect.js'
const isObject = (target: any) => target !== null && typeof target == 'object'
export const reactive = <T extends object>(target: T): any => {
  return new Proxy(target, {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver) as object
      // 收集依赖
      track(target, key)
      if (isObject(res)) {
        return reactive(res)
      }
      return res
    },
    set(target, key, value, receiver) {
      let res = Reflect.set(target, key, value, receiver)
      // 触发依赖
      trigger(target, key)
      return res
    },
  })
}
​

测试代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import { reactive } from './reactive.js'
    import { effect } from './effect.js'
    const user = reactive({
      name: 'xxx',
      age: 18,
    })
    effect(() => {
      // 这里会执行一次收集依赖
      document.querySelector('#app').innerHTML = `<h1>${user.name} + ${user.age}</h1> `
    })
    setTimeout(() => {
      //这里首先执行get,然后执行set
      user.name = 'yyy'
      setTimeout(() => {
        user.age = 20
      }, 1000)
    }, 1000)
  </script>
</html>

为什么需要Reflect来返回

特定情况会造成上下文的错乱,有可能造成 this 指向错误,产生死循环,具体看霍春阳的《Vue设计与实现》,或者看这篇文章

juejin.cn/post/708091…

总结

这里只是简易的实现了响应式系统,vue中真正的响应式系统还设计了很多东西,待我有空一点点去啃食他。

参考

juejin.cn/post/708685…