源码视角,vue3为什么推荐用ref,而不是reactive

ref 和 reactive 是 Vue3 中实现响应式数据的核心 API。ref 用于包装基本数据类型,而 reactive 用于处理对象和数组。尽管 reactive 似乎更适合处理对象,但 Vue3 官方文档更推荐使用 ref

1e81de1370834ecc8d8c3876249468b7.png

 

我的想法,ref就是比reactive好用,官方也是这么说的,不服来踩!下面我们从源码的角度详细讨论这两个 API,以及 Vue3 为什么推荐使用ref而不是reactive

 

ref 的内部工作原理

ref 是一个函数,它接受一个内部值并返回一个响应式且可变的引用对象。这个引用对象有一个 .value 属性,该属性指向内部值。

// 深响应式
export function ref(value?: unknown) {return createRef(value, false)
}// 浅响应式
export function shallowRef(value?: unknown) {return createRef(value, true)
}function createRef(rawValue: unknown, shallow: boolean) {// 如果传入的值已经是一个 ref,则直接返回它if (isRef(rawValue)) {return rawValue}// 否则,创建一个新的 RefImpl 实例return new RefImpl(rawValue, shallow)
}class RefImpl<T> {// 存储响应式的值。我们追踪和更新的就是_value。(这个是重点)private _value: T// 用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看)private _rawValue: T // 用于依赖跟踪的 Dep 类实例public dep?: Dep = undefined// 一个标记,表示这是一个 ref 实例public readonly __v_isRef = trueconstructor(value: T,public readonly __v_isShallow: boolean,) {// 如果是浅响应式,直接使用原始值,否则转换为非响应式原始值this._rawValue = __v_isShallow ? value : toRaw(value)// 如果是浅响应式,直接使用原始值,否则转换为响应式值this._value = __v_isShallow ? value : toReactive(value)// toRaw 用于将响应式引用转换回原始值// toReactive 函数用于将传入的值转换为响应式对象。对于基本数据类型,toReactive 直接返回原始值。// 对于对象和数组,toReactive 内部会调用 reactive 来创建一个响应式代理。// 因此,对于 ref 来说,基本数据类型的值会被 RefImpl 直接包装,而对象和数组// 会被 reactive 转换为响应式代理,最后也会被 RefImpl 包装。// 这样,无论是哪种类型的数据,ref 都可以提供响应式的 value 属性,// 使得数据变化可以被 Vue 正确追踪和更新。// export const toReactive = (value) => isObject(value) ? reactive(value) : value}get value() {// 追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。trackRefValue(this)// 返回存储的响应式值return this._value}set value(newVal) {// 判断是否应该使用新值的直接形式(浅响应式或只读)const useDirectValue =this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)// 如果需要,将新值转换为非响应式原始值newVal = useDirectValue ? newVal : toRaw(newVal)// 如果新值与旧值不同,更新 _rawValue 和 _valueif (hasChanged(newVal, this._rawValue)) {this._rawValue = newValthis._value = useDirectValue ? newVal : toReactive(newVal)// 触发依赖更新triggerRefValue(this, DirtyLevels.Dirty, newVal)}}
}

在上述代码中,ref 函数通过 new RefImpl(value) 创建了一个新的 RefImpl 实例。这个实例包含 getter 和 setter,分别用于追踪依赖和触发更新。使用 ref 可以声明任何数据类型的响应式状态,包括对象和数组。

import { ref } from 'vue' let state = ref({ count: 0 })
state.value.count++

注意,ref核心是返回响应式且可变的引用对象,而reactive核心是返回的是响应式代理,这是两者本质上的核心区别,也就导致了ref优于reactive,我们接着看下reactive源码实现。

reactive 的内部工作原理

reactive 是一个函数,它接受一个对象并返回该对象的响应式代理,也就是 Proxy

function reactive(target) {if (target && target.__v_isReactive) {return target}return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers,reactiveMap)
}function createReactiveObject(target,isReadonly,baseHandlers,collectionHandlers,proxyMap
) {if (!isObject(target)) {return target}const existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}const proxy = new Proxy(target, baseHandlers)proxyMap.set(target, proxy)return proxy
}

reactive的源码相对就简单多了,reactive 通过 new Proxy(target, baseHandlers) 创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。

import { reactive } from 'vue' let state = reactive({ count: 0 })
state.count++

到这里我们可以看出 ref 和 reactive 在声明数据的响应式状态上,底层原理是不一样的。ref 采用 RefImpl对象实例,reactive采用Proxy代理对象。

ref 更深入的理解

