Vue3
作为Vue2
的下一个主要版本,不仅在性能上进行了优化,还引入了一些强大的新特性。然而,与Vue2
相比,Vue3
并非仅仅是一个简单的升级,而是一个经过重新设计的框架,注重了性能、可维护性和可扩展性。Vue3
带来了许多强大的新特性,如Composition API
、createRenderer
和Teleport
等,以及对性能的全面优化。尽管在学习曲线上可能需要一些时间,但这些变化使得Vue
更加现代、灵活,为前端开发者提供了更好的开发体验。
1、响应式原理不同
vue2
通过Object.definedProperty()
的get()
和set()
来做数据劫持、结合和发布订阅者模式来实现,Object.definedProperty()
会遍历每一个属性。
vue3
通过Proxy
代理的方式实现。
Proxy
的优势:不需要像Object.definedProperty()
的那样遍历每一个属性,有一定的性能提升proxy
可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问都必须通过这一层拦截。这个拦截可以对外界的访问进行过滤和改写。
当属性过多的时候利用Object.definedProperty()
要通过遍历的方式监听每一个属性。利用proxy
则不需要遍历,会自动监听所有属性,有利于性能的提升.
2、diff算法不同 1. vue2中的diff算法
遍历每一个虚拟节点,进行虚拟节点对比,并返回一个patch
对象,用来存储两个节点不同的地方。
用patch
记录的消息去更新dom
缺点:比较每一个节点,而对于一些不参与更新的元素,进行比较是有点消耗性能的。
特点:特别要提一下Vue
的patch
是即时的,并不是打包所有修改最后一起操作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 vueconst _withScopeId = n => (_pushScopeId (scope-id),n=n (),_popScopeId (),n)const _hoisted_1 = { id : app }const _hoisted_2 = _withScopeId (() => _createElementVNode (h1, null , vue3虚拟DOM 讲解, -1 ))const _hoisted_3 = _withScopeId (() => _createElementVNode (p, null , 今天天气真不错, -1 ))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 ) ])) }
注意第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 export const enum PatchFlags { TEXT = 1 , CLASS = 1 << 1 , STYLE = 1 << 2 , PROPS = 1 << 3 , FULL_PROPS = 1 << 4 , HYDRATE_EVENTS = 1 << 5 , STABLE_FRAGMENT = 1 << 6 , KEYED_FRAGMENT = 1 << 7 , UNKEYED_FRAGMENT = 1 << 8 , NEED_PATCH = 1 << 9 , DYNAMIC_SLOTS = 1 << 10 , HOISTED = -1 , BAIL = -2 }
4. 事件缓存 Vue3
的cacheHandler
可在第一次渲染后缓存我们的事件。相比于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 vueconst _withScopeId = n => (_pushScopeId (scope-id),n=n (),_popScopeId (),n)const _hoisted_1 = { id : app }const _hoisted_2 = _withScopeId (() => _createElementVNode (h1, null , vue3事件缓存讲解, -1 ))const _hoisted_3 = _withScopeId (() => _createElementVNode (p, null , 今天天气真不错, -1 ))const _hoisted_4 = _withScopeId (() => _createElementVNode (span, { onCLick : () => {} }, [ _createElementVNode (span) ], -1 )) 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 ), _hoisted_4 ])) }
观察以上渲染函数,会发现click
事件节点为静态节点(HOISTED
为-1
),即不需要每次重新渲染。
3、打包优化 1. Tree-shaking Tree-shaking
是一种模块打包中的概念,常见于webpack
、rollup
等工具。它的目标是移除JavaScript
上下文中未被引用的代码,以减小最终打包出的文件大小。该过程主要依赖于import
和export
语句,通过检测代码模块是否被导出、导入,并且是否被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 (() => { });
Vue3
中针对全局和内部的API进行了重构,并考虑到tree-shaking
的支持。因此,全局API
现在只能作为ES
模块构建的命名导出进行访问。
1 2 3 4 5 import { nextTick } from 'vue' ; nextTick (() => { });
通过这一更改,只要模块绑定器支持tree-shaking
,则Vue
应用程序中未使用的api
将从最终的捆绑包中消除,获得最佳文件大小。
3. 受此更改影响的全局API
Vue.nextTick
Vue.observable (用 Vue.reactive 替换)
Vue.version
Vue.compile (仅全构建)
Vue.set (仅兼容构建)
Vue.delete (仅兼容构建)
内部API也有诸如transition
、v-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 (); 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
,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default
和fallback
。Suspense
确保加载完异步内容时显示默认插槽,并将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
是一种能够将我们的模板移动到DOM
中Vue app
之外的其他位置的技术,就有点像哆啦A梦的“任意门”。
在vue2
中,像modals
,toast
等这样的元素,如果我们嵌套在Vue
的某个组件内部,那么处理嵌套组件的定位、z-index
和样式就会变得很困难。
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在Vue
应用范围之外渲染它。
1 2 3 4 5 6 7 <button @click ="showToast" class ="btn" > 打开 toast</button > <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的优先级
在vue2
中v-for
的优先级高于v-if
,可以放在一起使用,但是不建议这么做,会带来性能上的浪费
在vue3
中v-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),一个逻辑会散乱在文件不同位置(data
、props
、computed
、watch
、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
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 >
总结:
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
引入了ref
和reactive
函数来将使得变量成为响应式的数据。
使用ref
的时候在js中取值的时候需要加上.value
。
reactive
更推荐去定义复杂的数据类型ref
更推荐定义基本类型。
13、生命周期 下表包含:Vue2
和Vue3
生命周期的差异
Vue2(选项式API) Vue3(setup) 描述 beforeCreate - 实例创建前 created - 实例创建后 beforeMount onBeforeMount DOM挂载前调用 mounted onMounted DOM挂载完成调用 beforeUpdate onBeforeUpdate 数据更新之前被调用 updated onUpdated 数据更新之后被调用 beforeDestroy onBeforeUnmount 组件销毁前调用 destroyed onUnmounted 组件销毁完成调用
Vue3
里,除了将两个destroy
相关的钩子,改成了unmount
,剩下的需要注意的,就是在<script setup>
中,不能使用beforeCreate
和created
两个钩子。
如果你熟悉相关的生命周期,只需要记得在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 ( ) {}, beforeUnmount ( ) {}, 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 >
总结:
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 (() => { return value.value .split ('' ).reverse ().join ('' ) }) </script >
16、watch 这一部分,我们需要注意一下了,Vue3
中watch
有两种写法。一种是直接使用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 )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 (() => { anotherCount.value = count.value - 1 }) </script >
computed
和watch
所依赖的数据必须是响应式的。Vue3
引入了watchEffect
,watchEffect
相当于将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-model
在vue2
中是双向绑定的语法糖。这里不讨论它在input
标签的使用;只是看一下它和sync
在组件中的使用
我们都知道Vue
中的props
是单向向下绑定的;每次父组件更新时,子组件中的所有props
都会刷新为最新的值;但是如果在子组件中修改props
,Vue
会向你发出一个警告(无法在子组件修改父组件传递的值);可能是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得混乱难以理解。
但是可以在父组件使用子组件的标签上声明一个监听事件,子组件想要修改props
的值时使用$emit
触发事件并传入新的值,让父组件进行修改。为了方便vue
就使用了v-model
和sync
语法糖。
选项式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}} </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 ( ){ 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 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 = ( ) => { 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、路由 vue3
和vue2
路由常用功能只是写法上有些区别
选项式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) { next () } ,beforeRouteLeave (to, from , next)=>{ next () }, methods :{ toPage ( ){ this .$router .push (xxx) } }, created ( ){ this .$router .params 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) { next () }, beforeRouteLeave (to, from , next)=>{ next () }, setup ( ) { const router = useRouter () const route = useRoute () const toPage = ( ) => { router.push (xxx) } route.params 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 ) { next () } }; </script > <script setup > import { useRoute, useRouter,onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' const router = useRouter ()const route = useRoute ()const toPage = ( ) => {router.push (xxx)} route.params route.query onBeforeRouteUpdate ((to, from , next )=> { next () }) onBeforeRouteLeave ((to, from , next )=> { next () }) </script >
19、组件通信 Vue
中组件通信方式有很多,其中选项式API
和组合式API
实现起来会有很多差异,这里将介绍如下组件通信方式:
方式 Vue2 Vue3 父传子 props props 子传父 $emit emits 父传子 $attrs attrs 子传父 $listeners 无(合并到 attrs方式) 父传子 provide provide 子传父 inject inject 子组件访问父组件 $parent 无 父组件访问子组件 $children 无 父组件访问子组件 $ref expose&ref 兄弟传值 EventBus mitt
(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时,不要使用解构 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
传递的属性和特性绑定属性 (class
和style
)之外的所有属性。 子组件使用$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 ) console .log (this .$listeners ) } } </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 ) 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) 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 ) console .log (this .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 ) console .log (inject ('msg2' ).value ) } } </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 ) console .log (inject ('msg2' ).value ) </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 () onMounted (() => { console .log (child.value .msg ) child.value .childFun ('父组件信息' ) }) return { child } } } </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 () 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 >
注意
(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 <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 > <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
然后像Vue2
中bus.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 <template > <button @click ="sendMsg" > 传值</button > </template > <script > import Mitt from './mitt.js' export default { setup ( ) { const sendMsg = ( ) => { Mitt .emit ('sendMsg' ,'兄弟的值' ) } return { sendMsg } }, } </script > <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 <template > <button @click ="sendMsg" > 传值</button > </template > <script setup > import Mitt from './mitt.js' const sendMsg = ( ) => { Mitt .emit ('sendMsg' , '兄弟的值' ) } </script > <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 = ( ) => { count.value += 1 } </script >