一文告诉你 Vue3 的新特性好在哪里

2,490 阅读17分钟

本节将告诉您,相比 Vue 2,Vue 3 的优势是什么,以及 Vue 3 到底有哪些新特性值得我们学习

Vue 2 的核心模块和历史遗留问题

从下图你能看到,Vue 2 是一个响应式驱动的、内置虚拟 DOM、组件化、用在浏览器开发,并且有一个运行时把这些模块很好地管理起来的框架

image.png

Vue 2 能把上面所说的这些模块很好地管理起来,看起来已经足够好了。不过事实真的如此么?聪明的你估计已经猜到了,Vue 2 还是有缺陷的,所以后面才会升级迭代

我在下面列举了一些 Vue 2 常见的缺陷,你可以对照你的实际开发经验,看看是否也遇到过这些问题:

首先从开发维护的角度看,Vue 2 是使用 Flow.js 来做类型校验。但现在 Flow.js 已经停止维护了,整个社区都在全面使用 TypeScript 来构建基础库,Vue 团队也不例外

然后从社区的二次开发难度来说,Vue 2 内部运行时,是直接执行浏览器 API 的。但这样就会在 Vue 2 的跨端方案中带来问题,要么直接进入 Vue 源码中,和 Vue 一起维护,比如 Vue 2 中你就能见到 Weex 的文件夹

要么是要直接改为复制一份全部 Vue 的代码,把浏览器 API 换成客户端或者小程序的。比如 mpvue 就是这么做的,但是 Vue 后续的更新就很难享受到

最后从我们普通开发者的角度来说,Vue 2 响应式并不是真正意义上的代理,而是基于 Object.defineProperty() 实现的。对于 Object.defineProperty() 这个 API 的细节,我们在后面讲源码时会讲到,现在你只需要知道这个 API 并不是代理,而是对某个属性进行拦截,所以有很多缺陷,比如:删除数据就无法监听,需要 $delete 等 API 辅助才能监听到

并且,Option API 在组织代码较多组件的时候不易维护。对于 Option API 来说,所有的 methods、computed 都在一个对象里配置,这对小应用来说还好。但代码超过 300 行的时候,新增或者修改一个功能,就需要不停地在 data,methods 里跳转写代码,我称之为上下反复横跳


Vue 3 新特性

前面这些问题并不是 Vue 2 有意为之,大部分是发展的过程中碰见的。Vue 3 就是继承了 Vue 2 具有的响应式、虚拟 DOM,组件化等所有优秀的特点,并且全部重新设计,解决了这些历史包袱的新框架,是一个拥抱未来的前端框架

接下来我们就来具体看看 Vue 3 新特性,我将分成 6 个具体方面向你展开介绍。其中,响应式系统、Composition API 组合语法、新的组件和 Vite 是你需要重视的;自定义渲染器这方面的知识,你想用 Vue 开发跨端应用时会用到;如果你想对 Vue 源码作出贡献,RFC机制你也需要好好研究,并且得对 TypeScript 重构有很好的经验

1. RFC 机制

Vue 3 的第一个新特性和代码无关,而是 Vue 团队开发的工作方式

这个改变让 Vue 社区更加有活力,不管是课程后面会提到的 <script setup>,还是 Vue 3 引入的 ref API,你都可以在这个项目中看到每个需求从诞生到最终被 Vue 采纳的来龙去脉,这能帮助我们更好地了解 Vue 的发展

Vue 很长一段时间都是尤雨溪一个人维护,感慨尤雨溪战斗力的同时,社区也有很多人对 Vue 的稳定性提出质疑。后来尤雨溪吸纳了社区的人,并成立了 Core Team。Vue 3 在此基础之上更进一步,全面拥抱社区,任何对 Vue 感兴趣的人都可以参与新特性的讨论

image.png

RFC 的引入,让 Vue 生态更加开放,在开发方式的新特性之外,我们搞技术的还是要回归代码,下面我来说说 Vue 3 在代码层面所做的具体优化

2. 响应式系统

Vue 2 的响应式机制是基于 Object.defineProperty() 这个 API 实现的,此外,Vue 还使用了 Proxy,这两者看起来都像是对数据的读写进行拦截,但是 defineProperty 是拦截具体某个属性,Proxy 才是真正的“代理”

怎么理解这两者的区别呢?我们首先看 defineProperty 这个 API,defineProperty 的使用,要明确地写在代码里,下面是示例代码:

Object.defineProperty(obj, 'name', {
    get() {},
    set() {},
})

当项目里 读取 obj.name 和 修改 obj.name 的时候被 defineProperty 拦截;但 defineProperty 对不存在的属性无法拦截,所以 Vue 2 中所有数据必须要在 data 里声明

而且,如果 obj.name 是一个数组的时候,对数组的操作,并不会改变 obj.name 的指向

虽然我们可以通过拦截 .push 等操作实现部分功能,但是对数组的长度的修改等操作还是无法实现拦截,所以还需要额外的 $set 等 API

Proxy 这个 API 就是真正的代理了,我们先看它的用法:

new Proxy(obj, {
    get(key) { },
    set(key, value) { },
}

需要注意的是,虽然 Proxy 拦截 obj 这个数据,但 obj 具体是什么属性,Proxy 则不关心并且统一全都拦截;而且 Proxy 还可以监听更多的数据格式,比如 SetMap,这是 Vue 2 做不到的

更重要的是,我觉得 Proxy 代表一种方向,就是框架会越来越多的拥抱浏览器的新特性。在 Proxy 普及之前,我们是没有办法完整的监听一个 JavaScript 对象的变化,只能使用 Object.defineProperty() 去实现一部分功能

前端框架利用浏览器的新特性来完善自己,才会让前端这个生态更繁荣,抛弃旧的浏览器是早晚的事

这里你掌握 Proxy 的优势就可以了,具体的使用我们后面会自己手写一个,帮助你深入理解


3. 自定义渲染器

Vue 2 内部所有的模块都是揉在一起的,这样做会导致不好扩展的问题,刚才我也提到了这一点。Vue 3 是怎么解决这个问题的呢?那就是拆包,使用最近流行的 monorepo 管理方式,响应式、编译和运行时全部独立了,变成下图所示的模样:

image.png

我们能看到,在 Vue 3 的组织架构中,响应式独立了出来。而 Vue 2 的响应式只服务于 Vue,Vue 3 的响应式就和 Vue 解耦了,你甚至可以在 Node.js 和 React 中使用响应式

渲染的逻辑也拆成了平台无关渲染逻辑和浏览器渲染 API 两部分

在这个架构下,Node 的一些库,甚至 React 都可以依赖响应式。在任何时候,如果你希望数据被修改了之后能通知你,你都可以单独依赖 Vue 3 的响应式

那么,在你想使用 Vue 3 开发小程序、开发 canvas 小游戏以及开发客户端的时候,就不用全部 fork Vue 的代码,只需要实现平台的渲染逻辑就可以

image.png

就像动画片《战神金刚》,五个机器人可以独立执行任务,但关键时刻,高呼一声“我来组成头部”,就可以合体,从而发挥整体的作用。Vue 3 也是一样,响应式、编译和运行时几部分组合在一起就是运行在浏览器端的 Vue 3,每个模块又都可以独立扩展出新的功能


4. 全部模块使用 TypeScript 重构

众所周知 JavaScript 是弱类型的语言,而 TypeScript 可以给我们带来两个好处,一个就是带来了更方便的提示;其次就是让代码变得更健壮

我们还是结合例子来看看,在下面这段代码中,我们首先定义了 name 这个变量,在定义的时候标记的是一个字符串,因而后面给它赋值时,赋值为数字就会报错

之后,我们定义一个类型 Person,里面的变量 name 是字符串类型,变量 age 是数字类型。违反这个设置的数据就报错,这在多人协同和长期维护的项目里带来的收益是巨大的,因为这样可以使错误的代码在编译阶段就被发现,从而避免程序上线运行后,可能会发生的更大的异常错误

let name:string = '我是个靓仔'
    name = 1 // 报错
    interface Person {
    name: string;
    age: number;
    }
let me:Person = {
    name:'靓仔圣',
    age:18
}

所以大部分开源的框架都会引入类型系统,来对 JavaScript 进行限制。这样做的原因,就是我们前面提到的两点:第一点是,类型系统带来了更方便的提示;第二点是,类型系统让代码更健壮

Vue 2 那个时代基本只有两个技术选型,Facebook 家的 Flow.js 和微软家的 TypeScript。Vue 2 选 Flow.js 没问题,但是现在 Flow.js 被抛弃了。Vue 3 选择了 TypeScript,TypeScript 官方也对使用 TypeScript 开发 Vue 3 项目的团队也更加友好


5. Composition API 组合语法 🔥

先举个 Vue 2 中的简单例子,一个累加器,并且还有一个计算属性显示累加器乘以 2 的结果

<div id="app">
    <h1 @click="add"> {{count}} * 2 = {{double}} </h1>
</div>

<script>
export default {
    data(){
        return { count:1 }
    },
   methods:{
     add(){
        this.count++
    },
    computed:{
        double(){
           return this.count*2
       }
   }
}

再来看看 Vue 3 的写法

<div id="app">
   <h1 @click="add">{{state.count}} * 2 = {{double}}</h1>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
const {reactive,computed} = Vue
let App = {
   setup(){
     const state = reactive({ count:1 })
     const double = computed(()=>state.count*2)
     function add(){
       state.count++
     }
     return {state,add,double}
  }
}

Vue.createApp(App).mount('#app')
</script>

使用 Composition API 后,你是不是也觉得代码看起来很烦琐,没有 Vue 2Options API 的写法简单好懂,但 Options API 的写法也有几个很严重的问题:

  1. 由于所有数据都挂载在 this 之上,因而 Options API 的写法对 TypeScript 的类型推导很不友好

  2. 新增功能都得修改 data、method 等配置,并且代码量多的时候,会经常上下反复横跳

  3. 代码不好复用,Vue 2 的组件很难抽离通用逻辑,只能使用 mixin,还会带来命名冲突的问题


Vue 3Composition API ,虽然看起来烦琐了一些,但是带来了诸多好处:

  1. 所有 API 都是 import 引入的,页面中没用到的功能,打包的时候会被清理掉 ,减小包的大小

  2. 不再上下反复横跳,我们可以把一个功能模块都放在一起书写,维护更轻松

  3. 代码方便复用,可以把一个功能所有的逻辑数据封装在一个独立的函数里,复用代码非常容易

Composition API 对我们开发 Vue 项目起到了巨大的帮助。下面这个示例图很好地说明了问题:每一个功能模块的代码颜色一样,左边是 Options API,一个功能的代码零散的分布在 data,methods 等配置内,维护起来很麻烦,而右边的 Compositon API 就不一样了,每个功能模块都在一起维护

image.png

其实还可以更进一步,如果每个颜色块代码,我们都拆分出去一个函数,我们就会写出类似上面右侧风格的代码,每个数据来源都清晰可见,而且每个功能函数都可以在各个地方复用


6. 新一代工程化工具 Vite

Vite 不在 Vue 3 的代码包内,和 Vue 也不是强绑定,Vite 的竞品是 Webpack,而且按照现在的趋势看,使用率超过 Webpack 也是早晚的事

主要原理就是根据你的 import 依赖逻辑,形成一个依赖图,然后调用对应的处理工具,把整个项目打包后,放在内存里再启动调试

由于要预打包,所以复杂项目的开发,启动调试环境需要 3 分钟都很常见,Vite 就是为了解决这个时间资源的消耗问题出现的

下图展示了 Webpack 的工作原理,Webpack 要把所有路由的依赖打包后,才能开始调试

image.png


而下图所示的是 Vite 的工作原理,一开始就可以准备联调,然后根据首页的依赖模块,再去按需加载,这样启动调试所需要的资源会大大减少

image.png


Vue 2项目如何升级到Vue 3 ?

如何把 Vue 2 的项目升级到 Vue 3 ,这是 Vue 用户最关心的事情之一;但是也请不要着急,因为并不是所有项目都适合升级

就像苹果新出了款手机,哪怕新特性被人们说得天花乱坠,但是,是不是把老手机换掉,也需要斟酌,毕竟升级总是需要成本的

应不应该从 Vue 2 升级到 Vue 3 ?

应不应该升级?这个问题不能一概而论;如果你要开启一个新项目,那直接使用 Vue 3 无疑是最佳的选择

在我们使用 Vue 2 开发应用的时候,不管怎么去组织代码,总是无法避免在 data、template、methods 中上下反复横跳,这种弊端在项目规模上来之后会更加明显;而且由于 vue-cli 是基于 Webpack 开发的,当项目规模上来后,每执行一下,调试环境就要 1 分钟时间,这也是大部分复杂项目的痛点之一

而 Vue 3 的 Composition API 带来的代码组织方式更利于封装代码,维护起来也不会上下横跳;Vite 则带来了更丝滑的调试体验;Vue 3 的正式版已经发布有一年了,无论是辅助工具,还是周边库都已经非常完善了,足以胜任大型的项目开发;并且,现在也有越来越多的公司正在尝试和体验 Vue 3;所以新项目可以直接拥抱 Vue 3 的生态,这也是现在很多团队在做的尝试

而且对于 Vue 2,官方还会再维护两年,但两年后的问题和需求,官方就不承诺修复和提供解答了,现在继续用 Vue 2 其实是有这个隐患的

Vue 3 也不是没有问题,由于新的响应式系统用了 Proxy,会存在兼容性问题;也就是说,如果你的应用被要求兼容 IE11,就应该选择 Vue 2;而且,Vue 团队也已经放弃 Vue3 对 IE11 浏览器的支持;其实,官方原来是有计划在 Vue 3 中支持 IE11,但后来由于复杂度和优先级的问题,这个计划就搁置了下来

不过,站在 2021 看待现在前端的世界,你能发现浏览器和 JavaScript 本身已经有了巨大的发展;大部分的前端项目都在直接使用现代的语言特性,而且微软本身也在抛弃 IE,转而推广 Edge;所以 Vue 官方在重新思考后,决定让 Vue 3 全面拥抱未来,把原来准备投入到 Vue 3 上支持 IE11 的精力转投给 Vue 2.7

那么 Vue 2.7 会带来什么内容呢 ?

Vue 2.7 会移植 Vue 3 的一些新特性,让你在 Vue 2 的生态中,也能享受 Vue 3 的部分新特性;在 Vue 3 发布之前,Vue 2 项目中就可以基于 @vue/composition-api 插件,使用 Composition API 语法,Vue 2 会直接内置这个插件,在 Vue 2 中默认也可以用 Compositon 来组合代码

后面,我会带你学的 <script setup> 语法,也会在 Vue 2 中得到支持;当然,如果我们想用更精简的方式去组织代码,也是没问题的,因为 Vite 中也正式支持了 Vue 2

综上所述,我们用下图来回答这一讲开头的问题,要不要使用 Vue 3,还是要“因地制宜”,在不同的场景下,我们选择合适的方式即可

image.png

Vue 3 不兼容的那些写法

通过前面的分析,在选择 Vue 2 还是 Vue 3 这个问题上,相信你现在已经有了自己的取舍;如果最后你依然决定要升级 Vue 3,那我就先带你了解一下 Vue 3 不支持的那些写法、之后为你讲解它的生态现状,最后,我们再进入到实操升级的环节

了解一下 Vue 3 不兼容的那些具体语法,除了可以帮你在升级项目后,避免写的代码无法使用,还会让你更好地适应 Vue 3;同样,也为了避免八股文的形式,我在这里介绍几个重要的变更,后面课程中用到一些写法的时候,我再详细地告诉你;即使现在说太多细节,可能你也记不住

这一部分内容,主要是针对有 Vue 2 开发经验的,希望更快地适应 Vue 3 的同学;在全面实战 Vue 3 之前,你不必完整阅读官方的指南,因为 Vue 3 的大部分 API 都是对 Vue 2 兼容的

首先,我们来看一下 Vue 2 和 Vue 3 在项目在启动上的不同之处;在 Vue 2 中,我们使用 new Vue() 来新建应用,有一些全局的配置我们会直接挂在 Vue 上,比如我们通过 Vue.use 来使用插件,通过 Vue.component 来注册全局组件,如下面代码所示:

Vue.component('el-counter', {
    data(){
        return {count: 1}
    },
    template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

let VueRouter = require('vue-router')
Vue.use(VueRouter)

在上面的代码里,我们注册了一个 el-counter 组件,这个组件是全局可用的,它直接渲染一个按钮,并且在点击按钮的时候,按钮内的数字会累加

然后我们需要注册路由插件,这也是 Vue 2 中我们使用 vue-router 的方式;这种形式虽然很直接,但是由于全局的 Vue 只有一个,所以当我们在一个页面的多个应用中独立使用Vue 就会非常困难

看下面这段代码,我们在 Vue 上先注册了一个组件 el-counter,然后创建了两个 Vue 的实例;这两个实例都自动都拥有了 el-couter 这个组件,但这样做很容易造成混淆

Vue.component('el-counter',...)
new Vue({el:'#app1'})
new Vue({el:'#app2'})

为了解决这个问题,Vue 3 引入一个新的 API ,createApp,来解决这个问题,也就是新增了 App 的概念;全局的组件、插件都独立地注册在这个 App 内部,很好的解决了上面提到的两个实例容易造成混淆的问题。下面的代码是使用 createApp 的简单示例:

const { createApp } = Vue
const app = createApp({})
app.component(...)
app.use(...)
app.mount('#app1')
const app2 = createApp({})
app2.mount('#app2')

createApp 还移除了很多我们常见的写法,比如在 createApp 中,就不再支持 filter$on$off$set$delete 等 API;不过这都不用担心,后面我会告诉你怎么去实现类似这些 API 的功能

在 Vue 3 中,v-model 的用法也有更改;在后面讲到组件化,也就是我们需要深度使用 v-model 的时候,我会再细讲

Vue 3 生态现状介绍

在 Vue 生态中,现在所有官方库的工具都全面支持 Vue 3 了,但仍然有一些生态库还处于候选或者刚发布的状态;所以,升级 Vue 3 的过程中,除了 Vue 3 本身的语法变化,生态也要注意选择。有一些周边的生态库可能还存在不稳定的情况,开发项目的时候我们时刻关注项目的 GitHub 即可

Vue-cli4 已经提供内置选项,你当然可以选择它支持的 Vue 2;如果你对 Vite 不放心的话,Vue-cli4 也全面支持 Vue 3,这还是很贴心的

vue-router 是复杂项目必不可少的路由库,它也包含一些写法上的变化,比如从 new Router 变成 createRouter;使用方式上,也全面拥抱 Composition API 风格,提供了 useRouteruseRoute 等方法

Vuex 4.0 也支持 Vue 3,不过变化不大;有趣的是 Vue 官方成员还发布了一个 Pinia,Pania 的 API 非常接近 Vuex5 的设计,并且对 Composition API 特别友好,更优雅一些;在课程后续的项目里,我们会使用更成熟的 Vuex4;其他生态诸如 Nuxt、组件库 Ant-design-vue、Element 等等,都有 Vue 3 的版本发布

使用自动化升级工具进行 Vue 的升级

小项目不用多说,从 Vue 2 升级到 Vue 3 之后,对于语法的改变之处,我们挨个替换写法就可以;但对于复杂项目,我们需要借助几个自动化工具来帮我们过渡

首先是在 Vue 3 的项目里,有一个 @vue/compat 的库,这是一个 Vue 3 的构建版本,提供了兼容 Vue 2 的行为。这个版本默认运行在 Vue 2 下,它的大部分 API 和 Vue 2 保持了一致;当使用那些在 Vue 3 中发生变化或者废弃的特性时,这个版本会提出警告,从而避免兼容性问题的发生,帮助你很好地迁移项目;并且通过升级的提示信息,@vue/compat 还可以很好地帮助你学习版本之间的差异

在下面的代码中,首先我们把项目依赖的 Vue 版本换成 Vue 3,并且引入了@vue/compat

"dependencies": {
- "vue": "^2.6.12",
+ "vue": "^3.2.19",
+ "@vue/compat": "^3.2.19"
...
},
"devDependencies": {
- "vue-template-compiler": "^2.6.12"
+ "@vue/compiler-sfc": "^3.2.19"
}

然后给 vue 设置别名 @vue/compat,也就是以 compat 作为入口,代码如下:

// vue.config.js
module.exports = {
    chainWebpack: config => {
        config.resolve.alias.set('vue', '@vue/compat')
        ......
    }
}

这时你就会在控制台看到很多警告,以及很多优化的建议;我们参照建议,挨个去做优化就可以了

在 @vue/compat 提供了很多建议后,我们自己还是要慢慢做修改;但从另一个角度看,“偷懒”是优秀程序员的标志,社区就有能够做自动化替换的工具,比较好用的就是阿里出品的 gogocode,官方文档也写得很详细,就不在这里赘述了

自动化替换工具的原理很简单,和 Vue 的 Compiler 优化的原理是一样的,也就是利用编译原理做代码替换;如下图所示,我们利用 babel 分析左边 Vue 2 的源码,解析成 AST,然后根据 Vue 3 的写法对 AST 进行转换,最后生成新的 Vue 3 代码

image.png

对于替换过程的中间编译成的 AST,你可以理解为用 JavaScript 的对象去描述这段代码,这和虚拟 DOM 的理念有一些相似,我们基于这个对象去做优化,最终映射生成新的 Vue3 代码

关于 AST 的细节,在课程后面的 Vue 3 生态源码篇中,我会带你手写一个迷你版的 Vue 3 Compiler,那时你会对 AST 和它背后的编译原理有一个更深的认识