Vue3.0新特性以及使用经验总结

开发 前端
今年初新项目也开始使用 Vue3.0 来开发,这篇文章就是在使用后的一个总结, 包含 Vue3 新特性的使用以及一些使用经验分享。

 前言

Vue3.0 在去年 9 月正式发布了,看大家都有在热情的拥抱 Vue3.0。今年初新项目也开始使用 Vue3.0 来开发,这篇文章就是在使用后的一个总结, 包含 Vue3 新特性的使用以及一些使用经验分享。

为什么要升级 Vue3

使用 Vue2.x 的小伙伴都熟悉,Vue2.x 中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法。那 Vue3.x 中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下 Vue2.x 版本这么写有什么缺陷,所有才会进行升级变更的。

回顾 Vue2.x 实现加减 

  1. <template>  
  2.   <div class="homePage">  
  3.     <p>count: {{ count }}</p>     
  4.     <p>倍数: {{ multiple }}</p>      
  5.      <div>  
  6.       <button style="margin-right: 10px" @click="increase">加1</button>  
  7.       <button @click="decrease">减一</button>      
  8.     </div>        
  9.   </div>  
  10. </template>  
  11. <script>  
  12. export default {  
  13.   data() {  
  14.     return { count: 0 };  
  15.   },  
  16.   computed: {  
  17.     multiple() {  
  18.       return 2 * this.count;  
  19.     },  
  20.   },  
  21.   methods: {  
  22.     increase() {  
  23.       this.count++; 
  24.     },  
  25.     decrease() {  
  26.       this.count--;  
  27.     },  
  28.   },  
  29. };  
  30. </script> 

上面代码只是实现了对count的加减以及显示倍数, 就需要分别在 data、methods、computed 中进行操作,当我们增加一个需求,就会出现下图的情况:

当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:

甚至一个功能还有会依赖其他功能,全搅合在一起。

当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在 data、methods、computed 以及 mounted 中反复的跳转,这其中的的痛苦写过的都知道。

那我们就想啊, 如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:

那么 vue2.x 版本给出的解决方案就是 Mixin, 但是使用 Mixin 也会遇到让人苦恼的问题:

  1.  命名冲突问题
  2.  不清楚暴露出来的变量的作用
  3.  逻辑重用到其他 component 经常遇到问题

关于上面经常出现的问题我就不一一举例了,使用过的小伙伴多多少少都会遇到。文章的重点不是 Mixin, 如果确实想知道的就留言啦~

所以,我们 Vue3.x 就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识Composition API。

Composition API

setup

setup 是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API的入口。

setup 执行时机

我在学习过程中看到很多文章都说 setup 是在 beforeCreate和created之间, 这个结论是错误的。实践是检验真理的唯一标准, 于是自己去检验了一下: 

  1. export default defineComponent({  
  2.   beforeCreate() {  
  3.     console.log("----beforeCreate----");  
  4.   },  
  5.   created() {  
  6.     console.log("----created----");  
  7.   },  
  8.   setup() {  
  9.     console.log("----setup----");  
  10.   },  
  11. }); 

setup 执行时机是在 beforeCreate 之前执行,详细的可以看后面生命周期讲解。

setup 参数

使用setup时,它接受两个参数:

  1.  props: 组件传入的属性
  2.  context

setup 中接受的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。错误代码示例, 这段代码会让 props 不再支持响应式: 

  1. // demo.vue  
  2. export default defineComponent ({  
  3.     setup(props, context) {  
  4.         const { name } = props  
  5.         console.log(name)  
  6.     },  
  7. }) 

那在开发中我们想要使用解构,还能保持props的响应式,有没有办法解决呢?大家可以思考一下,在后面toRefs学习的地方为大家解答。接下来我们来说一下setup接受的第二个参数context,我们前面说了setup中不能访问 Vue2 中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrs、slot 和emit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。

reactive、ref 与 toRefs

