Vue3.0
相关资料:
Vue3.0优点
1.源码体积小:Vue2
压缩后的体积大约有20kb,Vue3
压缩后的体积只有10kb。Vue3
中移除了部分API,如 filter
、$on
、$off
、$once
、$destory
、按键修饰符全部改用按键名,不再支持按键值。
2.响应式系统升级:将原来的 Object.defineProperty
替换成了ES6中的 Proxy
,它是通过在目标对象前加了一层拦截,代理的是对象而不是对象的属性,比原来的颗粒度变大了。现在可以监听到对象的新增与删除属性的操作、数组的索引赋值操作、数组的 length
属性操作。
3.更友好的静态类型支持:Vue2
需要额外采用 Flow 作为静态类型检查,它是由 Facebook 出品的 JavaScript
静态类型检查工具;而 Vue3
直接全部采用 TS
进行开发,对 TS
支持更友好。
4.性能提升:Vue3
重写了虚拟 DOM
和底层的 Diff
算法,拥有更好的 tree-shaking
支持,整体性能得很大的一个提升。
5.CompositionAPI:Vue3
最大的一个变动应该就是推出了 CompositionAPI
,可以说它受ReactHook
启发而来;它我们编写逻辑更灵活,便于提取公共逻辑,代码的复用率得到了提高,也不用再使用 mixin
担心命名冲突的问题。
6.更先进的组件:Teleport
- 传送/瞬移组件、Suspense
- 等待异步组件过程中可以先提前渲染占位内容、Fragment
- 多根性质等等。
7.自定义渲染器:Vue3
支持创建自定义的渲染器,能实现跨平台,通过改写 Vue
底层渲染逻辑,如渲染成小程序形式等等。
初始化项目
初始化 Vue3+Vite
项目:
npm init vue@latest
npm init vite@latest projectName
初始化 Vue3+Webpack
项目:vue create projectName
Composition API
Composition API 包含:reactive、ref、toRefs、watch、wactchEffect、computed、生命周期钩子
setup
setup 是 Vue3.x 新增的一个选项,它是组合式 Composition API
的入口。
<script>
export default {
setup(props, context) {
return {}
}
}
</script>
参数说明:
props
:这个还是和Vue2
使用的组件之间通信的props
一样。context
:定义上下文,这个参数身上有一些比较常用的属性,比如context.emit
:等同于Vue2
的this.$emit
。context.slots
:等同于Vue2
的this.$slots
。context.attrs
:等同于Vue2
的this.$attrs
。context.expose()
:当前组件对外暴露属性的函数。
这三个属性都是自动同步最新的值,所以每次使用拿到的都是最新值。
setup 中接受的props
是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。
reactive、ref 与 toRefs
在 vue2.x 中, 定义数据都是在data
中, 但是 Vue3.x 可以使用reactive
和ref
来进行数据定义。toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象。
watch 与 watchEffect 的用法
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, [options])
参数说明:
- source: 可以支持 string,Object,Function,Array,用于指定要侦听的响应式变量
- callback: 执行的回调函数
- options:支持 deep、immediate 和 flush 选项。
侦听 reactive 定义的数据
import { defineComponent, ref, reactive, toRefs, watch } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
setTimeout(() => {
state.age++;
}, 1000);
// 修改age值时会触发 watch的回调
watch(
() => state.age,
(curAge, preAge) => {
console.log("新值:", curAge, "老值:", preAge);
}
);
return {
...toRefs(state),
};
},
});
侦听 ref 定义的数据
const year = ref(0);
setTimeout(() => {
year.value++;
}, 1000);
watch(year, (newVal, oldVal) => {
console.log("新值:", newVal, "老值:", oldVal);
});
侦听多个数据
上面两个例子中,我们分别使用了两个 watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch([() => state.age, year], ([curAge, newVal], [preAge, oldVal]) => {
console.log("新值:", curAge, "老值:", preAge); console.log("新值:", newVal,
"老值:", oldVal); });
侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type: "三室两厅",
},
},
});
watch(
() => state.room,
(newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
},
{ deep: true }
);
如果不使用第三个参数deep:true
, 是无法监听到数据变化的。 前面我们提到,默认情况下,watch 是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true
即可。关于flush
配置,还在学习,后期会补充
stop 停止监听
我们在组件中创建的watch
监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()
函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止监听
stopWatchRoom()
}, 3000)
还有一个监听函数watchEffect
, 在我看来watch
已经能满足监听的需求,为什么还要有watchEffect
呢?虽然我没有 get 到它的必要性,但是还是要介绍一下watchEffect
,首先看看它的使用和watch
究竟有何不同。
import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
let year = ref(0)
setInterval(() =>{
state.age++
year.value++
},1000)
watchEffect(() => {
console.log(state);
console.log(year);
}
);
return {
...toRefs(state)
}
},
});
执行结果首先打印一次state
和year
值;然后每隔一秒,打印state
和year
值。 从上面的代码可以看出, 并没有像watch
一样需要先传入依赖,watchEffect
会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下:
- watchEffect 不需要手动传入依赖
- watchEffect 会先执行一次用来自动收集依赖
- watchEffect 无法获取到变化前的值, 只能获取变化后的值
上面介绍了 Vue3 Composition API
的部分内容, 还有很多非常好用的 API, 建议直接查看官网 composition-api。 其实我们也能进行自定义封装。
生命周期
Vue3.x 生命周期
Vue2.x 与 Vue3.x 生命周期区别
setup
是在beforCreate
之前执行的beforeCreate
和created
被setup
替换了,但是 Vue3 中仍然可以使用, 因为 Vue3 是向下兼容的,也就是实际使用的是 vue2 的。Vue3
生命周期钩子都以on+xxx
开头,并且需要手动导入且只能在setup()
函数内部使用- Vue3.x 还新增用于调试的钩子函数
onRenderTriggered
和onRenderTricked
- Vue3.x 中的钩子是需要从 vue 中导入的
简单对比 vue2.x 与 vue3.x 响应式
我刚上手 Vue2.x 的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set
更新,什么时候用$forceUpdate
强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty
。
这里就简单对比一下Object.defineProperty
与 Proxy 的区别
Object.defineProperty
只能劫持对象的属性, 而 Proxy 是直接代理对象
由于Object.defineProperty
只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作
Object.defineProperty
对新增属性需要手动进行Observe
因为Object.defineProperty
劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty
进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用$set
才能保证新增的属性也是响应式的, $set
内部也是通过调用Object.defineProperty
去处理的。
Suspense
Suspense
是 Vue3.x 中新增的特性,Vue2.x 中应该经常遇到这样的场景:
<template>
<div>
<div v-if="!loading">
...
</div>
<div v-if="loading">
加载中...
</div>
</div>
</template>
在前后端交互获取数据时, 是一个异步过程,一般都会提供一个加载中的动画,当数据返回时配合v-if
来控制数据显示。 如果使用过vue-async-manager
这个插件来完成上面的需求, 对Suspense
可能不会陌生,Vue3.x 感觉就是参考了vue-async-manager
. Vue3.x 新出的内置组件Suspense
, 它提供两个template
slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容, 通过使用Suspense
组件进行展示异步渲染就更加的简单。:::warning 如果使用 Suspense
, 要返回一个 promise :::Suspense
组件的使用:
<Suspense>
<template #default>
<async-component></async-component>
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</Suspense>
asyncComponent.vue
:
<<template>
<div>
<h4>这个是一个异步加载数据</h4>
<p>用户名:{{user.nickname}}</p>
<p>年龄:{{user.age}}</p>
</div>
</template>
<script>
import { defineComponent } from "vue"
import axios from "axios"
export default defineComponent({
setup(){
const rawData = await axios.get("http://xxx.xinp.cn/user")
return {
user: rawData.data
}
}
})
</script>
从上面代码来看,Suspense
只是一个带插槽的组件,只是它的插槽指定了default
和 fallback
两种状态。
片段(Fragment)
在 Vue2.x 中, template
中只允许有一个根节点,但是在 Vue3.x 中,你可以直接写多个根节点。
更好的 Tree-Shaking
Vue3.x 在考虑到 tree-shaking
的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module
的引用方式进行具名引用, 比如在 Vue2.x 中,我们要使用 nextTick
:
// vue2.x
import Vue from "vue"
Vue.nextTick(()=>{
...
})
Vue.nextTick()
是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick()
只是 Vue.nextTick()
的一个简易包装,只是为了方便而把后者的回调函数的 this
绑定到了当前的实例。虽然我们借助webpack
的tree-shaking
, 但是不管我们实际上是否使用Vue.nextTick()
, 最终都会进入我们的生产代码, 因为 Vue 实例是作为单个对象导出的, 打包器无法坚持出代码总使用了对象的哪些属性。 在 Vue3.x 中改写成这样:
import { nextTick } from "vue"
nextTick(() =>{
...
})
受影响的 API
这是一个比较大的变化, 因为以前的全局 API 现在只能通过具名导入,这一更改会对以下 API 有影响:
Vue.nextTick
Vue.observable
(用Vue.reactive
替换)Vue.version
Vue.compile
(仅限完整版本时可用)Vue.set
(仅在 2.x 兼容版本中可用)Vue.delete
(与上同)
内置工具
出来上面的 API 外, 还有许多内置的组件 以上仅适用于 ES Modules
builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。
变更
slot 具名插槽语法
在 Vue2.x 中, 具名插槽的写法:
<!-- 子组件中:-->
<slot name="title"></slot>
在父组件中使用:
<template slot="title">
<h1>歌曲:成都</h1>
<template>
如果我们要在 slot 上面绑定数据,可以使用作用域插槽,实现如下:
// 子组件
<slot name="content" :data="data"></slot>
export default {
data(){
return{
data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]
}
}
}
复制代码
<!-- 父组件中使用 -->
<template slot="content" slot-scope="scoped">
<div v-for="item in scoped.data">{{item}}</div>
<template>
在 Vue2.x 中具名插槽和作用域插槽分别使用slot
和slot-scope
来实现, 在 Vue3.0 中将slot
和slot-scope
进行了合并同意使用。 Vue3.0 中v-slot
:
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<div v-for="item in scoped.data">{{item}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
自定义指令
首先回顾一下 Vue 2 中实现一个自定义指令:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
在 Vue 2 中, 自定义指令通过以下几个可选钩子创建:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下: 所以在 Vue3 中, 可以这样来自定义指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的 v-focus
指令, 如下:
<input v-focus />
复制代码
v-model 升级
在使用 Vue 3 之前就了解到 v-model
发生了很大的变化, 使用过了之后才真正的 get 到这些变化, 我们先纵观一下发生了哪些变化, 然后再针对的说一下如何使用:
- 变更:在自定义组件上使用
v-model
时, 属性以及事件的默认名称变了 - 变更:
v-bind
的.sync
修饰符在 Vue 3 中又被去掉了, 合并到了v-model
里 - 新增:同一组件可以同时设置多个
v-model
- 新增:开发者可以自定义
v-model
修饰符
有点懵?别着急,往下看 在 Vue2 中, 在组件上使用 v-model
其实就相当于传递了value
属性, 并触发了input
事件:
<!-- Vue 2 -->
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :value="searchValue" @input="searchValue=$event"><search-input>
这时v-model
只能绑定在组件的value
属性上,那我们就不开心了, 我们就像给自己的组件用一个别的属性,并且我们不想通过触发input
来更新值,在.sync
出来之前,Vue 2 中这样实现:
// 子组件:searchInput.vue
export default {
model:{
prop: 'search',
event:'change'
}
}
修改后, searchInput 组件使用v-model
就相当于这样:
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :search="searchValue" @change="searchValue=$event"><search-input>
但是在实际开发中,有些场景我们可能需要对一个 prop 进行 “双向绑定”, 这里以最常见的 modal 为例子:modal 挺合适属性双向绑定的,外部可以控制组件的visible
显示或者隐藏,组件内部关闭可以控制 visible
属性隐藏,同时 visible 属性同步传输到外部。组件内部, 当我们关闭modal
时, 在子组件中以 update:PropName 模式触发事件:
this.$emit('update:visible', false)
然后在父组件中可以监听这个事件进行数据更新:
<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
复制代码
此时我们也可以使用v-bind.sync
来简化实现:
<modal :visible.sync="isVisible"></modal>
复制代码
上面回顾了 Vue2 中v-model
实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢? 在 Vue3 中, 在自定义组件上使用v-model
, 相当于传递一个modelValue
属性, 同时触发一个update:modelValue
事件:
<modal v-model="isVisible"></modal>
<!-- 相当于 -->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>
复制代码
如果要绑定属性名, 只需要给v-model
传递一个参数就行, 同时可以绑定多个v-model
:
<modal v-model:visible="isVisible" v-model:content="content"></modal>
<!-- 相当于 -->
<modal
:visible="isVisible"
:content="content"
@update:visible="isVisible"
@update:content="content"
/>
复制代码
不知道你有没有发现,这个写法完全没有.sync
什么事儿了, 所以啊,Vue 3 中又抛弃了.sync
写法, 统一使用v-model
异步组件
Vue3 中 使用 defineAsyncComponent
定义异步组件,配置选项 component
替换为 loader
,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下:
<template>
<!-- 异步组件的使用 -->
<AsyncPage />
</tempate>
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// 无配置项异步组件
AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
// 有配置项异步组件
AsyncPageWithOptions: defineAsyncComponent({
loader: () => import(".NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: () => import("./ErrorComponent.vue"),
loadingComponent: () => import("./LoadingComponent.vue"),
})
},
}
</script>