深入vue3源码解读 -- 1、响应式的基础概念

🧭 学习主线

读取响应式数据时收集依赖,修改响应式数据时触发依赖重新执行。

可以把它拆成三个关键角色:

角色 作用 对应代码
📦 响应式数据 保存值,并拦截读取和修改 ref/ RefImpl
🧲 依赖收集 在读取 .value时记录谁用到了这个值 get value()
🔁 触发更新 在修改 .value时重新执行依赖函数 set value()

🧠 1. 什么是响应式

Vue的响应式系统核心在于响应式对象的属性与 effect 副作用函数之间建立的依赖关系

🔹 1.1 普通函数访问响应式数据

💡 源码理解

普通函数里虽然读取了 count.value,但是 Vue 并不知道这个函数以后需要被重新执行。

原因是:这次读取没有处在 effect 的收集环境里,所以 count 没有机会把这个函数记录下来。后面就算 count.value 变了,也找不到要重新执行的函数。

javascript 复制代码
import { ref } from 'vue'
const count = ref(0)

// 普通函数
function fn(){
  console.log(count.value)
}

fn() // 打印 0

setTimeout(()=>{
  count.value = 1 // 修改值不会触发 fn 重新执行
})

虽然fn读取了响应式数据count.value,但由于它不是在effect中执行的,因此当count.value发生变化时,该函数不会重新执行

✅ 这里要记住

ref 本身只是让数据具备"可追踪"的能力,但是否真的追踪,还要看读取发生在哪里。


🔹 1.2 effect中访问响应式数据

💡 源码理解

effect 的作用可以先简单理解成:告诉响应式系统"这个函数是需要被追踪的"。

effect 内部读取 count.value 时,count 就可以把当前正在执行的函数保存起来。等后面 count.value 被修改时,再把这个函数拿出来重新执行。

javascript 复制代码
import { ref, effect } from 'vue'

const count = ref(0)

effect(()=>{
  console.log(count.value) // 首次执行打印 0
})

setTimeout(()=>{
  count.value = 1 // 触发 effect 重新执行,打印 1
})

🔁 执行流程

  1. effect 先执行一次传入的函数
  2. 函数执行时读取 count.value
  3. get value() 被触发,开始收集依赖
  4. setTimeout 中修改 count.value
  5. set value() 被触发,通知依赖重新执行

🛠️ 2、源码中去实现

📁 1、在reactivity/src中新建三个ts文件

📄 1、新建effect.ts

💡 源码理解

第一版 effect 先不要想复杂,它最基础的能力就是:接收一个函数,并立即执行它。

这一步只是搭建入口,后面才会继续给它加"当前正在执行的 effect"这个状态。

php 复制代码
export function effect(fn){
  fn()
}

📄 2、新建index.ts

💡 源码理解

index.ts 的作用是统一出口。

以后外部使用时,不需要分别去找 ref.tseffect.ts,只要从当前模块入口导入即可。

javascript 复制代码
export * from './ref'
export * from './effect'

📄 3、新建ref.ts

💡 源码理解

ref(value) 的本质不是直接返回原始值,而是把原始值包一层对象。

这样做的原因是:只有包成对象之后,才可以通过 get value()set value() 拦截读取与修改。

javascript 复制代码
class RefImpl{
  constructor(value) {
    this._value = value
  }
}

export function ref(value){
  return new RefImpl(value)
}

✅ 这里要记住

count 不是数字 0,而是一个 RefImpl 实例;真正的值被放在 _value 里。


📁 2、在reactivity/src同级新建examples文件夹(存放测试案例)

📄 1、新建01-demo.html

✅ 1、1 vue中实现1s之后打印1

💡 源码理解

这个案例先用 Vue 官方的 refeffect 跑通效果,目的是给后面自己实现源码一个对照目标。

学习源码时不要一上来就写实现,先确认最终行为是什么:首次打印一次,1 秒后值变化,再打印一次。

xml 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.prod.js'

      const count = ref(0)

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


      setTimeout(() => {
        count.value = 1
      }, 1000)
    </script>
  </body>
</html>
✅ 1、2 我们如何去实现?

逐步去完善reactivity/src的ts文件


📄 1、ref.ts

💡 源码理解

这一版 ref.ts 开始具备响应式的核心雏形:

代码位置 作用
_value 保存真实的值
[ReactiveFlgs.IS_REF] = true 给当前对象打上 ref 标记
get value() 读取 .value时触发
set value() 修改 .value时触发
isRef 判断一个值是不是 ref

这里的 console.log(' 有人访问我了 ')console.log(' 我的值变了 ') 只是为了观察流程。真正源码里,这两个位置分别会做"依赖收集"和"触发更新"。

javascript 复制代码
enum ReactiveFlgs = {
  IS_REF = '__v_isRef' // ref 标记,证明是一个 ref
}

/*
 * Ref 的类
 * */
class RefImpl{
  // 保存实际的值
  _value;