在 vue2.x 中, 定义数据都是在data中, 但是 Vue3.x 可以使用reactive和ref来进行数据定义。那么ref和reactive他们有什么区别呢?分别什么时候使用呢?说到这里,我又不得不提一下,看到很多网上不少文章说 (reactive用于处理对象的双向绑定,ref则处理 js 基础类型的双向绑定)。我其实不太赞同这样的说法,这样很容易初学者认为ref就能处理 js 基本类型, 比如ref也是可以定义对象的双向绑定的啊, 上段代码: 

  1. setup() {  
  2.    const obj = ref({count:1, name:"张三"})  
  3.    setTimeout(() => 
  4.        objobj.value.count = obj.value.count + 1  
  5.        obj.value.name = "李四"  
  6.    }, 1000)  
  7.    return{  
  8.        obj  
  9.    }  
  10.  } 

我们将obj.count和obj.name绑定到页面上也是可以的;但是reactive函数确实可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等。接下来使用代码展示一下ref、reactive的使用:

运行效果:

上面的代码中,我们绑定到页面是通过user.name,user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢? 答案是不能直接对user进行结构, 这样会消除它的响应式, 这里就和上面我们说props不能使用 ES6 直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用toRefs。toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象。具体使用方式如下: 

  1. <template>  
  2.   <div class="homePage">  
  3.     <p>第 {{ year }} 年</p>  
  4.     <p>姓名:{{ nickname }}</p>  
  5.     <p>年龄:{{ age }}</p>  
  6.   </div>  
  7. </template>  
  8. <script>  
  9. import { defineComponent, reactive, ref, toRefs } from "vue";  
  10. export default defineComponent({  
  11.   setup() {  
  12.     const year = ref(0);  
  13.     const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });  
  14.     setInterval(() => {  
  15.       year.value++;  
  16.       user.age++;  
  17.     }, 1000);  
  18.     return {  
  19.       year,  
  20.       // 使用reRefs  
  21.       ...toRefs(user),  
  22.     };  
  23.   },  
  24. });  
  25. </script> 

生命周期钩子

我们可以直接看生命周期图来认识都有哪些生命周期钩子 (图片是根据官网翻译后绘制的):

从图中我们可以看到 Vue3.0 新增了setup,这个在前面我们也详细说了, 然后是将 Vue2.x 中的beforeDestroy名称变更成beforeUnmount; destroyed 表更为 unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mount和unmount的过程。其他 Vue2 中的生命周期仍然保留。上边生命周期图中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:

我们可以看到beforeCreate和created被setup替换了(但是 Vue3 中你仍然可以使用, 因为 Vue3 是向下兼容的, 也就是你实际使用的是 vue2 的)。其次,钩子命名都增加了on; Vue3.x 还新增用于调试的钩子函数onRenderTriggered和onRenderTricked 下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x 中的钩子是需要从 vue 中导入的: 

  1. import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated,  
  2. onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked,  
  3. onRenderTriggered } from "vue"; export default defineComponent({ //  
  4. beforeCreate和created是vue2的 beforeCreate() {  
  5. console.log("------beforeCreate-----"); }, created() {  
  6. console.log("------created-----"); }, setup() { console.log("------setup-----");  
  7. // vue3.x生命周期写在setup中 onBeforeMount(() => {  
  8. console.log("------onBeforeMount-----"); }); onMounted(() => {  
  9. console.log("------onMounted-----"); }); // 调试哪些数据发生了变化  
  10. onRenderTriggered((event) =>{ console.log("------onRenderTriggered-----",event);  
  11. }) }, }); 

关于生命周期相关的内容就介绍到这里,下面我们介绍一下 Vue3.x 中watch有什么不同。

watch 与 watchEffect 的用法

watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。

  1. watch(source, callback, [options]) 

参数说明:

  •  source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
  •  callback: 执行的回调函数
  •  options:支持 deep、immediate 和 flush 选项。

接下来我会分别介绍这个三个参数都是如何使用的, 如果你对 watch 的使用不明白的请往下看:

