Vue3的来由

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

回顾Vue2.x实现加减

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

上面代码只是实现了对count的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:
在实战项目中邂逅Vue3.0 - 图1
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:
在实战项目中邂逅Vue3.0 - 图2
甚至一个功能还会依赖其他功能,这样全搅合在一起,后期的维护成本可想而知。当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转,这其中的痛苦写过的都知道。
那开发者们就会想, 如果一个需求可以按照逻辑进行分割,将上面这张图变成下面这张图,是不是就清晰很多了呢,同时这样的代码可读性和可维护性也都将变的更高:
在实战项目中邂逅Vue3.0 - 图3
那么其实在vue2.x版本官方也给出了对应的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:

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

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

Composition API(部分)

在实战项目中邂逅Vue3.0 - 图4

setup

setup是Vue3.x新增的一个选项,他是组件内使用Composition API的入口。
setup执行时机
实践出真知,如下:

  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. })

在实战项目中邂逅Vue3.0 - 图5
可以看出,setup执行时机是在beforeCreate之前执行,详细的可以看后面生命周期讲解。
WARNING
由于在执行setup时尚未创建组件实例,因此在 setup 选项中没有 this。
setup参数
使用setup时,它接受两个参数:

  1. props: 组件传入的属性
  2. context: Vue实例上下文对象

    #Props

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

