Vue3作为Vue2的下一个主要版本,不仅在性能上进行了优化,还引入了一些强大的新特性。然而,与Vue2相比,Vue3并非仅仅是一个简单的升级,而是一个经过重新设计的框架,注重了性能、可维护性和可扩展性。Vue3带来了许多强大的新特性,如Composition APIcreateRendererTeleport等,以及对性能的全面优化。尽管在学习曲线上可能需要一些时间,但这些变化使得Vue更加现代、灵活,为前端开发者提供了更好的开发体验。

1、响应式原理不同

  1. vue2通过Object.definedProperty()get()set()来做数据劫持、结合和发布订阅者模式来实现,Object.definedProperty()会遍历每一个属性。
  2. vue3通过Proxy代理的方式实现。
  3. Proxy的优势:不需要像Object.definedProperty()的那样遍历每一个属性,有一定的性能提升proxy可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问都必须通过这一层拦截。这个拦截可以对外界的访问进行过滤和改写。
  4. 当属性过多的时候利用Object.definedProperty()要通过遍历的方式监听每一个属性。利用proxy则不需要遍历,会自动监听所有属性,有利于性能的提升.

2、diff算法不同

1. vue2中的diff算法

  • 遍历每一个虚拟节点,进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方。
  • patch记录的消息去更新dom
  • 缺点:比较每一个节点,而对于一些不参与更新的元素,进行比较是有点消耗性能的。
  • 特点:特别要提一下Vuepatch是即时的,并不是打包所有修改最后一起操作DOM,也就是在vue中边记录变更新。(React则是将更新放入队列后集中处理)。

2. vue3中的diff算法

  • 在初始化的时候会给每一个虚拟节点添加一个patchFlags,是一种优化的标识。
  • 只会比较patchFlags发生变化的节点,进行识图更新。而对于patchFlags没有变化的元素作静态标记,在渲染的时候直接复用。

3. vue3中的虚拟DOM

虚拟DOM上增加patchFlag字段。我们借助Vue3 Template Explorer来看

1
2
3
4
5
<div id="app">
<h1>vue3虚拟DOM讲解</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
</div>

渲染函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue

const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: app }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3虚拟DOM讲解, -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(div, _hoisted_1, [
_hoisted_2,
_hoisted_3,
_createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}

注意第3个_createElementVNode的第4个参数即patchFlag字段类型。

字段类型情况:1代表节点为动态文本节点,那在diff过程中,只需比对文本对容,无需关注class、style等。除此之外,发现所有的静态节点(HOISTED-1),都保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// patchFlags 字段类型列举
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态类名
STYLE = 1 << 2, // 动态样式
PROPS = 1 << 3, // 动态属性,不包含类名和样式
FULL_PROPS = 1 << 4, // 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
HYDRATE_EVENTS = 1 << 5, // 带有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 不会改变子节点顺序的 fragment
KEYED_FRAGMENT = 1 << 7, // 带有 key 属性的 fragment 或部分子节点
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的fragment
NEED_PATCH = 1 << 9, // 只会进行非 props 的比较
DYNAMIC_SLOTS = 1 << 10, // 动态的插槽
HOISTED = -1, // 静态节点,diff阶段忽略其子节点
BAIL = -2 // 代表 diff 应该结束
}

4. 事件缓存

Vue3cacheHandler可在第一次渲染后缓存我们的事件。相比于Vue2无需每次渲染都传递一个新函数。加一个click事件。

1
2
3
4
5
6
<div id="app">
<h1>vue3事件缓存讲解</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
<span onCLick=() => {}><span>
</div>

渲染函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue

const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: app }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3事件缓存讲解, -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [
/*#__PURE__*/_createElementVNode(span)
], -1 /* HOISTED */))

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(div, _hoisted_1, [
_hoisted_2,
_hoisted_3,
_createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_hoisted_4
]))
}

观察以上渲染函数,会发现click事件节点为静态节点(HOISTED-1),即不需要每次重新渲染。

3、打包优化

1. Tree-shaking

Tree-shaking是一种模块打包中的概念,常见于webpackrollup等工具。它的目标是移除JavaScript上下文中未被引用的代码,以减小最终打包出的文件大小。该过程主要依赖于importexport语句,通过检测代码模块是否被导出、导入,并且是否被JavaScript文件使用来实现。

以一个例子说明,当使用tree-shaking时,只有那些真正被其他文件引用的模块会被保留在最终的打包文件中,未被引用的代码将被消除,从而优化了项目的性能和加载速度。

2. nextTick在Vue2和Tree-shaking的挑战