侦听 reactive 定义的数据 

  1. import { defineComponent, ref, reactive, toRefs, watch } from "vue";  
  2. export default defineComponent({  
  3.   setup() {  
  4.     const state = reactive({ nickname: "xiaofan", age: 20 });  
  5.     setTimeout(() => {  
  6.       state.age++; 
  7.     }, 1000);  
  8.     // 修改age值时会触发 watch的回调  
  9.     watch(  
  10.       () => state.age,  
  11.       (curAge, preAge) => {  
  12.         console.log("新值:", curAge, "老值:", preAge);  
  13.       }  
  14.     );  
  15.     return { 
  16.       ...toRefs(state),  
  17.     };  
  18.   },  
  19. }); 

侦听 ref 定义的数据 

  1. const year = ref(0);  
  2. setTimeout(() => {  
  3.   year.value++;  
  4. }, 1000);  
  5. watch(year, (newVal, oldVal) => {  
  6.   console.log("新值:", newVal, "老值:", oldVal);  
  7. }); 

侦听多个数据

上面两个例子中,我们分别使用了两个 watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据: 

  1. watch([() => state.age, year], ([curAge, newVal], [preAge, oldVal]) => { 
  2. console.log("新值:", curAge, "老值:", preAge); console.log("新值:", newVal, 
  3. "老值:", oldVal); }); 

侦听复杂的嵌套对象

我们实际开发中,复杂数据随处可见, 比如: 

  1. const state = reactive({  
  2.   room: {  
  3.     id: 100,  
  4.     attrs: {  
  5.       size: "140平方米",  
  6.       type: "三室两厅",  
  7.     },  
  8.   },  
  9. });  
  10. watch( 
  11.   () => state.room,  
  12.   (newType, oldType) => {  
  13.     console.log("新值:", newType, "老值:", oldType);  
  14.   },  
  15.   { deep: true }  
  16. ); 

如果不使用第三个参数deep:true, 是无法监听到数据变化的。前面我们提到,默认情况下,watch 是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。关于flush配置,还在学习,后期会补充

stop 停止监听

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下: 

  1. const stopWatchRoom = watch(() => state.room, (newType, oldType) => {  
  2.     console.log("新值:", newType, "老值:", oldType);  
  3. }, {deep:true}); 
  4. setTimeout(()=> 
  5.     // 停止监听  
  6.     stopWatchRoom()  
  7. }, 3000) 

还有一个监听函数watchEffect, 在我看来watch已经能满足监听的需求,为什么还要有watchEffect呢?虽然我没有 get 到它的必要性,但是还是要介绍一下watchEffect,首先看看它的使用和watch究竟有何不同。 

  1. import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";  
  2. export default defineComponent({  
  3.   setup() {  
  4.     const state = reactive({ nickname: "xiaofan", age: 20 });  
  5.     let year = ref(0)  
  6.     setInterval(() => 
  7.         state.age++  
  8.         year.value++  
  9.     },1000)  
  10.     watchEffect(() => {  
  11.         console.log(state);  
  12.         console.log(year);  
  13.       }  
  14.     );  
  15.     return {  
  16.         ...toRefs(state)  
  17.     }  
  18.   },  
  19. }); 

执行结果首先打印一次state和year值;然后每隔一秒,打印state和year值。从上面的代码可以看出, 并没有像watch一样需要先传入依赖,watchEffect会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下:

  1.  watchEffect 不需要手动传入依赖
  2.  watchEffect 会先执行一次用来自动收集依赖
  3.  watchEffect 无法获取到变化前的值, 只能获取变化后的值

上面介绍了 Vue3 Composition API的部分内容, 还有很多非常好用的 API, 建议直接查看官网 composition-api。其实我们也能进行自定义封装。

自定义 Hooks

