深入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 执行函数 → 函数读取响应式数据 → 响应式数据收集函数 → 数据变化 → 函数重新执行

相关推荐
梦帮科技5 小时前
GRAVIS v4.0:基于Web的极速套利架构设计与实时数据流实现
前端·人工智能·rust·自动化·区块链·智能合约·数字货币
爱勇宝5 小时前
办公资料反复修改、补传、交接混乱,我做了个桌面工具来解决这件事
前端·后端·程序员
微信开发api-视频号协议6 小时前
企业微信外部群开发自动化实践过程
java·前端·微信·自动化·企业微信·ipad
甲维斯6 小时前
Fable5:20美金的顶级设计师!
前端·人工智能
kyriewen6 小时前
我同时用了 Claude Code、Cursor 和 Codex,说说三个工具的真实差距——2026 年选错工具比不用 AI 更浪费时间
前端·ai编程·claude
Jackson__6 小时前
为了拯救我的腰椎和颈椎,我做了款浏览器插件!
前端·javascript·开源
闲猫7 小时前
Python FastAPI + SQLAlchemy 入门教程:从零搭建你的第一个 Web 应用
前端·python·fastapi
林小帅7 小时前
NestJS v11 + Prisma v7 ESM 迁移配置解析
前端·javascript·后端
IT_陈寒7 小时前
React的setState竟然不是立刻生效的,害我调试半天
前端·人工智能·后端
简简单单就是我_hehe7 小时前
前端埋点日志解决方案:完整 User-Agent 解析工具实现,爬虫识别、设备品牌、操作系统、浏览器版本全解析(附完整源码)
前端·爬虫