关注公众号: 微信搜索 前端工具人
; 收货更多的干货
一、 开篇
官方中文文档链接: https://vue3js.cn/docs/zh/
;
发布时间:2020年9月18号晚上,已正式发布 vue3.0 beta
版本
二、vue2.0 项目的建议
引用官方文档作者的话:
提示:
我们仍在开发 Vue 3 的专用迁移版本,该版本的行为与 Vue 2 兼容,运行时警告不兼容。如果你计划迁移一个非常重要的 Vue 2 应用程序,我们强烈建议你等待迁移版本完成以获得更流畅的体验
。
目前作者的意思是:对于 vue2.0
的项目强烈不建议
升到 vue3.0
;因为目前的beta
版本以及现有的框架及插件,不是很支持和兼容vue3.0
语法; 所以肯定有很多预想不到的问题;对于跃跃欲试升级vue3.0
的小伙伴们,只能等待官方的兼容版本开发完,在做迁移; 毕竟线上项目不是开玩笑的,出了一个bug
都可能是重大损失。 这个锅家里没矿的基本背不动...
三、介绍
Vue3带来些什么? 参考至: 公众号:前端早读课文章
详细文档请参考: 官方中文文档链接
- 更快
- 重构了
Virtual DOM
- 标记静态内容,并区分动态内容
- 更新时只
diff
动态的部分
- 重构了
双向数据绑定
Object.defineProperty() --> Proxy API
Proxy
对于复杂的数据结构减少了循环递归的监听;初始渲染循环递归是非常耗性能的;Proxy
对于数组的变异方法(会修改原数组),不在需要单独用数组原生方法重写、处理- 语法也比
defineProperty
简洁多了,直接监听某个属性即可;
- 事件缓存
vue2
中,针对绑定事件,每次触发都要重新生成全新的function
去更新;Vue3
中,提供了事件缓存对象cacheHandlers
,当cacheHandlers
开启的时候,编译会自动生成一个内联函数,将其变成一个静态节点,这样当事件再次触发时,就无需重新创建函数直接调用缓存的事件回调方法即可
- 重构了
- 更小 (
Tree shaking
支持)- 简而言之: 不会把所有的都打包进来,只会打包你用到的
api
;大项目你会发现热加载、初始渲染提升了很多 - 很大程度的减少了开发中的冗余代码,提升编译速度
- 简而言之: 不会把所有的都打包进来,只会打包你用到的
- 更易于维护
Vue3
从Flow
迁移到TypeScript
- 多人协同开发的情况下,用了
TypeScript
之后的酸爽你会吐槽,为什么早不出现TypeScript
- 多人协同开发的情况下,用了
- 代码目录结构遵循monorepo
- 核心观点: 代码分割到一个个小的模块中, 开发者大部分只是工作在少数的几个文件夹,并且也只会编译自己负责的模块;而不是整个项目编译
- 新功能和特性
Composition API
- 不要在意越来越像react-hook;毕竟别人的优点是值得自己学习的;
Composition API
函数式开发,很大程度的提高组件、业务逻辑的复用性;高度解耦;提升代码质量、开发效率;减少代码体积
- 提升开发效率
vite
的支持 (当然目前来说vite
功能还不够强大和稳定, 但尤大把它作为vue3
官方构建工具,那肯定尤大会完善它的; 可自由选择webpack
还是vite
)vite
在开发环境下基于浏览器原生ES imports
开发,在生产环境下基于Rollup
打包- 快速的冷启动
- 即时的模块热更新
- 真正的按需编译
vue2.0
相信很多小伙伴都是结合webpack
开发; 但是有没有发现初期项目小的时候很爽运行、编译、热加载都很快; 项目一大... 打包、运行、改个功能热加载的时候... 我们先去上个厕所/接个水;忙的时候挺烦这环节vue3.0
结合vite
作者的介绍是不跟项目体积庞大而影响,开始啥样现在也啥样; 当然夸张是夸张了点, 但是相差应该不大;
四、对 vue2 的变化
4.1. 生命周期的变化
- 改成了按需引入, 大部分需要 + on;
- vue3取消beforeCreate、 created了这两个钩子,统一用setup代替;
- beforeDestroy -> onBeforeUnmount; destroyed -> onUnmounted;
4.2. 多根节点
vue2.x 需要去将组件包裹在 div 中,否则报错;
4.3. 异步组件
- Vue3 提供 Suspense组件,允许等待异步组件时渲染内容;
- 附带两个命名插槽:default和fallback。Suspense确保加载完异步内容时显示默认插槽,并将fallback插槽用作加载状态;
4.4. Teleport
- vue3 提供Teleport组件可将部分DOM移动到 Vue app之外的位置。比如项目中常见的Dialog组件
4.5. Composition 组合式API 及 新增了Hooks
- Vue2 是 选项式API(Option API),一个逻辑会散乱在文件不同位置(data、props、computed、watch等),导致代码的可读性变差,需要上下来回跳转文件位置;
- Composition API 增强了代码的可读性、内聚性,组合式API 还提供了较为完美的逻辑复用性方案
- 整合业务代码逻辑,提取公共逻辑(vue2采用mixin-命名冲突数据来源不清晰)
4.6. 数据劫持优化
- Vue2 针对常用数组原型方法push、pop、shift、unshift、splice、sort、reverse进行了hack处理;提供Vue.set监听对象/数组新增属性。对象的新增/删除响应,还可以new个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象
- Vue3.x改用
Proxy
替代Object.defineProperty
- Proxy只会代理对象的第一层,Vue3是怎样处理这个问题的呢?
- 判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
- 监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
- Proxy 与 Object.defineProperty 优劣对比
- Proxy 可以直接监听对象而非属性 (支持 Map、Set、WeakMap 和 WeakSet)
- Proxy 可以直接监听数组的变化
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
- Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平
- Proxy一定要配合Reflect使用。恰恰是为什么触发代理对象的劫持时保证正确的 this 上下文指向
4.7. 编译优化 虚拟DOM
- Vue3 相比于 Vue2 虚拟DOM 上增加patchFlag字段, 值 1 代表节点为动态文本节点,那在 diff 过程中,只需比对文本对容,无需关注 class、style等。除此之外,发现所有的静态节点,都保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建
4.8. 事件缓存
Vue3 的 cacheHandler可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数
4.9. Diff 优化
vue3实现了静态模板分析,重写了diff算法; 增加 patchFlag帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对
4.10. 源码体积优化、打包优化
- tree-shaking:模块打包webpack、rollup等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于import和export语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用; Vue 应用程序中未使用的api将从最终的捆绑包中消
- 引入tree-shaking:通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段会删除这些没有用的代码
- 如果在项目中没有引入transition、keepAlive等组件,那么他们对应的代码就不会打包,从而减少项目引入vue.js包体积的目的 除
4.11. 源码方向
vue2.0采用flow进行编写,而3.0源码全部采用Ts进行开发,对TS支持友好
4.12. 作用域插槽
2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能
4.13. 新增Fragment、Teleport、Suspense 组件
五、 环境搭建
// 对于 Vue 3,应该 npm 上可用的 Vue CLI v4.5 作为 @vue/cli@next
yarn global add @vue/cli@next
# OR
npm install -g @vue/cli@next
// 创建项目
npm init vite-app <project-name>
# OR
yarn create vite-app <project-name>
// 下载依赖及运行项目, 已 npm 方式为例; 详细步骤官方文档的安装页都有
cd <project-name>
npm install
npm run dev // 项目就能跑起来并且访问了
拓展: 项目引入其他插件比如 vue-router4.0、vuex4.0、typescript等请参考 Vue3.0环境搭建
六、 语法介绍
上手之前应该先阅读一遍 vue3 对于 vue2 的一些变更; 详情点击 官网文档重大变更
vue2
中使用的是Options API
; vue3
中的是Composition API
简称纯函数式API
6.1、 setup
vue3
组件入口为 setup(){}
函数作为入口, 默认只执行一次;执行顺序在 beforeCreate
之后 created
之前;
...
// 使用props和this
setup (props, ctx) {
// props 组件间传递的参数;
// ctx 组件的实例的执行上下文(可以理解为 vue2 this)
/* 可执行 下面等操作:例 ctx.$emit()
attrs: Object
emit: ƒ ()
listeners: Object
parent: VueComponent
refs: Object
root: Vue
*/
// 注意 steup 中没有this了, 拿不到this
}
6.2、 生命周期
我记得早期是说 vue3
中是移除掉了 beforeCreate
、created
两个生命周期; 但是实践的时候我发现还是可以写的; 因为vue2
、 vue3
写法目前相兼容;
created () { console.log('created') }
setup (props, ctx) {
console.log('setup')
// mounted 新写法 记住一句话 所有的方式都是以函数的形式呈现
onMounted(() => {})
}
mounted () { console.log('mounted') }
// 执行顺序 setup created mounted
虽然兼容但尽量不要这样写;向前看齐嘛; 强烈推荐全部都放在steup
函数中
6.3、 reactive、ref、toRefs、isRef
创建响应式对象 reactive、ref、toRefs
用法, 对应 vue2
中的 data
推荐写法3
// 写法一:响应式数据一多, return 要很多次; 使用数据的时候要通过state拿到
<template>
<div>
<p>{{state.count}}</p>
</div>
</template>
import {reactive} from 'vue'
...
setup(props, ctx) {
const state = reactive({
count: 0
})
return { state }
}
// 写法二
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive} from 'vue'
...
setup(props, ctx) {
const state = reactive({
count: 0
})
return {
count: state.count
}
}
// 写法三:推荐 通过 toRefs 代理对象, 再通过解构的方式取值
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive, toRefs} from 'vue'
...
setup(props, ctx) {
const state = reactive({
count: 0
})
return {
...toRefs(state)
}
}
// 写法四:通过 ref() 函数包装, 返回值是一个对象,对象上只包含一个 value 属性, 就是要的属性值
<template>
<div>
<p>{{count}}</p>
<p>{{count1}}</p>
</div>
</template>
import {reactive, toRefs, ref} from 'vue'
...
setup(props, ctx) {
// 父组件传递count属性
// 写法1
const count = ref(props.count)
console.log(count.value) // 对应props.count的值
// 写法2
const state = reactive({
count1: ref(props.count)
})
return {
count,
...toRefs(state)
}
}
// isRef 来判断某个值是否为 ref() 创建出来的对象
import { ref, isRef } from 'vue';
export default {
setup(props, ctx) {
const refCount = ref(0)
const count = isRef(refCount) ? refCount : 1
}
};
6.4、 computed
例子场景:结合 vue-router
根据当前路劲为count
赋值, 也扩展下vue-router
的用法
<template>
<div>
<p>{{count}}</p>
<p>{{count1}}</p>
</div>
</template>
import {reactive, toRefs, computed} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
const route = useRoute()
const state = reactive({
// 计算属性 写法1
count: computed(() => {
return route.path
})
})
// 计算属性 写法2
const count1 = computed(() => {
return route.path
})
return {
...toRefs(state),
// 计算属性不需要通过 toRefs 结构, 因为他就是一个具体的值就是响应式的
count1
}
}
6.5、watch 、watchEffect
例子场景:同 computed 一样
watchEffect
与 watch
有什么不同:
watchEffect
不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行watch
只能监听指定的属性watch
可以获取到新值与旧值,而watchEffect
不行watchEffect
在组件初始化的时候就会执行一次用以收集依赖(与computed
同理),后续收集的依赖发生变化,这个回调才会再次执行
// watch 用法 监听单个属性
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive, toRefs, watch} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
const route = useRoute()
const state = reactive({
count: 0,
})
// 监听路由路劲, immediate 是否立即执行一次
watch(() => route.path, (newValue) => {
state.count = newValue
}, { immediate: true })
return {
...toRefs(state),
}
}
// watch 用法 监听ref数据源
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive, toRefs, ref, watch} from 'vue'
...
setup(props, ctx) {
// 定义数据源
let count = ref(0);
// 指定要监视的数据源
watch(count, (count, prevCount) => {
console.log(count, prevCount)
})
setInterval(() => {
count.value += 2
}, 2000)
console.log(count.value)
return {
count
}
}
// watch 用法 监听多个属性
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive, toRefs, watch} from 'vue'
...
setup(props, ctx) {
const state = reactive({
name: 'vue',
age: 3
})
watch(
// 监听name、 age
[() => state.name, () => state.age],
// 如果属性改变、则执行以下回调
([newName, newAge], [oldname, oldAge]) => {
console.log(oldname, oldname)
console.log(oldAge, oldAge)
},
{ lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
)
setTimeout(() => {
state.name = 'react'
state.age += 1
}, 3000)
return {
...toRefs(state),
}
}
// watchEffect 用法
<template>
<div>
<p>{{count}}</p>
</div>
</template>
import {reactive, toRefs, ref, watchEffect} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
const route = useRoute()
const state = reactive({
count: 0,
})
// 当 route.path 变化时就会执行打印, 有点类似 react-hook 的 useEffect 第二个参数效果
watchEffect(() => {
count = route.path
console.log(route.path)
})
// watchEffect、 watch 都可以主动停止监听
const stop = watchEffect(() => {
count = route.path
console.log(route.path)
})
// 在某个时机下 执行 stop() 停止watchEffect监听
if (...) { stop() }
return {
...toRefs(state),
}
}
七、Vue3.2 语法介绍
7.1. 变量、方法不需要 return 出来
7.2. 组件不需要在注册
// 组件命名采用的是大驼峰,引入后不需要在注册, 在使用的使用直接是小写和横杠的方式连接
import TestCom from "../components/TestCom.vue" ===> <test-com></test-com>
7.3. 新增 defineProps
const props = defineProps({
info:{
type:String,
default:'----'
},
time:{
type:String,
default:'0分钟'
},
})
7.4. 新增 defineEmits
let $myemit = defineEmits(['myAdd','myDel'])
let hander1Click=() :void=>{
$myemit('myAdd','新增的数据')
}
let hander2Click=() :void=>{
$myemit('myDel','删除的数据')
}
7.5. defineExpose 获取子组件中的属性值
// 将组件中的属性暴露出去,这样父组件可以获取
defineExpose({
sex,
info
})
7.6. 新增指令 v-memo
- v-memo会记住一个模板的子树,元素和组件上都可以使用。
- 该指令接收一个固定长度的数组作为依赖值进行[记忆比对]。
- 如果数组中的每个值都和上次渲染的时候相同,则整个子树的更新会被跳过。
- 即使是虚拟 DOM 的 VNode 创建也将被跳过,因为子树的记忆副本可以被重用。
7.7. style v-bind
<style scoped>
span {
/* 使用v-bind绑定state中的变量 */
color: v-bind('state.color');
}
</style>
7.8. 辅助函数
- useAttrs: 获取 attrs 数据
- useSlots: 获取 slots 插槽数据
- useCssModule: 类似于 react 的 className 语法
7.9. 顶层await支持
必须与 Suspense 组合使用
7.10. 指令
// 局部自定义指令
// 必须以 小写字母v开头的小驼峰 的格式来命名本地自定义指令
// 在模板中使用时,需要用中划线的格式表示,不可直接使用vMyDirective
const vMyDirective = {
beforeMount: (el, binding, vnode) => {
el.style.borderColor = 'red'
},
updated(el, binding, vnode) {
if (el.value % 2 !== 0) {
el.style.borderColor = 'blue'
} else {
el.style.borderColor = 'red'
}
},
}
// 自定义全局指令,可以在main.js 的app上注册就可以全局使用
app.directive("focus", {
mounted(el, bindings, vnode, preVnode) {
console.log("focus mounted");
el.focus();
}
})
// 指令生命周期
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次
7.11. 命名空间组件
// components/Form/index.js
import Form from './Form.vue'
import Input from './Input.vue'
import Label from './Label.vue'
// 把Input、Label组件挂载到 Form 组件上
Form.Input = Input
Form.Label = Label
export default Form
八、 Vue3中移除的一些API和方法
8.1 取消KeyboardEvent.keyCode
在Vue2.x
中,绑定键盘事件会用到如下代码:
<!-- keyCode version -->
<input v-on:keyup.13="submit" />
<!-- alias version -->
<input v-on:keyup.enter="submit" />
或者是:
Vue.config.keyCodes = {
f1: 112
}
<!-- keyCode version -->
<input v-on:keyup.112="showHelpText" />
<!-- custom alias version -->
<input v-on:keyup.f1="showHelpText" />
在事件中,给keyup
配置一个指定按钮的keyCode
(数字)在Vue3
中将不会生效,但是依然可以使用别名,例如:
<input v-on:keyup.delete="confirmDelete" />
8.2 移除 off 和 $once方法
在Vue2.x
中可以通过EventBus
的方法来实现组件通信:
var EventBus = new Vue()
Vue.prototype.$EventBus = EventBus
...
this.$EventBus.$on() this.$EventBus.$emit()
这种用法在Vue3
中就不行了,在Vue3
中移除了 $on,$off
等方法(参考rfc),而是推荐使用mitt
方案来代替:
import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// fire an event
emitter.emit('foo', { a: 'b' })
8.3 移除filters
在Vue3
中,移除了组件的filters
项,可以使用methods
的或者computed
来进行替代:
<template>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
替换为:
<template>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
九、Vue3中改变的API和写法
9.1 实例初始化
在vue2.x
中通过new Vue()
的方法来初始化:
import App from './App.vue'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
在vue3
中Vue
不再是一个构造函数,通过createApp
方法初始化:
import App from './App.vue'
createApp(App).use(store).mount('#app')
9.2 全局API调用方式改变
在Vue2.x
中,大部分全局API
都是通过Vue.xxx
或者Vue.abc()
方式调用,例如:
import Vue from 'vue'
Vue.mixin()
Vue.use()
而在Vue3
中,这些方式将会改变,取而代之的是如下:
import { createApp } from 'vue'
const app = createApp({})
app.mixin()
app.use()
同时,可以只引入一些需要的API
,不需要的不用引入,这样也符合Three Shaking
的要求,例如:
import { nextTick,reactive,onMounted } from 'vue'
nextTick(() => {
})
onMounted(() => {
})
由于Vue3
中全局API
都会通过app.xxx
的方法调用,所以之前通过Vue.prototype.xxx
绑定的全局方法和变量将无法使用,可以采用如下方式来代替:
//在main.js中:
app.config.globalProperties.http = function(){}
//在vue组件中:
this.http()
9.3 render方法修改
在Vue2.x
中,有时会自定义render
方法来返回模板内容,如下:
export default {
render(h) {
return h('div')
}
}
在Vue3
中,h
通过vue
来引入,如下:
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
9.4 新的异步组件创建方式
在Vue2.x
中,尤其是在Vue Router
中,会经常使用到异步组件,借助webpack
的打包方式,可以将一个组件的代码进行异步获取,例如:
const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
在Vue3
中,提供了defineAsyncComponent()
方法创建异步组件,同时可以返回一个Promise
对象来自己控制加载完成时机,如下:
import { defineAsyncComponent } from 'vue'
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
})
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
十、参考链接
- 前端早读课文章: 最全的Vue3.0升级指南
- Vue中文社区文章:一篇文章上手Vue3中新增的API