这应该是全网最详细的Vue3.5版本解读

11,648次阅读
没有评论

共计 6239 个字符,预计需要花费 16 分钟才能阅读完成。

前言

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

加入欧阳的高质量 vue 源码交流群、欧阳平时写文章参考的多本 vue 源码电子书

版本号

这次的版本号是 天元突破红莲螺岩 ,这是 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 模块中。详情可以查看我之前的文章:Vue3.5 新增的 baseWatch 让 watch 函数和 Vue 组件彻底分手

还有一点就是这个 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的结果如下图:
这应该是全网最详细的 Vue3.5 版本解读

从上图中可以看到 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 服务端渲染中那么就会报警告了,如下图:
这应该是全网最详细的 Vue3.5 版本解读

上面报错的意思是服务端和客户端生成的 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 元素中。

在之前有个限制,就是不能将

放在 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 = refHTMLInputElement>();

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

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

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

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

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

const inputEl = useTemplateRefHTMLInputElement>("inputRef");

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

在 template 中 ref 属性的值也是字符串 "inputRef",所以useTemplateRef 函数的返回值就指向了 DOM 元素 input 输入框。这个比 3.5 之前的体验要好很多了,详情可以查看我之前的文章:牛逼!Vue3.5 的 useTemplateRef 让 ref 操作 DOM 更加丝滑

总结

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

这些功能在一些特殊场景中还是很有用的,欧阳的个人看法还是得将 Vue 升到 3.5。

最后推荐一下欧阳自己写的开源电子书 vue3 编译原理揭秘,看完这本书可以让你对 vue 编译的认知有质的提升,并且这本书初、中级前端能看懂。完全免费,只求一个 star。

原文地址: 这应该是全网最详细的 Vue3.5 版本解读

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-26发表,共计6239字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)