WHAT - Vue3 响应性连接丢失问题(含解构场景)

本文讨论了Vue3中ref和reactive对象的正确访问方式,强调了ref必须通过.value访问的原因,以及解构操作可能导致的响应性连接丢失场景,包括ProxyInterceptor、reactive的深层响应性以及props解构的注意事项,推荐使用toRefs处理props以保持响应性。
摘要由CSDN通过智能技术生成

一、前言

先抛几个问题:

  1. ref 必须通过 .value 的方式访问?
  2. Vue 3 中解构不能随意使用?比如 props 原始值解构(可能会)或 reactive() 解构等存在响应性连接丢失场景?
  3. reactive() 有哪些响应性连接丢失场景?Vue3 禁止解构 reactive()

二、为什么 ref 必须通过 .value 的方式访问

来看看 ref 具体实现:

import { reactive } from './reactive'
import { trackEffects, triggerEffects } from './effect'
export const isObject = (value) => {
  return typeof value === 'object' && value !== null;
}
// 将对象转换为响应式
function toReactive(value) {
  // reactive() 对对象中深层嵌套对象同样会进行代理
  return isObject(value) ? reactive(value) : value;
}
class RefImpl {
  public _value;
  public dep = new Set; // 依赖收集
  public __v_isRef = true; // ref 标识

  constructor(public rawValue, public _shallow) {
    // rawValue 传递进来的值
    // _shallow 浅 ref 表示不需要再次代理
    this._value = _shallow ? rawValue : toReactive(rawValue);
  }
  get value() {
    trackEffects(this.dep); // 取值的时候依赖收集
    return this._value;
  }
  set value(newValue) {
    if(newValue != this.rawValue) {
      this._value = this._shallow ? newValue : toReactive(newValue);
      this.rawValue = newVal;
      triggerEffects(this.dep);
    }
  }
}

上述代码中,可以发现对于原始值默认会包装为一个对象 toReactive(rawValue),然后通过 get value()set value() 属性访问器(Accessor Properties)特性定义原始值的访问和设置行为,从而导致必须有 .value 的操作。这也解答了第一个问题。

这里要先知道一点,vue3 中,通过 getter / setters 来实现 ref,通过 Proxy 来实现 reactive。

注意,在 ref 实现里我们还发现另外两个额外的特性:

  1. _shallow

_shallow 应用于如 shallowRef()

浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。因为我们通过源码知道,ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map,ref 会使它们的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到。

  1. isObject

isObject(value) ? reactive(value) : value;

import { ref } from 'vue'
const obj = { a: 1, b: { c: 2 } };
const count = ref(obj)
function change1() {
  count.value.b = { c: 3 };
}
function change2() {
  count.value.b.c = 4;
}

上述两个 change 方法都会触发视图更新。说明如果传给 ref 的是一个对象,它会将其进行 reactive() 封装,我们知道 reactive 是一个深层嵌套响应式实现。

三、解构响应性连接丢失场景

1. Proxy Intercepting

先看下述代码:

const obj = {
  name: 'foo',
}
const handler = {
  get: function(target, key) {
    console.log('get', key);
    return Reflect.get(...arguments);
  },
  set: function(target, key, value) {
    console.log('set', key, '=', value);
    return Reflect.set(...arguments);
  },
}
const data = new Proxy(obj, handler);
data.name = 'foo2';
console.log(data.name);
// set name = foo2
// get name
// foo2

上述代码中,我们发现,proxy 的使用本身就是对于对象的拦截。

请添加图片描述

2. reactive()

首先,对于普通对象,如果我们在对象内嵌套一层对象,然后进行 proxy 代理,是否能拦截这个嵌套的对象?

答案是不能。

const obj = {
  count: 1,
  b: {
    c: 2
  }
};
const handler = {
  get: function(target, key) {
    console.log('get', key);
    return Reflect.get(...arguments);
  },
  set: function(target, key, value) {
    console.log('set ', key, '=', value);
    return Reflect.set(...arguments);
  },
}
const data = new Proxy(obj, handler);
console.log(data.count);
console.log(data.b)
console.log(data.b.c)
// get count
// 1
// get b
// {c: 2}
// get b
// 2

可以发现访问 data.b.c 没有触发对象 c 的 get 执行,只 b 对象执行了 get,即无法拦截。

因此如果想要对深层嵌套的对象实现响应性,需要对嵌套的对象也进行一层 proxy 代理,在 vue3 中 reactive() 其实就是一个深层嵌套响应式实现。

如下是 reactive() 简单实现:

const data = reactive(obj)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log("get");
      if(typeof target[key] === 'object') {
        return reactive(target[key]); // 递归去进行 proxy 绑定
      }
      return Reflect.get(target, key, receiver); // Reflect.get(...arguments);
    },
    set(target, key, value, receiver) {
      console.log("set");
      return Reflect.set(target, key, value, receiver); // Reflect.set(...arguments);
    }
  })
}

明白了 reactive() 简单的实现,回到标题,如果我们进行解构,会发生什么?

const obj = {
  a: {
    count: 1
  },
  b: 1
}
const proxy = reactive(obj);
const { a, b } = proxy;
console.log('---')
console.log(a);
console.log(b);
console.log(a.count);
// get
// get
// ---
// ---console.log(a);
// Proxy(Object) {count: 1}
// 1
// ---console.log(a.count);
// get
// 1

可以发现解构之后,console.log(b); 获取不到 obj.ba 以及 a.count 均可以正常访问。这是为什么?

需要先学习一下解构赋值原理。解构赋值区分:

  • 原始类型的赋值:按值传递
  • 引用类型的赋值:按引用传递
const a = {
  b: 1
}
const c = a.b;

上述代码,访问 c 相当于直接访问这个值,绕过了 a 的 proxy 也就失去了响应性连接。

const a={
  b: {
    d: 3
  }
}
const c = a.b;

访问 c 相当于直接访问 b 的代理对象,因此并没有失去响应性连接。

因此,Vue3 推荐索性不用解构(毕竟有时候会出现响应性连接丢失,开发者埋怨怎么 Vue 有问题,其实不然)。

3. props.x

同样的,props 不推荐解构。

如果想要解构,可以使用 toRefsprops 解构为响应式的引用,以确保在解构后的属性上保持响应式绑定。

toRefs 函数将 props 对象中的每个属性都转换为一个独立的响应式引用,并返回一个新的对象,该对象包含了这些响应式引用。

以下是一个示例,演示了如何在 Vue 3 中使用 toRefs 来解构 props 并保持响应式绑定:

<template>
  <div>
    <p>{{ name }}</p>
    <p>{{ age }}</p>
  </div>
</template>
<script setup props="props">
import { toRefs } from 'vue';
const { name, age } = toRefs(props);
</script>

需要注意的是,toRefs 只会在组件初始化时执行一次,所以解构后的属性仅在组件初始化时保持响应式绑定。什么意思?即解构后得到的属性,如果在组件的生命周期内重新分配或修改,这些属性将不再保持与 props 对象的同步更新。

下面是 toRefs 函数的简单实现:

import { ref, isRef } from 'vue';
export function toRefs(obj) {
  const ret = {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const value = obj[key];
      ret[key] = isRef(value) ? value : ref(value);
    }
  }
  return ret;
}

上述代码首先遍历传入的响应式对象 obj 的每个属性,然后判断属性是否是 ref 对象,如果是,直接赋值,否则用 ref 封装原始值。最终返回一个普通对象 ret,该对象的每个属性都是原始响应式对象中属性的引用。

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值