nextTick为例,对比Vue2中的全局API暴露在Vue实例上的情况,即使未使用nextTick,它仍然存在于Vue实例中,无法通过tree-shaking进行消除。这是因为在Vue2中,全局API是直接挂载在Vue构造函数上,而tree-shaking对全局对象的属性无法有效地进行检测和消除。这意味着即使你的应用中并未使用nextTick,它仍会被打包到最终的输出中,造成一定的性能损耗。

Vue3中,为了解决这个问题,Vue的一些全局API被重新设计,使其更适合tree-shaking。这种变化使得未使用的全局API在打包时能够被有效地消除,减小最终打包文件的体积。这是Vue3中对性能优化的一项改进之一。

1
2
3
4
5
import Vue from 'vue';

Vue.nextTick(() => {
// 一些和DOM有关的东西
});

Vue3中针对全局和内部的API进行了重构,并考虑到tree-shaking的支持。因此,全局API现在只能作为ES模块构建的命名导出进行访问。

1
2
3
4
5
import { nextTick } from 'vue';   // 显式导入

nextTick(() => {
// 一些和DOM有关的东西
});

通过这一更改,只要模块绑定器支持tree-shaking,则Vue应用程序中未使用的api将从最终的捆绑包中消除,获得最佳文件大小。

3. 受此更改影响的全局API

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅全构建)
  • Vue.set (仅兼容构建)
  • Vue.delete (仅兼容构建)
  • 内部API也有诸如transitionv-model等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。Vue3将所有运行功能打包也只有约22.5kb,比Vue2轻量很多。

4、全局属性(globalProperties)

vue2全局属性的配置:

比如对于一些第三方插件,vue2中通常使用prototype原型来挂载到vue对象中:

1
2
3
import Vue from 'vue'
Vue.prototype.$http=Axiox
Vue.prototype.$echart= Echart

vue3全局属性的配置:

vue3中提供了一个名为globalProperties的全局属性配置,可以代替vue2中的prototype:

1
2
app.config.globalProperties.$http = Axios
app.config.globalProperties.$echart = Echart

使用示例:

1
2
3
4
5
6
7
8
9
10
11
import { getCurrentInstance } from 'vue'
setup () {
// 获取当前组件的上下文,下面两种方式都能获取到组件的上下文。
const { ctx } = getCurrentInstance(); // 方式一,这种方式只能在开发环境下使用,生产环境下//的ctx将访问不到
const { proxy } = getCurrentInstance(); // 方式二,此方法在开发环境以及生产环境下都能放到组件上下文对象(推荐)
onMounted(() => {
console.log(ctx.$http) // 不推荐!不推荐!
console.log(proxy.$http) // 推荐
})
.......
}

5、多根节点(framents)

Vue3中,组件现在支持有多个根节点

1
2
3
4
5
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>

异步组件(Suspense)

Vue3提供Suspense组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如loading,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:defaultfallbackSuspense确保加载完异步内容时显示默认插槽,并将fallback插槽用作加载状态。

1
2
3
4
5
6
7
8
9
10
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
</template>

List组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示Loading…(即fallback插槽内容),加载完成时显示自身(即default插槽内容)。

6、Teleport

  • Teleport是一种能够将我们的模板移动到DOMVue app之外的其他位置的技术,就有点像哆啦A梦的“任意门”。
  • vue2中,像modalstoast等这样的元素,如果我们嵌套在Vue的某个组件内部,那么处理嵌套组件的定位、z-index和样式就会变得很困难。
  • 通过Teleport ,我们可以在组件的逻辑位置写模板代码,然后在Vue应用范围之外渲染它。
1
2
3
4
5
6
7
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一个 Toast 文案</div>
</div>
</teleport>

7、createRenderer

  • 通过createRenderer,能够构建自定义渲染器,能够将vue的开发模型扩展到其他平台。
  • 可以将其生成在canvas画布上。
  • 了解createRenderer的基本使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { createRenderer } from '@vue/runtime-core'

    const { render, createApp } = createRenderer({
    patchProp,
    insert,
    remove,
    createElement,
    // ...
    })

    export { render, createApp }

    export * from '@vue/runtime-core'

8、v-if和v-for的优先级

  • vue2v-for的优先级高于v-if,可以放在一起使用,但是不建议这么做,会带来性能上的浪费
  • vue3v-if的优先级高于v-for,一起使用会报错。可以通过在外部添加一个标签,将v-for移到外层

9、插槽方式不同

vue2中的插槽

匿名插槽

1
2
3
4
5
6
7
8
9
<!--子组件-->
<div>
<slot></slot>
</div>