export default defineComponent ({ setup(props, context) { const { secooName } = props console.log(secooName) }, })

  1. 那在开发中我们**想要使用解构,还能保持props的响应式**,有没有办法解决呢?大家可以思考一下,在后面toRefs学习的地方为大家解答。
  2. <a name="ZaSl1"></a>
  3. #### [#](https://fe.secoo.com/article/vue/%E5%9C%A8%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE%E4%B8%AD%E9%82%82%E9%80%85Vue3.0.html#context)Context
  4. 接下来我们来说一下setup接受的第二个参数context,我们前面说了setup中不能访问Vue2中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrsslotsemit,分别对应Vue2.x中的$attrs属性、slots插槽 $emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
  5. ```css
  6. // demo.vue
  7. export default {
  8. setup(props, context) {
  9. // Attribute (非响应式对象)
  10. console.log(context.attrs)
  11. // 插槽 (非响应式对象)
  12. console.log(context.slots)
  13. // 触发事件 (方法)
  14. console.log(context.emit)
  15. }
  16. }

reactive、ref与toRefs

在vue2.x中, 定义数据都是在data中, 但是Vue3.x 可以使用reactive和ref来进行数据定义。
那么ref和reactive他们有什么区别呢?分别什么时候使用呢?
区别:
图片引用自VueConf 2021大会上分享的一张ppt
在实战项目中邂逅Vue3.0 - 图6
使用场景:
ref和reactive都可以处理对js对象的双向绑定,但是vue官网建议:如果将对象分配为 ref 值,则通过 reactive 方法使该对象具有高度的响应式。如果对于处理js基础类型的双向绑定,那么只能使用ref来定义,因为reactive函数仅可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。
总结:

  1. js基础类型定义使用ref
  2. js引用类型建议使用reactive

接下来使用代码展示一下ref、reactive的使用:

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

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

  1. <template>
  2. <div>
  3. <p>count: {{ count }}</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 count = ref(0);
  13. const user = reactive({ nickname: "hhh", age: 26, gender: "男" });
  14. setInterval(() => {
  15. count.value++
  16. user.age++
  17. }, 1000)
  18. const { nickname, age } = toRefs(user)
  19. return {
  20. count,
  21. nickname,
  22. age
  23. }
  24. },
  25. });
  26. </script>

这里有一个个人建议的点是,有的同学可能会直接在返回值中去解构,像下面代码这样:

  1. return {
  2. count,
  3. ...toRefs(user)
  4. }

这样解构的方式有一个缺点就是导出的值不够直观,如果该数据是在当前组件中定义的,这样解构问题不是很大。如果这个数据是从vuex或其他依赖方法中获取的话,直接导出给页面绑定,那么在后期维护的时候,你可能会疑惑页面绑定的值不知从何而来,像下面代码这样:
不推荐示例

  1. setup() {
  2. // 伪代码,假设vuex state里有data: { nickname: 'hhh'、age: 26 }这两条数据
  3. const store = useStore()
  4. return {
  5. ...store.state.data
  6. }
  7. },

推荐示例

  1. setup() {
  2. // 伪代码,假设vuex state里有data: { nickname: 'hhh'、age: 26 }这两条数据
  3. const store = useStore()
  4. const { nickname, age } = store.state.data
  5. return {
  6. nickname,
  7. age
  8. }
  9. },

所以这里总结,如果模版依赖的数据是当前组件中定义的,使用…toRefs(data) 方式导出,否则使用const { data1, data2 } = toRefs(data) 方式导出,这样相对更加直观,也方便维护。

生命周期钩子

我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片是根据官网翻译后绘制的): 在实战项目中邂逅Vue3.0 - 图7
从图中我们可以看到Vue3.0新增了setup,这个在前面我们也详细说了, 然后是将Vue2.x中的beforeDestroy名称变更成beforeUnmount; destroyed表更为unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mount和unmount的过程。其他Vue2中的生命周期仍然保留。
上边生命周期图中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:
在实战项目中邂逅Vue3.0 - 图8
我们可以看到beforeCreate和created被setup替换了(但是Vue3中你仍然可以使用, 因为Vue3是向下兼容的, 也就是你实际使用的是vue2的)。其次,钩子命名都增加了on; Vue3.x还新增用于调试的钩子函数onRenderTriggered和onRenderTricked
下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x中的钩子是需要从vue中导入的:

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

关于生命周期相关的内容就介绍到这里。

computed 的用法

接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。

  1. const count = ref(1)
  2. const plusOne = computed(() => count.value + 1)
  3. console.log(plusOne.value) // 2
  4. plusOne.value++ // 错误

或者,它也可以使用具有 get 和 set 函数的对象来创建可写的 ref 对象。

  1. const count = ref(1)
  2. const plusOne = computed({
  3. get: () => count.value + 1,
  4. set: val => {
  5. count.value = val - 1
  6. }
  7. })
  8. plusOne.value = 1
  9. console.log(count.value) // 0

watch 与 watchEffect 的用法

watch

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

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

参数说明:

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

侦听ref定义的数据

  1. import { defineComponent, ref, reactive, toRefs, watch } from "vue";
  2. export default defineComponent({
  3. setup() {
  4. const year = ref(0)
  5. setTimeout(() => {
  6. year.value ++
  7. },1000)
  8. watch(year, (newVal, oldVal) =>{
  9. console.log("新值:", newVal, "老值:", oldVal);
  10. })
  11. return {
  12. year
  13. }
  14. },
  15. });

侦听reactive定义的数据

  1. const state = reactive({ nickname: "hhh", age: 26 });
  2. setTimeout(() => {
  3. state.age++
  4. },1000)
  5. // 修改age值时会触发 watch的回调
  6. watch(
  7. () => state.age,
  8. (curAge, preAge) => {
  9. console.log("新值:", curAge, "老值:", preAge);
  10. }
  11. );

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

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

侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:

  1. const state = reactive({
  2. person: {
  3. id: 100,
  4. attrs: {
  5. height: "175cm",
  6. weight: "70kg"
  7. }
  8. }
  9. });
  10. watch(() => state.person, (newVal, oldVal) => {
  11. console.log("新值:", newVal, "老值:", oldVal);
  12. }, {
  13. deep: true
  14. });

如果不使用第三个参数deep:true, 是无法监听到数据变化的。
前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。
stop 停止监听
我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:

  1. const stopWatchPerson = watch(() => state.person, (newVal, oldVal) => {
  2. console.log("新值:", newVal, "老值:", oldVal);
  3. }, {
  4. deep: true
  5. });
  6. setTimeout(() => {
  7. // 停止监听
  8. stopWatchPerson()
  9. }, 3000)

watchEffect

为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
我对其的个人理解是,如果传入回调函数的数据是具有依赖性的,那么这个数据有变化watchEffect就会执行。

  1. const count = ref(0)
  2. watchEffect(() => console.log(count.value))
  3. // -> logs 0
  4. setTimeout(() => {
  5. count.value++
  6. // -> logs 1
  7. }, 100)

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

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

    自定义 Hooks

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

function useCount (initValue = 1) { const count = ref(initValue)

const multiple = computed(() => count.value * 2)

const increase = (arg) => { if (typeof arg !== ‘undefined’) { count.value += arg } else { count.value += 1 } }

const decrease = (arg) => { if (typeof arg !== ‘undefined’) { count.value -= arg } else { count.value -= 1 } }

return { count, multiple, increase, decrease } }

export default useCount

  1. 接下来在组件中使用useCount这个hook:
  2. ```css
  3. <template>
  4. <p>count: {{ count }}</p>
  5. <p>倍数: {{ multiple }}</p>
  6. <div>
  7. <button @click="increase">加1</button>
  8. <button @click="() => decrease(2)">减2</button>
  9. </div>
  10. </template>
  11. <script>
  12. import useCount from "@/hooks/useCount";
  13. setup() {
  14. const { count, multiple, increase, decrease } = useCount(9);
  15. return {
  16. count,
  17. multiple,
  18. increase,
  19. decrease,
  20. };
  21. },
  22. </script>

