vue3学习与my-vue3实现02: 完成一个基础的响应式系统

111 阅读2分钟

在这篇文章中我们将要实现一个最基本的响应式系统,即在修改对象特定值时,自动重新执行注册的副作用函数,实现更新。如以下代码所示:

// vue/examples/reactivity/basicImplementationOfReactive.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="../../dist/vue.js"></script>
</head>
<body>

</body>
<script>
    const { reactive, effect } = Vue
    const obj = reactive({
      name: 'echo',
    })
    effect(() => {
      document.body.innerText = obj.name
    })
  	// 2s后自动更新body中的内容为yahaha
    setTimeout(() => {
      obj.name = 'yahaha'
    }, 2000)
</script>
</html>

首先我们需要知道的是一个响应式系统的工作流程:

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

从上面介绍的内容我们可以看到我们需要“桶”来存放副作用函数,同时也需要一个提供一个用来注册副作用函数的机制。

“桶”

// reactivity/src/effect.ts
// 存储副作用的容器
export const targetSet = new Set<() => any>()

注册副作用函数的方法

// reactivity/src/effect.ts
// 用一个全局变量存储被注册的副作用函数
export let activeEffect: undefined | (() => any)

// 用于注册副作用函数
export function effect(fn: () => any) {
  // 当调用effect注册副作用函数时,将fn赋值给activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()

实现响应式系统的工作流程

参照vue3的源码,我们将读取和设置操作以以下代码形式实现:

// reactivity/src/baseHandlers.ts
import { activeEffect, targetSet } from './effect'

const get = createGetter()
const set = createSetter()

function createGetter() {
  // 此处target应该为object类型,但是ts类型检查无法通过,此处先这样声明,在后续实现中进行修改
  return function get(target: Record<string, any>, key: string) {
    // 将activeEffect中存储的副作用函数收集到桶中
    if (activeEffect)
      targetSet.add(activeEffect)
    return target[key]
  }
}

function createSetter() {
  return function set(target: Record<string, any>, key: string, newValue: unknown) {
    target[key] = newValue
    // 在设置操作时执行副作用函数
    targetSet.forEach(fn => fn())
    return true
  }
}
// proxy的代理函数
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
}

我们在reactive.ts中添加reactive方法,从而代理我们想要响应式更新的数据。

// reactivity/src/reactive.ts
import { mutableHandlers } from './baseHandlers'

export function reactive(target: object) {
  return new Proxy(target, mutableHandlers)
}

方法导出

将reactivity库中的函数导出:

// reactivity/src/index.ts
export { effect } from './effect'
export { reactive } from './reactive'

然而在vue库中也进行导出:

// vue/src/index.ts
export { reactive, effect } from '@my-vue3/reactivity'

编译后就可以看到文章开头所提到的效果了。

存在的问题

// vue/examples/reactivity/problem.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>

  </body>
  <script>
    const { reactive, effect } = Vue
    const obj = reactive({
      name: 'echo',
    })
    effect(() => {
      // 打印两次
      console.log('effect run')
      document.body.innerText = obj.name
    })
    setTimeout(() => {
      // 副作用函数并没有对此字段进行读取
      obj.noExist = 'hello yahaha'
    }, 2000)
  </script>
</html>

如上面的代码所示,如果我们修改了一个了文章开头的示例代码中的setTimeout中obj不存在的字段时,在effect的匿名函数中并没有对这个字段进行读取,但是'effect run'还是会执行两次。这个问题我将在下一篇文章中分析并解决。

代码示例

github.com/KoiraCMT/my…