<!--父组件-->
<child>
<span>我是插槽插入的内容</span>
</child>

具名插槽

1
2
3
4
5
6
7
8
9
<!--子组件-->
<div>
<slot name="person"></slot>
</div>

<!--父组件-->
<child>
<span slot="person">我是插槽插入的内容</span>
</child>

作用域插槽:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。不过,我们可以在父组件中使用slot-scope特性从子组件获取数据

1
2
3
4
5
6
7
8
<!--子组件-->
<div>
<slot :data="data"></slot>
</div>
<!--父组件-->
<child>
<span slot-scope="data">我是插槽插入的内容</span>
</child>

vue3中的插槽

匿名插槽:和在vue2中一样

具名插槽:

1
2
3
4
5
6
7
8
9
10
<!--子组件-->
<div>
<slot name="person"></slot>
</div>
<!--父组件-->
<child>
<template v-slot:person>
<span>我是插槽插入的内容</span>
</template>
</child>

作用域插槽:

1
2
3
4
5
6
7
8
<!--子组件-->
<div>
<slot :data="data"></slot>
</div>
<!--父组件-->
<child>
<span #data>我是插槽插入的内容</span> === <span #default="{data}">我是插槽插入的内容</span>
</child>

总结:

  • 具名插槽使用方式不同:vue2使用slot='',vue3使用v-slot:''
  • 作用域插槽使用方式不同:vue2中在父组件中使用slot-scope="data"从子组件获取数据,vue3中在父组件中使用#data或者#default="{data}"获取

10、样式穿透

vue2中的用法

1
2
/deep/ .类名{}
::v-deep .类名{}

vue3中的用法

1
2
:deep (.类名{})
::v-deep(.类名{})

11、选项式API(Options API)与组合式API(Composition API)

Vue2是选项API(Options API),一个逻辑会散乱在文件不同位置(datapropscomputedwatch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。

Vue3组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script>
export default {
data() {
return {
count: 1,
}
},
methods: {
onClick() {
this.count += 1
}
}
}
</script>

组合式API

所有的对象和方法都需要return才能使用,太繁琐,除了旧项目,可以用这种方式体验Vue3的新特性以外,不建议了解这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script>
import { ref } from 'vue'
export default {
//注意这部分
setup() {
let count = ref(1)
const onClick = () => {
count.value += 1
}
return {
count,
onClick,
}
}
}
</script>

setup语法糖

注意: <script setup>本质上是第二种写法的语法糖,掌握了这种写法,其实第二种写法也基本上就会了。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(1)
const onClick = () => {
count.value += 1
}
</script>

总结:

  • 选项式Api是将datamethods包括后面的watchcomputed等分开管理,而组合式Api则是将相关逻辑放到了一起(类似于原生js开发)。

  • setup语法糖则可以让变量方法不用再写return,后面的组件甚至是自定义指令也可以在我们的template中自动获得。

12、ref和reactive

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
import { ref, reactive, defineComponent } from 'vue'
export default defineComponent({
setup() {
let msg = ref('hello world')
let obj = reactive({
name:'vue3',age:3
})
const changeData = () => {
msg.value = 'hello vue3'
obj.name = 'hello world'
}
return {
msg,
obj,
changeData
}
}
})
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, reactive } from 'vue'
let msg = ref('hello world')
let obj = reactive({
name:'vue3',age:3
})
const changeData = () => {
msg.value = 'hello vue3'
obj.name = 'hello world'
}
</script>

ref和reactive

  • 选项式api中,data函数中的数据都具有响应式,页面会随着data中的数据变化而变化,而组合式api中不存在data函数,所以为了解决这个问题Vue3引入了refreactive函数来将使得变量成为响应式的数据。

  • 使用ref的时候在js中取值的时候需要加上.value

  • reactive更推荐去定义复杂的数据类型ref更推荐定义基本类型。

13、生命周期

下表包含:Vue2Vue3生命周期的差异

Vue2(选项式API)Vue3(setup)描述
beforeCreate-实例创建前
created-实例创建后
beforeMountonBeforeMountDOM挂载前调用
mountedonMountedDOM挂载完成调用
beforeUpdateonBeforeUpdate数据更新之前被调用
updatedonUpdated数据更新之后被调用
beforeDestroyonBeforeUnmount组件销毁前调用
destroyedonUnmounted组件销毁完成调用

Vue3里,除了将两个destroy相关的钩子,改成了unmount,剩下的需要注意的,就是在<script setup>中,不能使用beforeCreatecreated两个钩子。