开篇Vue2.x实现,分散在data, methods, computed等, 如果刚接手项目,实在无法快速将data字段和methods关联起来,而Vue3的方式可以很明确的看出,将count相关的逻辑聚合在一起, 看起来清晰多了, 而且useCount还可以扩展更多的功能。

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

其实在Vue3.x 还没有发布beta的时候, 很火的一个话题就是Vue3.x 将使用Proxy 取代Vue2.x 版本的 Object.defineProperty。那么为何要将Object.defineProperty换掉呢,咱们可以简单聊一下。
我相信大家在刚上手Vue2.x的时候就会经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,大家是否也一度充满疑惑。后来的学习过程中开始接触源码,才知道一切的根源都是Object.defineProperty。

简单对比

想要深入了解为什么Vue3.0使用Proxy实现数据劫持,而弃掉defineProperty的原因的小伙伴,可以自行查阅相关资料,这里我们就简单对比一下Object.defineProperty与Proxy:

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

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

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

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

手写一个简单的Vue3.0源码的响应式

  1. function trigger () {
  2. console.log('视图更新触发');
  3. }
  4. function isObject(target) {
  5. return typeof target === 'object' && target !== null
  6. }
  7. // 哈希表
  8. let toProxy = new WeakMap()
  9. let toRaw = new WeakMap()
  10. // 响应式代理函数
  11. function reactive(target) {
  12. if (!isObject(target)) {
  13. return target
  14. }
  15. const proxy = toProxy.get(target)
  16. if (proxy) {
  17. return proxy
  18. }
  19. if (toRaw.has(target)) {
  20. return target
  21. }
  22. const handles = {
  23. set(target, key, value, receiver) {
  24. const hadKey = Array.isArray(target) ? key < target.length : target.hasOwnProperty(key)
  25. if (!hadKey) {
  26. trigger() // add
  27. } else {
  28. trigger() // set
  29. }
  30. let res = Reflect.set(target, key, value, receiver)
  31. return res
  32. },
  33. get(target, key) {
  34. const val = Reflect.get(target, key)
  35. if (isObject(target[key])) {
  36. return reactive(val)
  37. }
  38. return val
  39. },
  40. deleterProperty(target, key) {
  41. delete Reflect.deleteProperty(target, key)
  42. }
  43. }
  44. const observed = new Proxy(target, handles)
  45. toProxy.set(target, observed)
  46. toRaw.set(observed)
  47. return observed
  48. }

Teleport

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

Teleport 是什么

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

Teleport的使用

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

  1. // index.html
  2. <body>
  3. <div id="app"></div>
  4. + <div id="dialog"></div>
  5. </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的使用,不相关的代码就省略了。

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

Dom渲染效果如下: 在实战项目中邂逅Vue3.0 - 图9
可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与

同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制。

Suspense

WARNING
试验性
Suspense 是一个试验性的新特性并且其 API 可能随时更改。特此声明以便社区能够为当前的实现提供反馈。
它不应该被用在生产环境。
虽然官网已经申明这是一个试验性的特性,不应该在生产环境使用,但是我们也可以提前了解下这个新增的特性的用处。我们先通过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来控制数据显示。
Vue3.x新推出的内置组件Suspense, 它提供两个template slot, 刚开始会渲染一个fallback状态下的内容,直到到达某个条件后才会渲染default状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单了
WARNING
如果使用Suspense让后代组件触发 fallback 的方式是从 setup 函数返回一个promise
Suspense 组件的使用:

  1. <Suspense>
  2. <template #default>
  3. <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. async setup(){
  13. const rawData = await axios.get("http://xxx.sec.cn/user")
  14. return {
  15. user: rawData.data
  16. }
  17. }
  18. })
  19. </script>

