上个月 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.component
、Vue.mixin
、Vue.extend
、Vue.nextTick
;常见的全局配置有:Vue.config.silent
、Vue.config.devtools
、Vue.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 的全局 API | 3.x 的应用实例 API |
---|---|
Vue.config | app.config |
Vue.config.productionTip | 移除 |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.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 变化的系列文章,欢迎各位前来拍砖指正!
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 劉凯里 原文链接:https://juejin.im/post/6856011196224126989