开篇的时候我们使用 Vue2.x 写了一个实现加减的例子, 这里可以将其封装成一个 hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。useCount.ts 实现: 

  1. import { ref, Ref, computed } from "vue";  
  2. type CountResultProps = {  
  3.   count: Ref<number> 
  4.   multiple: Ref<number> 
  5.   increase: (delta?: number) => void;  
  6.   decrease: (delta?: number) => void;  
  7. }; 
  8. export default function useCount(initValue = 1): CountResultProps {  
  9.   const count = ref(initValue);  
  10.   const increase = (delta?: number): void => {  
  11.     if (typeof delta !== "undefined") {  
  12.       count.value += delta;  
  13.     } else {  
  14.       count.value += 1;  
  15.     }  
  16.   };  
  17.   const multiple = computed(() => count.value * 2);  
  18.   const decrease = (delta?: number): void => {  
  19.     if (typeof delta !== "undefined") {  
  20.       count.value -delta 
  21.     } else {  
  22.       count.value -1 
  23.     }  
  24.   };  
  25.   return {  
  26.     count,  
  27.     multiple,  
  28.     increase,  
  29.     decrease,  
  30.   };  

接下来看一下在组件中使用useCount这个 hook: 

  1. <template>  
  2.   <p>count: {{ count }}</p>  
  3.   <p>倍数: {{ multiple }}</p>  
  4.   <div>  
  5.     <button @click="increase()">加1</button>  
  6.     <button @click="decrease()">减一</button>  
  7.   </div>  
  8. </template>  
  9. <script lang="ts">  
  10. import useCount from "../hooks/useCount";  
  11.  setup() { 
  12.      const { count, multiple, increase, decrease } = useCount(10);  
  13.         return {  
  14.             count,  
  15.             multiple,  
  16.             increase,  
  17.             decrease,  
  18.         };  
  19.     },  
  20. </script> 

开篇 Vue2.x 实现,分散在data,method,computed等, 如果刚接手项目,实在无法快速将data字段和method关联起来,而 Vue3 的方式可以很明确的看出,将 count 相关的逻辑聚合在一起, 看起来舒服多了, 而且useCount还可以扩展更多的功能。项目开发完之后,后续还会写一篇总结项目中使用到的「自定义 Hooks 的文章」,帮助大家更高效的开发, 关于Composition API和自定义 Hooks 就介绍到这里, 接下来简单介绍一下 vue2.x 与 vue3 响应式对比。

简单对比 vue2.x 与 vue3.x 响应式

其实在 Vue3.x 还没有发布 bate 的时候, 很火的一个话题就是Vue3.x 将使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty。没有无缘无故的爱,也没有无缘无故的恨。为何要将Object.defineProperty换掉呢,咋们可以简单聊一下。我刚上手 Vue2.x 的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty。对这块想要深入了解的小伙伴可以看这篇文章 为什么 Vue3.0 不再使用 defineProperty 实现数据监听?要详细解释又是一篇文章,这里就简单对比一下Object.defineProperty 与 Proxy

    1.  Object.defineProperty只能劫持对象的属性, 而 Proxy 是直接代理对象

由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作

    2.  Object.defineProperty对新增属性需要手动进行Observe

因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的, $set内部也是通过调用Object.defineProperty去处理的。

Teleport

Teleport 是 Vue3.x 新推出的功能, 没听过这个词的小伙伴可能会感到陌生;翻译过来是传送的意思,可能还是觉得不知所以,没事下边我就给大家形象的描述一下。

Teleport 是什么呢?

Teleport 就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport 的特性呢,看一个小例子:在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。此时就需要 Teleport 上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。接下来就举个小例子,看看 Teleport 的使用方式

Teleport 的使用

我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素: 

  1. <body>  
  2.   <div id="app"></div>  
  3.   <div id="dialog"></div>  
  4. </body> 

定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致: 

  1. <template>  
  2.   <teleport to="#dialog">  
  3.     <div class="dialog">  
  4.       <div class="dialog_wrapper">  
  5.         <div class="dialog_header" v-if="title">  
  6.           <slot name="header">  
  7.             <span>{{ title }}</span>  
  8.           </slot>  
  9.         </div>  
  10.       </div>  
  11.       <div class="dialog_content">  
  12.         <slot></slot>  
  13.       </div>  
  14.       <div class="dialog_footer">  
  15.         <slot name="footer"></slot>  
  16.       </div>  
  17.     </div>  
  18.   </teleport>  
  19. </template> 

最后在一个子组件Header.vue中使用Dialog组件, 这里主要演示 Teleport 的使用,不相关的代码就省略了。header组件 

  1. <div class="header">  
  2.     ...  
  3.     <navbar />  
  4.     <Dialog v-if="dialogVisible"></Dialog>  
  5. </div>  
  6. ... 

Dom 渲染效果如下:

图片.png 可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.

Suspense

Suspense是 Vue3.x 中新增的特性, 那它有什么用呢?别急,我们通过 Vue2.x 中的一些场景来认识它的作用。Vue2.x 中应该经常遇到这样的场景: 

  1. <template>  
  2. <div>  
  3.     <div v-if="!loading">  
  4.         ...  
  5.     </div>  
  6.     <div v-if="loading">  
  7.         加载中...  
  8.     </div>  
  9. </div>  
  10. </template> 

在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。如果你使用过vue-async-manager这个插件来完成上面的需求, 你对Suspense可能不会陌生,Vue3.x 感觉就是参考了vue-async-manager. Vue3.x 新出的内置组件Suspense, 它提供两个template slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单。:::warning 如果使用 Suspense, 要返回一个 promise :::Suspense 组件的使用: 

  1. <Suspense>  
  2.       <template #default>  
  3.           <async-component></async-component>  
  4.       </template>  
  5.       <template #fallback>  
  6.           <div>  
  7.               Loading...  
  8.           </div>  
  9.       </template> 
  10.  </Suspense> 

asyncComponent.vue: 

  1. <<template>  
  2. <div>  
  3.     <h4>这个是一个异步加载数据</h4>  
  4.     <p>用户名:{{user.nickname}}</p>  
  5.     <p>年龄:{{user.age}}</p>  
  6. </div>  
  7. </template>  
  8. <script>  
  9. import { defineComponent } from "vue"  
  10. import axios from "axios"  
  11. export default defineComponent({  
  12.     setup(){  
  13.         const rawData = await axios.get("http://xxx.xinp.cn/user")  
  14.         return {  
  15.             user: rawData.data  
  16.         }  
  17.     }  
  18. })  
  19. </script> 

从上面代码来看,Suspense 只是一个带插槽的组件,只是它的插槽指定了default 和 fallback 两种状态。

片段(Fragment)

在 Vue2.x 中, template中只允许有一个根节点: 

  1. <template>  
  2.     <div>  
  3.         <span></span>  
  4.         <span></span>  
  5.     </div>  
  6. </template> 

但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽: 

  1. <template>  
  2.     <span></span>  
  3.     <span></span>  
  4. </template> 

更好的 Tree-Shaking

Vue3.x 在考虑到 tree-shaking的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module的引用方式进行具名引用, 比如在 Vue2.x 中,我们要使用 nextTick: 

  1. // vue2.x  
  2. import Vue from "vue"  
  3. Vue.nextTick(()=> 
  4.     ...  
  5. }) 

