实现最小版本的 ref 和 effect

2025-09-14 13:17:28

上一篇补充了链表的基础概念。

这一篇先实现一个最小闭环,主要是增加 vitest

官方行为

先用官方 Vue 看一下 refeffect 的基础表现:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reactivity Demo</title>
</head>
<body>
<script type="module">
  import { effect, ref } from '../../../node_modules/vue/dist/vue.esm-browser.js'
  const count = ref(0)
  effect(() => {
    console.log('count.value', count.value)
  })
  setTimeout(() => {
    count.value++
  }, 1000)
</script>
</body>
</html>

控制台会输出:

count.value 0
count.value 1

这里发生了两件事:

  • effect 注册后会立即执行一次,所以先输出 0
  • count.value++ 修改了响应式数据,所以依赖它的 effect 会重新执行,再输出 1

文件结构

packages/reactivity/src 下先创建三个文件:

packages/reactivity/src/
  effect.ts
  ref.ts
  index.ts

它们的职责分别是:

  • effect.ts:管理副作用函数的注册、执行和依赖收集
  • ref.ts:实现 ref,在 valuegetset 中完成追踪与触发
  • index.ts:统一导出对外 API

单元测试

先安装测试工具:

pnpm add vitest -D -w

项目后面会用到 @vue/shared,所以先在 vitest.config.ts 里把别名配置好:

import { resolve } from 'node:path'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true
  },
  resolve: {
    alias: {
      '@vue/shared': resolve(import.meta.dirname, 'packages/shared/src')
    }
  }
})

然后新建测试文件:

packages/reactivity/__tests__/ref.spec.ts

测试先只覆盖两个能力:

  • ref 可以创建带有 value 的响应式对象
  • value 变化后,读取过它的 effect 会重新执行
import { describe, expect, it } from 'vitest'
import { effect, ref } from '../src'

describe('ref', () => {
  it('ref is defined', () => {
    expect(ref).toBeDefined()
  })

  it('ref 是响应式的', () => {
    const count = ref(0)
    expect(count.value).toBe(0)

    let dummy

    effect(() => {
      dummy = count.value
    })

    expect(dummy).toBe(0)
    count.value++
    expect(dummy).toBe(1)
  })
})

此时执行测试会报错,因为 refeffect 还没有实现。

视频

有小伙伴的自动录制,我截取了一下(但没声音)。但是最小版本我觉得问题不大