一文看懂 Vue 3 到底有什么不同

3,700 阅读3分钟

上个月 18 号,尤雨溪大大在知乎官宣:Vue 3 正式进入 RC 阶段。所谓 RC 阶段,就是 Release Candidate,代表 Vue 3 已经已经做好了发布的准备,这段时间会集中做一些准备性的工作,比如写写文档、写写注释以及改改 bug 什么的,不会再加入新的 feature 了。
之前的一篇文章里,我概括了 Vue 3 此次最引人注目的几个新特性:Composition API,新的响应式系统(基于 Proxy),Teleport 和 Suspense 等,这些都是全新版本,也就意味着没有历史包袱,学就对了。
我写这篇文章的目的,是要总结一些 Vue 2.x 版本中就有的特性,但它们在即将到来的 Vue 3 中有了较大的变化,俗称:Breaking Changes
掌握了这些变化后,基本就能平滑的从过渡到 Vue 3.0 中去(学废了抠 1,没学废抠眼珠子)。
话不多说,让我们现在就开始!

全局 API 需要通过 Vue 实例调用

众所周知,Vue 2.x 中有很多全局均可调用的 API 和配置,它们能在全局范围内改变 Vue 的行为。
比如常见的全局 API 有:Vue.componentVue.mixinVue.extendVue.nextTick;常见的全局配置有:Vue.config.silentVue.config.devtoolsVue.config.productionTip 等。
我们可以在自己的应用的任何地方调用这些 API 和配置,这样是很方便的,但是也会造成一些问题。
首先要明确的一点是,Vue 2.x 在设计之初,并没有一个叫做 App(应用)的概念,我们大多数人眼中的 App 只不过是通过 new Vue() 创建的一个 Vue 实例罢了。学过 JS 的我们都知道:由同一个 Vue 构造函数创建的实例会共享来自构造函数的配置(污染!)。
因此,为了规避这个问题,Vue 3 正式引入了 App 的概念,美其名曰:应用实例

创建一个应用实例:createApp

调用 createApp 会返回一个 Vue 应用实例:

import { createApp } from 'vue';
const app = createApp();

任何会在全局范围内影响 Vue 行为的 API 都会被迁移到应用实例当中去(也就是说,我们不能再 Vue.xxx 了,而需要改写成 app.xxx)。

2.x 的全局 API3.x 的应用实例 API
Vue.configapp.config
Vue.config.productionTip移除
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use

其他不会在全局影响 Vue 行为的 api 都已改造为具名导出的构建方式(named exports),就像之前尤雨溪尤大在直播里说的那样:为了支持 TreeShaking

全局 API 需要手动引入

既然聊到了 TreeShaking,那就顺着这个话题继续说下去(尽管 Vue 3 的 TreeShaking 官方已经在各种场合强调了很多次了)。

如果你还不知道 TreeShaking 是什么,建议去 Google 一下(没有梯子,百度也行)。

为了支持 TreeShaking,Vue 官方团队把能拆的都拆出来了,比如说我们之前用到的 Vue.nextTick,这个函数的代码是始终在 Vue 里面的,就算 build 了以后也还是存在。如果某些开发者没有用到这个 API 或者干脆用了 setTimeout ,理想情况下,源码中有关 $nextTick 函数的部分是不应该被打包进生产的代码 中的,于是,我们需要作出这样的改变:

// 2.x 版本
import Vue from 'vue';
Vue.nextTick(() => {
	console.log('Hi');
});

// 3.x 版本
import { nextTick } from 'vue';
nextTick(() => {
	console.log('Hi again');
});

收到影响的 API 有:

  • Vue.nextTick
  • Vue.observable(被 Vue.reactive 替换)
  • Vue.version
  • Vue.compile
  • Vue.set(仅存于兼容版本)
  • Vue.delete(仅存于兼容版本)

defineAsyncComponent 定义异步组件

在 Vue 2.x 版本中,如果想要实现页面组件的按需加载(Code Splitting),直接用动态 import 就可以了。但 Vue 3 中,新增了一个用来显示定义异步组件的方法:defineAsyncComponent

import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => import('./component'));
// 或者(更高级的用法)
const AsyncComponent = defineAsyncComponent({
	// 这里之前是 componnet,现在改叫 loader 了
    loader: () => import('./component'),
    delay: 1000,
    timeout: 3000,
    errorComponent: Error,
    loadingComponnet: Loading,
});

Render 函数的 API 改变

(如果你和我也一样,也是模版语法的使用者,那么这条就可以忽略了~)
Render 函数在 Vue 3 中做了如下变更:

  • h 需要从全局导入进来(不再是 render 函数的参数了)
  • VNodes 具备了更扁平的属性结构

来直观的感受一下写法上的不同:

// Vue 2.x 的写法
export default {
	render(h) {
    	return h('div');
    }
}

// Vue 3.x 的写法
import { h } from 'vue';
export default {
	render() {
    	return h('div');
    }
}

domProps` 是 VNode 属性中的一个“嵌套列表”:

{
    class: ['button', 'confirm-button'],
    style: { color: 'red' },
    attrs: { id: 'confirm' },
    domProps: { innerHTML: '' },
    on: { click: confirmCreate },
    key: 'submit-button',
}

在 3.x 版本中,VNode 的所有属性都已经实现了“扁平化”的处理:

{
    class: ['button', 'confirm-button'],
    style: { color: 'red' },
    id: 'submit',
    innerHTML: '',
    onClick: confirmCreate,
    key: 'submit-button',
}

其实我也很少用 render 函数,毕竟 template 还是蛮香的。

自定义指令的钩子函数改名

在 Vue 2.x 中,如果我们需要实现一个指令,那么一定会接触到指令的 5 个钩子函数:

  • bind**:当指令绑定在对应元素时触发。只会触发一次。
  • inserted:当对应元素被插入到 DOM 的父元素时触发。
  • update:当元素更新时,这个钩子会被触发(此时元素的后代元素还没有触发更新)。
  • componentUpdated:当整个组件(包括子组件)完成更新后,这个钩子触发。
  • unbind:当指令被从元素上移除时,这个钩子会被触发。也只触发一次。

来看个例子:

<h1 v-highlight="red">这是一串被高亮为红色的字</h1>
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value;
  }
});

如上是一个很灵活的做法,通过指令传值的做法,可以供开发者根据使用场景的不同提供不同的参数,以达到不同的效果。
而在 Vue 3 中,官方团队为了这些钩子“更有记忆点”,所以修改了它们的姓名,尽量与组件的生命周期钩子名称相近(主要还是为了增强代码的可读性以及风格统一,其实这两种“钩子”并没有什么卵关系):

  • bind => beforeMount
  • inserted => mounted
  • beforeUpdate: 新的钩子,会在元素自身更新前触发
  • update => 移除!
  • componentUpdated => updated
  • beforeUnmount: 新的钩子,当元素自身被卸载前触发
  • unbind => unmounted

所以,新版的自定义指令大概会长这个样子:

const NewDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {},
}

呐,上面的那个例子就会被改写成:

const app = Vue.createApp({});

app.directive('highlight', {
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value;
  },
});

改了名字之后,是不是更好记了呢?

v-model 全面升级

首先从上帝视角看一下 v-model 发生了什么改变:

  • 重大变更:在自定义组件上使用 v-model 时,属性以及事件的默认名称变了:
    • 属性:value :arrow_right: modelValue
    • 事件:input :arrow_right: update:modelValue
  • 重大变更v-bind.sync 修饰符和组件的 model 选项被干掉了,取而代之的是 v-model 参数
  • 新特性: 支持同一组件同时设置多个 v-model
  • 新特性:支持开发者自定义 v-model 修饰符

遥想当年,Vue 2.0 发布的时候,v-model 只能绑定在组件的 value 属性上。开发者可就着急了:“淦,我自己的组件想用一个别的属性都不行吗”,其实也是支持的:v-bind.sync 了解一下。

在 Vue 2.2 的版本,组件支持了自定义的 model ,开发者可以通过这个设置和 v-model 关联的属性和事件。用了几次还是挺香的,但还有一个没法 hack 的短板:单个组件无法绑定多个 v-model

在即将到来的 Vue 3 中,为了减少给开发者带来的迷惑,官方团队对双向绑定的 API 重新做了“标准化”处理,同时也让 v-model 的使用更加灵活。

2.x 是这么玩的

在 Vue 2.x 版本中,在组件上使用 v-model 相当于传递了 value 属性并触发了 input 事件:

<KyrieInput v-model="name" />
<!-- 不同的写法,相同的本质 -->
<KyrieInput :value="name" @input="name = $event" />

如果想要改变 v-model 绑定的属性或者事件,需要在子组件内添加一个 model

// KyrieInput.vue
export default {
    model: {
        prop: 'title',
        event: 'change',
    },
}

经过上面的改造,现在 KyrieInput 这个组件 v-model 的本质变成了下面这样:

<KyrieInput :title="name" @change="name = $event" />

使用 v-bind.sync 实现组件属性的双向绑定

在某些场景下,我们需要实现对组件属性的双向绑定,比如:实现一个弹窗组件,用一个 visible 属性控制弹窗的出现和隐藏,可以在组件内部和外部关闭这个弹窗。官方团队推荐用 update:propName 的方式去解决这个问题。比如上面 KyrieInput 的例子,我们可以用下面这句话在组件内部改变 title 的值:

this.$emit('update:title', '关注我的你很酷');

然后在父组件监听这个事件:

<KyrieInput :title="name" @update:title="name = $event" />

方便起见,.sync 修饰符应运而生:

<KyrieInput :title.sync="name" />

3.x 应该这么玩儿

在 3.x 版本中,在自定义组件上使用 v-model 相当于传递了一个 modelValue 属性以及触发一个 update:modelValue 事件:

<KyrieInput v-model="name" />
<!-- 这两行其实是一样的 -->
<KyrieInput :modelValue="name" @update:modelValue="name = $event" />

v-model 参数

如果要改变绑定的属性名,只需要给 v-model 传递一个参数就好了:

<KyrieInput v-model:title="name" />
<!-- 等同于 -->
<KyrieInput :title="name" @update:title="name = $event" />

不仅如此,这个写法还彻底代替了 .sync 修饰符,并且支持统一组件绑定多个 v-model

<KyrieInput v-model:title="name" v-model:content="info" />
<!-- 相当于 -->
<KyrieInput
	:title="name"
    @update:title="name = $event"
    :content="info"
    @update:content="info = $event"
/>

最后

这就是目前我总结的关于Vue 3 的重大变更,各位看官在做版本迁移(如非必要,大可不必)的时候一定要注意哦,或者你正想用 Vue 3 写一个新的项目,这些有重大改变的写法到时候一定要留意。
此外,我在自己的"攻肿号"中正在维护一个关于 Vue 3 变化的系列文章,欢迎各位前来拍砖指正!