news 2026/4/18 6:59:05

手写 Vue 3 的 ref 实现:从零开始理解响应式核心

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 Vue 3 的 ref 实现:从零开始理解响应式核心

手写 Vue 3 的 ref 实现:从零开始理解响应式核心

在 Vue 3 的组合式 API(Composition API)中,ref无疑是最基础也是最核心的 API 之一。它不仅是原始类型数据(如numberstring)实现响应式的唯一途径,更是连接模板与逻辑层的桥梁。然而,许多开发者只知其“然”——通过.value访问和修改,却不知其“所以然”。

要真正掌握 Vue 3 的响应式精髓,我们必须撕开 API 的表层,深入其基于 Proxy 和依赖追踪的底层逻辑。今天,我们将通过手写一个迷你版的ref,彻底解构 Vue 3 响应式系统的核心原理。


一、 响应式的基石:依赖收集与触发

在实现ref之前,我们必须先理解 Vue 响应式系统的两大支柱:依赖收集(Track)触发更新(Trigger)

Vue 3 利用 ES6 的WeakMap来建立一个精密的“依赖地图”。当组件渲染或副作用函数(effect)执行时,会发生以下过程:

  1. 读取数据(Get):访问响应式对象的属性时,系统会记录下“是谁(当前活跃的 effect)读取了哪个对象的哪个属性”。
  2. 修改数据(Set):修改响应式对象的属性时,系统会找到“所有读取过该属性的 effect”,并通知它们重新执行。

我们先手写这套核心的依赖管理系统:

// 全局变量:存储当前正在执行的副作用函数letactiveEffect=null;// 依赖地图:WeakMap<target, Map<key, Set<effect>>>consttargetMap=newWeakMap();// 依赖收集函数functiontrack(target,key){if(!activeEffect)return;// 没有活跃的 effect,无需收集letdepsMap=targetMap.get(target);if(!depsMap){depsMap=newMap();targetMap.set(target,depsMap);}letdep=depsMap.get(key);if(!dep){dep=newSet();depsMap.set(key,dep);}// 将当前 effect 添加到依赖集合中dep.add(activeEffect);}// 触发更新函数functiontrigger(target,key){constdepsMap=targetMap.get(target);if(!depsMap)return;constdep=depsMap.get(key);if(dep){// 复制一份依赖执行,避免死循环consteffectsToRun=newSet(dep);effectsToRun.forEach(effect=>effect());}}// 副作用函数包装器functioneffect(fn){const_effect=function(){activeEffect=_effect;// 设置为当前活跃 effectfn();// 执行函数,触发依赖收集activeEffect=null;// 重置};_effect();return_effect;}

有了这套机制,我们就可以开始构建ref的实体了。


二、 解剖 RefImpl:Object.defineProperty 的伪装

ref的本质并不是魔法,而是一个包装类。当你调用ref(0)时,Vue 内部创建了一个RefImpl的实例。这个实例并不是直接暴露原始值,而是将其包装在一个对象中,并通过getter/setter拦截对.value的访问。

核心逻辑

  1. 构造函数:接收初始值,内部存储_value,并通过toReactive将对象类型转换为响应式 Proxy。
  2. get value():读取时,执行track收集依赖,返回内部值。
  3. set value():修改时,执行trigger触发更新。

让我们手写这个核心类:

// 判断是否为对象functionisObject(val){returnval!==null&&typeofval==='object';}// 模拟 reactive,将对象转为 ProxyfunctiontoReactive(value){returnisObject(value)?reactive(value):value;}// 简化版 reactive 实现(基于 Proxy)functionreactive(target){returnnewProxy(target,{get(target,key,receiver){constres=Reflect.get(target,key,receiver);track(target,key);// 收集依赖returnisObject(res)?reactive(res):res;// 深层响应式},set(target,key,value,receiver){constoldValue=target[key];constres=Reflect.set(target,key,value,receiver);if(oldValue!==value){trigger(target,key);// 触发更新}returnres;}});}// RefImpl 类classRefImpl{constructor(value){this._value=toReactive(value);// 对象转响应式,原始值直接存储this.dep=newSet();// 专属依赖集合}getvalue(){// 收集依赖到 RefImpl 自身的 dep 中if(activeEffect){this.dep.add(activeEffect);}returnthis._value;}setvalue(newVal){if(newVal!==this._value){this._value=toReactive(newVal);// 触发所有依赖此 ref 的 effectthis.dep.forEach(effect=>effect());}}}// ref 工厂函数functionref(value){returnnewRefImpl(value);}

