【vue3】不响应性的几个坑:reactive、ref、props、函数传参

vue 提供了响应性的功能,用起来非常方便,只是 compositionAPI 把响应性提取出来之后,就出现了各种各样的小问题,在这里汇总一下。

目录

  • reactive:以 reactive 为例,说清楚丢失响应性的根本原因,其他的丢失响应性也是这个原因。
  • ref:表面上看挺好,其实也是一堆坑
  • props:这是重灾区,稍不注意就中招
  • 函数传参:其实 watch 是一个函数

事先约定

因为描述起来比较绕口,所以先做几个约定:

  • ref.value:这是一个筐,啥都能往里装,小米、苹果、鸭梨、大象、轿车、大楼等都能装。
  • ref.value 的:筐里装的东东,我们选个代表:小米
  • http://props.xxx:这也是一个筐,父组件传啥就装啥,如果能把地球传来也能装。
  • http://props.xxx 的:父组件传过来的东东,比如小米。一般是基础类型,其实也可以传 reactive。
如果使用 ref 的话,父组件在默认的情况下,只会传 小米,不会传筐。

reactive

reactive 非常好用,只是不能整体赋值,否则会失去响应性,官方不想想如何弥补,而是一刀切的推荐使用 ref,其实 ref 一样有坑。

整体赋值的情况

还是先看看 reactive 的情况,举例说明:

// ✔ 正确用法,必须使用 const 定义
const foo1 = reactive ({name:'jyk'})
const foo2 = reactive ({name:'jyk'})

console.log(foo1 === foo2) // false,两个不同对象的代理,地址(指针)不同

// ✘ 错误用法 ,不要使用 let、var,整体赋值也没个阻拦(js会报错)。
let foo = reactive ({name:'jyk'})

const mychange = () => {
  foo = reactive ({name:'jyk999'}) // 不响应
}

整体赋值为啥不响应了呢?因为地址变更了呀。

看第一段,foo1 和 foo2 的地址是不同的,那么在事件里面,给 foo 整体赋值,这 foo 里面保存的就是另外一个地址了。

本质原因:setup 有一个 return,把各种响应性交给 template,这时候 template 会记录各种地址,在事件里面变更了地址,那么 template 呢?还在关注以前的地址呢,不知道新的地址,所以就无法响应了。

这一点很重要,vue 里面丢失响应性,大多都是这种原因。

多层的 reactive

reactive 是深层响应的,所以可以设计为多层对象,多层情况就更复杂了。

我们还是先看例子:

 const more = reactive({
    name: 'jyk',
    info: {
      name: '第二层'
    }
  })

  // 默认深层监听,各种支持
  watch(more, () => {
    console.log('watch more:', more)
  })

  // 监听的是筐还是 【小米】?是【小米】!
  watch(more.info, () => {
    // 不会响应第二层的整体赋值
    console.log('watch more.info:', more.info)
  })

  // 监听的是筐的变化,换新筐才能监听
  watch(() => more.info, () => {
    // 可以响应第二层的整体赋值
    console.log('watch () => more.info:', more.info)
  })
 
   // 手动深层监听,筐、**小米**都能监听
  watch(() => more.info, () => {
    console.log('深层 watch () => more.info:', more.info)
  },{deep:true})
  
  const myChange2 = () => {
    more.info = reactive({name:'jyk999'})
    console.log('改变第二层后的 more:', more)
  }
  • watch -> more:默认深层监听,支持各层的变化。
  • watch -> http://more.xxx:表面上看是监听筐,但是实际上监听的是小米!为啥?别忘了 watch 是一个函数,这就涉及到函数传参的问题。筐里的 小米 换成 苹果 了,这个 watch 也就失效了
  • watch -> () => http://more.xxx:这是啥?这是一个匿名函数!,这可不是小米,每次都会调用这个函数,所以可以得到最新的小米 。
  • watch -> :手动深层监听,既能支持筐,又能支持小米
会不会丢失响应性?要看赋值方式,也要看监听方式。

解构

这个就不多说了,尽量别解构就OK了。

ref

如果上面的原理都理解了,那么 ref 的问题就清楚了,还是小米的问题。

  • ref 是 class 的实例,也就是一个对象。
  • ref.value 是对象的是个属性,相当于一个,啥都能往里装。
  • 这个筐有响应性,但是筐里的东东有没有响应性,那就不一定了。