当你使用 new RefImpl(value) 创建一个 RefImpl 实例时,这个实例大致上会包含以下几部分:

  1. 内部值:实例存储了传递给构造函数的初始值。
  2. 依赖收集:实例需要跟踪所有依赖于它的效果(effect),例如计算属性或者副作用函数。这通常通过一个依赖列表或者集合来实现。
  3. 触发更新:当实例的值发生变化时,它需要通知所有依赖于它的效果,以便它们可以重新计算或执行。

RefImpl 类似于发布-订阅模式的设计,以下是一个简化的 RefImpl 类的伪代码实现,展示这个实现过程:

class Dep {constructor() {this.subscribers = new Set();}depend() {if (activeEffect) {this.subscribers.add(activeEffect);}}notify() {this.subscribers.forEach(effect => effect());}
}let activeEffect = null;function watchEffect(effect) {activeEffect = effect;effect();activeEffect = null;
}class RefImpl {constructor(value) {this._value = value;this.dep = new Dep();}get value() {// 当获取值时,进行依赖收集this.dep.depend();return this._value;}set value(newValue) {if (newValue !== this._value) {this._value = newValue;// 值改变时,触发更新this.dep.notify();}}
}// 使用示例
let count = new RefImpl(0);watchEffect(() => {console.log(`The count is: ${count.value}`); // 订阅变化
});count.value++; // 修改值,触发通知,重新执行watchEffect中的函数

Dep 类负责管理一个依赖列表,并提供依赖收集和通知更新的功能。RefImpl 类包含一个内部值 _value 和一个 Dep 实例。当 value 被访问时,通过 get 方法进行依赖收集;当 value 被赋予新值时,通过 set 方法触发更新。

 

ref 和 reactive 尽管两者在内部实现上有所不同,但它们都能满足我们对于声明响应式变量的要求,但是 reactive 却存在一定的局限性。

reactive 的局限性

在 Vue3 中,reactive API 通过 Proxy 实现了一种响应式数据的方法,尽管这种方法在性能上比 Vue2 有所提升,但 Proxy 的局限性也导致了 reactive 的局限性,这些局限性可能会影响开发者的使用体验。

仅对引用数据类型有效

reactive 主要适用于对象,包括数组和一些集合类型(如 Map 和 Set)。对于基础数据类型(如 stringnumber 和 boolean),reactive 是无效的。这意味着如果你尝试使用 reactive 来处理这些基础数据类型,将会得到一个非响应式的对象。

import { reactive } from 'vue';
const state = reactive({ count: 0 });

使用不当会失去响应

  1. 直接赋值对象:如果直接将一个响应式对象赋值给另一个变量,将会失去响应性。这是因为 reactive 返回的是对象本身,而不仅仅是代理。

    import { reactive } from 'vue';let state = reactive({ count: 0 });
    state = { count: 1 }; // 失去响应性
    
  2. 直接替换响应式对象:同样,直接替换一个响应式对象也会导致失去响应性。

    import { reactive } from 'vue';let state = reactive({ count: 0 });
    state = reactive({ count: 1 }); // 失去响应性
    
  3. 直接解构对象:在解构响应式对象时,如果直接解构对象属性,将会得到一个非响应式的变量。

    const state = reactive({ count: 0 });let { count } = state;
    count++; // count 仍然是 0
    

    解决这个问题,需要使用 toRefs 函数来将响应式对象转换为 ref 对象。

    import { toRefs } from 'vue';const state = reactive({ count: 0 });
    let { count } = toRefs(state);
    count++; // count 现在是 1
    
  4. 将响应式对象的属性赋值给变量:如果将响应式对象的属性赋值给一个变量,这个变量的值将不会是响应式的。

    let state = reactive({ count: 0 })let count = state.count
    count++  // count 仍然是 0
    console.log(state.count)
    

使用 reactive 声明响应式变量的确存在一些不便之处,尤其是对于喜欢使用解构赋值的开发者而言。这些局限性可能会导致意外的行为,因此在使用 reactive 时需要格外注意。相比之下,ref API 提供了一种更灵活和统一的方式来处理响应式数据。

为什么推荐使用 ref ?

ref()它为响应式编程提供了一种统一的解决方案,适用于所有类型的数据,包括基本数据类型和复杂对象。以下是推荐使用 ref 的几个关键原因:

统一性

ref 的核心优势之一是它的统一性。它提供了一种简单、一致的方式来处理所有类型的数据,无论是数字、字符串、对象还是数组。这种统一性极大地简化了开发者的代码,减少了在不同数据类型之间切换时的复杂性。

import { ref } from 'vue';let num = ref(0);
let str = ref('Hello');
let obj = ref({ count: 0 });// 修改基本数据类型
num.value++;
str.value += ' World';// 修改对象
obj.value.count++;

深层响应性

ref 支持深层响应性,这意味着它可以追踪和更新嵌套对象和数组中的变化。这种特性使得 ref 非常适合处理复杂的数据结构,如对象和数组。

import { ref } from 'vue';let obj = ref({user: {name: 'xiaoming',details: {age: 18}}
});// 修改嵌套对象
obj.value.user.details.age++;

当然,为了减少大型不可变数据的响应式开销,也可以通过使用shallowRef来放弃深层响应性。

let shallowObj = shallowRef({ details: { age: 18, }, 
});

灵活性

ref 提供了高度的灵活性,尤其在处理普通赋值方面。这种灵活性使得 ref 在开发中的使用更加方便,特别是在进行复杂的数据操作时。

import { ref } from 'vue';let state = ref({count: 0,name: 'Vue'
});// 替换整个对象
state.value = {count: 10,name: 'Vue 4'
};
// 修改对象内的属性
state.value.count = 20;
state.value.name = 'Vue 5';
// 添加新的属性
state.value.newProperty = 'New Property';
// 删除属性
delete state.value.newProperty;
// 使用解构更新属性(注意要保持响应性)
let { count, name } = state.value;
state.value = { count: count + 1, name };
// 复杂操作,例如根据条件更新属性
if (someCondition) {state.value = {...state.value,name: 'Updated Name'};
}
console.log(state.value)

总结

ref 在 Vue3 中提供了一种更统一、灵活的响应式解决方案,还能避免了 reactive 的某些局限性。希望这篇文章对你有所帮助,有所借鉴。大家怎么认为呢,评论区我们一起讨论下!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/716532.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 中对包含关系的判断

本文将为您详细讲解 Java 中对包含关系的判断&#xff0c;包括数组、字符串等&#xff0c;并提供相应的代码例子。 1. 数组包含关系判断 在 Java 中&#xff0c;数组包含关系判断通常使用循环来实现。以下是几种常见的判断方法&#xff1a; 示例 1&#xff1a;使用 for…

Unity曲柄滑块四杆机构运动计算

一、运动效果 二、机构的介绍 曲柄长度&#xff1a;a&#xff0c;线段AB长度 连杆长度&#xff1a;b&#xff0c;线段BC长度 偏心距离&#xff1a;e&#xff0c;滑块轨迹与曲柄中心点A的垂直距离 三、已知点A点B和e的值&#xff0c;计算C点的位置 1、计算s的值 var h math.…

通过多进程并发方式(fork)实现服务器(注意要回收子进程)

以下内容为视频学习记录。 1、父进程accept后返回的文件描述符为cfd以及用于创建连接的lfd; 调用fork()创建子进程后&#xff0c;子进程继承cfd,lfd&#xff0c;通过该cfd与连接过来的客户端通信,lfd对子进程来说没用&#xff0c;可以直接close(lfd); 对于父进程来说&#x…

双非二本找实习前的准备day4

学习目标&#xff1a; 每天2-3到简单sql&#xff08;刷完即止&#xff09;&#xff0c;每天复习代码随想录上的题目3道算法&#xff08;时间充足可以继续&#xff09;&#xff0c;背诵的八股的问题也在这里记录了 今日碎碎念&#xff1a; 1&#xff09;偶尔还是贪玩游戏&…

Vue中的计算属性和方法有什么区别?

Vue.js是一款流行的JavaScript前端框架&#xff0c;提供了丰富的功能和便捷的开发方式。在Vue中&#xff0c;计算属性和方法是常用的两种方式来处理数据和逻辑。但它们之间存在一些区别&#xff0c;本文将详细介绍Vue中计算属性和方法的区别&#xff0c;并通过示例代码加深理解…

183896-00-6,Biotin-C3-PEG3-C3-NH2,可以选择性降解靶蛋白

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;183896-00-6&#xff0c;Biotin-C3-PEG3-C3-NH2&#xff0c;Biotin-C3-PEG3-C3-amine&#xff0c;生物素-C3-PEG3-C3-胺 一、基本信息 【产品简介】&#xff1a;Biotin-PEG3-C3-NH2是一种PROTAC linker&#xff0c;…

381. 有线电视网络(网络流,最小割,《算法竞赛进阶指南》)

381. 有线电视网络 - AcWing题库 给定一张 n 个点 m 条边的无向图&#xff0c;求最少去掉多少个点&#xff0c;可以使图不连通。 如果不管去掉多少个点&#xff0c;都无法使原图不连通&#xff0c;则直接返回 n。 输入格式 输入包含多组测试数据。 每组数据占一行&#xf…

Python推导式大全与实战:精通列表、字典、集合和生成器推导式【第115篇—python:推导式】

Python推导式大全与实战&#xff1a;精通列表、字典、集合和生成器推导式 Python语言以其简洁、优雅的语法而闻名&#xff0c;其中推导式是其独特之处之一。推导式是一种在一行代码中构建数据结构的强大方式&#xff0c;它涵盖了列表、字典、集合和生成器。本篇博客将全面介绍…

YOLOv8实例分割实战:ONNX模型转换及TensorRT部署

课程链接&#xff1a;https://edu.csdn.net/course/detail/39320 PyTorch版的YOLOv8支持高性能的实时实例分割。 TensorRT是针对英伟达GPU的加速工具。 ONNX &#xff08;Open Neural Network Exchange&#xff09; 作为一个开放的网络模型中间表示&#xff08;IR&#xff0…

Redis命令大全

通用命令 KEYS pattern&#xff1a;查找所有符合给定模式&#xff08;pattern&#xff09;的 key。EXISTS key&#xff1a;检查指定 key 是否存在。TYPE key&#xff1a;返回指定 key 的数据类型。DEL key [key …]&#xff1a;删除指定的 key。RENAME key newkey&#xff1a;…

spring boot 修复 Spring Framework URL解析不当漏洞(CVE-2024-22243)

漏洞描述 当应用程序使用UriComponentsBuilder来解析外部提供的URL&#xff08;如通过查询参数&#xff09;并对解析的URL的主机执行验证检查时可能容易受到Open重定向攻击和SSRF攻击&#xff0c;导致网络钓鱼和内部网络探测等。 受影响产品或系统 6.1.0 < Spring Framew…

Vue项目的快速搭建

Vue项目的快速搭建 一、下载并安装node.js二、安装Vue脚手架三、创建vue项目四、项目启动五、VS Code下载安装 一、下载并安装node.js 首先确保已经安装了Node.js。如果没有安装&#xff0c;可以去官网&#xff08;https://nodejs.org/&#xff09;下载并安装最新版本的Node.j…

基于STC12C5A60S2系列1T 8051单片机的TM1638键盘数码管模块的数码管显示应用

基于STC12C5A60S2系列1T 8051单片机的TM1638键盘数码管模块的数码管显示应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍TM1638键盘数码管模块概述TM1638键盘数码管…

mybatis-传递参数的方式

mybatis 传递参数的7种方法 在实际开发过程中&#xff0c;增删改查操作都要涉及到请求参数的传递&#xff0c;今天这节就集中讲下在mybatis中传递参数的7中方法 单个参数的传递很简单没有什么好将的&#xff0c;这里主要说下多个参数的传递 1、第一种方式 匿名参数 顺序传递…

[electron]窗口 BrowserWindow

优雅的显示窗口 const {app, BrowserWindow} require(electron);function createMainwindow(){const mainwindow new BrowserWindow({x: 300,y: 400,width: 600,height: 600,});mainwindow.loadFile(index.html); }app.on(ready, ()>{createMainwindow(); });对于这样的代…

前端发起请求,后端模型需处理很久,怎样设置前端直接完成请求响应,后端计算完在返回结果给前端?

在这种情况下&#xff0c;可以采用异步处理的方式来解决。具体步骤如下&#xff1a; 前端发起请求&#xff1a;前端向后端发送请求&#xff0c;但是不等待后端处理完成而是立即得到响应。 后端异步处理&#xff1a;后端接收到请求后&#xff0c;不立即进行处理&#xff0c;而是…

Codeforces Round 886 (Div. 4)----->E. Cardboard for Pictures

一&#xff0c;思路&#xff1a; 这题我们可以通过二分 w来直接得到答案&#xff0c;时间复杂度是nlogn的级别&#xff0c;但是这里有个很坑的地方&#xff0c;就是假如你用二分做&#xff0c;会面临报 long long 的问题&#xff0c;但是问题不大&#xff0c;直接用 unsigned …

题目:金三银四求职季:如何脱颖而出

题目&#xff1a;金三银四求职季&#xff1a;如何脱颖而出 引言&#xff1a; 随着春天的脚步渐近&#xff0c;对于许多程序员来说&#xff0c;一年中最繁忙、最重要的面试季节也随之而来。金三银四&#xff0c;即三月和四月&#xff0c;被广大程序员视为求职的黄金时期。在这两…

蓝桥杯倒计时 41天 - KMP 算法

KMP算法 KMP算法是一种字符串匹配算法&#xff0c;用于匹配模式串P在文本串S中出现的所有位置。 例如S“ababac&#xff0c;P“aba”&#xff0c;那么出现的所有位置是13。 在初学KMP时&#xff0c;我们只需要记住和学会使用模板即可&#xff0c;对其原理只需简单理解&#xff…

用Socks5代理游戏,绕过“网络海关”去探险

1. 出海大冒险的开始 在游戏世界&#xff0c;就像在现实生活中一样&#xff0c;有时我们需要越过海洋去探索未知的世界。但是&#xff0c;网络上也有一些“海关”&#xff0c;限制我们访问某些网站或游戏服务器。这就是我们今天要克服的挑战&#xff01; 2. Socks5代理&#xf…