Vue.nextTick() 是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick() 只是 Vue.nextTick() 的一个简易包装,只是为了方便而把后者的回调函数的 this 绑定到了当前的实例。虽然我们借助webpack的tree-shaking, 但是不管我们实际上是否使用Vue.nextTick(), 最终都会进入我们的生产代码, 因为 Vue 实例是作为单个对象导出的, 打包器无法坚持出代码总使用了对象的哪些属性。在 Vue3.x 中改写成这样: 

  1. import { nextTick } from "vue"  
  2. nextTick(() => 
  3.     ... 
  4. }) 

受影响的 API

这是一个比较大的变化, 因为以前的全局 API 现在只能通过具名导入,这一更改会对以下 API 有影响:

  •  Vue.nextTick
  •  Vue.observable(用 Vue.reactive 替换)
  •  Vue.version
  •  Vue.compile(仅限完整版本时可用)
  •  Vue.set(仅在 2.x 兼容版本中可用)
  •  Vue.delete(与上同)

内置工具

出来上面的 API 外, 还有许多内置的组件 以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。::: 前面都是 Vue3.0 的一些新特性,后面着重介绍一下相对于 Vue2.x 来说, 有什么变更呢?

变更

slot 具名插槽语法

在 Vue2.x 中, 具名插槽的写法: 

  1. <!--  子组件中:-->  
  2. <slot name="title"></slot> 