如果你熟悉相关的生命周期,只需要记得在setup里,用on开头,加上大写首字母就行。

(Vue3)选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div></div>
</template>
<script>
export default {
beforeCreate() {},
created() {},

beforeMount() {},
mounted() {},

beforeUpdate() {},
updated() {},

// Vue2 里叫 beforeDestroy
beforeUnmount() {},
// Vue2 里叫 destroyed
unmounted() {},
// 其他钩子不常用,所以不列了。
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div></div>
</template>
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from 'vue'
onBeforeMount(() => {})
onMounted(() => {})
onBeforeUpdate(() => {})
onUpdated(() => {})
onBeforeUnmount(() => {})
onUnmounted(() => {})
</script>

总结:

  • 从上面可以看出Vue3中的组合式API采用hook函数引入生命周期;其实不止生命周期采用hook函数引入,像watchcomputed路由守卫等都是采用hook函数实现.

  • Vue3中的生命周期相对于Vue2做了一些调整,命名上发生了一些变化并且移除了beforeCreatecreated,因为setup是围绕beforeCreatecreated生命周期钩子运行的,所以不再需要它们。

14、methods

声明事件方法,我们只需要在script标签里,创建一个方法对象即可。剩下的在Vue2里是怎么写的,Vue3是同样的写法。

vue2写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div @click="onClick">
这是一个div
</div>
</template>
<script>
export default {
methods: {
onClick() {
console.log('clicked')
}
}
}
</script>

vue3写法

1
2
3
4
5
6
7
8
9
10
11
<template>
<div @click="onClick">
这是一个div
</div>
</template>
<script setup>
// 注意这部分
const onClick = () => {
console.log('clicked')
}
</script>

15、computed

vue2写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>
<script>
export default {
data() {
return {
value: 'this is a value'
}
},
computed: {
reversedValue() {
return value.split('').reverse().join('');
}
}
}
</script>

vue3写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>
<script setup>
import {ref, computed} from 'vue'
const value = ref('this is a value')
// 注意这里
const reversedValue = computed(() => {
// 使用 ref 需要 .value
return value.value.split('').reverse().join('')
})
</script>

16、watch

这一部分,我们需要注意一下了,Vue3watch有两种写法。一种是直接使用watch,还有一种是使用watchEffect
两种写法的区别是: watch需要你明确指定依赖的变量,才能做到监听效果,而watchEffect会根据你使用的变量,自动的实现监听效果。

(1)直接使用watch

vue2写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script>
export default {
data() {
return {
count: 1,
anotherCount: 0
}
},
methods: {
onClick() {
this.count += 1
}
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1
}
}
}
</script>

vue3写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(1)
const onClick = () => {
count.value += 1
}
const anotherCount = ref(0)
// 注意这里
// 需要在这里,
// 明确指定依赖的是 count 这个变量
watch(count, (newValue) => {
anotherCount.value = newValue - 1
})
</script>

(2)使用watchEffect

vue2写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script>
export default {
data() {
return {
count: 1,
anotherCount: 0
}
},
methods: {
onClick() {
this.count += 1
}
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1
}
}
}
</script>

vue3写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(1)
const onClick = () => {
count.value += 1
}
const anotherCount = ref(0)
// 注意这里
watchEffect(() => {
// 会自动根据 count.value 的变化,
// 触发下面的操作
anotherCount.value = count.value - 1
})
</script>

