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

reactive到底是干啥的?

terry 2周前 (10-02) 阅读数 40 #Vue
文章标签 reactive;响应式

p>刚开始学Vue3的时候,很多人都会被reactive搞晕——明明用它把数据变成响应式了,怎么有时候修改没效果?和ref一起用的时候又该咋选?甚至想不通它底层到底咋实现响应式的?今天就把reactive从基础用法、踩坑技巧,到原理和实战场景,一次性唠明白,看完你再用reactive绝对心里有底~


简单说,reactive是Vue3里让对象/数组变成“响应式”的核心工具,啥叫响应式?就是数据变了,页面自动跟着变,举个例子:普通对象修改后,Vue根本不知道数据变了,页面也不会更新;但用reactive包一下,修改对象里的属性,页面立马跟着刷新。

比如这样写:

import { reactive } from 'vue'  
const state = reactive({ count: 0 })  
function increment() {  
  state.count++ // 这里修改后,页面会自动更新  
}  

那它和Vue2的响应式有啥区别?Vue2用的是Object.defineProperty,只能监听对象已有的属性,新增、删除属性或者数组的push/pop这些操作监听不到,得用$set之类的方法,但Vue3的reactive基于Proxy,能直接监听对象新增属性、删除属性,甚至数组的变化(比如state.list.push(1)),不用额外操作,这也是Vue3响应式系统的大升级~

怎么正确用reactive?这些坑一定要躲!

先用对基础用法:导入reactive,把对象/数组传进去,得到的就是响应式对象,但实际开发里,这几个坑很容易踩:

直接替换整个对象,响应式就丢了

比如想把state换成新对象:

const state = reactive({ count: 0 })  
state = { count: 1 } // 坏例子!这样state不再是原来的Proxy对象,响应式直接没了  

正确做法是修改属性,不是替换对象

state.count = 1 // 这样才对,Proxy能监听到属性修改  

基本类型不能用reactive

reactive只认对象/数组,传数字、字符串这些基本类型没用,比如reactive(123),得到的还是普通值,不是响应式,这时候得用ref来处理基本类型~

解构赋值会“弄丢”响应式

如果直接解构reactive对象,解构出来的属性就不是响应式了:

const { count } = state  
count++ // 这里修改count,页面不会更新!因为count已经不是响应式的了  

解决办法是用toRefs,把reactive对象的每个属性转成ref:

import { toRefs } from 'vue'  
const { count } = toRefs(state)  
count.value++ // 这样修改,页面就会更新啦~  