在父组件中使用: 

  1. <template slot="title">  
  2.     <h1>歌曲:成都</h1>  
  3. <template> 

如果我们要在 slot 上面绑定数据,可以使用作用域插槽,实现如下: 

  1. // 子组件  
  2. <slot name="content" :data="data"></slot>  
  3. export default {  
  4.     data(){  
  5.         return{  
  6.             data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]  
  7.         }  
  8.     }  
  9.  
  1. <!-- 父组件中使用 -->  
  2. <template slot="content" slot-scope="scoped">  
  3.     <div v-for="item in scoped.data">{{item}}</div>  
  4. <template> 

在 Vue2.x 中具名插槽和作用域插槽分别使用slot和slot-scope来实现, 在 Vue3.0 中将slot和slot-scope进行了合并同意使用。Vue3.0 中v-slot: 

  1. <!-- 父组件中使用 -->  
  2.  <template v-slot:content="scoped">  
  3.    <div v-for="item in scoped.data">{{item}}</div>  
  4. </template> 
  5.  <!-- 也可以简写成: -->  
  6. <template #content="{data}">  
  7.     <div v-for="item in data">{{item}}</div>  
  8. </template> 

自定义指令

首先回顾一下 Vue 2 中实现一个自定义指令: 

  1. // 注册一个全局自定义指令 `v-focus`  
  2. Vue.directive('focus', {  
  3.   // 当被绑定的元素插入到 DOM 中时……  
  4.   inserted: function (el) {  
  5.     // 聚焦元素  
  6.     el.focus()  
  7.   } 
  8. }) 

在 Vue 2 中, 自定义指令通过以下几个可选钩子创建:

  •  bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  •  inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  •  update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  •  componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  •  unbind:只调用一次,指令与元素解绑时调用。

在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下:

所以在 Vue3 中, 可以这样来自定义指令: 

  1. const { createApp } from "vue"  
  2. const app = createApp({})  
  3. app.directive('focus', {  
  4.     mounted(el) {  
  5.         el.focus()  
  6.     }  
  7. }) 

然后可以在模板中任何元素上使用新的 v-focus指令, 如下:

  1. <input v-focus /> 

v-model 升级

在使用 Vue 3 之前就了解到 v-model 发生了很大的变化, 使用过了之后才真正的 get 到这些变化, 我们先纵观一下发生了哪些变化, 然后再针对的说一下如何使用:

  •  变更:在自定义组件上使用v-model时, 属性以及事件的默认名称变了
  •  变更:v-bind的.sync修饰符在 Vue 3 中又被去掉了, 合并到了v-model里
  •  新增:同一组件可以同时设置多个 v-model
  •  新增:开发者可以自定义 v-model修饰符

有点懵?别着急,往下看 在 Vue2 中, 在组件上使用 v-model其实就相当于传递了value属性, 并触发了input事件: 

  1. <!-- Vue 2 -->  
  2. <search-input v-model="searchValue"><search-input>  
  3. <!-- 相当于 -->  
  4. <search-input :value="searchValue" @input="searchValue=$event"><search-input> 