computedwatch所依赖的数据必须是响应式的。Vue3引入了watchEffectwatchEffect相当于将watch的依赖源和回调函数合并,当任何你有用到的响应式依赖更新时,该回调函数便会重新执行。不同于watch的是watchEffect的回调函数会被立即执行,即({ immediate: true }

(3)watch与watchEffect对比

watch介绍
watch用于观察一个或多个响应式引用或计算属性,并在它们更改时执行一个函数

watch参数

  • immediate: 是否立即执行回调函数。
  • deep: 是否深度观察(适用于对象或数组)。
  • flush: 控制回调何时执行(pre,post,sync

watchEffect介绍
watchEffect用于立即执行一个函数,并响应该函数内部所依赖的所有响应式引用或计算属性。

主要区别

  • 自动侦测依赖VS显式声明: watchEffect自动侦测函数内所用到的所有响应式引用,而watch需要你明确指定要观察的引用。
  • 立即执行: watchEffect创建时会立即执行一次,而watch默认不会,除非设置了immediate选项。
  • 旧值与新值: watch回调提供新值和旧值,而watchEffect不提供。
  • 多源观察: watch可以观察多个源,但watchEffect观察函数内的所有响应式引用。

相关点

  • 响应性: 两者都提供强大的响应性支持。
  • 生命周期: 在组件卸载时,两者都会自动停止观察。

使用场景
使用watch:

  • 当你需要访问旧值和新值。
  • 当你需要基于条件观察某个值。
  • 当你需要更细粒度的控制(如deep, flush 等选项)。

使用 watchEffect:

  • 当你需要依赖多个响应式引用,并希望所有这些改变都触发同一函数。
  • 当你不需要旧值,只关心新值。

17、v-model和sync

v-modelvue2中是双向绑定的语法糖。这里不讨论它在input标签的使用;只是看一下它和sync在组件中的使用

我们都知道Vue中的props是单向向下绑定的;每次父组件更新时,子组件中的所有props都会刷新为最新的值;但是如果在子组件中修改propsVue会向你发出一个警告(无法在子组件修改父组件传递的值);可能是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得混乱难以理解。

但是可以在父组件使用子组件的标签上声明一个监听事件,子组件想要修改props的值时使用$emit触发事件并传入新的值,让父组件进行修改。为了方便vue就使用了v-modelsync语法糖。

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!--父组件--->
<template>
<div>
<Child :changePval.sync="msg" />{{msg}}
<!-- 完整写法<Child :msg="msg" @update:changePval="msg=$event" /> -->
</div>
</template>
<script>
import Child from './Child'
export default {
components: {
Child
},
data(){
return {
msg:'父组件值'
}
}
}
</script>

<!---子组件-->
<template>
<div>
<button @click="changePval">改变父组件值</button>
</div>
</template>
<script>
export default {
data(){
return {
msg:'子组件元素'
}
},
methods:{
changePval(){
//点击则会修改父组件msg的值
this.$emit('update:changePval','改变后的值')
}
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--父组件--->
<template>
<div>
<!-- 完整写法<Child :msg="msg" @update:changePval="msg=$event" /> -->
<Child v-model:changePval="msg" />{{msg}}
</div>
</template>
<script setup>
import Child from './Child'
import { ref } from 'vue'
const msg = ref('父组件值')
</script>

<!--子组件--->
<template>
<button @click="changePval">改变父组件值</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emits = defineEmits(['changePval'])
const changePval = () => {
//点击则会修改父组件msg的值
emits('update:changePval','改变后的值')
}
</script>

vue3中移除了sync的写法,取而代之的式v-model:event的形式。其v-model:changePval="msg"或者:changePval.sync="msg"的完整写法为 :msg="msg" @update:changePval="msg=$event"。所以子组件需要发送update:changePval事件进行修改父组件的值

18、路由

vue3vue2路由常用功能只是写法上有些区别

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<button @click="toPage">路由跳转</button>
</div>
</template>
<script>
export default {
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被confirm前调用
next()
}
,beforeRouteLeave (to, from, next)=>{
//离开当前的组件,触发
next()
},
methods:{
toPage(){
//路由跳转
this.$router.push(xxx)
}
},
created(){
//获取params
this.$router.params
//获取query
this.$router.query
}
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<button @click="toPage">路由跳转</button>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用next()
next()
},
beforeRouteLeave (to, from, next)=>{
//离开当前的组件,触发next()
next()
},
setup() {
const router = useRouter()
const route = useRoute()
const toPage = () => {
router.push(xxx)
}
//获取params注意是route
route.params
//获取query
route.query
return {toPage}
}
})
</script>

setup语法糖

beforeRouteEnter作为路由守卫的示例是因为它在setup语法糖中是无法使用的;大家都知道setup中组件实例已经创建,是能够获取到组件实例的。而beforeRouteEnter是再进入路由前触发的,此时组件还未创建,所以是无法setup中的;如果想在setup语法糖中使用则需要再写一个setup语法糖script如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div>
<button @click="toPage">路由跳转</button>
</div>
</template>
<script>
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用next()
next()
}
};
</script>

