详细解读Vue3.5版本都更新了啥

目录
文章目录隐藏
  1. 版本号
  2. 响应式
  3. watch 的 deep 选项支持传入数字
  4. SSR 服务端渲染
  5. Custom Element 自定义元素改进
  6. useTemplateRef函数
  7. 结语

详细解读 Vue3.5 版本都更新了啥

Vue3.5 正式版在这两天发布了,网上已经有了不少关于 Vue3.5 版本的解读文章。但是码云笔记发现这些文章对 3.5 中新增的功能介绍都不是很全很细,所以导致不少小伙伴有个错觉,觉得 Vue3.5 版本不过如此,选择跳过这个版本等下个大版本再去更新。所以码云笔记写了这篇超级详细的 Vue3.5 版本解读文章,大家可以看看在 3.5 版本中有没有增加一些你期待的功能。

版本号

这次的版本号是天元突破红莲螺岩,这是 07 年出的一个二次元动漫,我是没看过的。在此之前我一直以为这次的版本号会叫黑神话:悟空,可能悟空不够二次元吧。

响应式

响应式相关的内容主要分为:重构响应式、响应式 props 支持解构、新增onEffectCleanup函数、新增base watch函数、新增onWatcherCleanup函数、新增pauseresume方法。

重构响应式

这次响应式的重构是属于 Vue 内部优化,对于普通开发者来说是无感的。重构后内存占用减少了 56%,优化手段主要是通过版本计数双向链表数据结构,灵感来源于Preact signals。后续我会出一系列关于响应式相关的源码文章,大家可以关注下码云笔记。

响应式 props 支持解构

在 3.5 中响应式 props 支持解构终于正式稳定了,在没有这个功能之前我们想要在 js 中访问 prop 必须要这样写:props.name,否则name将会丢失响应式。

有了响应式 props 解构后,在 js 中我们就可以直接解构出name来使用,比如下面这样的代码:

<script setup lang="ts">
const { name } = defineProps({
  name: String,
});

console.log(name);
</script>

defineProps搭配解构一起使用后,在编译时就可以将name处理成props.name。编译后简化的代码如下:

setup(__props) {
  console.log(__props.name);
  const __returned__ = {};
  return __returned__;
}

从上面的代码可以看到console.log(name)经过编译后变成了console.log(__props.name),这样处理后name当然就不会丢失响应式了。

新增 onEffectCleanup 函数

在组件卸载之前或者下一次watchEffect回调执行之前会自动调用onEffectCleanup函数,有了这个函数后你就不需要在组件的beforeUnmount钩子函数去统一清理一些 timer 了。比如下面这个场景:

import { watchEffect, ref } from "vue";
import { onEffectCleanup } from "@vue/reactivity";

const flag = ref(true);
watchEffect(() => {
  if (flag.value) {
    const timer = setInterval(() => {
      // 做一些事情
      console.log("do something");
    }, 200);
    onEffectCleanup(() => {
      clearInterval(timer);
    });
  }
});

上面这个例子在watchEffect中会去注册一个循环调用的定时器,如果不使用onEffectCleanup,那么我们就需要在beforeUnmount钩子函数中去清理定时器。

但是有了onEffectCleanup后,将clearInterval放在他的回调中就可以了。当组件卸载时会自动执行onEffectCleanup传入的回调函数,也就是会执行clearInterval清除定时器。

还有一点值得注意的是onEffectCleanup函数目前没有在vue包中暴露出来,如果你想使用可以像我这样从@vue/reactivity包中导入onEffectCleanup函数。

新增 base watch 函数

我们之前使用的watch函数是和 Vue 组件以及生命周期一起实现的,他们是深度绑定的,所以watch函数代码的位置在 vue 源码中的runtime-core模块中。

但是有的场景中我们只想使用 vue 的响应式功能,也就是 vue 源码中的reactivity模块,比如小程序vuemini。为此我们不得不将runtime-core模块也导入到项目中,或者像vuemini一样去手写一个 watch 函数。

在 3.5 版本中重构了一个base watch函数,这个函数的实现和 vue 组件没有一毛钱关系,所以他是在reactivity模块中。

