写点什么

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

  • 2024-09-06
    福建
  • 本文字数:4483 字

    阅读完需:约 15 分钟

版本号


这次的版本号是天元突破红莲螺岩,这是 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的结果如下图:



从上图中可以看到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 服务端渲染中那么就会报警告了,如下图:



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


总结


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


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


文章转载自:前端欧阳

原文链接:https://www.cnblogs.com/heavenYJJ/p/18397413

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
这应该是全网最详细的Vue3.5版本解读_JavaScript_快乐非自愿限量之名_InfoQ写作社区