<script setup>
import { useRoute, useRouter,onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const router = useRouter()
const route = useRoute()
const toPage = () => {router.push(xxx)
}
//获取params 注意是route
route.params
//获取query
route.query

//路由守卫
onBeforeRouteUpdate((to, from, next)=>{
//当前组件路由改变后,进行触发next()
next()
})
onBeforeRouteLeave((to, from, next)=>{
//离开当前的组件,触发next()
next()
})

</script>

19、组件通信

Vue中组件通信方式有很多,其中选项式API组合式API实现起来会有很多差异,这里将介绍如下组件通信方式:

方式Vue2Vue3
父传子propsprops
子传父$emitemits
父传子$attrsattrs
子传父$listeners无(合并到 attrs方式)
父传子provideprovide
子传父injectinject
子组件访问父组件$parent
父组件访问子组件$children
父组件访问子组件$refexpose&ref
兄弟传值EventBusmitt

(1)props

声明props我们可以用defineProps(),具体写法,我们看代码。

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!--父组件-->
<template>
<div><Child :foo="parentMsg" /></div>
</template>
<script>
import Child from './Child'
export default {
components:{
Child
},
data() {
return {
parentMsg: 'foo'
}
}
}
</script>


<!--子组件-->
<template>
<div>{{foo}}</div>
</template>
<script>
export default {
props:['foo']
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!---父组件-->
<template>
<div>
<Child :foo="parentMsg" />
</div>
</template>
<script>
import { ref } from 'vue'
import Child from './Child.vue'
export default {
components:{
Child
},
setup() {
const parentMsg = ref('foo')
return {parentMsg}
}
}
</script>

<!---子组件-->
<template>
<div>{{ parentMsg }}</div>
</template>
<script>
import { toRef } from "vue";
export default {
props: ["foo"],
// 如果这行不写,下面就接收不到
setup(props) {
console.log(props.foo)
//父组件信息
let parentMsg = toRef(props, 'foo')
return {parentMsg}
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--父组件-->
<template>
<div><Child :foo="parentMsg" /></div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMsg = ref('foo')
</script>

<!--子组件-->
<template>
<div>{{ parentMsg }}</div>
</template>
<script setup>
import { toRef, defineProps } from 'vue'
const props = defineProps(['foo'])
console.log(props.msg) //父组件信息
let parentMsg = toRef(props, 'foo')
</script>

注意

  • props中数据流是单项的,即子组件不可改变父组件传来的值

  • 在组合式API中,如果想在子组件中用其它变量接收props的值时需要使用toRef将props中的属性转为响应式。

使用props时,不要使用解构

1
2
3
4
5
6
7
8
<script setup>
const props = defineProps({
foo: String
})
// 不要这样写
const { foo } = props;
console.log(foo)
</script>

(2)emit

子组件可以通过emit发布一个事件并传递一些参数,父组件通过v-on进行这个事件的监听

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!--父组件-->
<template>
<div><Child @sendMsg="getFromChild" /></div>
</template>
<script>
import Child from './Child'
export default {
components:{
Child
},
methods: {
getFromChild(val) {
console.log(val) //我是子组件数据
}
}
}
</script>

<!--子组件-->
<template>
<div><button @click="sendFun">send</button></div>
</template>
<script>
export default {
methods:{
sendFun(){
this.$emit('sendMsg','我是子组件数据')
}
}
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!--父组件-->
<template>
<div><Child @sendMsg="getFromChild" /></div>
</template>
<script>
import Child from './Child'
export default {
components: {
Child
},
setup() {
const getFromChild = (val) => {
console.log(val) //子组件数据
}
return {getFromChild}
}
}
</script>

<!--子组件-->
<template>
<div><button @click="sendFun">send</button></div>
</template>

<script>
export default {
emits: ['sendMsg'],
setup(props, ctx) {
const sendFun = () => {
ctx.emit('sendMsg', '我是子组件数据')
}
return {sendFun}
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--父组件-->
<template>
<div><Child @sendMsg="getFromChild" /></div>
</template>
<script setup>
import Child from './Child'
const getFromChild = (val) => {
console.log(val) //我是子组件数据
}
</script>

<!--子组件-->
<template>
<div><button @click="sendFun">send</button></div>
</template>
<script setup>
import { defineEmits } from 'vue'
const emits = defineEmits(['sendMsg'])
const sendFun = () => {
emits('sendMsg', '我是子组件数据')
}
</script>

(3)attrs和listeners

子组件使用$attrs可以获得父组件除了props传递的属性和特性绑定属性 (classstyle)之外的所有属性。
子组件使用$listeners可以获得父组件(不含.native修饰器的)所有v-on事件监听器,在Vue3中已经不再使用;但是Vue3中的attrs不仅可以获得父组件传来的属性也可以获得父组件v-on事件监听器

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!--父组件-->
<template><div><Child @parentFun="parentFun" :msg1="msg1" :msg2="msg2"/></div>
</template>
<script>
import Child from './Child'
export default {
components:{
Child
},
data(){
return {msg1:'子组件msg1',msg2:'子组件msg2'}
},
methods: {
parentFun(val) {
console.log(`父组件方法被调用,获得子组件传值:${val}`)
}
}
}
</script>

<!--子组件-->
<template>
<div><button @click="getParentFun">调用父组件方法</button></div>
</template>
<script>
export default {
methods:{
getParentFun(){
this.$listeners.parentFun('我是子组件数据')
}
},
created(){
//获取父组件中所有绑定属性
console.log(this.$attrs)//{"msg1": "子组件msg1","msg2": "子组件msg2"}
//获取父组件中所有绑定方法
console.log(this.$listeners) //{parentFun:f}
}
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!--父组件-->
<template>
<div>
<Child @parentFun="parentFun" :msg1="msg1" :msg2="msg2" />
</div>
</template>
<script>
import Child from './Child'
import { ref } from 'vue'
export default defineComponent{
components: {
Child
},
setup() {
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
const parentFun = (val) => {
console.log(`父组件方法被调用,获得子组件传值:${val}`)
}
return {
parentFun,
msg1,
msg2
}
}
}
</script>

<!--子组件-->
<template>
<div>
<button @click="getParentFun">调用父组件方法</button>
</div>
</template>
<script>
export default {
emits: ['sendMsg'],
setup(props, ctx) {
//获取父组件方法和事件
console.log(ctx.attrs) //Proxy {"msg1": "子组件msg1","msg2": "子组件msg2"}
const getParentFun = () => {
//调用父组件方法
ctx.attrs.onParentFun('我是子组件数据')
}
return {
getParentFun
}
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!--父组件-->
<template>
<div>
<Child @parentFun="parentFun" :msg1="msg1" :msg2="msg2" />
</div>
</template>
<script setup>
import Child from './Child'
import { ref } from 'vue'
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
const parentFun = (val) => {
console.log(`父组件方法被调用,获得子组件传值:${val}`)
}
</script>

<!--子组件-->
<template>
<div>
<button @click="getParentFun">调用父组件方法</button>
</div>
</template>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
//获取父组件方法和事件
console.log(attrs) //Proxy {"msg1": "子组件msg1","msg2": "子组件msg2"}
const getParentFun = () => {
//调用父组件方法
attrs.onParentFun('我是子组件数据')
}
</script>

注意

  • Vue3中使用attrs调用父组件方法时,方法前需要加上on;如parentFun->onParentFun

(4)provide/inject

provide:是一个对象,或者是一个返回对象的函数。里面包含要给子孙后代属性

inject:一个字符串数组,或者是一个对象。获取父组件或更高层次的组件provide的值,既在任何后代组件都可以通过inject获得

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!---父组件-->
<script>
import Child from './Child'
export default {
components: {
Child
},
data() {
return {
msg1: '子组件msg1',
msg2: '子组件msg2'
}
},
provide() {
return {
msg1: this.msg1,
msg2: this.msg2
}
}
}
</script>

<!---子组件-->
<script>
export default {
inject:['msg1','msg2'],
created(){
//获取高层级提供的属性
console.log(this.msg1) //子组件msg1
console.log(this.msg2) //子组件msg2
}
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--父组件-->
<script>
import Child from './Child'
import { ref,provide } from 'vue'
export default {
components:{
Child
},
setup() {
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
provide("msg1", msg1)
provide("msg2", msg2)
return {}
}
}
</script>

<!--子组件-->
<template>
<div>
<button @click="getParentFun">调用父组件方法</button>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
console.log(inject('msg1').value) //子组件msg1
console.log(inject('msg2').value) //子组件msg2
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--父组件-->
<script setup>
import Child from './Child'
import { ref,provide } from 'vue'
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
provide("msg1",msg1)
provide("msg2",msg2)
</script>

<!--子组件-->
<script setup>
import { inject } from 'vue'
console.log(inject('msg1').value) //子组件msg1
console.log(inject('msg2').value) //子组件msg2
</script>

注意

  • provide/inject一般在深层组件嵌套中使用合适。一般在组件开发中用的居多。

(5)expose&ref

$refs可以直接获取元素属性,同时也可以直接获取子组件实例

选项式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!--父组件-->
<template>
<div>
<Child ref="child" />
</div>
</template>
<script>
import Child from './Child'
export default {
components: {
Child
},
mounted(){
//获取子组件属性
console.log(this.$refs.child.msg) //子组件元素

//调用子组件方法
this.$refs.child.childFun('父组件信息')
}
}
</script>

<!--子组件-->
<template>
<div>
<div></div>
</div>
</template>
<script>
export default {
data(){
return {
msg:'子组件元素'
}
},
methods:{
childFun(val){
console.log(`子组件方法被调用,值${val}`)
}
}
}
</script>

组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!--父组件-->
<template>
<div>
<Child ref="child" />
</div>
</template>
<script>
import Child from './Child'
import { ref, onMounted } from 'vue'
export default {
components: {
Child
},
setup() {
const child = ref() //注意命名需要和template中ref对应
onMounted(() => {
//获取子组件属性
console.log(child.value.msg) //子组件元素

//调用子组件方法
child.value.childFun('父组件信息')
})
return {
child //必须return出去 否则获取不到实例
}
}
}
</script>

<!--子组件-->
<template>
<div></div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const msg = ref('子组件元素')
const childFun = (val) => {
console.log(`子组件方法被调用,值${val}`)
}
return {
msg,
childFun
}
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!--父组件-->
<template>
<div>
<Child ref="child" />
</div>
</template>
<script setup>
import Child from './Child'
import { ref, onMounted } from "vue";
const child = ref() //注意命名需要和template中ref对应
onMounted(() => {
//获取子组件属性
console.log(child.value.msg) //子组件元素

//调用子组件方法
child.value.childFun('父组件信息')
})
</script>

<!--子组件-->
<template>
<div>
</div>
</template>
<script setup>
import { ref,defineExpose } from 'vue'
const msg = ref('子组件元素')
const childFun = (val) => {
console.log(`子组件方法被调用,值${val}`)
}
//必须暴露出去父组件才会获取到
defineExpose({
childFun,
msg
})
</script>

注意

  • 通过ref获取子组件实例必须在页面挂载完成后才能获取。

  • 在使用setup语法糖时候,子组件必须元素或方法暴露出去父组件才能获取到。

(6)EventBus/mitt

兄弟组件通信可以通过一个事件中心EventBus实现,既新建一个Vue实例来进行事件的监听,触发和销毁。

Vue3中没有了EventBus兄弟组件通信,但是现在有了一个替代的方案mitt.js,原理还是EventBus

选项式API

新建bus.js文件:

1
2
import Vue from 'vue'
export default new Vue()

组件1组件2的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!--组件1-->
<template>
<div>
<button @click="sendMsg">传值</button>
</div>
</template>
<script>
import Bus from './bus.js'
export default {
data(){
return {
msg:'子组件元素'
}
},
methods:{
sendMsg(){
Bus.$emit('sendMsg','兄弟的值')
}
}
}
</script>

<!--组件2-->
<template>
<div>
组件2
</div>
</template>
<script>
import Bus from './bus.js'
export default {
created(){
Bus.$on('sendMsg',(val)=>{
console.log(val)//兄弟的值
})
}
}
</script>

组合式API

首先安装mitt
npm i mitt -S

然后像Vue2bus.js一样新建mitt.js文件

mitt.js文件:

1
2
3
import mitt from 'mitt'
const Mitt = mitt()
export default Mitt

组件1组件2代码文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!--组件1-->
<template>
<button @click="sendMsg">传值</button>
</template>
<script>
import Mitt from './mitt.js'
export default {
setup() {
const sendMsg = () => {
Mitt.emit('sendMsg','兄弟的值')
}
return {
sendMsg
}
},
}
</script>

<!--组件2-->
<template>
<div>
组件2
</div>
</template>
<script>
import { onUnmounted } from 'vue'
import Mitt from './mitt.js'
export default {
setup() {
const getMsg = (val) => {
console.log(val)//兄弟的值
}
Mitt.on('sendMsg', getMsg)
onUnmounted(() => {
//组件销毁 移除监听
Mitt.off('sendMsg', getMsg)
})
}
}
</script>

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!--组件1-->
<template>
<button @click="sendMsg">传值</button>
</template>
<script setup>
import Mitt from './mitt.js'
const sendMsg = () => {
Mitt.emit('sendMsg', '兄弟的值')
}
</script>

<!--组件2-->
<template>
<div>
组件2
</div>
</template>
<script setup>
import { onUnmounted } from "vue";
import Mitt from './mitt.js'
const getMsg = (val) => {
console.log(val);//兄弟的值
}
Mitt.on('sendMsg', getMsg)
onUnmounted(() => {
//组件销毁 移除监听
Mitt.off('sendMsg', getMsg)
})
</script>

20、如何使用<script setup>编写组件

data需要注意的地方,整个data这一部分的内容,你只需要记住下面这一点:

以前在data中创建的属性,现在全都用ref()声明,在template中直接用,在script中记得加.value

vue2的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
methods: {
onClick() {
this.count += 1
}
}
}
</script>

vue3的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>
<script setup>
import { ref } from 'vue'
// 用这种方式声明
const count = ref(1);
const onClick = () => {
// 使用的时候记得 .value
count.value += 1
}
</script>