片段(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 Vue, { nextTick, reactive } from "vue"
  2. Vue.reactive // undefined
  3. nextTick(() => {
  4. ...
  5. })

常用的Vue语法变更

自定义指令

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

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

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

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

在 Vue3 中对自定义指令的 API进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下: 在实战项目中邂逅Vue3.0 - 图10
所以在 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 升级

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

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

在 Vue2 中, 在组件上使用 v-model 其实就相当于传递了value属性, 并触发了input事件:

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

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

  1. // 子组件:searchInput.vue
  2. export default {
  3. model: {
  4. prop: 'search', // 为了和v-model默认的value区分,将prop改为自定义的名称
  5. event: 'change' // 为了和v-model默认的input区分,将event改为change
  6. }
  7. }

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

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

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

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

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

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

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

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

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

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

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

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

由此看出,这个写法完全没有.async什么事了, 所以,Vue3 中又抛弃了.async写法, 统一使用v-model来进行数据的双向绑定了。
最后关于v-model的就是 Vue3.x 新增了自定义修饰符,让我们来看下这个功能。
在Vue2.x中我们知道v-model提供了.trim,.number,.lazy 3个内置修饰符,但是在某些情况下,我们可能还需要添加自己定义的修饰符。比如像下面这个需求。
需求:用户输入的字符中如果有字母,则进行大写转换。
让我们创建一个自第一修饰符capitalize,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop。
需要注意的是,当组件的created生命周期钩子触发时,modelModifiers prop 会包含其值为true的capitalize修饰符。

  1. <my-component v-model.capitalize="myText"></my-component>
  1. app.component('my-component', {
  2. props: {
  3. modelValue: String,
  4. modelModifiers: {
  5. default: () => ({})
  6. }
  7. },
  8. emits: ['update:modelValue'],
  9. template: `
  10. <input type="text"
  11. :value="modelValue"
  12. @input="$emit('update:modelValue', $event.target.value)">
  13. `,
  14. created() {
  15. console.log(this.modelModifiers) // { capitalize: true }
  16. }
  17. })

现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 元素触发 input 事件时,我们都将字符串大写。

  1. <div id="app">
  2. <my-component v-model.capitalize="myText"></my-component>
  3. {{ myText }}
  4. </div>
  1. const app = Vue.createApp({
  2. data() {
  3. return {
  4. myText: ''
  5. }
  6. }
  7. })
  8. app.component('my-component', {
  9. props: {
  10. modelValue: String,
  11. modelModifiers: {
  12. default: () => ({})
  13. }
  14. },
  15. emits: ['update:modelValue'],
  16. methods: {
  17. emitValue(e) {
  18. let value = e.target.value
  19. if (this.modelModifiers.capitalize) {
  20. value = value.charAt(0).toUpperCase() + value.slice(1)
  21. }
  22. this.$emit('update:modelValue', value)
  23. }
  24. },
  25. template: `<input
  26. type="text"
  27. :value="modelValue"
  28. @input="emitValue">`
  29. })
  30. app.mount('#app')

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + “Modifiers”:

  1. <my-component v-model:description.capitalize="myText"></my-component>
  1. app.component('my-component', {
  2. props: ['description', 'descriptionModifiers'],
  3. emits: ['update:description'],
  4. template: `
  5. <input type="text"
  6. :value="description"
  7. @input="$emit('update:description', $event.target.value)">
  8. `,
  9. created() {
  10. console.log(this.descriptionModifiers) // { capitalize: true }
  11. }
  12. })

ok, 到这里基本就将 Vue3.0 在这次实战项目中遇到或经常遇到的一些特性知识分享完毕了。希望对大家有所帮助~

总结

本人在实战项目中用Vue3新语法开发的一个最直观体验就是,灵活,复用逻辑解耦也变的更加方便,易维护。灵活性高的同时就要求我们要有组件逻辑解耦的能力,这样才能写好一个优秀的组件。还没用过的小伙伴赶快亲自体验一下吧~