关键点剖析

  • 为什么需要RefImpl因为 JavaScript 的基本类型(number, string)是按值传递的,无法直接给它们添加属性或拦截操作。必须把它们装进一个对象盒子里。
  • 对象的特殊处理:如果ref接收的是对象,内部会调用toReactive(即reactive函数),将其转换为深层响应式的 Proxy。这就是为么ref({ count: 0 })也能工作的原因。
  • 依赖存储:与reactive将依赖存在全局WeakMap不同,ref的依赖通常存储在实例自身的dep属性中(Vue 源码中更复杂,涉及Dep类),这使得查找更直接。

三、 实战演练:从零构建响应式计数器

理论讲完了,让我们用刚才手写的代码跑一个真实的案例。

// 1. 定义响应式数据constcount=ref(0);conststate=ref({name:'Vue3'});// 2. 定义副作用函数(模拟组件渲染)effect(()=>{console.log(`Count is:${count.value}`);document.getElementById('count').innerText=count.value;});effect(()=>{console.log(`Name is:${state.value.name}`);document.getElementById('name').innerText=state.value.name;});// 3. 模拟用户操作document.getElementById('btn').onclick=()=>{count.value++;// 触发第一个 effectstate.value.name='Vue 3.5';// 触发第二个 effect};

执行流程拆解

  1. 初始化effect执行时,读取count.value,此时activeEffect指向渲染函数。RefImplget value被触发,将渲染函数收集到count.dep中。
  2. 点击按钮:执行count.value++
  3. Setter 拦截set value被触发,比较新旧值(0 vs 1),发现变化。
  4. 触发更新:遍历count.dep,执行之前收集的渲染函数。
  5. 重新渲染:控制台打印 “Count is: 1”,DOM 更新。

对于对象类型state,当我们修改state.value.name时,实际上是触发了 Proxy 的set陷阱,进而调用trigger通知更新。


四、 进阶:Ref 与 Reactive 的爱恨情仇

在手写过程中,我们不可避免地要面对refreactive的区别。这不仅是语法糖,更是设计哲学的差异。

维度refreactive
适用类型通用(基本类型 + 对象)仅对象/数组/集合
访问方式必须通过.value直接访问属性obj.prop
底层实现包装类 + getter/setterProxy 代理
响应式替换可整体替换 (ref.value = {})不可整体替换 (会失去响应性)
解构问题不会丢失响应性 (TS 编译时自动解包)必须使用toRefs保持响应性

为什么推荐基本类型用ref
因为reactive无法处理number。如果你尝试reactive(0),Vue 会发出警告并返回原值。而ref通过包装类完美绕过了这个限制。

为什么对象推荐用reactive

  1. 代码简洁:不需要到处写.value
  2. 性能优化ref对对象的处理是“包装一层 Proxy”,而reactive是直接代理原对象。虽然差异微小,但在极端性能场景下,少一层包装意味着更少的拦截开销。
  3. 不可重赋值reactive对象本身不能被重新赋值(否则响应性丢失),这在一定程度上强制了不可变数据流的最佳实践。

手写toRefs的逻辑
为了解决reactive解构丢失响应性的问题,Vue 提供了toRefs。其原理非常简单:遍历对象的每个 key,创建一个对应的ref,其get指向原对象的属性,set时修改原对象。

functiontoRefs(proxyObj){constrefs={};for(letkeyinproxyObj){refs[key]={getvalue(){returnproxyObj[key];},setvalue(v){proxyObj[key]=v;}};}returnrefs;}

五、 避坑指南与最佳实践

在理解了底层实现后,我们可以规避一些常见的“坑”:

  1. 不要在 Effect 内部定义 Ref

    // 错误 ❌effect(()=>{constcount=ref(0);// 每次 effect 运行都会新建 ref,依赖关系混乱console.log(count.value);});// 正确 ✅constcount=ref(0);effect(()=>{console.log(count.value);});
  2. 不要直接给 Ref 变量赋值

    letcount=ref(0);count=1;// 错误 ❌!切断了与响应式对象的联系count.value=1;// 正确 ✅
  3. ShallowRef(浅层 Ref)
    对于大型对象,如果只关心引用的变化(整体替换),不关心内部属性变化,可以使用浅层 Ref。手写实现只需在 setter 中不进行toReactive转换即可。Vue 源码中的shallowRef就是通过标记位__v_isShallow来控制的。

  4. DOM 引用与组件实例
    ref的另一个用途是获取 DOM 元素或组件实例。在模板中使用ref="myRef",Vue 会在挂载后将元素赋值给myRef.value。这利用了同一个RefImpl机制,只是初始值为null,且在mounted钩子后才赋值。