还是写代码举例:

  const arr = ref([{
    name: 'jyk'
  }])

  // A 只监听筐的变化,不管小米如何变
  watch(arr, () => {
    console.log('watch arr:', arr)
  })

  // B 手动深层监听,可以监听筐和小米
  watch(arr, () => {
    console.log('手动深层 watch arr:', arr)
  },{deep:true})

  // C 只监听 ref 自己,和筐、小米都无关
  watch(() => arr, () => {
    console.log('父组件: watch () => arr:', arr)
  })
  
  // D 一开始能监听苹果,当使用 ref.value = [] 后,就不能监听苹果了。
  watch(arr.value, () => {
    console.log('watch arr.value:', arr)
  })

  // E 手动监听筐,不是深层,相当于 A
  watch(() => arr.value, () => {
    console.log('watch () => arr.value:', arr)
  })
  // F 深层的监听,都有,相当于 B
  watch(() => arr.value, () => {
    console.log('手动深层 watch () => arr.value:', arr)
  },{deep:true})

好吧,我承认,我自己都写蒙了,各种情况都要测试一下,看看实际效果如何。

不知道各种 watch 方法是否符合你们的预期。

最安全的方式(对象的情况)

watch(arr, () => {}, {deep:true}):

  • 手动深层监听的方式,是最全面的,各种情况都能监听到。
  • watch(arr,...) 等效于 watch(() => arr.value,...)

容易被误导的方式

watch(arr.value, () => {})

  • 你以为监听的是?其实是苹果
  • 使用 arr.value = [] 之后,这个 watch 就无效了。

以为是深层,其实不是

watch(arr, () => {}):是深层监听吗?不是!(和 reactive 的情况不同)

  • 查看 watch 源码可知,相当于 () => ref.value

使用风格不统一

  • ref 在 template 里面不需要使用 .value,在 js 代码里面需要使用 .value
  • 但是 作为 watch 的第一个参数的时候,又可以不使用 .value
  • watch 的第一个参数,ref 用不用 .value,行为又不一致。

不看 watch 源码的话,晕不晕?

  ...
  if (isRef(source)) {
    getter = () => source.value;
    forceTrigger = isShallow(source);
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source);
    forceTrigger = true;  // 等效于 深层监听
  } else if (isArray(source)) {
  ...
  } else if (isFunction(source)) {
  ...
  }

对第一个参数(source)的类型进行各种判断。

props

父子传值,父组件没事,子组件失去响应性了,为啥?还是因为地址变更了,子组件没能更新。

父组件

还是看看代码:

  const arr = ref([{
    name: 'jyk'
  }])
  
  // 变更记录集
  const change1 = () => {
    arr.value = [{name: 'jyk9999'}]
  }

  // 修改数组的一个元素
  const change2 = () => {
    arr.value[0] = {name: 'jyk3333'}
  }

子组件

子组件里的 template 是肯定有响应性的,而 props 是shallowReadonly,不是Readonly,也不是 ref,所以还是有一点点不一样的情况,我们来看看js代码里的 watch 的响应情况:

  // 方案一
  watch(props.listRef, () => {
    console.log('----子组件: watch -- props.listRef:', props.listRef)
  })
  
  // 方案二
  watch(props.listRef, () => {
    console.log('----子组件: 手动深层:watch -- props.listRef:', props.listRef)
  },{deep:true})

  // 方案三
  watch(() => props.listRef, () => {
    console.log('----子组件: watch -- () => props.listRef:', props.listRef)
  })
  // 方案四
  watch(() => props.listRef, () => {
    console.log('----子组件: 手动深层:watch -- () => props.listRef:', props.listRef)
  },{deep:true})

大家猜猜各种 watch 会如何响应性?

测试结果

当父组件修改数组元素的时候,子组件的 watch 情况:

  • 有响应性的
    • 方案一:watch -- props.listRef
    • 方案二:手动深层:watch -- props.listRef
    • 方案四:手动深层:watch -- () => props.listRef
  • 没有响应性的
    • 方案三:watch -- () => props.listRef

