Vue3新特性及和Vue2的部分差异性(持续更新汇中)


一、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,可以忽略)

vue2 生命周期

vue3 生命周期

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其实是一个语法糖,它的背后本质上是包含两个操作:

  1. v-bind绑定一个prop value属性
  2. 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 中,它发生了如下变化:

  1. 用于自定义组件时,v-model prop 和事件默认名称已更改:
  • prop:value -> modelValue
  • 事件:input -> update:modelValue
  1. v-bind.sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替
  2. 新增:现在可以在同一个组件上使用多个 v-model 绑定;
  3. 新增:现在可以自定义 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 中,我们可以定义dataobject或者function,但是我们知道在组件中如果dataobject的话会出现数据互相影响,因为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();//自定义一个鼠标移动事件

相比mixinshook解决了以下几点不足:

  1. 混入变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
  2. 多个mixins的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突。
  3. mixins和组件可能出现多对多的关系,复杂度较高(即一个组件可以引用多个mixins,一个mixins也可以被多个组件引用)。
  4. 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-forv-if有更高的优先级。

Vue3:v-ifv-for有更高的优先级。

在 Vue2.x 中,在一个元素上同时使用v-forv-ifv-for更高的优先级,因此在 Vue2.x 中做性能优化,有一个重要的点就是v-forv-if不能放在同一个元素上。

而在 Vue3 中,v-ifv-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 对象的一部分被传入。

钩子函数被重命名:
原来:bindinsertedupdatecomponentUpdatedunbind

现在:created(新增)、bind->beforeMountinserted->mountedbeforeUpdate(新增)、componentUpdated->updatedbeforeUnmount(新增)、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,仅在 beforeUpdateupdated 钩子中可用。

12.3 binding绑定对象属性

入参 说明 示例
name 指令名,不包含前缀 v-。 v-focus 中的 focus
value 指令所绑定的值(计算后)。 v-my-directive="1 + 1" 中,绑定值为 `2
oldValue 指令绑定的前一个值,仅在 beforeUpdateupdated 钩子中可用。无论值是否改变都可用。 -
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 } 组件选项。

它们将接收两个参数:propscontextcontext 参数是一个对象,包含三个属性:attrsemitslots 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)

  1. 新的 defineAsyncComponent 助手方法,用于显式地定义异步组件;
  2. component 选项被重命名为 loader,以明确组件定义不能直接被提供;
  3. Loader 函数本身不再接收 resolvereject 参数,且必须返回一个 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 函数不再接收 resolvereject 参数,且必须始终返回 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,包括 classstyle

18.1 Vue2.x 行为

Vue 2 的虚拟 DOM 实现对 classstyle attribute 有一些特殊处理。因此,与其它所有 attribute 不一样,它们没有被包含在 $attrs 中。

上述行为在使用 inheritAttrs: false 时会产生副作用:

$attrs 中的 attribute 将不再被自动添加到根元素中,而是由开发者决定在哪添加。
但是 classstyle 不属于 $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 的组件中,请确保样式仍然符合预期。如果你之前依赖了 classstyle 的特殊行为,那么一些视觉效果可能会遭到破坏,因为这些 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 中发布。因此,迁移可以一步到位:

  1. 在 3.x 中,将所有 this.$scopedSlots 替换为 this.$slots
  2. 将所有 this.$slots.mySlot 替换为 this.$slots.mySlot()

21、按键修饰符

  1. 不再支持使用数字 (即键码) 作为 v-on 修饰符(现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 名称-
    <input v-on:keyup.page-down="nextPage">
  2. 不再支持 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 来定义 enterleave 变换之间的过渡动画插帧。

然而,为了向下兼容,并没有变动 v-enter 类名:

.v-enter,
.v-leave-to {
    opacity: 0;
}

.v-leave,
.v-enter-to {
    opacity: 1;
}

这样做会带来很多困惑,类似 enterleave 含义过于宽泛,并且没有遵循类名钩子的命名约定

在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

全局函数 setdelete 以及实例方法 $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()

为了在声明 propsemits 选项时获得完整的类型推导支持,我们可以使用 definePropsdefineEmits API,它们将自动地在 <script setup> 中可用:

<script setup>
const props = defineProps({
    foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
  • definePropsdefineEmits 都是只能在 <script setup> 中使用的编译器宏。他们不需要导入,且会随着 <script setup> 的处理过程一同被编译掉。
  • defineProps 接收与 props 选项相同的值,defineEmits 接收与 emits 选项相同的值。
  • definePropsdefineEmits 在选项传入后,会提供恰当的类型推导。
  • 传入到 definePropsdefineEmits 的选项会从 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> 使用 slotsattrs 的情况应该是相对来说较为罕见的,因为可以在模板中直接通过 $slots$attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlotsuseAttrs 两个辅助函数:

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

useSlotsuseAttrs 是真实的运行时函数,它的返回与 setupContext.slotssetupContext.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> 部分,如 propsemits
  • <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() 传入第二个参数,是一个包含了 onTrackonTrigger 两个回调函数的对象:

  • 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:计算属性的 onTrackonTrigger 选项仅会在开发模式下工作。

侦听器调试

computed() 类似,侦听器也支持 onTrackonTrigger 选项:

watch(source, callback, {
    onTrack(e) {
        debugger
    },
    onTrigger(e) {
        debugger
    }
})

watchEffect(callback, {
    onTrack(e) {
        debugger
    },
    onTrigger(e) {
        debugger
    }
})

tips:侦听器的 onTrackonTrigger 选项仅会在开发模式下工作。

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 方法获取当前组件的实例,然后通过 ctxproxy 属性获得当前上下文,这样我们就能在setup中使用routervuex

import { getCurrentInstance } from "vue";
export default {
    setup() {
        let { proxy } = getCurrentInstance();
        proxy.$axios(...)
        proxy.$router(...)
    }
}

但不建议使用getCurrentInstance方法。如果要使用routervuex,推荐这样用:

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 数据更新了但页面没有更新的情况

本文参考:

延伸阅读:

  • 深入响应性原理:理解 Vue 响应性系统的底层细节。
  • 状态管理:多个组件间共享状态的管理模式。
  • 测试组合式函数:组合式函数的单元测试技巧。
  • VueUse:一个日益增长的 Vue 组合式函数集合。源代码本身就是一份不错的学习资料。

文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录