从零到一打造 Vue3 响应式系统 Day 3 - 订阅者模式:响应式设计基础

在正式开始实现我们自己的响应式 API 之前,我们先创建一个简单的测试环境,来观察 Vue 官方 refeffect 的实际情况。 先在 packages/reactivity/ 目录下新建一个 example 文件夹,并创建 index.html 文件:

  • 我们预期进入页面时,控制台会输出 0
  • 一秒后,控制台会输出 1

接着在本地启动这个 html 文件,这里可以使用 VS Code 的 Live Server 插件,即可在本地运行。

HTML 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  

  <script type="module">
    import { ref, effect } 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>

我们可以看到 console 控制台中进入页面时,出现了输出 0,并且一秒后又输出了 1

由于我们目前使用的是 Vue 官方提供的版本,因此这个行为是完全正常的。

我们现在开始实现,目前已知有两件事:

  • 我们进入页面时,传入 effect 的函数会执行。
  • ref 函数会接收一个初始值,并返回一个对象。我们可以通过该对象的 .value 属性来访问或修改这个值。

所以我们先在 packages/reactivity/src 下新建两个文件,分别是 ref.ts 以及 effect.ts,并且在 index.ts 中集中导出。

TypeScript 复制代码
// packages/reactivity/src/ref.ts
class RefImpl {
  _value; // 保存实际值
  constructor(value){
    this._value = value // 存储传入 ref 的值
  }
}

export function ref(value){
   return new RefImpl(value) // 创建一个 ref 实例
}
TypeScript 复制代码
// packages/reactivity/src/effect.ts
export function effect(fn){
  fn() // 执行传入的函数
}
TypeScript 复制代码
// packages/reactivity/src/index.ts
export * from './ref'
export * from './effect'

接着我们把官方的引用注释掉,引入我们自己的 dist 文件,看看是否成功。

JavaScript 复制代码
    // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { ref, effect } from '../dist/reactivity.esm.js'

    const count = ref(0)

    effect(() => {
      console.log('count.value ==>', count.value);
    })

    setTimeout(() => {
      count.value++
    }, 1000)

执行后会发现,第一次的输出是 undefined,且一秒后没有任何变化。这完全正常,毕竟我们还没实现任何依赖追踪的机制。

这次的失败,让我们明确了问题所在:

  1. 无法取值: count.value 读取到的是 undefined。这是因为我们还没有定义当读取 .value 时应该做什么事(缺少 getter 拦截)。
  2. 没有更新: 修改 count.value++ 后,effect 内的函数没有重新执行。这是因为 effectcount 之间没有建立任何关联(缺少订阅机制)。

为了解决这两个问题,我们需要引入响应式系统中最核心的设计模式。

我们接下来要解决的核心问题:依赖收集触发更新

响应式系统核心概念

JavaScript 复制代码
const count = ref(0)

effect(() => {
  console.log('count.value ==>', count.value);
})

setTimeout(() => {
  count.value++
}, 1000)

参考上方代码,我们现在想要做的是进入页面的时候,count 会输出 0,但我们一旦修改了 counteffect 的函数输出就会跟着改变,这也是我们在 Vue3 里面很常做的事。所以我们可以知道响应式的核心概念就是:当数据发生改变,相关的副作用会自动更新。

这个"数据改变,相关操作自动执行"的模式,其实可以用一个生活化的例子来比喻:出版社与订阅者

  1. 路人甲 (effect 函数) 订阅了科技杂志

    • 希望出版社将杂志自动送到他家,不用他去催促
    • 只要看杂志(读取 count.value)就自动成为订阅者 ← 这是依赖收集
  2. 出版社 (ref) 管理杂志内容

    • 拥有所有订阅者的名单 ← 依赖收集的结果
    • 负责存储最新的杂志内容(数据值)
  3. 自动配送机制

    • 当杂志有新版(count.value 被修改)
    • 出版社会自动寄送杂志给所有订阅者(执行 effect) ← 这是触发更新
JavaScript 复制代码
// 出版社(存储数据 + 管理订阅者)
const count = ref(0)  

// 路人甲订阅(当他"阅读"杂志时,自动成为订阅者)
effect(() => {
  console.log('count.value ==>', count.value); // 阅读杂志
})

// 出版社发行新版杂志
setTimeout(() => {
  count.value++  // 新版发行,自动通知所有订阅者
}, 1000)

Pub-Sub Pattern 发布订阅模式

这个"出版社-订阅者"的互动模式,在软件设计中被称为发布-订阅模式 (Publish-Subscribe Pattern) ,或简称 Pub-Sub。

传统发布订阅模式

JavaScript 复制代码
// 发布者(出版社)
class Publisher {
  constructor() {
    this.subscribers = []  // 订阅者名单
  }
  
  // 订阅方法
  subscribe(subscriber) {
    this.subscribers.push(subscriber)
    console.log(`${subscriber.name} 已订阅`)
  }
  
  // 发布方法
  publish(content) {
    console.log(`发布新内容: ${content}`)
    this.subscribers.forEach(sub => {
      sub.notify(content)  // 通知所有订阅者
    })
  }
}

// 订阅者
class Subscriber {
  constructor(name) {
    this.name = name
  }
  
  notify(content) {
    console.log(`${this.name} 收到: ${content}`)
  }
}

// 使用示例
const magazine = new Publisher()
const 路人甲 = new Subscriber('路人甲')
const 路人乙 = new Subscriber('路人乙')

magazine.subscribe(路人甲)  // 路人甲订阅
magazine.subscribe(路人乙)  // 路人乙订阅

magazine.publish('AI 特刊')  // 发布新刊

图解

这个模式的运作流程可以分为两个主要阶段:

1. 订阅阶段 (初始化):

  • 注册: 订阅者 (Subscriber) 需要主动向发布者 (Publisher) 进行注册。
  • 收集: 发布者将所有订阅者的信息收集起来,存放在一个名单中。

2. 发布阶段 (更新):

  • 发布: 当有新内容发布时,发布者会发出通知。
  • 通知: 发布者会遍历订阅者名单,将新内容逐一发送给所有订阅者。

Vue 发布订阅模式

JavaScript 复制代码
// 自动订阅(依赖收集)
effect(() => {
  console.log(count.value) // 读取即订阅
})

// 修改时自动通知
count.value++ // 自动触发更新

Vue 发布订阅模式,与一般传统发布订阅模式不同:

  • 自动订阅(依赖收集阶段)

    • 不需要手动调用 subscribe 方法
    • effect 读取 ref.value 时,自动建立订阅关系
    • ref 在被读取时,自动收集当前的 effect 作为订阅者
  • 自动发布(触发更新阶段)

    • 不需要手动调用 publish 方法
    • ref.value 被修改时,自动通知所有订阅者
    • 相关的 effect 自动重新执行

想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
拜无忧2 小时前
【知识点】vue3不常用api总结-针对前端中级-进阶
前端·vue.js·性能优化
Mintopia2 小时前
AIGC在电商Web端的个性化推荐技术实现
前端·javascript·aigc
双向332 小时前
前端性能优化:Webpack Tree Shaking 的实践与踩坑
前端
薄何2 小时前
在 Next.js 中企业官网国际化的实践
前端
NeverSettle_2 小时前
2025前端网络相关知识深度解析
前端·javascript·http
JarvanMo3 小时前
Flutter. Draggable 和 DragTarget
前端
练习时长一年3 小时前
后端接口防止XSS漏洞攻击
前端·xss
muchu_CSDN3 小时前
谷粒商城项目-P16快速开发-人人开源搭建后台管理系统
前端·javascript·vue.js
Bye丶L3 小时前
AI帮我写代码
前端·ai编程