当父组件替换数组的时候,子组件的 watch 情况:

  • 有响应性的
    • 方案三:watch -- () => props.listRef
    • 方案四:手动深层:watch -- () => props.listRef
  • 没有响应性的
    • 方案一:watch -- props.listRef
    • 方案二:手动深层:watch -- props.listRef

数组被替换后,再修改数组的元素,子组件的watch情况:

  • 有响应性的
    • 方案四:手动深层:watch -- () => props.listRef
  • 没有响应性的
    • 方案一:watch -- props.listRef
    • 方案二:手动深层:watch -- props.listRef
    • 方案三:watch -- () => props.listRef

是不是有点乱?其实一开始我也没发现这么多种情况,因为我喜欢使用 reactive,还是前些日子有一位掘友问我,子组件的 watch 为啥没反应了?我问他具体写法,才发现这么多种情况。

最全面的还是方案四。

函数传参

写了半天才反应过来,watch 也是一个函数,响应性作为第一个参数,传递前有响应性,那么函数里面接受的是什么呢?其实挺复杂的,所以 watch 内部做了一个长串的判断。

函数的参数,就不得不说说一个很古老的问题:

  • 传值:基础类型,string、number、boolean等,对应小米
  • 传地址:对象、数组、函数、正则、日期等,对应

基础问题不多说了。

### 回答1: 在 Vue3 中使用 TypeScript 的函数中定义的 const 变量可以在其他函数中使用。只需在其他函数中引用该 const 变量即可。 例如: ``` const myVar = &#39;hello&#39;; function firstFunction() { console.log(myVar); } function secondFunction() { console.log(myVar); } ``` 在 firstFunction 和 secondFunction 中都可以使用 myVar 。 ### 回答2: 在Vue 3和TypeScript中,我们可以通过在函数外部使用const关键字声明变量,并在其他函数中使用它。具体步骤如下: 首先,我们需要在Vue组件的顶层范围内声明一个常量。这可以是在组件内部,也可以是在Vue实例外部的其他地方。例如,在组件内部的setup函数之前声明一个常量。 然后,我们可以在其他函数中引用这个常量。在Vue 3中,可以直接在函数中使用该常量,而无需额外的特殊语法。 下面是一个示例代码: ```typescript <script setup lang="ts"> const greeting = "Hello, Vue 3" function sayHello() { console.log(greeting) } function displayGreeting() { console.log(greeting) } </script> ``` 在上面的代码中,我们在setup函数之前声明了一个常量greeting,并在sayHello函数和displayGreeting函数中使用它。sayHello函数和displayGreeting函数可以直接访问该常量并打印它。 这样,我们就可以在Vue 3和TypeScript中将常量定义在一个函数中,并在其他函数中使用。 ### 回答3: 在Vue 3和TypeScript中,如果想要让一个函数中的const变量在其他函数中使用,有几种方法可以实现。 首先,可以将const变量声明为外部函数的局部变量,并将其传递给其他函数。例如: ```typescript function firstFunction() { const myConst = "Hello"; secondFunction(myConst); } function secondFunction(constVariable: string) { console.log(constVariable); // 输出: Hello } ``` 在这个例子中,我们在第一个函数中声明了一个const变量`myConst`,并将其作为参数传递给第二个函数`secondFunction`。通过这种方式,我们可以在第二个函数中使用第一个函数中的const变量。 另外一种方法是通过将const变量声明为一个共享的全局变量,让所有函数都可以访问它。这可以通过将const变量声明在模块的顶部,并且在需要使用它的函数中导入它来实现。例如: ```typescript // constants.ts export const myConst = "Hello"; // firstFunction.ts import { myConst } from "./constants.ts"; function firstFunction() { console.log(myConst); // 输出: Hello secondFunction(); } // secondFunction.ts import { myConst } from "./constants.ts"; function secondFunction() { console.log(myConst); // 输出: Hello } ``` 在这个例子中,我们将const变量`myConst`声明在`constants.ts`模块中,并将其导入到需要使用它的函数中。这样,我们就可以在同的函数中共享使用这个const变量。 无论使用哪种方法,都可以实现在Vue 3 TypeScript函数之间共享和使用const变量。选择哪种方法取决于具体的情况和需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值