还有一点就是这个base watch函数对于普通开发者来说没有什么影响,但是对于一些下游项目,比如vuemini来说是和受益的。

新增 onWatcherCleanup 函数

和前面的onEffectCleanup函数类似,在组件卸载之前或者下一次watch回调执行之前会自动调用onWatcherCleanup函数,同样有了这个函数后你就不需要在组件的beforeUnmount钩子函数去统一清理一些 timer 了。比如下面这个场景:

import { watch, ref, onWatcherCleanup } from "vue";

watch(flag, () => {
  const timer = setInterval(() => {
    // 做一些事情
    console.log("do something");
  }, 200);
  onWatcherCleanup(() => {
    console.log("清理定时器");
    clearInterval(timer);
  });
});

onEffectCleanup函数不同的是我们可以从 vue 中 import 导入onWatcherCleanup函数。

新增 pause 和 resume 方法

有的场景中我们可能想在“一段时间中暂停一下”,不去执行watch或者watchEffect中的回调。等业务条件满足后再去恢复执行watch或者watchEffect中的回调。在这种场景中pauseresume方法就能派上用场啦。

下面这个是watchEffect的例子,代码如下:

<template>
  <button @click="count++">count++</button>
  <button @click="runner2.pause()">暂停</button>
  <button @click="runner2.resume()">恢复</button>
</template>

<script setup lang="ts">
import { watchEffect } from "vue";

const count = ref(0);
const runner = watchEffect(() => {
  if (count.value > 0) {
    console.log(count.value);
  }
});
</script>

在上面的 demo 中,点击count++按钮后理论上每次都会执行一次watchEffect的回调。

但是当我们点击了暂停按钮后就会执行pause方法进行暂停,在暂停期间watchEffect的回调就不会执行了。

当我们再次点击了恢复按钮后就会执行resume方法进行恢复,此时watchEffect的回调就会重新执行。

console.log的结果如下图:

新增 pause 和 resume 方法

从上图中可以看到count打印到 4 后就没接着打印了,因为我们执行了pause方法暂停了。当重新执行了resume方法恢复后可以看到count又重新开始打印了,此时从 8 开始打印了。

不光watchEffect可以执行pauseresume方法,watch一样也可以执行pauseresume方法。代码如下:

const runner = watch(count, () => {
  if (count.value > 0) {
    console.log(count.value);
  }
});

runner.pause()	// 暂停方法
runner.resume()	// 恢复方法

watch 的 deep 选项支持传入数字

在以前deep选项的值要么是false,要么是true,表明是否深度监听一个对象。在 3.5 中deep选项支持传入数字了,表明监控对象的深度。

比如下面的这个 demo:

const obj1 = ref({
  a: {
    b: 1,
    c: {
      d: 2,
      e: {
        f: 3,
      },
    },
  },
});

watch(
  obj1,
  () => {
    console.log("监听到 obj1 变化");
  },
  {
    deep: 3,
  }
);

function changeDeep3Obj() {
  obj1.value.a.c.d = 20;
}

function changeDeep4Obj() {
  obj1.value.a.c.e.f = 30;
}

在上面的例子watchdeep选项值是 3,表明监听到对象的第 3 层。

changeDeep3Obj函数中就是修改对象的第 3 层的d属性,所以能够触发watch的回调。

changeDeep4Obj函数是修改对象的第 4 层的f属性,所以不能触发watch的回调。

SSR 服务端渲染

服务端渲染 SSR 主要有这几个部分:新增useId函数、Lazy Hydration  懒加载水合、data-allow-mismatch

新增useId函数

有时我们需要生成一个随机数塞到 DOM 元素上,比如下面这个场景:

<template>
  <label :htmlFor="id">Do you like Vue3.5?</label>
  <input type="checkbox" name="vue3.5" :id="id" />
</template>

<script setup lang="ts">
const id = Math.random();
</script>

在这个场景中我们需要生成一个随机数id,在普通的客户端渲染中这个代码是没问题的。

但是如果这个代码是在 SSR 服务端渲染中那么就会报警告了,如下图:

SSR 服务端渲染

上面报错的意思是服务端和客户端生成的id不一样,因为服务端和客户端都执行了一次Math.random()生成id。由于Math.random()每次执行的结果都不同,自然服务端和客户端生成的id也不同。

