这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

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

我的想法,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 = true constructor(
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 和 _value
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发依赖更新
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

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

import { ref } from 'vue' 

const 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' 

const state = reactive({ count: 0 })
state.count++

到这里我们可以看出 refreactive 在声明数据的响应式状态上,底层原理是不一样的。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();
}
}
} // 使用示例
const count = new RefImpl(0); watchEffect(() => {
console.log(`The count is: ${count.value}`); // 订阅变化
}); count.value++; // 修改值,触发通知,重新执行watchEffect中的函数

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

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

reactive 的局限性

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

仅对引用数据类型有效

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

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

使用不当会失去响应

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

import { reactive } from 'vue';

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

const state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 失去响应性
  1. 直接解构对象:在解构响应式对象时,如果直接解构对象属性,将会得到一个非响应式的变量。
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

  首先来说,太不方便了!而且使用toRefs(),将响应式变量换成 ref 的形式,那我还不如直接使用ref()了,大家说是不是?

  1. 将响应式对象的属性赋值给变量:如果将响应式对象的属性赋值给一个变量,这个变量的值将不会是响应式的。

const state = reactive({ count: 0 })

let count = state.count
count++ // count 仍然是 0

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

为什么推荐使用 ref ?

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

统一性

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

import { ref } from 'vue';

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

深层响应性

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

import { ref } from 'vue';

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

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

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

灵活性

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

import { ref } from 'vue';

const state = ref({
count: 0,
name: 'Vue'
}); // 解构赋值
const { count, name } = state.value; // 直接修改解构后的变量
count++;
name = 'Vue3'; // 替换整个对象
state.value = {
count: 10,
name: 'Vue4'
};

总结

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

本文转载于:

https://juejin.cn/post/7329539838776246272

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--源码视角,Vue3为什么推荐使用ref而不是reactive的更多相关文章

  1. 如何在IDEA里给大数据项目导入该项目的相关源码(博主推荐)(类似eclipse里同一个workspace下单个子项目存在)(图文详解)

    不多说,直接上干货! 如果在一个界面里,可以是单个项目 注意:本文是以gradle项目的方式来做的! 如何在IDEA里正确导入从Github上下载的Gradle项目(含相关源码)(博主推荐)(图文详解 ...

  2. GitHub 上 1.3k Star 的 strman-java 项目有值得学习的地方吗?源码视角

    大家好,我是沉默王二. 很多初学编程的同学,经常给我吐槽,说:"二哥,你在敲代码的时候会不会有这样一种感觉,写着写着看不下去了,觉得自己写出来的代码就好像屎一样?" 这里我必须得说 ...

  3. Spark记录-源码编译spark2.2.0(结合Hive on Spark/Hive on MR2/Spark on Yarn)

    #spark2.2.0源码编译 #组件:mvn-3.3.9 jdk-1.8 #wget http://mirror.bit.edu.cn/apache/spark/spark-2.2.0/spark- ...

  4. 微信小程序源码推荐

    wx-gesture-lock  微信小程序的手势密码 WXCustomSwitch 微信小程序自定义 Switch 组件模板 WeixinAppBdNovel 微信小程序demo:百度小说搜索 sh ...

  5. 使用 IntelliJ IDEA 导入 Spark 最新源码及编译 Spark 源代码(博主强烈推荐)

    前言   其实啊,无论你是初学者还是具备了有一定spark编程经验,都需要对spark源码足够重视起来. 本人,肺腑之己见,想要成为大数据的大牛和顶尖专家,多结合源码和操练编程. 准备工作 1.sca ...

  6. [Dynamic Language] Python3.7 源码安装 ModuleNotFoundError: No module named '_ctypes' 解决记录

    Python3.7 源码安装 ModuleNotFoundError: No module named '_ctypes' 解决记录 源码安装时报错 File "/home/abeenser ...

  7. Vue3全局APi解析-源码学习

    本文章共5314字,预计阅读时间5-15分钟. 前言 不知不觉Vue-next的版本已经来到了3.1.2,最近对照着源码学习Vue3的全局Api,边学习边整理了下来,希望可以和大家一起进步. 我们以官 ...

  8. .net framework 源码调试 与 问题解决

    调试方式有二种, 看官方资料就OK. 官方地址: http://referencesource.microsoft.com/serversetup.aspx 1. 使用配置在线地址安装 2. 下载安装 ...

  9. Ubuntu12.04下载Android4.0.1源码全过程,附若干问题解决[转]

    学校里一直在做应用层开发,考虑到日后就业问题,这次决定研究源码和驱动,并进行编译.没想到就下载源码这一步折腾了我整整两天,期间遇到很多问题,哎,记录于此,希望日后再下源码的人不要再走无谓的弯路了.事实 ...

  10. [2013.7.5新鲜出炉] Ubuntu12.04下载Android4.0.1源码全过程----------------折腾两天,终于下好,附若干问题解决

    本文转至 http://blog.csdn.net/yanzi1225627/article/details/9255457 下载源码这一步折腾了我整整两天,期间遇到很多问题,哎,记录于此,希望日后再 ...

随机推荐

  1. 思维分析逻辑 5 DAY

    目录 如何分析 结构分析 对比分析 时间序列 相关性分析 机器学习 报告撰写 报告撰写三原则 标准化报告的组成 AB测试 AB测试流程 AB测试注意事项 如何分析 结构分析 对比分析 对比分析:所有的 ...

  2. JS Leetcode 220. 存在重复元素 III 题解分析,暴力解法与桶排序

    壹 ❀ 引 今天的题目来自LeetCode 220. 存在重复元素 III,难度中等,题目描述如下: 给你一个整数数组 nums 和两个整数 k 和 t .请你判断是否存在 两个不同下标 i 和 j, ...

  3. Host跨网段的ARP request

    写得很清楚的文章:ARP协议在同网段及跨网段下的工作原理 对应的官方ARP文档:RFC 826 对文档的解读:详解RFC 826文档 ARP跨网段进行request包发送,需要先检测自身子网是否与对方 ...

  4. NC16541 [NOIP2013]车站分级

    题目链接 题目 题目描述 一条单向的铁路线上,依次有编号为1, 2, -, n 的n 个火车站.每个火车站都有一个级别,最低为1 级.现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次 ...

  5. NC16742 [NOIP2002]字串变换

    题目链接 题目 题目描述 已知有两个字串 A, B及一组字串变换的规则(至多6个规则): A1 -> B1 A2 -> B2 规则的含义为:在A中的子串 A1可以变换为 B1.A2可以变换 ...

  6. Linux进程的创建与销毁

    Linux操作系统是一种多任务.多用户的操作系统,这意味着它可以同时运行多个进程,每个进程都可以执行不同的任务. 在本文中,我们将介绍如何在Linux系统中创建和销毁进程. 进程的创建 在Linux系 ...

  7. java 从零开始手写 redis(七)LRU 缓存淘汰策略详解

    前言 java从零手写实现redis(一)如何实现固定大小的缓存? java从零手写实现redis(三)redis expire 过期原理 java从零手写实现redis(三)内存数据如何重启不丢失? ...

  8. 机器学习策略篇:详解正交化(Orthogonalization)

    正交化 这是一张老式电视图片,有很多旋钮可以用来调整图像的各种性质,所以对于这些旧式电视,可能有一个旋钮用来调图像垂直方向的高度,另外有一个旋钮用来调图像宽度,也许还有一个旋钮用来调梯形角度,还有一个 ...

  9. Java - 将TXT文本文件转换为PDF文件

    与TXT文本文件,PDF文件更加专业也更适合传输,常用于正式报告.简历.合同等场合.项目中如果有使用Java将TXT文本文件转为PDF文件的需求,可以查看本文中介绍的免费实现方法. 免费Java PD ...

  10. 在Directory.Build.props中用全局变量来管理包的版本号

    1.顶级目录下放置Directory.Build.props文件 为每个软件产品,分配一块独立的windows盘符,在根目录下放置名为Directory.Build.props的文件即可.这个文件名是 ...