结语

通过手写ref,我们不仅复现了 Vue 3 响应式的核心,更看清了其设计的精妙之处:用 Proxy 劫持对象,用闭包包装原始值,用依赖追踪实现精准更新

ref不仅仅是一个 API,它是 Vue 3 组合式 API 的基石。理解了它,你就理解了为什么 Vue 能在不需要this的情况下管理状态,理解了computedwatch的运行机制,甚至能在面试中自信地剖析 Vue 与 React 状态管理的本质区别。

下次当你敲下const count = ref(0)时,请记住,你手中握着的,是一套精密运作的自动化引擎,而不仅仅是一个变量。掌握底层原理,才能在复杂的业务场景中驾驭自如,写出高性能、高可维护性的代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 11:41:12

智能体总控平台:架构设计、技术实现与应用场景解析

智能体总控平台&#xff1a;架构设计、技术实现与应用场景解析2026年&#xff0c;AI Agent正从“概念玩具”迈向企业级“数字员工”的规模化落地。Gartner预测&#xff0c;到2026年底全球超过30%的企业核心流程将由AI Agents辅助或主导。在这一浪潮中&#xff0c;智能体总控平台…

作者头像 李华
网站建设 2026/4/14 11:41:12

ChatTTS作品集展示:多种音色演绎不同情绪表达

ChatTTS作品集展示&#xff1a;多种音色演绎不同情绪表达 “它不仅是在读稿&#xff0c;它是在表演。” 如果你还在寻找一款能“开口说话”的AI语音工具&#xff0c;那么ChatTTS可能会让你彻底改变对语音合成的看法。它不像传统的TTS那样机械地朗读文字&#xff0c;而是像一个真…

作者头像 李华
网站建设 2026/4/14 11:38:27

如何快速解锁《鸣潮》120帧:完整使用WaveTools工具箱指南

如何快速解锁《鸣潮》120帧&#xff1a;完整使用WaveTools工具箱指南 【免费下载链接】WaveTools &#x1f9f0;鸣潮工具箱 项目地址: https://gitcode.com/gh_mirrors/wa/WaveTools 如果你正在玩《鸣潮》这款热门开放世界游戏&#xff0c;却因为60帧的锁帧限制而感到画…

作者头像 李华
网站建设 2026/4/14 11:37:37

Llama-3.2V-11B-cot图文推理教程:支持多轮追问与上下文记忆的实测

Llama-3.2V-11B-cot图文推理教程&#xff1a;支持多轮追问与上下文记忆的实测 1. 工具概览 Llama-3.2V-11B-cot是一款基于Meta多模态大模型开发的高性能视觉推理工具&#xff0c;专为双卡4090环境优化。它解决了传统大模型部署中的常见痛点&#xff0c;让普通用户也能轻松体验…

作者头像 李华
网站建设 2026/4/14 11:35:41

Windows超级管理器:8MB小工具竟能替代10款软件?实测22项隐藏功能

Windows超级管理器&#xff1a;8MB小工具竟能替代10款软件&#xff1f;实测22项隐藏功能 每次打开Windows电脑&#xff0c;桌面上总堆满各种功能单一的小工具——内存清理、启动项管理、文件粉碎……每个软件都占着宝贵的存储空间&#xff0c;运行时还偷偷吃内存。直到发现这款…

作者头像 李华
网站建设 2026/4/14 11:35:38

ENVI新版随机森林工具包实测:如何用‘偷懒’的随机抽样,快速训练高精度分类模型?

ENVI新版随机森林工具包实测&#xff1a;如何用‘偷懒’的随机抽样&#xff0c;快速训练高精度分类模型&#xff1f; 遥感影像分类一直是地理信息科学领域的核心课题。面对动辄数十GB的高分辨率卫星数据&#xff0c;传统分类方法往往力不从心。而随机森林算法凭借其出色的抗噪能…

作者头像 李华