这时v-model只能绑定在组件的value属性上,那我们就不开心了, 我们就像给自己的组件用一个别的属性,并且我们不想通过触发input来更新值,在.async出来之前,Vue 2 中这样实现: 

  1. // 子组件:searchInput.vue  
  2. export default {  
  3.     model:{  
  4.         prop: 'search',  
  5.         event:'change'  
  6.     }  

修改后, searchInput 组件使用v-model就相当于这样: 

  1. <search-input v-model="searchValue"><search-input>  
  2. <!-- 相当于 -->  
  3. <search-input :search="searchValue" @change="searchValue=$event"><search-input> 

但是在实际开发中,有些场景我们可能需要对一个 prop 进行 “双向绑定”, 这里以最常见的 modal 为例子:modal 挺合适属性双向绑定的,外部可以控制组件的visible显示或者隐藏,组件内部关闭可以控制 visible属性隐藏,同时 visible 属性同步传输到外部。组件内部, 当我们关闭modal时, 在子组件中以 update:PropName 模式触发事件:

  1. this.$emit('update:visible', false) 

然后在父组件中可以监听这个事件进行数据更新:

  1. <modal :visible="isVisible" @update:visible="isVisible = $event"></modal> 

此时我们也可以使用v-bind.async来简化实现:

  1. <modal :visible.async="isVisible"></modal> 

上面回顾了 Vue2 中v-model实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢? 在 Vue3 中, 在自定义组件上使用v-model, 相当于传递一个modelValue 属性, 同时触发一个update:modelValue事件: 

  1. <modal v-model="isVisible"></modal>  
  2. <!-- 相当于 -->  
  3. <modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal> 

如果要绑定属性名, 只需要给v-model传递一个参数就行, 同时可以绑定多个v-model: 

  1. <modal v-model:visible="isVisible" v-model:content="content"></modal>  
  2. <!-- 相当于 -->  
  3. <modal  
  4.     :visible="isVisible"  
  5.     :content="content"  
  6.     @update:visible="isVisible"  
  7.     @update:content="content"  
  8. /> 

不知道你有没有发现,这个写法完全没有.async什么事儿了, 所以啊,Vue 3 中又抛弃了.async写法, 统一使用v-model

异步组件

Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下: 

  1. <template>  
  2.   <!-- 异步组件的使用 -->  
  3.   <AsyncPage />  
  4. </tempate>  
  5. <script>  
  6. import { defineAsyncComponent } from "vue";  
  7. export default {  
  8.   components: {  
  9.     // 无配置项异步组件  
  10.     AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),  
  11.     // 有配置项异步组件  
  12.     AsyncPageWithOptions: defineAsyncComponent({  
  13.    loader: () => import(".NextPage.vue"),  
  14.    delay: 200,  
  15.    timeout: 3000,  
  16.    errorComponent: () => import("./ErrorComponent.vue"),  
  17.    loadingComponent: () => import("./LoadingComponent.vue"),  
  18.  })  
  19.   },  
  20.  
  21. </script> 

本文代码已经上传Github 

https://github.com/SandySY/vue-notes 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2020-10-13 08:24:31

Vue3.0系列

2009-09-25 17:26:55

使用Hibernate

2009-09-08 16:02:47

Linq使用Group

2010-04-21 14:53:46

Oracle游标

2009-10-15 09:27:00

2009-08-31 14:45:07

Visual C# 3

2020-09-18 14:01:21

vue3.0

2009-09-16 17:13:54

学习Linq

2009-09-29 16:32:11

OJB Hiberna

2009-08-19 09:24:43

AJAX引擎经验总结

2010-08-06 13:43:45

Flex调试

2020-08-25 09:50:35

Vue3.0命令前端

2022-02-06 22:13:47

VueVue3.0Vue项目

2009-11-17 11:24:00

PHP应用技巧

2009-09-11 13:29:31

LINQ查询操作

2009-08-20 17:35:47

Servlet和JSP

2009-09-16 17:44:54

LINQ to SQL

2010-06-12 17:37:18

UML实践指南

2009-09-08 10:57:55

LINQ查询操作

2011-07-08 13:15:52

JSP
点赞
收藏

51CTO技术栈公众号