以响应式著称的Vue,为什么会提供toRaw和markRaw

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

前言

众所周知,Vue作为典型得MVVM框架,是以响应式为基础,数据驱动视图。让我们开发起来更加方便,更加的得心应手。

何为MVVM:MVVM是Model-View-ViewModel的简写。MVVM模式有助于将应用程序的业务和表示逻辑与用户界面 (UI) 清晰分离。保持应用程序逻辑和UI之间的清晰分离有助于解决许多开发问题,并使应用程序更易于测试、维护和演变。它还可以显著提高代码重用机会,并允许开发人员和UI设计人员在开发应用各自的部分时更轻松地进行协作。

在Vue3中,响应式数据是通过Proxy进行数据拦截和发布订阅模式实现的。当数据发生变化时,Vue会自动重新渲染相关的视图,并更新DOM。这种机制使得Vue应用程序的开发变得更加简单和高效,因为它允许开发人员直接关注业务逻辑而不是手动更新视图。

toRow是将一个Proxy代理对象转换成普通对象,markRow则是将一个普通对象标记为不可Proxy代理的对象。那么,以响应式为中心的vue为什么会提供这两个api呢?

toRow

本应该使用reactivereadonly,shallowReactive,shallowReadonly代理的对象,使用toRowapi方法后,将会返回一个新的原始对象。也就是返回的新对象不再有响应式,改变这个原始对象也不再会影响到页面的显示。

const form = reactive({
  name: "iceCode",
});
const toRawForm = toRaw(form);
console.log(form, toRawForm);

343fc8a06ebe1dc10142f5443820155e.jpeg 可以看出正常使用reactive创建出的对象是由Proxy代理的对象,而toRaw得到的则是一个原始对象。

<template>
//使用v-model分别绑定两个值
  <ElInput v-model="form.name" />
  <ElInput v-model="rawForm.name" />
</template>

<script setup lang="ts">
//这里两个对象都是默认值的
const form = reactive({
  name: "iceCode",
});
const rawForm = toRaw(form);
watchEffect(() => {
//尝试监听原始对象的改变
  console.log(rawForm);
});
//并且监听一下键盘事件
document.addEventListener("keyup", (e) => {
  console.log(e.key);
});
</script>
9bbb9e1de973d9c611babe6040418c60.jpeg
视频转Gif_爱给网_aigei_com (1).gif

这里看到当代理对象改变的时候,原始值也改变,并且不会触发vue的监听事件,是因为Proxy的特性,无操作转发代理,改变代理对象时,会自动的转给原始对象。无法监听原始值的改变是是因为 Vue 的响应式系统通过代理这些对象来工作,当这些对象改变时,Vue 可以捕获到这些改变并相应地更新视图。对于原始对象,Vue 不会进行这种代理,因此,watchEffect 无法监听到原始对象的改变。
反之原始值的改变也会改变代理代理对象,但是代理对象的值对然被改变的,页面中绑定的值却不会改变。由于vue3中没有类似于vue2中的forceUpdate方法,也无法强制刷新页面,使页面dom更新

const form = reactive({
  name: "iceCode",
  age: 1,
});
const rawForm = toRaw(form);
//这里改变原始对象的值,然后打印代理对象的值
const log = () => {
  rawForm.name = "iceCodeeeeeee";
  console.log(form);
};

0255141636c16c5877191fbaa96e6c3d.jpeg 另外,需要知道的是,虽然原始对象和代理对象不是一个引用值(两个对象不相等),但是他们的原型一直都是相等的,有着同一个原型

const form = reactive({
  name: "iceCode",
  age: 1,
});
const rawForm = toRaw(form);
form.__proto__ = { phone: "138444" };
const reactiveForm = reactive(rawForm);
console.log(form.__proto__ === rawForm.__proto__);//true
console.log(reactiveForm.__proto__ === rawForm.__proto__);//true
console.log(form.__proto__ === reactiveForm.__proto__);//true
ac1769118fe2c5dfc97ad2990a94250c.jpeg
image.png

structuredClone