  // ref 标记,证明是一个 ref
  [ReactiveFlgs.IS_REF] = true

  constructor(value) {
    this._value = value
  }

  get value(){
    // 收集依赖
    console.log(' 有人访问我了 ')
    return this._value;
  }

  set value(newValue){
    // 触发更新
    console.log(' 我的值变了 ')
    this._value = newValue
  }
}


/*
 * 判断是不是一个 ref
 * @params value
 * */
export function isRef(value){
  return !!(value && value[ReactiveFlgs.IS_REF])
}

export function ref(value){
  return new RefImpl(value)
}

✅ 这里要记住

响应式不是值自己会动,而是读取和修改这两个动作被拦截了。


❓ 如何去收集依赖?如何去触发更新???

🧩 源码思路拆解

要让 count.value = 1 后重新执行 effect,需要解决两个问题:

问题 解决方式
读取时怎么知道是谁在读? 用一个全局变量保存当前正在执行的 effect
修改时怎么知道通知谁? 在 ref 实例上保存之前收集到的 effect

所以整体思路是:

  1. effect(fn) 执行前,把 fn 标记成当前活跃的副作用函数
  2. 执行 fn
  3. fn 内部读取 count.value
  4. get value() 发现当前有活跃的 effect,就把它保存到 subs
  5. 修改 count.value
  6. set value() 执行 subs

📄 effect.ts

💡 源码理解

activeSub 可以理解成一个临时变量,用来保存"当前正在被收集的函数"。

为什么需要这个变量?因为 get value() 触发的时候,它本身并不知道是谁读取了 .value。所以需要 effect 在外面先把当前函数放到一个公共位置,get value() 再从这个位置取到它。

scss 复制代码
// 用来保存当前正在执行的 effect   
// 相当于示例中的
// () => {
//  console.log('count.value =>', count.value)
//  }
export let activeSub

export function effect(fn){
  activeSub = fn()
  activeSub()  // 就是执行 fn()
  activeSub = underfined
}

✅ 这里要记住

学习这一段时重点看思想:effect 负责打开收集窗口,ref.value 的 getter 负责在窗口打开时把依赖记下来。


📄 ref.ts

💡 源码理解

最终这版 ref.ts 把依赖收集和触发更新串起来了。

关键点在这两处:

位置 做的事情
get value() 如果存在 activeSub,说明当前读取发生在 effect中,于是保存依赖
set value() 修改值之后,执行之前保存的依赖函数
javascript 复制代码
import { activeSub } from './effect'
enum ReactiveFlgs = {
  IS_REF = '__v_isRef' // ref 标记,证明是一个 ref
}

/*
 * Ref 的类
 * */
class RefImpl{
  // 保存实际的值
  _value;

  // ref 标记,证明是一个 ref
  [ReactiveFlgs.IS_REF] = true

  // 保存和 effect 之间的关联关系
  subs

  constructor(value) {
    this._value = value
  }

  get value(){
    // 收集依赖
    if(activeSub){
      // 如果 activeSub 有,保存起来,等我更新时触发
      this.subs = activeSub
    }
    return this._value;
  }

  set value(newValue){
    // 触发更新
    this._value = newValue
    this.subs?.()  // 可选链 ?.  activeSub 赋值给 this.subs 可能是空的
  }
}


/*
 * 判断是不是一个 ref
 * @params value
 * */
export function isRef(value){
  return !!(value && value[ReactiveFlgs.IS_REF])
}

export function ref(value){
  return new RefImpl(value)
}

🧠 最后总结

这一节可以先不用追求一次性还原 Vue 完整源码,先把最小响应式模型跑通:

阶段 发生了什么
创建 ref(0)创建一个 RefImpl实例
首次执行 effect执行传入的函数
读取 访问 count.value,触发 get value()
收集 get value()把当前 effect 保存起来
修改 count.value = 1,触发 set value()
更新 set value()重新执行之前保存的 effect

源码学习时最重要的是抓住这个闭环:

effect 执行函数 → 函数读取响应式数据 → 响应式数据收集函数 → 数据变化 → 函数重新执行

相关推荐
程序员黑豆1 小时前
JDK 下载安装与配置详细教程
java·前端·ai编程
hunterandroid1 小时前
文件存储:内部存储与外部存储
前端
NorBugs2 小时前
飞机大战 Low 版 (Made in AI)
前端
angerdream2 小时前
Android手把手编写儿童手机远程监控App之agentweb如何实现全屏
前端
星栈2 小时前
10 分钟跑起第一个 Dioxus 应用:`dx` CLI、`rsx!` 和热更新好不好用
前端·rust·前端框架
奋斗吧程序媛3 小时前
补充一个小知识点:有关@click.native
前端·vue.js
触底反弹3 小时前
🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!
前端·javascript·canvas
DJ斯特拉3 小时前
axios快速使用
开发语言·前端·javascript
还有多久拿退休金3 小时前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js