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

Vue3里markRaw到底是干嘛的?怎么用才不会踩坑?

terry 4天前 阅读数 987 #Vue

很多刚从Vue2转过来或者初学Vue3的朋友,第一次碰到markRaw这个API时,都会有点懵:平时都是想方设法让数据响应式,怎么还有个专门“掐断”响应式的东西?这玩意儿有啥用?会不会用错了反而把项目搞崩?今天咱们就把这些问题掰扯清楚,从原理到场景再到常见坑,一次性讲明白。

先搞懂markRaw的核心逻辑

要讲markRaw,得先简单提下Vue3的响应式系统基础,Vue3用Proxy替代了Vue2的Object.defineProperty,默认情况下,不管你用ref还是reactive包数据,哪怕是这个数据里嵌套的对象、数组、甚至是Map、Set,Vue都会自动给它们包上一层响应式Proxy,这样数据变化时视图才会跟着更新。

那markRaw就是专门“破局”的:它能把一个普通的对象、数组或者其他引用类型数据,标记成永远不会被Vue3响应式系统转换的“原生数据”,不管你之后怎么把它塞到ref里、reactive里,甚至塞到另一个已经响应式的对象的属性里,它都会保持原样,不会触发视图更新,也不会有Proxy那层包裹带来的性能小开销。

举个最直观的小例子你就懂了: 假设我们有个reactive对象user,里面有个config属性,值是某个第三方库(比如ECharts)的实例对象,如果不用markRaw,Vue会尝试把ECharts实例也包成Proxy——但第三方库的实例内部通常有自己的状态管理逻辑,包了Proxy之后,可能会出现监听事件失效、图表渲染异常、内存泄漏这些问题,这时候给config的赋值加个markRaw,问题就解决了。

markRaw到底有哪些实打实的使用场景?

别觉得markRaw是个“边缘API”,其实在实际项目里,它的出场率还挺高的,我整理了几个最常用、最实用的场景:

场景1:引入第三方库的实例

刚才提到的ECharts、Three.js、D3.js这类可视化或数据处理库的实例,是markRaw的“黄金搭档”,这类库的实例内部结构非常复杂,有大量的私有属性、原型方法,甚至内部会直接修改自己的原型链或者绑定DOM事件,如果被Vue3包成Proxy,一方面会让Vue3的响应式系统做很多无用的递归监听(性能浪费),另一方面很可能会破坏第三方库内部的逻辑,导致各种玄学bug。

比如你用ECharts画个折线图,先初始化一个容器,然后生成实例,这时候把实例存到组件的响应式数据里(方便后续调用resize、setOption这些方法),一定要记得加markRaw:

import { onMounted, reactive, markRaw } from 'vue'
import * as echarts from 'echarts'
const state = reactive({
  chart: null
})
onMounted(() => {
  const container = document.getElementById('chart-container')
  // 这里必须加markRaw
  state.chart = markRaw(echarts.init(container))
  state.chart.setOption({/* 你的配置 */})
})

如果不加markRaw,可能你的图表第一次能渲染出来,但缩放窗口时调用resize没反应,或者后续改数据调用setOption视图不更新——别怀疑,大概率是Proxy搞的鬼。

场景2:存储静态的、不需要响应式的大对象/数组

有些数据是一次性从后端拿过来,或者前端硬编码的,比如国家地区的三级联动数据、某个复杂的配置字典(几千甚至上万条那种),这些数据在整个组件生命周期里都不会变,或者即使变了也不需要视图跟着更新,如果直接塞到reactive里,Vue3会递归遍历每一个键值对、每一个数组元素,给它们全包上Proxy,这会占用大量的内存,还会拖慢组件的初始化速度。

比如三级联动的省市区数据,假设是个3层嵌套、总共有十几万条小对象的数组:

// 硬编码的大数组,或者import进来的
import areaData from './area-data.json'
import { reactive, markRaw } from 'vue'
const state = reactive({
  // 不加markRaw的话,初始化可能会有肉眼可见的卡顿
  areas: markRaw(areaData)
})

这时候用markRaw就能完美解决性能问题,而且因为数据本身不需要响应式,不会影响任何功能。

场景3:避免响应式循环依赖导致的内存泄漏

有些特殊的业务场景里,可能会出现两个对象互相引用的情况,比如你有个commentList数组,里面每个comment对象都有一个user属性指向对应的用户,而每个user对象又有一个comments属性指向自己发过的所有评论,如果这两个对象都被包成了响应式Proxy,Vue3的响应式系统在监听的时候,可能会因为循环依赖而无法正常释放内存,导致内存泄漏。

这时候可以根据业务需求,把其中一个引用标记成markRaw,比如如果user的信息在评论列表里只是用来展示头像和昵称,不会动态变化,那可以把每个comment里的user对象加个markRaw:

import { reactive, markRaw, onMounted } from 'vue'
import { getCommentList } from './api'
const state = reactive({
  comments: []
})
onMounted(async () => {
  const res = await getCommentList()
  // 处理数据时,把user标记为原生
  state.comments = res.data.map(comment => ({
    ...comment,
    user: markRaw(comment.user)
  }))
})

这样就切断了响应式的循环依赖,避免了内存泄漏的风险。

场景4:优化组件内部的临时状态

有时候组件里会有一些临时的计算变量,比如分页加载时的“当前请求页码+1的临时变量”,或者处理表单验证时的“临时错误信息容器(处理完就清空,不需要视图展示)”,这些数据完全不需要响应式,直接用普通变量存着就行,但如果有人不小心把它们塞到了reactive里,那加个markRaw也能快速“止损”——当然最好的做法还是直接不用响应式API存这类数据。

markRaw的使用限制和常见踩坑点

markRaw虽然好用,但也不是随便乱用的,有些限制和坑一定要注意,不然反而会给自己找麻烦。

限制1:只能对引用类型数据生效

markRaw只能用来标记对象、数组、Map、Set、函数这些引用类型的数据,对字符串、数字、布尔值、undefined、null这些基本类型(原始值)是完全没用的,因为Vue3的响应式系统本身就不会给基本类型包Proxy(ref里的基本类型是被包裹在一个有value属性的对象里的,响应式的是那个外层对象,不是基本类型本身)。

比如你写markRaw(123),然后把它赋给ref(markRaw(123))ref.value还是会响应式变化——因为ref的响应式和markRaw标记的123没关系,响应式的是ref创建的那个包装对象。

限制2:只能标记“当前对象”,不能递归标记内部属性

这是很多人容易踩的一个大坑!markRaw只能让它直接标记的那个对象本身不被转换为响应式,它内部嵌套的对象、数组还是会被Vue3的响应式系统自动处理的

举个踩坑的例子:

import { reactive, markRaw } from 'vue'
const rawUser = markRaw({
  name: '张三',
  address: {
    city: '北京',
    district: '朝阳区'
  }
})
const state = reactive({
  user: rawUser
})
// 这个不会触发视图更新,因为user本身是markRaw的
state.user.name = '李四'
// 这个会触发视图更新!因为address是嵌套在user里的,没被markRaw标记
state.user.address.district = '海淀区'

如果你想让整个嵌套结构都不被响应式转换,那得用Vue3的另一个API——shallowReactive?不对不对,shallowReactive是只让外层对象响应式,内部嵌套的不响应式,哦对了,如果要整个嵌套结构“原生到底”,可以递归遍历这个对象,给每一层嵌套的引用类型都加个markRaw,或者直接用原生的JSON.parse(JSON.stringify())先深拷贝一份(但要注意深拷贝的局限性,比如不能拷贝函数、Symbol、循环依赖的对象)。

坑1:误以为加了markRaw的对象就完全“不可修改”

很多人会把markRaw和“不可变数据”搞混,markRaw只是不让对象被Vue3响应式系统处理,它本身还是个普通的、可修改的对象!修改它的属性完全没问题,只是修改之后不会触发视图更新而已。