代理对象和原始对象说的有点多,来看toRaw在什么时候能够用到,我接触过的场景是需要深拷贝一个form对象时,使用到了structuredClone方法。
structuredClone:全局的 structuredClone()  方法使用结构化克隆算法[1]将给定的值进行深拷贝[2]。接收两个参数,第一个是被克隆(深拷贝)的对象。可以是任何结构化克隆支持的类型[3]。第二个是一个可转移对象[4]的数组,里面的  并没有被克隆,而是被转移到被拷贝对象上(一般不会被用到),返回一个深拷贝的原始值。

//普通js文件中

const form = {
  name: "iceCode",
  age: 24,
  phone: "13800138000",
  behavior: ["sleep", "eat"],
};
form.__proto__ = {
  sex: "male",
};
const cloneForm = structuredClone(form);
cloneForm.age = 18;
cloneForm.behavior.push("work");
//改变深拷贝的值,完全不会影响原始值
console.log(form, cloneForm);
//{
//  name: 'iceCode',
//  age: 24,
//  phone: '13800138000',
//  behavior: [ 'sleep', 'eat' ]
//} {
//  name: 'iceCode',
//  age: 18,
//  phone: '13800138000',
//  behavior: [ 'sleep', 'eat', 'work' ]
//}
console.log(form === cloneForm);//false
console.log(form.__proto__ === cloneForm.__proto__);//false

注意:structuredClone方法无法拷贝原型链,也就是为什么他们的原型不相等。无法拷贝函数(也包括我们熟知的一些构造函数,如Promise等) 、无法拷贝不可遍历的字段,如Symbol或者被标记为readonly的字段、正则`RegExp`[5]的lastIndext字段不会被保留,错误 Error 类型,仅支持Error[6]、EvalError[7]、RangeError[8]、ReferenceError[9]、SyntaxError[10]、TypeError[11]、URIError[12](或其他会被设置为 Error 的)。
并且关键的structuredClone无法拷贝Proxy代理的对象,因为Proxy对象是一种特殊类型的对象,它由一个target对象和一个handler对象组成。handler对象定义了Proxy对象的特殊行为,比如get、set、apply等方法。这些方法是在运行时动态调用的,而structuredClone算法在拷贝对象时无法模拟这些动态行为。

说到这应该都知道vue3中的响应数据都是由Proxy代理的对象,如果想使用structuredClone方法对一个响应式的form对象进行拷贝是无法拷贝的,并且会报错。

vite和webpack的区别

这里简单的说一下vitewebpack的区别,为什么会说到vitewebpack呢。因为两个在运行时的时候有所不同,导致使用structuredClone方法拷贝一个Proxy代理对象最后得到的结果也有所不同。

Webpack 是一种前端资源构建工具,也是一个静态模块打包器,是目前前端领域里范围最广,最为常用的打包工具。Webpack的社区极其获取,它的扩展性也非常好,几乎可以实现任何你想要的功能。在Webpack 看来,前端的所有资源文件都会作为模块处理。它会根据模块的依赖关系进行静态分析,打包生成对应的静态资源。使得工程中的各种资源能够被打包成一个整体的bundle.js文件。Webpack的热更新是全量更新,每次修改代码后都需要全局编译。也就是我们页面中所看到的代码都是Webpack进行处理过的的代码。

vite是一个前端构建工具,它能够提供丰富的功能,如快速冷启动、即时热更新和真正的按需编译等,并且vite的社区也比活跃,生态方面也正逐渐追赶Webpackvite不是Webpack的替代品,两者的方向不同,只是通常会相互作比较而已。vite是一种面向现代浏览器的更轻、更快的 Web 应用开发工具。它基于 ECMAScript 标准的原生模块系统(ES Modules)实现,当开发者修改代码后,Vite会即时在浏览器中编译和打包代码,然后将更改的部分直接传递给浏览器,并重新加载。

这里,在以Webpack为基础的项目中,使用structuredClone拷贝一个Proxy代理对象,会经过Webpack的处理,然后再显示到页面,这种处理后的代码并不会使这个本来应该错误的写法报错,但是在以vite为基础的项目中,会以原生js正确的方式进行处理,并且会返回一段错误。

//在vite和webpack的vue3项目中分别写入这段代码,看一下最后结果
const forms = reactive( {
  name:'iceCode'
} )
const cloneForms = structuredClone( forms )
console.log('cloneForms',cloneForms);

