• 记录--源码视角,Vue3为什么推荐使用ref而不是reactive


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

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

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

    ref 的内部工作原理

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

    1. // 深响应式
    2. export function ref(value?: unknown) {
    3. return createRef(value, false)
    4. }
    5. // 浅响应式
    6. export function shallowRef(value?: unknown) {
    7. return createRef(value, true)
    8. }
    9. function createRef(rawValue: unknown, shallow: boolean) {
    10. // 如果传入的值已经是一个 ref,则直接返回它
    11. if (isRef(rawValue)) {
    12. return rawValue
    13. }
    14. // 否则,创建一个新的 RefImpl 实例
    15. return new RefImpl(rawValue, shallow)
    16. }
    17. class RefImpl<T> {
    18. // 存储响应式的值。我们追踪和更新的就是_value。(这个是重点)
    19. private _value: T
    20. // 用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看)
    21. private _rawValue: T
    22. // 用于依赖跟踪的 Dep 类实例
    23. public dep?: Dep = undefined
    24. // 一个标记,表示这是一个 ref 实例
    25. public readonly __v_isRef = true
    26. constructor(
    27. value: T,
    28. public readonly __v_isShallow: boolean,
    29. ) {
    30. // 如果是浅响应式,直接使用原始值,否则转换为非响应式原始值
    31. this._rawValue = __v_isShallow ? value : toRaw(value)
    32. // 如果是浅响应式,直接使用原始值,否则转换为响应式值
    33. this._value = __v_isShallow ? value : toReactive(value)
    34. // toRaw 用于将响应式引用转换回原始值
    35. // toReactive 函数用于将传入的值转换为响应式对象。对于基本数据类型,toReactive 直接返回原始值。
    36. // 对于对象和数组,toReactive 内部会调用 reactive 来创建一个响应式代理。
    37. // 因此,对于 ref 来说,基本数据类型的值会被 RefImpl 直接包装,而对象和数组
    38. // 会被 reactive 转换为响应式代理,最后也会被 RefImpl 包装。
    39. // 这样,无论是哪种类型的数据,ref 都可以提供响应式的 value 属性,
    40. // 使得数据变化可以被 Vue 正确追踪和更新。
    41. // export const toReactive = (value) => isObject(value) ? reactive(value) : value
    42. }
    43. get value() {
    44. // 追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。
    45. trackRefValue(this)
    46. // 返回存储的响应式值
    47. return this._value
    48. }
    49. set value(newVal) {
    50. // 判断是否应该使用新值的直接形式(浅响应式或只读)
    51. const useDirectValue =
    52. this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    53. // 如果需要,将新值转换为非响应式原始值
    54. newVal = useDirectValue ? newVal : toRaw(newVal)
    55. // 如果新值与旧值不同,更新 _rawValue 和 _value
    56. if (hasChanged(newVal, this._rawValue)) {
    57. this._rawValue = newVal
    58. this._value = useDirectValue ? newVal : toReactive(newVal)
    59. // 触发依赖更新
    60. triggerRefValue(this, DirtyLevels.Dirty, newVal)
    61. }
    62. }
    63. }

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

    1. import { ref } from 'vue'
    2. const state = ref({ count: 0 })
    3. state.value.count++

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

    reactive 的内部工作原理

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

    1. function reactive(target) {
    2. if (target && target.__v_isReactive) {
    3. return target
    4. }
    5. return createReactiveObject(
    6. target,
    7. false,
    8. mutableHandlers,
    9. mutableCollectionHandlers,
    10. reactiveMap
    11. )
    12. }
    13. function createReactiveObject(
    14. target,
    15. isReadonly,
    16. baseHandlers,
    17. collectionHandlers,
    18. proxyMap
    19. ) {
    20. if (!isObject(target)) {
    21. return target
    22. }
    23. const existingProxy = proxyMap.get(target)
    24. if (existingProxy) {
    25. return existingProxy
    26. }
    27. const proxy = new Proxy(target, baseHandlers)
    28. proxyMap.set(target, proxy)
    29. return proxy
    30. }
    reactive的源码相对就简单多了, reactive 通过  new Proxy(target, baseHandlers) 创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。
    1. import { reactive } from 'vue'
    2. const state = reactive({ count: 0 })
    3. state.count++

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

    ref 更深入的理解

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

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

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

    1. class Dep {
    2. constructor() {
    3. this.subscribers = new Set();
    4. }
    5. depend() {
    6. if (activeEffect) {
    7. this.subscribers.add(activeEffect);
    8. }
    9. }
    10. notify() {
    11. this.subscribers.forEach(effect => effect());
    12. }
    13. }
    14. let activeEffect = null;
    15. function watchEffect(effect) {
    16. activeEffect = effect;
    17. effect();
    18. activeEffect = null;
    19. }
    20. class RefImpl {
    21. constructor(value) {
    22. this._value = value;
    23. this.dep = new Dep();
    24. }
    25. get value() {
    26. // 当获取值时,进行依赖收集
    27. this.dep.depend();
    28. return this._value;
    29. }
    30. set value(newValue) {
    31. if (newValue !== this._value) {
    32. this._value = newValue;
    33. // 值改变时,触发更新
    34. this.dep.notify();
    35. }
    36. }
    37. }
    38. // 使用示例
    39. const count = new RefImpl(0);
    40. watchEffect(() => {
    41. console.log(`The count is: ${count.value}`); // 订阅变化
    42. });
    43. 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 来处理这些基础数据类型,将会得到一个非响应式的对象。

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

    使用不当会失去响应

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

    1. import { reactive } from 'vue';
    2. const state = reactive({ count: 0 });
    3. state = { count: 1 }; // 失去响应性
    1. 直接替换响应式对象:同样,直接替换一个响应式对象也会导致失去响应性。
    1. import { reactive } from 'vue';
    2. const state = reactive({ count: 0 });
    3. state = reactive({ count: 1 }); // 失去响应性
    1. 直接解构对象:在解构响应式对象时,如果直接解构对象属性,将会得到一个非响应式的变量。
    1. const state = reactive({ count: 0 });
    2. let { count } = state;
    3. count++; // count 仍然是 0
       好家伙!常用的解构赋值不能用。为了解决这个问题,需要使用  toRefs 函数来将响应式对象转换为  ref 对象。
    1. import { toRefs } from 'vue';
    2. const state = reactive({ count: 0 });
    3. let { count } = toRefs(state);
    4. count++; // count 现在是 1

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

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

    1. const state = reactive({ count: 0 })
    2. let count = state.count
    3. count++ // count 仍然是 0

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

    为什么推荐使用 ref ?

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

    统一性

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

    1. import { ref } from 'vue';
    2. const num = ref(0);
    3. const str = ref('Hello');
    4. const obj = ref({ count: 0 });
    5. // 修改基本数据类型
    6. num.value++;
    7. str.value += ' World';
    8. // 修改对象
    9. obj.value.count++;

    深层响应性

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

    1. import { ref } from 'vue';
    2. const obj = ref({
    3. user: {
    4. name: 'xiaoming',
    5. details: {
    6. age: 18
    7. }
    8. }
    9. });
    10. // 修改嵌套对象
    11. obj.value.user.details.age++;
    当然,为了减少大型不可变数据的响应式开销,也可以通过使用 shallowRef来放弃深层响应性。
    1. const shallowObj = shallowRef({
    2. details: { age: 18, },
    3. });

    灵活性

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

    1. import { ref } from 'vue';
    2. const state = ref({
    3. count: 0,
    4. name: 'Vue'
    5. });
    6. // 解构赋值
    7. const { count, name } = state.value;
    8. // 直接修改解构后的变量
    9. count++;
    10. name = 'Vue3';
    11. // 替换整个对象
    12. state.value = {
    13. count: 10,
    14. name: 'Vue4'
    15. };

    总结

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

    本文转载于:

    https://juejin.cn/post/7329539838776246272

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

     

  • 相关阅读:
    深度学习实战06-循环神经网络(RNN)实现股票预测
    NLP入门——语言结构/语言建模
    自我解剖与未来展望
    echarts X轴类目名太长时隐藏,hover时显示全部
    用python画折线图
    SpringSecurity(十六)---OAuth2的运行机制(中)-密码、客户端凭据授权类型以及刷新令牌
    STC51单片机33——液晶12864显示
    第三章、基于Ruoyi-Vue开发脚手架之多账户登录体系实现
    eNSP-OSPF协议其他区域不与骨干区域相连解决方法1
    .NET 开源工作流: Slickflow流程引擎高级开发(十) -- BpmnJS流程设计器集成
  • 原文地址:https://blog.csdn.net/qq_40716795/article/details/136229869