【vue3.5】 的两个小坑:props的解构,监听的暂停、恢复(补充stop)

2,018 阅读5分钟

vue 又发布了一个小版本:v3.5 。各路大神也都在写文章介绍,自我感觉 这应该是全网最详细的Vue3.5版本解读 就挺全的,我也是按照介绍一一尝试一遍,然后发现了几个小坑:props的解构以及监听的暂停、恢复。

解构 props 的小坑

在vue3.3 版本里面就推出了这个功能,只是需要手动在config里面做设置,到了 vue 3.5 正式转正,不需要在做设置了。

什么坑呢?还是举个例子说明。

使用 toRefs 解构 props

以前想要解构 props 可以使用 toRefs:

  // 设置一个函数
  const foo1 = (v: any) => {
    // 函数内部监听传入的参数,可以有响应性
    watch (v, (val) => {
      console.log('函数内部监听 name:', val)
    })
  }
  // 定义 props
  const props = defineProps({name: String})
  // 使用 toRefs 解构
  const { name } = toRefs(props)
  console.log(name) // name 是 ref
  // 函数传参
  foo1(name) // name 其实是一个 ref,所以传参后,依然有响应性

toRefs 解构后,得到的是一个 ref:

ObjectRefImpl {
  _object: Proxy,
  _key: "name",
  _defaultValue: undefined,
  __v_isRef: true,
  _value: undefined
}

所以这个 name 本身是有响应性的,函数传参后依旧可以使用 watch 对其进行监听。

props 直接解构

我们再来看看3.5 的 props 的解构:

  // 直接对 props 解构
  const { name } = defineProps({name: String})
  console.log(name) // 是 string
  
  // 直接监听 name,有响应
  watch (() => name, (val) => {
    console.log('直接监听 name :', val)
  })
  // 函数传参
  foo1(name) // 传递的不是 props ,也不是 ref,而是 string
  • 直接监听 name ,可以有响应性。
  • 函数传参后,监听参数,没有响应性。

如果按照 toRefs 的思维方式去思考,函数传参后,也应该有响应性,但是,现实并非如此。

我们看看“翻译”后的代码,就很明显了。

  setup(__props, { expose: __expose }) {
    __expose();
    console.log(__props.name); // 代码里的 name
    watch(() => __props.name, (val) => {  // 代码里的 name
      console.log("直接监听 name", val);
    });
    const foo1 = (v) => { // 传入的是 string
      watch(() => v, (val) => {
        console.log("函数内部监听 name", val);
      });
    };
    foo1(__props.name); // 传入的是 __props.name 的值

解构后的 name,被翻译成了 __props.name,后面直接使用 __props.name ,自然会有响应性。

但是调用函数的时候呢,foo1(__props.name),这样看就很明显了,传递的是 __props.name的值 ,这个值是一个 string,函数收到string,当然没有响应性。

如果不解构,直接用 props.xxx 的话,也就不会有这个错觉了。所以,这个算不算是一个小坑?

watch 的 pause、resume 的小坑(还有stop)

暂停、恢复:在暂停期间不进行监听,这个没有问题,那么恢复之后呢?

举个 watchEffect 的例子:

  const count = ref(0)
  const runner2 = watchEffect(() => {
    console.log('watchEffect监听:', count.value)
  })
  <button @click="count++">count++</button>
  <button @click="runner2.pause()">暂停</button>
  <button @click="runner2.resume()">恢复</button>

代码很简单:

  • 我们按 count++ ,会不断打印:1、2...;
  • 如果按暂停,那么按 count++ 不会打印,到这都没有问题;
  • 然后我们按恢复,大家猜猜会如何?会立即打印一个数字,比如 6 。(只打印最后一次的数值,中间的不会打印出来)

不知道这和大家预想的是否一致,反正我觉得,按恢复后,不应该立即执行。
或者说,如果我想实现:按恢复后,不立即执行,要如何调整代码?(目前没有想到办法)

举个 watch 的例子

  const count = ref(0)
  const runner3 = watch(count, (newValue, oldValue)=> {
    console.log(`watch监听,新值:${newValue};旧值:${oldValue}`)
  })
  <button @click="count++">count++</button>
  <button @click="runner3.pause()">暂停</button>
  <button @click="runner3.resume()">恢复</button>

运行结果:

watch监听,新值:1;旧值:0
watch监听,新值:2;旧值:1
watch监听,新值:3;旧值:2
watch监听,新值:7;旧值:3

暂停后,旧值没有同步,恢复后用暂停时的旧值和新值对比,发现不一样,于是触发回调函数。
这个,不知道和大家的预想是否一致。

怎么说呢,可能符合某些需求,但是不符合我的一个需求。

前面写了一个列表数据的文章: 【vue3】compositionAPI的最佳实践:列表篇,里面用到了两个 watch:

  • 一个监听查询条件,
  • 一个监听翻页的页号。

如果查询条件变化了,那么翻页的 watch 就应该暂时失效,否则会重复向后端申请数据。
如果使用这种方式的话,恢复监听后,翻页的 watch 后依然会被执行,这样和我的预期就不一样了。

我的应用场景是这样的:

  • 查询条件变更,触发 watch,更新数据。然后页号设置为 1 。
  • 翻页时页号变更,触发 watch,更新数据。

上面的逻辑没啥问题,只是如果先翻到第二页,然后设置查询条件的时候,会更新两次数据:查询一次;查询里面修改页号,又更新一次。
所以,我期望,查询条件变更的时候,翻页的 watch 暂停,不去更新数据。

但是,恢复后,依然会用 2 和 1 做对比,于是,数据又被更新一次。

无语了。

想过判断新旧值,但是,这不对呀。

stop (补充)

看了看源码,发现还有一个 stop 的用法,停止监听后,就一直停止,恢复不过来了。

  const stop = () => {
    runner.stop() // 停止监听,不能恢复
  }
  
  const pause = () => {
    runner.pause() // 暂停后可以恢复
  }
  
  const resume = () => {
    runner.resume()
  }

修改了一种警告

这是无意间发现的,以前用递归子组件的方式写了一个n级菜单,虽然运行正常,但是会出现警告:

[Vue warn]: Vue received a Component that was made a reactive object. 
This can lead to unnecessary performance overhead and should be avoided
by marking the component with `markRaw` or using `shallowRef` instead of `ref`. 
Component that was made reactive:  {name: "Document", render: ƒ} 
 

看了好久也不知道要如何改,如果加上 markRaw,那么菜单如何动态更新?

这次升级后发现,这种警告消失了。

另外,又增加了一致警告:当 watch 监听的对象没有响应性的时候,会出现警告!
这种警告就比较有意义,避免一些莫名其妙的情况

小结

这次更新版本,主要是内部性能优化,从使用的角度来看,似乎吸引度不高。
增加的一些新功能(语法糖),没有想到有实际的应用场景,当然,可能是我的脑洞不够大。