(如果只需要单个属性,也可以用toRefconst count = toRef(state, 'count')

数组操作要注意,但不用像Vue2那样纠结了

Vue2里修改数组要小心翼翼,比如this.list[0] = 1监听不到,但Vue3用reactive后,直接state.list[0] = 1就能触发更新,因为Proxy能拦截数组的索引修改~ 不过如果是替换整个数组,比如state.list = [1,2,3],只要不是直接替换reactive对象本身(比如state是reactive包的,list是它的属性),这样修改是没问题的,因为修改的是list属性,Proxy能监听到~

reactive和ref有啥区别?选哪个更合适?

很多人学的时候,总搞不清这俩啥时候用,先看核心区别:

对比项 reactive ref
处理的数据类型 对象/数组(复杂类型) 基本类型(字符串、数字等)+ 对象
是否需要.value 不需要(直接改属性) 需要(因为ref是个包装对象,值存在.value里)
响应式原理 直接对对象做Proxy代理 内部用reactive包装对象,基本类型单独处理

举个场景例子:

  • 如果你要存一个“用户年龄”(数字),用ref更顺手:const age = ref(18),修改时age.value++
  • 如果你要存“用户信息对象”(包含name、age、address),用reactive更清晰:const user = reactive({ name: '小明', age: 18 }),修改时user.age++

但有时候也会混用:比如reactive对象里嵌套ref,或者ref包裹reactive对象,比如做一个表单,表单整体用reactive,里面某个需要单独控制的字段用ref,不过大部分时候,基础值用ref,复杂对象用reactive」就不会错~

当你在组合式函数(类似Vue2的mixins,但更灵活)里返回响应式数据时,如果返回的是对象,用reactive封装更清晰;如果返回单个值,用ref更方便,比如写个useCounter函数,返回count和increment方法,用ref包count更简单~

reactive的原理是啥?Proxy怎么实现“数据变了页面更新”?

想彻底搞懂reactive,得扒开它的底层逻辑,Vue3的响应式系统,核心是Proxy + 依赖收集

reactive(target)会创建一个Proxy对象,这个Proxy就像个“中间人”,拦截对target对象的读取(get)、修改(set)等操作,举个极简版的Proxy例子:

const target = { count: 0 }  
const proxy = new Proxy(target, {  
  get(target, key) {  
    console.log(`读取了${key}属性`)  
    return target[key]  
  },  
  set(target, key, value) {  
    console.log(`修改了${key}属性,新值是${value}`)  
    target[key] = value  
    return true  
  }  
})  
proxy.count // 触发get,打印“读取了count属性”  
proxy.count = 1 // 触发set,打印“修改了count属性,新值是1”  

Vue里的reactive,就是在这个基础上,加入了依赖收集和触发更新的逻辑:

  • 依赖收集(track):当组件渲染时,会访问reactive对象的属性(触发Proxy的get),这时候Vue会把当前组件的“更新函数”(比如渲染函数)收集起来,存在这个属性对应的“依赖集合”里。
  • 触发更新(trigger):当修改属性时(触发Proxy的set),Vue会找到这个属性对应的所有依赖,执行它们(比如重新渲染组件)。

这样就实现了“数据变,页面变”的响应式,对比Vue2的Object.defineProperty,Proxy能监听的操作更多(比如对象新增属性、删除属性、数组索引修改),而且不需要像Vue2那样递归遍历对象的所有属性来做拦截,性能和灵活性都更强~

不过要注意,Proxy只能代理对象/数组,所以reactive只处理复杂类型,基本类型得交给ref~

实际项目里,reactive要怎么和其他API配合?

光懂原理还不够,得知道在项目里咋和computed、watch、生命周期这些配合着用~

和computed一起用

computed里访问reactive的属性,会自动跟踪依赖,比如根据用户年龄计算是否成年:

const user = reactive({ age: 18 })  
const isAdult = computed(() => user.age >= 18)  

当user.age变化时,isAdult会自动更新,因为computed内部会跟踪user.age的依赖~

和watch一起用

watch可以监听reactive对象的单个属性或整个对象:

  • 监听单个属性(自动深度监听?不,得手动开):
    watch(  
    () => user.age,  
    (newVal, oldVal) => {  
      console.log('年龄变了', newVal, oldVal)  
    }  
    )  
  • 监听整个reactive对象(需要开深度监听,因为默认只监听引用变化):
    watch(  
    () => user,  
    (newVal, oldVal) => {  
      console.log('用户信息变了', newVal, oldVal)  
    },  
    { deep: true } // 开深度监听  
    )  

    不过实际开发中,更推荐监听具体属性,性能更好~

和生命周期钩子配合

比如在onMounted里初始化数据,或者在onUnmounted里清理定时器,用reactive存数据的话,直接修改属性就行:

const state = reactive({ list: [] })  
onMounted(() => {  
  fetchData().then(data => {  
    state.list = data // 修改reactive对象的属性,触发更新  
  })  
})  

组件间传递reactive数据

如果用provide/inject传递reactive对象,子组件修改后,父组件也会同步更新(因为是同一个Proxy对象),但要注意,如果子组件直接替换整个对象,父组件的响应式就丢了,所以尽量在子组件里修改属性,不是替换对象~

举个实战例子:做一个todo列表

const todos = reactive([{ text: '学习Vue', done: false }])  
function addTodo() {  
  todos.push({ text: '写文章', done: false }) // reactive的数组push会触发更新  
}  
function toggleDone(index) {  
  todos[index].done = !todos[index].done // 修改数组元素的属性,触发更新  
}  

模板里循环todos,绑定done的状态,点击按钮调用addTodo和toggleDone,页面会自动更新,这就是reactive在实际项目里的流畅用法~

reactive的局限性?有没有替代方案?

reactive不是万能的,这些场景下得换思路:

处理基本类型:必须用ref

前面说过,reactive不认基本类型,所以存字符串、数字这些,只能用ref,比如const count = ref(0),修改时count.value++

对象替换导致响应式丢失:用ref包对象?

如果业务里就是需要频繁替换整个对象(比如每次请求回来的新数据直接替换),用ref更合适,因为ref的.value可以直接替换,内部的reactive会处理新对象的响应式。

const data = ref({ count: 0 })  
data.value = { count: 1 } // 这样没问题,ref内部会把新对象转成响应式  

大对象只关心外层变化:用shallowReactive

如果有个超级大的对象,比如树形结构,只需要外层属性变化时触发更新,内层不管,这时候用shallowReactive(浅响应式),它只会给对象的第一层属性做Proxy,内层对象修改不会触发更新,性能更好。

const shallowState = shallowReactive({  
  info: { name: '小明' }, // info是普通对象,不是响应式  
  count: 0 // count是响应式  
})  
shallowState.count++ // 触发更新  
shallowState.info.name = '小红' // 不触发更新,因为info是普通对象  

防止数据被修改:用readonly

如果有一些全局配置数据,不想被组件意外修改,可以用readonly把reactive对象变成只读:

const config = reactive({ theme: 'light' })  
const readonlyConfig = readonly(config)  
readonlyConfig.theme = 'dark' // 报错!不能修改只读对象  

这样能避免数据被错误修改,保证程序稳定性~

看完这些,再用reactive应该不会慌了吧?它是Vue3处理对象/数组响应式的核心,用法上要避开替换对象、解构丢失响应式这些坑,和ref配合着用更灵活,原理是Proxy实现的依赖收集和触发,实际项目里和computed、watch、生命周期配合,能cover大部分场景,要是遇到基本类型、大对象性能问题,还有ref、shallowReactive这些替代方案,下次写Vue3代码,关于响应式的选择和操作,心里就有清晰的逻辑啦~

版权声明

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

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门