刚才的踩坑例子里,state.user.name = '李四'其实已经成功修改了rawUser的name属性,只是视图没变化,如果你之后用console.log打印rawUser,会发现name已经变成李四了。

坑2:用markRaw处理了需要响应式的数据

这是最严重的一个坑!比如你有个用户的昵称是可以修改的,修改之后需要视图跟着更新,但你不小心给用户对象加了markRaw,那不管你怎么改昵称,视图都不会动,这时候排查问题会比较麻烦,因为数据本身是变了的,就是视图不更新。

所以使用markRaw之前,一定要先问自己三个问题:这个数据会变吗?变了之后需要视图更新吗?如果不变或者不需要更新,再用markRaw。

坑3:把markRaw用在provide/inject里

这个也是个容易忽略的坑,假设你在父组件里provide了一个用markRaw标记的对象,子组件inject之后,如果想修改这个对象的属性并且让父组件或者其他兄弟组件的视图更新,那肯定是不行的,而且provide/inject本身如果要传递响应式数据,父组件应该传递ref或者reactive的对象,子组件直接用就行——除非你传递的就是静态的、不需要共享响应的配置。

markRaw和shallowReactive、shallowRef的区别

很多人会把这几个API搞混,这里简单对比一下,帮你理清:

  1. markRaw:标记单个引用类型数据,让它“永远不被响应式系统转换”,不管怎么嵌套到其他响应式数据里都没用;不改变数据的可修改性,只是不触发视图更新;不能处理基本类型。
  2. shallowReactive:创建一个“浅层响应式”的对象,只有外层的属性是响应式的,内部嵌套的引用类型属性是原生的;只作用于它直接创建的那个对象;可以修改,外层属性变化会触发视图更新,内部嵌套的不会。
  3. shallowRef:创建一个“浅层响应式”的ref,只有当你直接修改ref.value(比如把整个对象替换掉)时才会触发视图更新,修改ref.value内部的属性不会;可以处理基本类型(但处理基本类型时和普通ref没区别,因为基本类型只能整体替换)。

举个简单的对比例子:

import { markRaw, shallowReactive, shallowRef, reactive } from 'vue'
// 1. markRaw的例子
const rawObj = markRaw({ a: 1, b: { c: 2 } })
const state1 = reactive({ obj: rawObj })
state1.obj.a = 11 // 视图不更新,obj.a变了
state1.obj.b.c = 22 // 视图会更新!因为b没被标记
// 2. shallowReactive的例子
const shallowObj = shallowReactive({ a: 1, b: { c: 2 } })
shallowObj.a = 11 // 视图会更新
shallowObj.b.c = 22 // 视图不更新,b.c变了
// 3. shallowRef的例子
const shallowRefObj = shallowRef({ a: 1, b: { c: 2 } })
shallowRefObj.value.a = 11 // 视图不更新,a变了
shallowRefObj.value = { a: 11, b: { c: 22 } } // 视图会更新

什么时候用markRaw,什么时候用shallow系列API?

最后再总结一下,帮你快速判断用哪个:

  • 如果是第三方库实例静态大对象/数组需要切断循环依赖的引用,优先用markRaw。
  • 如果是只需要外层响应式、内部不需要的配置对象(比如用户的主题配置,外层的“主题模式”是响应式的,内部的“颜色值、字体大小”虽然会变但不需要立刻触发所有用颜色的地方更新,等主题模式切换的时候统一处理),可以用shallowReactive。
  • 如果是只需要整体替换的大列表/大对象(比如分页加载的列表,每次翻页都是直接替换整个数组,不需要修改数组内部的元素),可以用shallowRef。

markRaw是Vue3响应式系统里的一个“补位API”,它不是让你用来破坏响应式的,而是让你在不需要响应式的地方,把性能和稳定性还给项目,只要用对了场景,它能帮你解决很多头疼的问题,还能提升项目的性能。

版权声明

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

上一篇:使用npm创建项目 下一篇:npm
热门