一文看懂 Vue 3 到底有什么不同__Vue.js
发布于 4 年前 作者 banyungong 1729 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

上个月 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 的全局 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

回到顶部