Code前端首页关于Code前端联系我们

Vue3中的toRaw到底有什么用?什么时候必须用?

terry 1小时前 阅读数 25 #Vue

日常开发Vue3项目时,我们总离不开响应式数据——不管是ref、reactive还是computed生成的内容,都能自动触发视图更新,但偶尔也会遇到响应式数据“不听话”的情况:比如直接存本地存储存成Proxy、传给第三方库报错、或者想直接拿到原始对象做些不想要触发更新的操作,这时候,Vue3官方API里的toRaw就该出场了。

什么是Vue3的toRaw?

toRaw是Vue3响应式系统里的“还原器”,能把通过reactive、shallowReactive、readonly、shallowReadonly生成的Proxy对象,变回最开始定义的那个普通JavaScript原始对象,ref的话,它会直接返回内部.value对应的原始值(注意:ref本身不是Proxy,但.value如果是用上面的Proxy生成的,用toRaw(ref.value)才是还原原始对象)。

举个小例子更清楚: 假设我们先写一个普通的user对象,再用reactive把它变成响应式的,这时候打印响应式对象,会看到它挂着Proxy的标识;用toRaw处理之后,就又变回了最初的user,连内存地址都没变——这点很重要,说明toRaw没有复制新对象,只是“剥掉了”Vue套在外面的响应式代理层。

如果是shallowReactive的话,toRaw只会剥掉最外层的Proxy,嵌套对象如果也是Proxy的话,处理后还是保留原来的状态,这点和shallowReactive的浅层响应式特性一致,readonly生成的Proxy用toRaw处理,结果就是原始对象,不再有只读限制,操作原始对象的话,原readonly对象的内容也会跟着变(毕竟是同一个内存地址),不过这时候操作不会触发任何视图更新,也不会被readonly的规则拦截。

toRaw的核心使用场景有哪些?

知道了它是还原器,接下来就得搞明白什么时候真的需要用它,乱用toRaw可能会破坏响应式系统,比如把还原后的原始对象又赋值给响应式属性,反而会导致属性失去响应式,所以下面这几个场景是真的绕不开toRaw的:

场景1:把响应式数据存进localStorage/sessionStorage

这应该是最常见的场景了,很多新手会直接把reactive的对象存进localStorage,结果存进去的不是想要的JSON,而是"[object Proxy]"这种没用的字符串,因为Proxy对象不能被JSON.stringify直接序列化,浏览器没法识别它的内部内容。

这时候只要先用toRaw拿到原始对象,再序列化就行,比如刚才的user例子:

import { reactive, toRaw } from 'vue'
const user = reactive({
  name: '张三',
  age: 25
})
// 错误做法
localStorage.setItem('user', JSON.stringify(user)) // 存进去的是"[object Object]"?不对,得看浏览器,有的严格点会报错,有的会把Proxy当成普通对象但属性不一定全
// 哦不对,很多现代浏览器JSON.stringify会把自己内部有Symbol.iterator或者toJSON方法的对象处理,但Vue的默认Proxy没有暴露toJSON,所以大部分情况下存的内容要么不对,要么少部分深层嵌套可能有问题,最稳妥的还是用toRaw
// 正确做法
localStorage.setItem('user', JSON.stringify(toRaw(user)))

不过要注意,还原后的原始对象如果有循环引用,JSON.stringify还是会报错,这时候得用专门处理循环引用的库,或者手动先处理掉循环引用的部分。

场景2:调用不兼容Proxy的第三方库

现在很多新的第三方库已经适配了Vue3的Proxy响应式数据,但还是有不少旧库(比如一些基于jQuery或者ES5时期开发的图表库、状态管理辅助库),内部会直接访问对象的原型、或者用Object.defineProperty重新定义属性、或者对普通对象和Proxy对象做了严格区分,这时候传Proxy进去就会报错或者功能异常。

比如举个常见的例子:ECharts早期的某些版本,如果你直接把reactive的series数组传进去,更新数据时可能不会触发图表重绘,或者干脆渲染不出来,这时候可以用toRaw把series数组或者整个配置对象还原成原始数据,再传给ECharts的setOption方法。

用一些纯JavaScript的工具函数时,比如lodash的cloneDeep,如果传Proxy进去,它可能会把Proxy层也复制下来,生成一个新的Proxy对象,但这个新对象可能没有Vue的响应式系统绑定,反而不好用,这时候如果只想要一个纯普通的深拷贝对象,就可以先用toRaw拿到原始数据,再cloneDeep。

场景3:做一些不想要触发视图更新的操作

有时候我们需要修改数据,但不想让页面跟着变,

  1. 调试时临时打印或修改一些中间状态,不想影响用户看到的界面;
  2. 批量修改多个响应式属性,但想等所有修改完之后再统一触发更新(不过批量修改的话,Vue3其实已经做了优化,大部分情况不需要手动控制,除非是性能极其敏感的场景,这时候可能还要配合nextTick或者flush: 'post'的computed/watch,但toRaw也是一个临时的备选方案);
  3. 把响应式数据当成临时变量存储一些业务无关的元数据,这些元数据不需要展示在页面上。

比如调试时:

const user = reactive({ name: '张三' })
const rawUser = toRaw(user)
rawUser.name = '李四' // 此时原始对象变了,但视图不会更新,控制台打印user的话,name还是张三?不对不对,这里要注意!rawUser和user指向的是同一个内存地址的对象!如果修改rawUser的属性,user的属性值其实已经变了,但是因为是通过原始对象修改的,没有经过Vue的Proxy拦截,所以Vue的响应式系统不知道数据变了,自然不会触发视图更新!这点一定要记住,别用toRaw修改数据,不然页面状态和数据状态不一致,会出大bug!

所以这个场景一定要慎用,最好只用来读取原始数据的状态,或者把原始对象复制一份之后再修改。

场景4:配合markRaw一起使用

Vue3还有一个API叫markRaw,用来标记某个对象永远不会被Vue变成响应式的,如果我们有一个已经被markRaw标记过的对象,后来不小心把它放到了reactive或者ref的value里,这时候它会不会变成响应式的?答案是不会,因为markRaw给它加了一个内部的Symbol标记,Vue的响应式系统会识别这个标记,跳过代理。

但有时候我们可能不确定某个对象有没有被代理过,或者有没有被标记过markRaw,这时候可以用toRaw来验证,比如如果一个对象用toRaw处理之后还是自己本身,那要么它本来就是普通对象,要么它被markRaw标记过了。

用toRaw时需要注意哪些坑?

前面已经提到过一些,这里再总结一下,避免踩坑:

  1. 修改原始对象会导致数据和视图不同步:因为toRaw返回的是同一个内存地址的对象,通过它修改属性,不会触发Vue的响应式更新,页面上展示的还是旧数据,这很容易造成业务逻辑错误,除非你真的知道自己在做什么,否则不要用toRaw返回的原始对象做修改操作。
  2. 对ref的处理:直接对ref调用toRaw,返回的还是ref本身!因为ref不是Proxy对象,只是一个带有value属性的普通对象(或者在某些情况下是一个包装过的对象,但不是Proxy),要拿到ref内部value对应的原始对象,必须用toRaw(ref.value)。
  3. 对computed的处理:computed返回的也是一个ref或者readonly ref,它的value可能是响应式的,也可能是普通的,对computed本身调用toRaw没用,对computed.value调用toRaw,如果value是Proxy生成的,就会返回原始值,否则返回value本身,不过computed一般是用来计算展示数据的,很少需要用toRaw处理它的value。
  4. 不要过度依赖toRaw:Vue3的响应式系统设计得已经很完善了,大部分情况下不需要用到toRaw,只有在上面提到的几个明确的场景下才用,不然会破坏代码的响应式逻辑,增加维护成本。

有没有替代toRaw的方法?

其实在某些场景下,有其他方法可以达到类似的效果,

  1. 存本地存储时:除了用toRaw,还可以给响应式对象加一个toJSON方法,JSON.stringify会优先调用这个方法,
    const user = reactive({
    name: '张三',
    age: 25,
    toJSON() {
     return {
       name: this.name,
       age: this.age
     }
    }
    })
    localStorage.setItem('user', JSON.stringify(user))

    不过这种方法需要手动维护toJSON的内容,每次给响应式对象加属性,都要记得更新toJSON,不如toRaw方便。

  2. 临时存储元数据时:除了用toRaw拿到原始对象存,还可以用Vue3的provide/inject或者专门的状态管理库(比如Pinia)的非响应式部分存,这样更规范。
  3. 调用第三方库时:如果第三方库支持响应式数据,尽量用响应式的,不然更新数据时还要手动调用toRaw再传给第三方库,再手动触发第三方库的更新方法,比较麻烦。

toRaw是Vue3响应式系统里的一个“逃生舱”API,平时不用开,但遇到特殊情况时,没有它还真不行,只要掌握了它的用法和注意事项,就能在开发中少走很多弯路。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门