useId函数的作用就是为了解决这个问题。

当然useId也可以用于客户端渲染的一些场景,比如在列表中我们需要一个唯一键,但是服务端又没有给我们,这时我们就可以使用useId给列表中的每一项生成一个唯一键。

Lazy Hydration  懒加载水合

异步组件现在可以通过 defineAsyncComponent() API 的 hydrate 选项来控制何时进行水合。(码云笔记觉得这个普通开发者用不上,所以就不细讲了)

data-allow-mismatch

SSR 中有的时候确实在服务端和客户端生成的 html 不一致,比如在 DOM 上面渲染当前时间,代码如下:

<template>
  <div>当前时间是:{{ new Date() }}</div>
</template>

这种情况是避免不了会出现前面useId例子中的那种警告,此时我们可以使用data-allow-mismatch属性来干掉警告,代码如下:

<template>
  <div data-allow-mismatch>当前时间是:{{ new Date() }}</div>
</template>

Custom Element 自定义元素改进

这个我也觉得平时大家都用不上,所以就不细讲了。

Teleport 组件新增 defer 延迟属性

Teleport组件的作用是将 children 中的内容传送到指定的位置去,比如下面的代码:

<div id="target"></div>
<Teleport to="#target">被传送的内容</Teleport>

文案被传送的内容最终会渲染在id="target"的 div 元素中。

在之前有个限制,就是不能将<div id="target">放在Teleport组件的后面。

这个也很容易理解 DOM 是从上向下开始渲染的,如果先渲染到Teleport组件。然后就会去找 id 的值为target的元素,如果找不到当然就不能成功的将Teleport组件的子节点传送到target的位置。

在 3.5 中为了解决这个问题,在Teleport组件上新增了一个defer延迟属性。

加了defer延迟属性后就能将target写在Teleport组件的后面,代码如下:

<Teleport defer to="#target">被传送的内容</Teleport>
<div id="target"></div>

defer延迟属性的实现也很简单,就是等这一轮渲染周期结束后再去渲染Teleport组件。所以就算是target写在Teleport组件的后面,等到渲染Teleport组件的时候target也已经渲染到页面上了。

useTemplateRef函数

vue3 中想要访问 DOM 和子组件可以使用 ref 进行模版引用,但是这个 ref 有一些让人迷惑的地方。

比如定义的 ref 变量到底是一个响应式数据还是 DOM 元素?

还有 template 中 ref 属性的值明明是一个字符串,比如ref="inputEl",怎么就和 script 中同名的inputEl变量绑到一块了呢?

3.5 中的useTemplateRef函数就可以完美的解决了这些问题。

这是 3.5 之前使用 ref 访问 input 输入框的例子:

<input type="text" ref="inputEl" />

const inputEl = ref<HTMLInputElement>();

这个写法很不符合编程直觉,不知道有多少同学和我一样最开始用 vue3 时会给ref属性绑定一个响应式变量。比如这样::ref="inputEl"

更加要命的是这样写还不会报错,就是inputEl中的值一直是undefined

最后一番排查后才发现ref属性应该是绑定的变量名称:ref="inputEl"

使用useTemplateRef函数后就好多了,代码如下:

<input type="text" ref="inputRef" />

const inputEl = useTemplateRef<HTMLInputElement>("inputRef");

使用useTemplateRef函数后会返回一个 ref 变量,useTemplateRef函数传的参数是字符串"inputRef"

在 template 中ref属性的值也是字符串"inputRef",所以useTemplateRef函数的返回值就指向了 DOM 元素 input 输入框。这个比 3.5 之前的体验要好很多了。

结语

对于前端开发者来说 Vue3.5 版本中还是新增了许多有趣的功能的,比如:onEffectCleanup函数、onWatcherCleanup函数、pauseresume方法、watchdeep选项支持传入数字、useId函数、Teleport组件新增defer延迟属性、useTemplateRef函数。

这些功能在一些特殊场景中还是很有用的,码云笔记的个人建议还是得将 Vue 升到 3.5。

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系maynote@foxmail.com处理
码云笔记 » 详细解读Vue3.5版本都更新了啥

发表回复