webpack项目中:这里显示的直接就是一个原始对象,并且正常运行。

dfed70a719a6f2538811d7e022226bce.jpeg
vite项目中:这里报错直接结束运行,代码无法在向下执行。

c660243e12a1397a39bf7e86659db2a3.jpeg
image.png

最后好在vue提供了toRaw这个方法,在这个种极个别的情况下使用,避免一些无法处理的问题。当然toRaw这个方法不仅仅用在这里,还有很多情况下也可以用到,这里只是说明了我在项目中使用toRaw所解决的问题。

  1. 对象比较:当需要对两个对象进行比较时,使用toRaw方法可以将对象转换为原始对象,然后进行比较。这样可以避免由于Vue的响应式系统对对象属性的更改而导致的比较结果不准确的问题。

  2. 获取真实值:在Vue的响应式系统中,对象属性的值是观察者所追踪的值,而不是实际的原始值。如果你需要获取对象的真实值,可以使用toRaw方法来获取原始对象。

  3. 调试和测试:在开发过程中,有时候需要查看Vue组件或实例的内部状态。使用toRaw方法可以将组件或实例转换回原始对象,从而可以查看其属性、方法和数据。这在调试和测试过程中非常有用。

markRaw

这个api的相关使用就比较少了,markRaw是将一个对象标记为不可代理的对象,它无法标记已经被代理的对象,如果标记一个已经被代理的对象是无法生效为一个不可代理的对象的,返回对象本身。

<template>
//双向绑定数据,如果是代理对象就已经可以动态输入
  <ElInput v-model="markF.name" />
  <ElInput v-model="markObj.name" />
  <ElButton @click="log">改变</ElButton>
</template>

<script setup lang="ts">
//定义两个代理对象,查看却别
const f = reactive({ name: "iceCode" });
const fs = reactive({ name: "iceCode" });
//标记一个代理对象为不可代理的对象
const markF = markRaw(f);
//标记一个普通对象为一个不可代理的对象
const markObj = markRaw({ name: "iceCode" });
const rec = reactive(markObj);
console.log(markF, markObj, fs);
const log = () => {
  console.log(isReactive(markF), isReactive(rec));
};
</script>

9b7c34a5479fd6c26345664d85d85381.jpeg 可以看出当被标记不可代理的对象时,仍是Proxy的代理对象,只不过是多了一个_v_skip的属性作为标识,当时用isReactive判断这个对象是否是有reactive创建的代理时,一开始是被代理的对象识别的为true,被标记为不可代理的普通对象,即便之后再用reactive代理,依旧时false

既然不能被代理,也就意味着永远不能是响应式的,既然在vue中不是响应式的那有什么用呢。可能正常项目中我们用不到,但是在引入第三方插件的时候,也许并不希望第三方插件变成一个响应式对象。那么可以将引入的第三方插件对象标记为一个不可代理的对象,这样避免在使用时不小心将第三方插件标记为一个响应对象,从而影响性能。

比如以下是pinia的一个插件示例:
当添加外部属性、来自其他库的类实例或仅仅是非响应式的东西时,您应该在将对象传递给 pinia 之前使用 markRaw() 包装对象。这是一个将路由添加到每个 store 的示例:

import { markRaw } from 'vue'
// 根据您的路由所在的位置进行调整
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

最后

大多情况下,作为普通的开发者,对于这两个api也许很难接触到,但也并不是完全接触不到。特别是toRaw,在极个别情况下,请求的参数,是一个Proxy的代理对象时是会报错的,在请求前将Proxy对象转成一个普通对象再发送请求,就可以解决这个问题。

另外在写这篇内容的时候我发现一个问题,可能是Vue的bug

本来那个普通对象被标记为不可代理的对象之后,绑定v-mode之后,输入任何内容都不应该会改变页面中的内容的,但是,这个只是被标记为不可代理对象,再聚焦不同的输入框时,却改变了内容。有没有大佬可以可以解读一下这是为什么。可能是个vue的Bug吧

作者:iceCode

链接:https://juejin.cn/post/7292966352868229183

结语

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值