一、Vue3的改动(和Vue2的差异性,非兼容性改变)
1.生命周期钩子函数命名发生变化
Vue2 生命周期
Vue2 钩子函数 | Vue3 钩子函数(选项式API) | Vue3 钩子函数(组合式API) |
---|---|---|
beforeCreate | beforeCreate | - |
created | created | - |
beforeMount | beforeMount | onBeforeMount |
mounted | mounted | onMounted |
beforeUpdate | beforeUpdate | onBeforeUpdate |
updated | updated | onUpdated |
activated | activated | onActivated |
deactivated | deactivated | onDeactivated |
beforeDestory | beforeUnmount | onBeforeUnmount |
destoryed | unmounted | onUnmounted |
errorCaptured(可以忽略) | errorCaptured(可以忽略) | onErrorCaptured(可以忽略) |
- | renderTracked (dev only,可以忽略) | onRenderTracked(dev only,可以忽略) |
- | renderTriggered (dev only,可以忽略) | onRenderTriggered(dev only,可以忽略) |
- | serverPrefetch(SSR only,可以忽略) | onServerPrefetch(SSR only,可以忽略) |
2.全局 API
2.1 Vue声明变化
新增一个新的全局 API:createApp
,调用 createApp
返回一个应用实例。
<div id="app">
{{message}}
</div>
<script>
//Vue2
var vm = new Vue({
el: '#app',
data: {
message: 'Vue',
},
// ...
})
//Vue3
const app = {
data() {
return {
message: 'Hello Vue!'
}
},
// ...
}
Vue.createApp(app).mount('#app')
//Vue3 或
const app= Vue.createApp({
data() {
return {
message: 'Hello Vue!'
}
},
// ...
})
app.mount('#app')
//Vue3 或
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue!'
}
},
// ...
}).mount('#app')
</script>
从技术上讲,Vue 2 没有app
的概念,我们定义的应用只是通过 new Vue()
创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置。
// 这会影响到所有根实例
Vue.mixin({
/* ... */
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
2.2 全局globalProperties
app.config.globalProperties
替换Vue.prototype
在 Vue 2 中, Vue.prototype
通常用于添加所有组件都能访问的 property
。
在 Vue 3 中与之对应的是 app.config.globalProperties
。这些 property
将被复制到应用中,作为实例化组件的一部分。
// 之前 - Vue 2
Vue.prototype.$http = () => {}
// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
添加一个可以在应用的任何组件实例中访问的全局 property
。组件的 property
在命名冲突具有优先权。
用法:
//定义
app.config.globalProperties.foo = 'bar'
//使用
app.component('child-component', {
template: '#child-component',
mounted() {
console.log(this.foo) // 'bar'
}
})
//app指 const app = Vue.createApp({})
//或
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
console.log(proxy.foo) // 'bar'
2.3 Vue.extend 移除
Vue.extend
移除,现用createApp
代替
在 Vue 2.x 中,Vue.extend
曾经被用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。
在 Vue 3.x 中,我们已经没有组件构造器的概念了。应该始终使用 createApp
这个全局 API 来挂载组件:
// 之前 - Vue 2
// 创建构造器
const Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p >',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建一个 Profile 的实例,并将它挂载到一个元素上
new Profile().$mount('#mount-point')
// 之后 - Vue 3
const Profile = {
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p >',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
}
Vue.createApp(Profile).mount('#mount-point')
类型推断
在 Vue 2 中,Vue.extend
也被用来为组件选项提供 TypeScript 类型推断。在 Vue 3 中,为了达到相同的目的,defineComponent
全局 API 可以用来作为 Vue.extend
的替代方案。
需要注意的是,虽然 defineComponent
的返回类型是一个类似构造器的类型,但是它的目的仅仅是为了 TSX 的推断。在运行时 defineComponent
里基本没有什么操作,只会原样返回该选项对象。
组件继承
在 Vue 3 中,我们强烈建议使用 组合式 API 来替代继承与 mixin。如果因为某种原因仍然需要使用组件继承,你可以使用 extends 选项 来代替 Vue.extend
。
2.4 全局 API Treeshaking
Vue 2.x 中的这些全局 API 受此更改的影响:
- Vue.nextTick (通过导入
import { nextTick } from 'vue'
直接调用) - Vue.observable (用 Vue.reactive 替换)
- Vue.version
- Vue.compile (仅完整构建版本)
- Vue.set (仅兼容构建版本)
- Vue.delete (仅兼容构建版本)
任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是 Vue2 全局 API 及其相应的实例 API 列表:
2.x 全局 API | 3.x 实例 API (app ) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | 移除 |
Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
Vue.extend | 移除 |
对内联模板特性的支持已被移除(inline-template),详情参阅:内联模板
3、v-model
v-model
其实是一个语法糖,它的背后本质上是包含两个操作:
v-bind
绑定一个propvalue
属性v-on
指令给当前元素绑定input
事件
在 Vue2.x 中,v-model
相当于绑定value
属性和input
事件,它本质也是一个语法糖:
<input type="text" v-model="message">
<!-- 相当于 -->
<input type="text" :value="message" @input = "message = $event.target.value">
在 Vue3.x 中,它发生了如下变化:
- 用于自定义组件时,
v-model
prop 和事件默认名称已更改:
- prop:
value
->modelValue
- 事件:
input
->update:modelValue
v-bind
的.sync
修饰符和组件的model
选项已移除,可在v-model
上加一个参数代替- 新增:现在可以在同一个组件上使用多个
v-model
绑定; - 新增:现在可以自定义
v-model
修饰符。
3.1 .sync修饰符
首先我们知道,在子组件中可以通过$emit
向父组件通信,通过这种间接的方式改变父组件的data
,从而实现子组件改变props
的值。
子组件使用$emit
向父组件发送事件:
this.$emit("update:msg","newValue")
父组件监听这个事件并更新一个本地的数据msg:
<son :msg="msg" @uptate:msg="val=>msg=val"></son>
为了方便这种写法,Vue提供了.sync
修饰符,说白了就是一种简写的方式,我们可以将其当作是一种语法糖,比如v-on: click
可以简写为@click
。
而上边父组件的这种写法,换成sync
的方式就像下边这样:
<son :msg.sync="msg"></son>
3.2 Vue3 移除.sync
Vue3 中将v-bind
和.sync
进行了功能的整合,抛弃了.sync
。
<ChildComponent :title.sync="pageTitle" />
<!-- 替换为 -->
<ChildComponent v-model:title="pageTitle" />
在 Vue3.x 中,自定义组件上的v-model
相当于传递了 modelValue
prop 并接收抛出的 update:modelValue
事件:
<ChildComponent v-model="pageTitle" />
<!-- 相当于 -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
若需要更改 model
的名称,现在我们可以为 v-model
传递一个参数,以作为组件内 model
选项的替代:
<ChildComponent v-model:title="pageTitle" />
<!-- 相当于 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
在自定义组件上使用多个 v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 相当于 -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
Vue
.sync
的历史:
Vue.sync
修饰符最初存在于 Vue 1.0 版本里,但是在 2.0 中被移除了。但是在 2.0 发布之后的实际应用中,Vue官方发现.sync
还是有其适用之处,比如在开发可复用的组件库时。开发者需要做的只是让子组件改变父组件状态的代码更容易被区分。从 2.3.0 起官方重新引入了.sync
修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on
监听器。
4、v-bind合并
在 Vue2.x 中,如果一个元素同时定义了v-bind="object"
和一个相同的单独的属性,那么这个单独的属性会覆盖object
中的绑定:
<div id="red" v-bind="{ id: 'blue' }"></div>
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 最后结果都相同 -->
<div id="red"></div>
然而在 Vue3 中,如果一个元素同时定义了v-bind="object"
和一个相同的单独的属性,那么声明绑定的顺序决定了最后的结果(后者覆盖前者):
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>
5、移除 v-on.native 修饰符
5.1 Vue2.x语法
默认情况下,传递给带有 v-on
的组件的事件监听器只能通过 this.$emit
触发。要将原生 DOM
监听器添加到子组件的根元素中,可以使用 .native
修饰符:
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
5.2 Vue3.x语法
v-on
的 .native
修饰符已被移除。同时,新增的 emits
选项允许子组件定义真正会被触发的事件。
对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false
)。
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
<!-- MyComponent.vue -->
<script>
export default {
emits: ['close']
}
//click将被作为原生事件添加到子组件的根元素中
</script>
6、router和router-view
6.1 new Router
变成 createRouter
Vue Router
不再是一个类,而是一组函数。现在你不用再写 new Router()
,而是要调用 createRouter
:
// 以前是
import VueRouter from 'vue-router'
const router = new VueRouter({
//...
})
//现在
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
6.2 新的 history
配置取代 mode
mode: 'history'
配置已经被一个更灵活的 history
配置所取代。根据你使用的模式,你必须用适当的函数替换它:
"history"
:createWebHistory()
"hash"
:createWebHashHistory()
"abstract"
:createMemoryHistory()
下面是一个完整的代码段:
import { createRouter, createWebHistory } from 'vue-router'
// 还有 createWebHashHistory 和 createMemoryHistory
createRouter({
history: createWebHistory(),
routes: [],
})
在 SSR
上使用时,你需要手动传递相应的 history
:
// router.js
let history = isServer ? createMemoryHistory() : createWebHistory()
let router = createRouter({ routes, history })
// 在你的 server-entry.js 中的某个地方
router.push(req.url) // 请求 url
router.isReady().then(() => {
// 处理请求
})
原因:为未使用的 history
启用摇树,以及为高级用例(如原生解决方案)实现自定义 history
。
6.3 路由及路由守卫
6.4 router-view
<!-- 之前(Vue 2.x) -->
<!-- 此组件不需要被缓存 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
<!-- 此组件需要被缓存 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 之后(Vue 3.x) -->
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
7、Vuex
Vue3 使用 createStore
//Vue2里使用vuex
//main.js引入
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store,
render: h => h(App)
})
.$mount('#app')
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {...},
getters: {...},
mutations: {...},
actions:{...},
modules: {...},
})
// Vue3里使用vuex
// main.js引入
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
.use(store)
.mount('#app')
// store/index.js
import { createStore } from 'vuex'
export const store = createStore({
state:()=>{
return{...}
},
getters: {...},
mutations: {...},
actions:{...},
modules: {...},
})
现在 Vue3 推荐使用pinia
替代Vuex
// main.js引入
import { createApp } from "vue";
import App from "./App.vue";
import {createPinia} from 'pinia';
const pinia = createPinia()
createApp(App)
.use(pinia)
.mount("#app");
// store/index.js
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => {
return {...};
},
// 也可以这样定义
// state: () => ({ count: 0 })
getters: { ... },
actions: {...},
});
更多查看,Vue状态管理模式之Vuex和Pinia
8、data、mixin
在 Vue2.x 中,我们可以定义data
为object
或者function
,但是我们知道在组件中如果data
是object
的话会出现数据互相影响,因为object是引用数据类型;
在 Vue3 中,data
只接受function
类型,通过function
返回对象;同时Mixin
的合并行为也发生了改变,当mixin
和基类中data
合并时,会执行浅拷贝合并(只合并根级属性);
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1,
address: {
prov: 2,
city: 3,
},
}
}
}
}
const Component = {
mixins: [Mixin],
data() {
return {
user: {
id: 2,
address: {
prov: 4,
},
}
}
}
}
// Vue2结果:
{
id: 2,
name: 'Jack',
address: {
prov: 4,
city: 3
}
}
// vue3结果:
user: {
id: 2,
address: {
prov: 4,
},
}
我们看到最后合并的结果,Vue2.x 会进行深拷贝,对data
中的数据向下深入合并拷贝;而 Vue3 只进行浅层拷贝,对data
中数据发现已存在就不合并拷贝。
Vue3中推荐使用
Composition API
中的自定义hook
替代mixins
。
// 自定义一个鼠标移动事件
import { ref, onMounted, onUnmounted, watch } from "vue";
export function useMouse() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
页面使用
import {useMouse} from "./hook";
const { x, y } = useMouse();//自定义一个鼠标移动事件
相比mixins
,hook
解决了以下几点不足:
- 混入变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
- 多个
mixins
的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突。 mixins
和组件可能出现多对多的关系,复杂度较高(即一个组件可以引用多个mixins
,一个mixins
也可以被多个组件引用)。hook
本质就是一个函数。使用时调用这个函数,那么我们就很清楚复用功能代码的来源,同时减少页面代码,保持页面干净整洁。
更多Mixins
知识,查看如何在Vue中管理Mixins
9、移除 过滤器 filter
在 Vue2.x 中,我们还可以通过过滤器filter
来处理一些文本内容的展示:
<template>
<div>{{ status | statusText }}</div>
</template>
<script>
export default {
props: {
status: {
type: Number,
default: 1
}
},
filters: {
statusText(value){
if(value === 1){
return '订单未下单'
} else if(value === 2){
return '订单待支付'
} else if(value === 3){
return '订单已完成'
}
}
}
}
</script>
最常见的就是处理一些订单的文案展示等;
然而在 Vue3 中,过滤器filter
已经删除,不再支持了,官方建议使用方法调用
或者计算属性computed
来进行代替。
如果全局注册过滤器,可通过全局属性以让它能够被所有组件使用。
// main.js
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
//页面
<p>{{ $filters.currencyUSD(accountBalance) }}</p >
10、根节点支持区别
Vue2.0中,template
仅支持单个根节点div
Vue3.0中 ,template
取消了限制,可以支持多个根节点div
<!-- Vue2 -->
<template>
<div></div>
</template>
<!-- Vue3 -->
<template>
<div></div>
<div></div>
</template>
11、v-for 和 v-if
11.1 v-for 唯一的key
在 Vue2.x 中,我们都知道v-for
每次循环都需要给每个子节点一个唯一的key
,还不能绑定在template
标签上,
<template v-for="item in list">
<div :key="item.id">...</div>
<span :key="item.id">...</span>
</template>
而在 Vue3 中,key
值应该被放置在template
标签上,这样我们就不用为每个子节点设一遍:
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
Vue中key
作用:key
是每一个节点(vnode
)的唯一标识(id),也是diff
的一种优化策略,可以根据key
,更准确, 更快的找到对应的vnode
节点。
必要性:当我们对数据进行更新的时候,譬如在数组中插入、移除数据时,设置的key
值能让Vue底层高效的对新旧vnode
进行diff
,然后将比对出的结果用来更新真实的DOM
11.2 v-for 中的 ref
属性
Vue2.x 中,在v-for
上使用ref
属性,通过this.$refs
会得到一个数组:
<template>
<div v-for="item in list" ref="setItemRef"></div>
</template>
<script>
export default {
data(){
list: [1, 2]
},
mounted () {
console.log(this.$refs.setItemRef) // [div, div]
}
}
</script>
但是这样可能不是我们想要的结果;
因此 Vue3 不再自动创建数组,而是将ref
的处理方式变为了函数,该函数默认传入该节点:
<template>
<div v-for="item in 3" :ref="setItemRef"></div>
</template>
<script>
import { reactive, onMounted } from 'vue'
export default {
setup() {
let itemRefs = reactive([])
const setItemRef = el => {
itemRefs.push(el)
}
onMounted(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
}
</script>
11.3 v-if/v-else/v-else-if key
不再是必须的
对于 v-if/v-else/v-else-if
的各分支项 key
将不再是必须的,因为现在 Vue 会自动生成唯一的 key
。
<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>
<!-- Vue 3.x (推荐方案:移除 key) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
<!-- Vue 3.x (替代方案:确保 key 始终是唯一的) -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="b">No</div>
11.4 v-for和v-if优先级
Vue2.x:v-for
比v-if
有更高的优先级。
Vue3:v-if
比v-for
有更高的优先级。
在 Vue2.x 中,在一个元素上同时使用v-for
和v-if
,v-for
有更高的优先级,因此在 Vue2.x 中做性能优化,有一个重要的点就是v-for
和v-if
不能放在同一个元素上。
而在 Vue3 中,v-if
比v-for
有更高的优先级。因此下面的代码,在 Vue2.x 中能正常运行,但是在 Vue3 中v-if
生效时并没有item
变量,因此会报错:
<template>
<div v-for="item in list" v-if="item % 2 === 0" :key="item">{{ item }}</div>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3, 4, 5],
};
},
};
</script>
12、自定义指令directive
自定义指令directive
钩子函数被重命名,且expression
字符串不再作为 binding
对象的一部分被传入。
钩子函数被重命名:
原来:bind
、inserted
、update
、componentUpdated
、unbind
现在:created
(新增)、bind->beforeMount
、inserted->mounted
、beforeUpdate
(新增)、componentUpdated->updated
、beforeUnmount
(新增)、unbind->unmounted
Vue2的自定义指令的钩子函数看这里,Vue自定义指令及使用
//option API
//定义
const focus = {
mounted: (el) => el.focus()
}
// 使用
export default {
directives: {
// 在模板中启用 v-focus
focus
}
}
// template
<input v-focus />
HTML部分:
12.1 自定义指令的钩子函数
Vue3 提供了自定义指令的7个钩子函数,它们都是可选的:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
钩子函数 | 说明 |
---|---|
created | 只调用一次,在绑定元素的 attribute 前,或事件监听器应用前调用。 |
beforeMount | 只调用一次,在元素被插入到 DOM 前调用。 |
mounted | 只调用一次,在绑定元素的父组件,及他自己的所有子节点都挂载完成后调用。 |
beforeUpdate | 绑定元素的父组件更新前调用 |
updated | 组件更新时调用。 无论绑定值是否发生变化,只要被绑定元素所在的模板被更新即可调用。Vue.js 会通过比较更新前后的绑定值,忽略不必要的模板更新操作。 |
beforeUnmount | 只调用一次,绑定元素的父组件卸载前调用。 |
unmounted | 只调用一次,绑定元素的父组件卸载后调用。 |
12.2 钩子函数参数
入参 | 说明 |
---|---|
el | 指令所绑定的元素,可以用来直接操作 DOM。 |
binding | 一个绑定对象(如下表),包含以下 property。 |
vnode | Vue 编译生成的虚拟节点。 |
prevNode | 之前的渲染中代表指令所绑定元素的 VNode ,仅在 beforeUpdate 和 updated 钩子中可用。 |
12.3 binding
绑定对象属性
入参 | 说明 | 示例 |
---|---|---|
name | 指令名,不包含前缀 v-。 | v-focus 中的 focus |
value | 指令所绑定的值(计算后)。 | v-my-directive="1 + 1" 中,绑定值为 `2 |
oldValue | 指令绑定的前一个值,仅在 beforeUpdate 和 updated 钩子中可用。无论值是否改变都可用。 |
- |
arg | 传给指令的参数,可选。 | v-my-directive:foo 中,参数为 "foo" 。 |
modifiers | 一个包含修饰符的对象。 | v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true } 。 |
instance | 使用该指令的组件实例,可选。 | - |
dir | 指令的定义对象,可选。 | - |
13、移除eventBus
Vue3中移除了eventBus里的$on
、$off
和 $once
实例方法已被移除。
解决方案(使用第三方插件):
// eventBus.js
import emitter from 'tiny-emitter/instance'
export default {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args),
}
在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信
虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。根据具体情况来看,有多种事件总线的替代方案:
- Props 和事件应该是父子组件之间沟通的首选。兄弟节点可以通过它们的父节点通信。
- provide / inject 允许一个组件与它的插槽内容进行通信。这对于总是一起使用的紧密耦合的组件非常有用。
- provide / inject 也能够用于组件之间的远距离通信。它可以帮助避免“prop 逐级透传”,即 prop 需要通过许多层级的组件传递下去,但这些组件本身可能并不需要那些 prop。
- Prop 逐级透传也可以通过重构以使用插槽来避免。如果一个中间组件不需要某些 prop,那么表明它可能存在关注点分离的问题。在该类组件中使用 slot 可以允许父节点直接为它创建内容,因此 prop 可以被直接传递而不需要中间组件的参与。
- 全局状态管理,比如 Pinia。
14、Vue3 函数式组件
现在,在 Vue3 中,所有的函数式组件都是用普通函数创建的。换句话说,不需要定义 { functional: true }
组件选项。
它们将接收两个参数:props
和 context
。context
参数是一个对象,包含三个属性:attrs
、emit
和 slots
property。它们分别相当于组件实例的 $attrs
、$emit
和 $slots
这几个属性。
此外,h
现在是全局导入的,而不是在render
函数中隐式提供。
在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。
// App.vue
<template>
<hello>欢迎</hello>
</template>
<script>
import { defineComponent,h } from "vue";
const Hello = (props,context)=>{
return h('h1',props,context.slots)
};
Hello.props = ['name']
export default defineComponent({
components:{
Hello
}
});
</script>
15、异步组件(新增 defineAsyncComponent)
- 新的
defineAsyncComponent
助手方法,用于显式地定义异步组件; component
选项被重命名为loader
,以明确组件定义不能直接被提供;Loader
函数本身不再接收resolve
和reject
参数,且必须返回一个Promise
。
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncModal = () => import('./Modal.vue')
或者,对于带有选项的更高阶的组件语法:
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
现在,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent
助手方法中来显式地定义:
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),//component 选项现在被重命名为 loader,以明确组件定义不能直接被提供
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
此外,与 2.x 不同,loader
函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise
。
// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
/* ... */
}
// 3.x 版本
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
更多有关异步组件用法的详细信息,请参阅:
指南:异步组件
16、emits 选项(新增)
Vue 3 现在提供一个 emits
选项,和现有的 props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
在 Vue 2 中,你可以定义一个组件可接收的 prop
,但是你无法声明它可以触发哪些事件,
在 Vue 3 中,和 prop
类似,现在可以通过 emits
选项来定义组件可触发的事件。
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
强烈建议使用
emits
记录每个组件所触发的所有事件。
这尤为重要,因为我们移除了.native
修饰符。任何未在emits
中声明的事件监听器都会被算入组件的$attrs
,并将默认绑定到组件的根节点上。
对于向其父组件透传原生事件的组件来说,这会导致有两个事件被触发:
<!-- 子组件 -->
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: [] // 不声明事件
}
</script>
当一个父级组件拥有 click
事件的监听器时:
<my-button v-on:click="handleClick"></my-button>
该事件现在会被触发两次:
一次来自
$emit()
。另一次来自应用在根元素上的原生事件监听器。
现在你有两个选项:正确地声明
click
事件。当你真的在的事件处理器上加入了一些逻辑时,这会很有用。 移除透传的事件,因为现在父组件可以很容易地监听原生事件,而不需要添加
.native
。适用于你只想透传这个事件。
17、移除$listeners
$listeners
对象在 Vue 3 中已被移除。事件监听器现在是$attrs
的一部分:
{
text: '这是一个 attribute',
onClose: () => console.log('close 事件被触发')
}
18、$attrs 包含 class & style
$attrs
现在包含了所有传递给组件的 attribute,包括 class
和 style
。
18.1 Vue2.x 行为
Vue 2 的虚拟 DOM 实现对 class
和 style
attribute 有一些特殊处理。因此,与其它所有 attribute 不一样,它们没有被包含在 $attrs
中。
上述行为在使用 inheritAttrs: false
时会产生副作用:
$attrs
中的 attribute
将不再被自动添加到根元素中,而是由开发者决定在哪添加。
但是 class
和 style
不属于 $attrs
,它们仍然会被应用到组件的根元素中:
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
像这样使用时:
<my-component id="my-id" class="my-class"></my-component>
将生成以下 HTML:
<!-- class 和 style 不属于 $attrs,它们仍然会被应用到组件的根元素中 -->
<label class="my-class">
<input type="text" id="my-id" />
</label>
18.2 Vue3.x 行为
$attrs
包含了所有的 attribute,这使得把它们全部应用到另一个元素上变得更加容易了。现在上面的示例将生成以下 HTML:
<label>
<input type="text" id="my-id" class="my-class" />
</label>
在使用了
inheritAttrs: false
的组件中,请确保样式仍然符合预期。如果你之前依赖了class
和style
的特殊行为,那么一些视觉效果可能会遭到破坏,因为这些 attribute 现在可能被应用到了另一个元素中。
19、渲染函数 API
此更改不会影响
<template>
用户。
h
现在是全局导入,而不是作为参数传递给渲染函数- 更改渲染函数参数,使其在有状态组件和函数组件的表现更加一致
VNode
现在有一个扁平的 prop 结构
19.1 渲染函数参数
在 2.x 中,render
函数会自动接收 h
函数 (它是 createElement
的惯用别名) 作为参数:
// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}
在 3.x 中,h
函数现在是全局导入的,而不是作为参数自动传递。
// Vue 3 渲染函数示例
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
19.2 VNode Prop 格式化
在 2.x 中,domProps
包含 VNode
prop 中的嵌套列表:
// 2.x
{
staticClass: 'button',
class: { 'is-outlined': isOutlined },
staticStyle: { color: '#34495E' },
style: { backgroundColor: buttonColor },
attrs: { id: 'submit' },
domProps: { innerHTML: '' },
on: { click: submitForm },
key: 'submit-button'
}
在 3.x 中,整个 VNode
prop 的结构都是扁平的。使用上面的例子,来看看它现在的样子。
// 3.x 语法
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '',
onClick: submitForm,
key: 'submit-button'
}
19.3 注册组件
在 2.x 中,注册一个组件后,把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作:
// 2.x
Vue.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
Clicked {{ count }} times.
</button>
`
})
export default {
render(h) {
return h('button-counter')
}
}
在 3.x 中,由于 VNode
是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent
方法:
// 3.x
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter')
return () => h(ButtonCounter)
}
}
20、 插槽统一
this.$slots
现在将插槽作为函数公开。移除 this.$scopedSlots
当使用渲染函数,即 h
时,2.x 曾经在内容节点上定义 slot
数据 property。
// 2.x 语法
h(LayoutComponent, [
h('div', { slot: 'header' }, this.header),
h('div', { slot: 'content' }, this.content)
])
可以使用以下语法引用作用域插槽:
// 2.x 语法
this.$scopedSlots.header
在 3.x 中,插槽以对象的形式定义为当前节点的子节点:
// 3.x Syntax
h(LayoutComponent, {}, {
header: () => h('div', this.header),
content: () => h('div', this.content)
})
当你需要以编程方式引用作用域插槽时,它们现在被统一到 $slots
选项中了。
// 2.x 语法
this.$scopedSlots.header
// 3.x 语法
this.$slots.header()
大部分更改已经在 2.6 中发布。因此,迁移可以一步到位:
- 在 3.x 中,将所有
this.$scopedSlots
替换为this.$slots
。 - 将所有
this.$slots.mySlot
替换为this.$slots.mySlot()
。
21、按键修饰符
- 不再支持使用数字 (即键码) 作为 v-on 修饰符(现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 名称-
<input v-on:keyup.page-down="nextPage">
) - 不再支持
config.keyCodes
自定义别名
在 Vue 2 中,keyCodes 可以作为修改 v-on 方法的一种方式。
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
此外,也可以通过全局的 config.keyCodes
选项定义自己的别名。
Vue.config.keyCodes = {
f1: 112
}
<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />
<!-- 自定义别名版本 -->
<input v-on:keyup.f1="showHelpText" />
从 KeyboardEvent.keyCode
已被废弃开始,Vue 3 继续支持这一点就不再有意义了。
因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 名称。
<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">
<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit">
对于某些标点符号键,可以直接把它们包含进去,以,
键为例:
<input v-on:keypress.,="commaPress">
语法的限制导致某些特定字符无法被匹配,比如 "
、'
、/
、=
、>
和 .
。对于这些字符,你应该在监听器内使用 event.key
代替。
22、$children 移除
$children
实例 property 已从 Vue 3.0 中移除,不再支持。如果你需要访问子组件实例,使用模板引用($ref)。
23、propsData 移除
propsData
选项之前用于在创建 Vue 实例的过程中传入 prop
,现在它被移除了。如果想为 Vue 3 应用的根组件传入 prop
,请使用 createApp
的第二个参数。
在 2.x 中,我们可以在创建 Vue 实例的时候传入 prop:
const Comp = Vue.extend({
props: ['username'],
template: '<div>{{ username }}</div>'
})
new Comp({
propsData: {
username: 'Evan'
}
})
propsData
选项已经被移除。如果你需要在实例创建时向根组件传入 prop
,你应该使用 createApp
的第二个参数:
const app = createApp(
{
props: ['username'],
template: '<div>{{ username }}</div>'
},
{ username: 'Evan' }
)
24、被挂载的应用不会替换元素
在 Vue 2.x 中,当挂载一个具有 template
的应用时,被渲染的内容会替换我们要挂载的目标元素。
我们为 new Vue()
或 $mount
传入一个 HTML
元素选择器:
new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
// 或
const app = new Vue({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.$mount('#app')
我们把应用挂载到拥有匹配被传入选择器 (在这个例子中是 id="app"
) 的 div 的页面时:
<body>
<div id="app">
Some app content
</div>
</body>
在渲染结果中,上面提及的 div
将会被应用所渲染的内容替换:
<body>
<div id="rendered">Hello Vue!</div>
</body>
在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML
。
当我们挂载一个应用时,其渲染内容会替换我们传递给 mount
的元素的 innerHTML
:
const app = Vue.createApp({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.mount('#app')
当这个应用挂载到拥有匹配 id="app"
的 div
的页面时,结果会是:
<body>
<div id="app" data-v-app="">
<div id="rendered">Hello Vue!</div>
</div>
</body>
25、Props 的 default
工厂函数不再可以访问 this
上下文
生成 prop
默认值的工厂函数不再能访问 this
。
取而代之的是:
组件接收到的原始 prop
将作为参数传递给默认函数;
inject API 可以在默认函数中使用。
import { inject } from 'vue'
export default {
props: {
theme: {
default (props) {
// `props` 是传递给组件的、
// 在任何类型/默认强制转换之前的原始值,
// 也可以使用 `inject` 来访问注入的 property
return inject('theme', 'default-theme')
}
}
}
}
26、transition
26.1 过渡的 class 名更改
过渡类名 v-enter
修改为 v-enter-from
、过渡类名 v-leave
修改为 v-leave-from
。
在 v2.1.8 版本之前,每个过渡方向都有两个过渡类:初始状态与激活状态。
在 v2.1.8 版本中,引入了 v-enter-to
来定义 enter
或 leave
变换之间的过渡动画插帧。
然而,为了向下兼容,并没有变动 v-enter
类名:
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
这样做会带来很多困惑,类似 enter
和 leave
含义过于宽泛,并且没有遵循类名钩子的命名约定。
在Vue3 中,为了更加明确易读,我们现在将这些初始状态重命名为:
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
<transition>
组件的相关 prop
名称也发生了变化:
leave-class
已经被重命名为leave-from-class
(在渲染函数或 JSX 中可以写为:leaveFromClass
)enter-class
已经被重命名为enter-from-class
(在渲染函数或 JSX 中可以写为:enterFromClass
)
26.2 Transition
作为根节点
当使用 <transition>
作为根结点的组件从外部被切换时将不再触发过渡效果。
在 Vue 2 中,通过使用 <transition>
作为一个组件的根节点,过渡效果存在从组件外部触发的可能性:
<!-- 模态组件 -->
<template>
<transition>
<div class="modal"><slot/></div>
</transition>
</template>
<!-- 用法 -->
<modal v-if="showModal">hello</modal>
切换 showModal
的值将会在模态组件内部触发一个过渡效果。
这是无意为之的,并不是设计效果。一个 <transition>
原本是希望被其子元素触发的,而不是 <transition>
自身。
这个怪异的现象现在被移除了。
换做向组件传递一个 prop
就可以达到类似的效果:
<template>
<transition>
<div v-if="show" class="modal"><slot/></div>
</transition>
</template>
<script>
export default {
props: ['show']
}
</script>
<!-- 用法 -->
<modal :show="showModal">hello</modal>
26.3 Transition Group 根元素
<transition-group>
不再默认渲染根元素,但仍然可以用 tag
attribute 创建根元素。
在 Vue 2 中,<transition-group>
像其它自定义组件一样,需要一个根元素。默认的根元素是一个 <span>
,但可以通过 tag
attribute 定制。
<transition-group tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</transition-group>
在 Vue 3 中,我们有了片段的支持,因此组件不再需要根节点。所以,<transition-group>
不再默认渲染根节点。
如果像上面的示例一样,已经在 Vue 2 代码中定义了 tag
attribute,那么一切都会和之前一样
如果没有定义 tag
attribute,而且样式或其它行为依赖于 <span>
根元素的存在才能正常工作,那么只需将 tag="span"
添加到 <transition-group>
:
<transition-group tag="span">
<!-- -->
</transition-group>
27、VNode 生命周期事件
在 Vue 2 中,我们可以通过事件来监听组件生命周期中的关键阶段。这些事件名都是以 hook:
前缀开头,并跟随相应的生命周期钩子的名字。
在 Vue 3 中,这个前缀已被更改为 vue:
。额外地,这些事件现在也可用于 HTML
元素,和在组件上的用法一样。
@hook:updated="onUpdated"
=> @vue:updated="onUpdated"
destroyed
生命周期选项被重命名为 unmounted
beforeDestroy
生命周期选项被重命名为 beforeUnmount
28、Watch 侦听数组
当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组被改变时触发回调,必须指定 deep
选项。
29、$destroy
实例方法
用户不应该再手动管理单个 Vue 组件的生命周期。
30、全局函数 set 和 delete
全局函数 set
和 delete
以及实例方法 $set
和$delete
。基于代理的变化检测已经不再需要它们了
31、样式穿透
在vue3.0中,/deep/
以及::v-deep
已经不被推荐,推荐使用 :deep()
替代
/* Vue 2.0 写法 */
::v-deep .carousel-btn.prev {
left: 270px;
}
/* Vue 3.0 更改为以下写法 */
:deep(.carousel-btn.prev) {
left: 270px;
}
/* 或是 */
:deep() {
.class {}
}
有关Vue 3 ::v-deep usage as a combinator has been deprecated. Use ::v-deep() instead的内容,推荐使用:deep()
二、Vue3 新增内容
1、组合式 API
详情参考:组合式 API
2、单文件组件中的组合式 API 语法糖 (<script setup>
)
<script setup>
是在单文件组件 (SFC
) 中使用组合式 API 的编译时语法糖。当同时使用 SFC
与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯
TypeScript
声明props
和自定义事件。 - 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的
IDE
类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
2.1 基本语法
<script setup>
console.log('hello script setup')
</script>
里面的代码会被编译成组件 setup()
函数的内容。这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。
2.2 顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import
导入的内容) 都能在模板中直接使用:
<script setup>
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>
import
导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper
函数,而不需要通过 methods
选项来暴露它:
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
2.3 响应式
响应式状态需要明确使用响应式 API 来创建。和 setup()
函数的返回值一样,ref
在模板中使用的时候会自动解包:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
2.4 使用组件
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用:
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
这里 MyComponent
应当被理解为像是在引用一个变量。如果你使用过 JSX,此处的心智模型是类似的。其 kebab-case
格式的 <my-component>
同样能在模板中使用
不过,我们强烈建议使用 PascalCase
格式以保持一致性。同时这也有助于区分原生的自定义元素。
2.4.1 动态组件
由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup>
中要使用动态组件的时候,应该使用动态的 :is
来绑定:
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
请注意组件是如何在三元表达式中被当做变量使用的。
2.4.2 递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue
的组件可以在其模板中用 <FooBar/>
引用它自己。
请注意这种方式相比于导入的组件优先级更低。如果有具名的导入和组件自身推导的名字冲突了,可以为导入的组件添加别名:
import { FooBar as FooBarChild } from './components'
2.4.3 命名空间组件
可以使用带 .
的组件标签,例如 <Foo.Bar>
来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
2.5 使用自定义指令
全局注册的自定义指令将正常工作。
本地的自定义指令在 <script setup>
中不需要显式注册,但他们必须遵循 vNameOfDirective
这样的命名规范:
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
2.6 defineProps() 和 defineEmits()
为了在声明 props
和 emits
选项时获得完整的类型推导支持,我们可以使用 defineProps
和 defineEmits
API,它们将自动地在 <script setup>
中可用:
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
defineProps
和defineEmits
都是只能在<script setup>
中使用的编译器宏。他们不需要导入,且会随着<script setup>
的处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
接收与emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推导。- 传入到
defineProps
和defineEmits
的选项会从 setup 中提升到模块的作用域。因此,传入的选项不能引用在 setup 作用域中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块作用域内。
2.7 defineExpose()
使用 <script setup>
的组件是默认关闭的——即通过模板引用或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
可以通过 defineExpose
编译器宏来显式指定在 <script setup>
组件中要暴露出去的属性:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number }
(ref 会和在普通实例中一样被自动解包)
2.8 useSlots() 和 useAttrs()
在 <script setup>
使用 slots
和 attrs
的情况应该是相对来说较为罕见的,因为可以在模板中直接通过 $slots
和 $attrs
来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots
和 useAttrs
两个辅助函数:
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
useSlots
和 useAttrs
是真实的运行时函数,它的返回与 setupContext.slots
和 setupContext.attrs
等价。它们同样也能在普通的组合式 API 中使用。
2.9 与普通的 <script>
一起使用
<script setup>
可以和普通的 <script>
一起使用。普通的 <script>
在有这些需要的情况下或许会被使用到:
- 声明无法在 `` 中声明的选项,例如
inheritAttrs
或插件的自定义选项。 - 声明模块的具名导出 (named exports)。
- 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
<script>
// 普通 <script>, 在模块作用域下执行 (仅一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
在同一组件中将 <script setup>
与 <script>
结合使用的支持仅限于上述情况。具体来说:
- 不要为已经可以用
<script setup>
定义的选项使用单独的<script>
部分,如props
和emits
。 - 在
<script setup>
中创建的变量不会作为属性添加到组件实例中,这使得它们无法从选项式 API 中访问。我们强烈反对以这种方式混合 API。
如果你发现自己处于以上任一不被支持的场景中,那么你应该考虑切换到一个显式的 setup()
函数,而不是使用 <script setup>
。
2.10 顶层 await
<script setup>
中可以使用顶层 await
。结果代码会被编译成 async setup()
:
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
3、常用API
ref、reactive
ref:将单个值转成响应式对象,如:数字0、1,字符串”你好”等
reactive:将对象转成响应式对象,如:
{name:'张三',age:18}
let height = ref(170);
let person = reactive({
name:'张三',
age:20,
sex:'男'
});
toRef、toRefs
- toRef:将对象中的某个属性单独转成响应式对象,并和原来的值关联
- toRefs:将对象中的所有属性单独转成响应式对象,并和原来的值关联
const name = toRef(person,'name')
const p = toRefs(person)
watch,computed,watchEffect
- watch:监视函数,被监视的属性发生变化时调用
- watchEffect:监视被使用的属性是否发生变化
- computed:计算属性,返回被计算后的值
计算属性调试
我们可以向 computed()
传入第二个参数,是一个包含了 onTrack
和 onTrigger
两个回调函数的对象:
onTrack
将在响应属性或引用作为依赖项被跟踪时被调用。onTrigger
将在侦听器回调被依赖项的变更触发时被调用。
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// 当 count.value 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 count.value 被更改时触发
debugger
}
})
// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)
// 更改 count.value,应该会触发 onTrigger
count.value++
tips:计算属性的
onTrack
和onTrigger
选项仅会在开发模式下工作。
侦听器调试
和 computed()
类似,侦听器也支持 onTrack
和 onTrigger
选项:
watch(source, callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
watchEffect(callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
tips:侦听器的
onTrack
和onTrigger
选项仅会在开发模式下工作。
4、不常用API
shallowRef、shallowReactive、readonly、shallowReadonly
shallowRef:如果参数是原子类型数据,作用和ref
一样;如果是对象,则对象中的数据都不是响应式,但是可以替换对象
shallowReactive:只有对象的第一层属性是响应式,其他层次数据不是响应式
readonly:让一个响应式数据变成只读(深只读)
shallowReadonly:让一个响应式数据变成只读(浅只读)
toRaw、markRaw
toRaw:将一个由reactive
生成的响应式对象转为普通对象
markRaw:标记一个对象,使其永远不再成为响应式对象;在往响应式对象中添加对象时使用
customRef
自定义ref,传入参数track、trigger,使得Vue追踪以及触发模板更新
provide、inject,祖孙组件间数据传递
响应式数据的判断
isRef:检查一个值是否位一个ref对象
isReactive:检查一个对象是否是由reactive创建的响应式代理
isReadonly:检查一个对象是否是由readonly创建的只读代理
isProxy:检查一个对象是否是由reactive或者readonly方法创建的代理
5、 新增三个组件
5.1 Fragment
- 在Vue2中: 组件必须有一个根 结点 , 很多时候会添加一些没有意义的节点用于包裹。Fragment组件就是用于解决这个问题的
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
5.2 Teleport
<Teleport>
是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
Teleport
是一种能够将我们的组件html结构移动到指定位置的技术
<teleport to="移动位置">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
5.3 Suspense (实验性功能)
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
用法
- 异步引入组件
- 使用Suspense包裹组件,并配置好default 与 fallback
- default:就是组件要显示的内容
- fallback:就是组件没加载完全的“备胎”(类似于img标签的alt属性)
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</div>
</template>
三、Vue3.0 是如何变快的?
- diff 算法优化
- Vue2 中的虚拟dom 是进行全量对比
- Vue3 新增静态标记
- hoistStatic 静态提升
- Vue2 中无论元素是否参与更新,每次都会重新创建,然后在渲染
- Vue3 中对于不参与更新的元素,会做静态提升,只被创建一次,在渲染时直接复用即可
- cacheHandlers 事件侦听器缓存
- 默认情况下默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
- ssr 渲染
- 当有大量静态的内容的时候,这些内容会被当作纯字符串推进一个buffer里面,即使存在动态的绑定,会通过模版插值嵌入进去,这样会比通过虚拟dom来渲染的快上很多很多
- 当静态内容大到一定量级的时候,会用_createStaticVNode方法在客户端去生成一个static node。这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。
四、问题汇总
1.vue3没有this怎么办?
在vue3中,新的组合式API中没有this,那我们如果需要用到this
怎么办?
解决方法:
getCurrentInstance
方法获取当前组件的实例,然后通过 ctx
或 proxy
属性获得当前上下文,这样我们就能在setup
中使用router
和vuex
了
import { getCurrentInstance } from "vue";
export default {
setup() {
let { proxy } = getCurrentInstance();
proxy.$axios(...)
proxy.$router(...)
}
}
但不建议使用getCurrentInstance
方法。如果要使用router
和vuex
,推荐这样用:
import { computed } from 'vue'
import { useStore } from 'vuex'
import { useRoute, useRouter } from 'vue-router'
export default {
setup () {
const store = useStore()
const route = useRoute()
const router = useRouter()
return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),
// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
// 使用 mutation
increment: () => store.commit('increment'),
// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
}
}
大家不要依赖 getCurrentInstance
方法去获取组件实例来完成一些主要功能,否则在项目打包后,可能会报错的。
2.Vue 数据更新了但页面没有更新的情况
如果,你发现自己需要在vue中做一次强制更新,99.9%的情况,是你在某个地方做错了事
1.vue无法检测实例被创建时,不存在于data中的property
原因:参数没有在data
里声明
由于Vue会在初始化实例时,对property
执行getter/setter
转化,所以,property
必须在data
对象上存在才能让Vue将它转化为响应式的。
场景:
var vm = new Vue({
data:{},
// 页面不会变化
template: '<div>{{message}}</div>'
})
vm.message = 'Hello!' // `vm.message` 不是响应式的
解决办法:
var vm = new Vue({
data: {
// 声明 a、b 为一个空值字符串
message: '',
},
template: '<div>{{ message }}</div>'
})
vm.message = 'Hello!'
2.vue无法检测对象property的添加或移除
原因:
由于JavaScript(ES5)的限制,Vue不能检测到对象属性的添加或删除。因为vue.js在初始化实例时将属性转为getter/setter
,所以属性必须在data
对象上才能让Vue转换它,才能让它使响应的。
场景:
var vm = new Vue({
data:{
obj: {
id: 001
}
},
// 页面不会变化
template: '<div>{{ obj.message }}</div>'
})
vm.obj.message = 'hello' // 不是响应式的
delete vm.obj.id // 不是响应式的
解决办法:
//动态添加 -- vue.set
Vue.set(vm.obj,propertyName,newValue);
//动态添加 -- vm.$set
vm.$set(vm.obj,propertyName,newValue);
//动态添加多个
// 代替Object.assign(this.obj,{a:1,b:2})
this.obj = Object.assign({},this.obj,{a:1,b:2});
//动态移除--vm.$delect
Vue.delete(vm.obj,propertyName);
//动态移除 --vm.$delete
vm.$delete(vm.obj,propertyName);
3.vue不能检测通过数组索引值,直接修改一个数组项
原因:Vue不能检测数组和对象的变化。
场景:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items[2] = 'm' // 不是响应性的
解决办法:
//Vue.set
Vue.set(vm.items,indexOfItem,newValue);
//vm.$set
vm.$set(vm.items,indexOfItem,newValue);
//Array.prototype.splice
vm.items.splice(indexOfItem,1,newValue);
更多有关数据更新了但是页面没有更新的情况,查看Vue 数据更新了但页面没有更新的情况
本文参考:
延伸阅读: