Vue3新特性
1、Composition API
1.1 什么是 Composition API
- 函数式编程,逻辑复用途经–函数组合(组合式 Composition API)
- 面对对象编程,逻辑复用的途径–继承(选项式 选项式 API )
函数式编程思想将组件 UI逻辑 和状态逻辑解耦,让组件得到更大的复用性
vue2 选项式API
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
}
</script>
Vue3 组合式(更拥抱这种写法)
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)//或者const count = reactive({value:0})
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
</script>
1.2 常用 Composition API
Q1: 说说你对 setup 的理解
- 组合式 API 的入口
Q2: ref 和 reactive 有什么区别?
- ref 内部使用了 reactive
- ref(obj) === reactive({ value: obj })
Q3: ref 和 shallowRef 的区别,以及 reactive 和 shallowReactive 的区别?
- shallow 表示浅层,这里均是指的响应值作用在第一层,即 .value,不过我们可以使用 triggerRef(xxx) 来在深层内容变更后,手动触发更新,需要注意的是 shallowReactive 没有对应方法
Q4: watchEffect 与 watch 的区别
- watch 是懒执行,属性改变的时候执行,而 watchEffect 是默认会执行一次,然后属性改变也会执行。
- watch 是需要传入侦听的数据源,而 watchEffect 是自动收集数据源作为依赖;
- watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。
1.2.1 setup
setup() 钩子是在组件中使用组合式 API 的入口
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)//或者const count = reactive({value:0})
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
</script>
或
<script >
import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
expose({
increment
})
return () => h('div', count.value)
}
}
</script>
1.2.2 ref
ref 函数 用于定义一个响应式对象
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
1.2.3 reactive
用法同 ref
const obj = reactive({ count: 0 })
obj.count++
1.2.4 computed
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者通过对象 get、set 指定的方式
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
1.2.5 watchEffect、watchPostEffect、watchSyncEffect
后两者是前者的语法糖,就是将第二个参数中的 flush,指定为对应值,分别为:flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
初始化 执行顺序:watchEffect,watchSyncEffect,watchPostEffect
变化时 执行顺序:watchSyncEffect,watchEffect,watchPostEffect
监听回调又叫监听副作用。
简单使用
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// -> 输出 1
具有清除与停止侦听的功能
const stop = watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
// 什么时候需要停止的话,那就
stop()
1.2.6 watch
先来简单回顾一下vue2.x版本中watch的使用
watch: {
dataName (val, oldVal){
console.log("改变前的数据-" + oldVal, "改变后的数据-" + val)
}
}
Vue3中的watch
watch 自定义监听
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
<template>
<button @click="change">count is: {{ state.count }}</button>
</template>
<script>
import { reactive, watch } from 'vue'
export default {
setup () {
let state = reactive({count: 0})
let change = () => state.count++;
watch(state, () => {
console.log(state, '改变')
})
return { state, change }
}
}
</script>
注意上面的代码,第一个参数传入的 state 对象
,第二个参数是回调函数
,只要 state 中任意的属性发生改变都会执行回调函数,和 vue2 的区别是不要写 deep 属性,默认就是深度监听了。
监听state 对象,没有旧的值,只有新的值
现在是监听整个对象,当然我们也可以监听对象上的某个属性,注意下面代码的写法:第一个是回调函数
,第二个参数也是回调函数
。
监听state 对象上的某个属性,有旧的值,有新的值
<template>
<button @click="change">count is: {{ state.count }}</button>
</template>
<script>
import { reactive, watch } from 'vue'
export default {
setup () {
let state = reactive({count: 0})
let change = () => state.count++;
watch(() => state.count, (oldVlaue, newValue) => {//() => state.count, getter方式
console.log(oldVlaue, newValue, '改变')
})
return { state, change }
}
}
</script>
上面都是以创建响应式对象函数reactive()方式定义的数据,下面再看看使用ref()初始化的数据怎样实现数据监听
<template>
<button @click="change">count is: {{ count }}</button>
</template>
<script>
import { reactive, watch } from 'vue'
export default {
setup () {
let count = ref(0)
let change = () => count.value++;
watch(count, () => {
console.log(count.value '改变')
})
return { count , change }
}
}
</script>
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
2、生命周期
2.1 Vue3生命周期钩子
- setup() : 开始创建组件之前,在
beforeCreate
和created
之前执行,创建的是data
和
method
- onBeforeMount() : 组件挂载到节点上之前执行的函数;
- onRenderTracked() : 响应式的收集依赖;追踪,订阅
- onMounted() : 组件挂载完成后执行的函数;
- onRenderTriggered() : 响应式的触发依赖变更,发布
- onBeforeUpdate(): 组件更新之前执行的函数;
- onUpdated(): 组件更新完成之后执行的函数;
- onBeforeUnmount(): 组件卸载之前执行的函数;
- onUnmounted(): 组件卸载完成后执行的函数;
- onActivated(): 被包含在
<keep-alive>
中的组件,会多出两个生命周期钩子函数,被激活时执行; - onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行;
- onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数。
PS: 使用<keep-alive>
组件会将数据保留在内存中,比如我们不想每次看到一个页面都重新加载数据,就可以使用<keep-alive>
组件解决。
2.2 vue2 和 vue3 关于生命周期的对比
3、异步组件
当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载;这样的做得目的可以很好的提高用户体验。
一般用于分割代码,按需加载
为了实现这个功能,Vue3中为我们提供了一个方法,即defineAsyncComponent
,这个方法可以传递两种类型的参数,分别是函数类型
和对象类型
传递工厂函数作为参数
<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>
<script setup>
import LogoImg from './components/LogoImg.vue'
import HelloWorld from './components/HelloWorld.vue'
</script>
现在我们就将组件修改为异步组件,示例代码如下:
<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'
// 简单用法
const HelloWorld = defineAsyncComponent(() =>
import('./components/HelloWorld.vue'),
)
</script>
传递对象类型作为参数
defineAsyncComponent方法也可以接收一个对象作为参数,该对象中有如下几个参数:
- loader:同工厂函数;
- loadingComponent:加载异步组件时展示的组件;
- errorComponent:加载组件失败时展示的组件;
- delay:显示loadingComponent之前的延迟时间,单位毫秒,默认200毫秒;
- timeout:如果提供了timeout,并且加载组件的时间超过了设定值,将显示错误组件,默认值为Infinity(单位毫秒);
- suspensible:异步组件可以退出控制,并始终控制自己的加载状态。具体可以参考文档;
- onError:一个函数,该函数包含4个参数,分别是error、retry、fail和attempts,这4个参数分别是错误对象、重新加载的函数、加载程序结束的函数、已经重试的次数。
<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'
import LoadingComponent from './components/loading.vue'
import ErrorComponent from './components/error.vue'
// 定义一个耗时执行的函数,t 表示延迟的时间, callback 表示需要执行的函数,可选
const time = (t, callback = () => {}) => {
return new Promise(resolve => {
setTimeout(() => {
callback()
resolve()
}, t)
})
}
// 记录加载次数
let count = 0
const HelloWorld = defineAsyncComponent({
// 工厂函数
loader: () => {
return new Promise((resolve, reject) => {
;(async function () {
await time(300) //模拟异步
const res = await import('./components/HelloWorld.vue')
if (++count < 3) {
// 前两次加载手动设置加载失败
reject(res)
} else {
// 大于3次成功
resolve(res)
}
})()
})
},
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 0,
timeout: 1000,
suspensible: false,
onError(error, retry, fail, attempts) {
// 注意,retry/fail 就像 promise 的 resolve/reject 一样:
// 必须调用其中一个才能继续错误处理。
if (attempts < 3) {
// 请求发生错误时重试,最多可尝试 3 次
console.log(attempts)
retry()
} else {
fail()
}
},
})
</script>
通常会与 Suspense 配合
用Suspense 包一下异步组件,用来在组件树中协调对异步依赖的处理。该特性目前还不是稳定版本,后续可能会有变更。
4、自定义指令
- 自定义指令着眼于组件,围绕组件去做增强
- composition api着眼于状态,围绕数据做功能增强
我们都知道指令是为了增强组件的,我们常见的指令有:v-if、v-show、v-model、v-bind:value、v-on:click 等。
自定义指令其实非常简单,我们需要始终关注以下几个问题:
- 指令的钩子函数,有点类似生命周期函数钩子
- 指令钩子函数中的参数
- 指令的逻辑处理
注册自定指令,有点像封装了一个组件,封装在外部。有点类似vue2的mixin(功能复用),不过比mixin好很多,没mixin那么多缺点。
自定义指令关注组件或者本身dom(如 v-if,作用于dom节点)。composition api更关注状态
自定义指令 v-focus 、v-drag案例
<template>
<input v-focus />
<div class="heyi" v-drag></div>
</template>
<script setup lang="ts">
// 注册自定义指令
import { vDrag } from "../directives/vDrag";
import { vFocus } from "../directives/vFocus";
</script>
<style scoped>
.heyi {
width: 100px;
height: 100px;
background-color: red;
}
</style>
vDrag组件
import { Directive } from "vue";//ts
// mixin xxxxxx
export const vDrag: Directive = {
mounted(el) {//el,当前dom
el.draggable = true;
el.addEventListener("dragstart", () => {
console.log("dragstart");
});
},
};
vFocus组件
import { Directive } from "vue";
// mixin xxxxxx
export const vFocus: Directive = {//ts
mounted(el) {
el.focus();
},
};
指令钩子:
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) {}
}
5、Teleport
该特性允许你将组件内的某个子组件挂载到任意 HTML 节点上,这个特性像极了 React 中的 createPortal。
整个vue应用都是挂载到根节点,然后下面子组件挂到父组件上行成一棵树。
假设我现在某一个组件想挂到别的地方去,而不是父组件上,就使用Teleport
假设不用Teleport
<template>
<div class="heyi"></div>
</template>
<script setup lang="ts"></script>
<style scoped>
.heyi {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
left: 0;
top: 0;
}
</style>
打开控制台查看,会发现 <div class="heyi"></div>
挂在我们的vue实例 #app底下,也就是正常的父级dom下面
使用了Teleport
<template>
<Teleport to="body">
<div class="heyi"></div>
</Teleport>
</template>
<script setup lang="ts"></script>
<style scoped>
.heyi {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
left: 0;
top: 0;
}
</style>
打开控制台查看,会发现 <div class="heyi"></div>
挂在body地下,不再是#app底下
- Teleport 组件创建
- 首先会在在主视图里插入注释节点或者空白文本节点
- 接着获取目标元素节点
- 最后调用mount方法创建子节点往目标元素插入 Teleport 组件的子节点
Teleport通常用来创建模态框modal
- 弹出层
- Popover 等
- tooltip
6、自定义 Hooks
假设我们需要封装一个计数器,该计数器用于实现数字的增加或者减少,并且我们可以指定数字可最大和最小值
使用:(负责视图)
<template>
<div>
<p>{{ current }} [max: 10; min: 1;]</p>
<div class="contain">
<button @click="inc()">
Inc()
</button>
<button @click="dec()" style="margin-left: 8px">
Dec()
</button>
<button @click="set(3)" style="margin-left: 8px">
Set(3)
</button>
<button @click="reset()" style="margin-left: 8px">
Reset()
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useCounter } from './useCounter'
const [current, { inc, dec, set, reset }] = useCounter(20, { min: 1, max: 10 })
</script>
useCounter就是自定义 Hooks(负责状态)
import { Ref, readonly, ref } from 'vue'
// 判断是否为数字
const isNumber = (value: unknown): value is number => typeof value === 'number'
export interface UseCounterOptions {
/**
* Min count
*/
min?: number
/**
* Max count
*/
max?: number
}
export interface UseCounterActions {
/**
* Increment, default delta is 1
* @param delta number
* @returns void
*/
inc: (delta?: number) => void
/**
* Decrement, default delta is 1
* @param delta number
* @returns void
*/
dec: (delta?: number) => void
/**
* Set current value
* @param value number | ((c: number) => number)
* @returns void
*/
set: (value: number | ((c: number) => number)) => void
/**
* Reset current value to initial value
* @returns void
*/
reset: () => void
}
export type ValueParam = number | ((c: number) => number)
function getTargetValue(val: number, options: UseCounterOptions = {}) {
const { min, max } = options
let target = val
if (isNumber(max)) {
target = Math.min(max, target)
}
if (isNumber(min)) {
target = Math.max(min, target)
}
return target
}
function useCounter(
initialValue = 0,
options: UseCounterOptions = {},
): [Ref<number>, UseCounterActions] {
const { min, max } = options
const current = ref(
getTargetValue(initialValue, {
min,
max,
}),
)
const setValue = (value: ValueParam) => {
const target = isNumber(value) ? value : value(current.value)
current.value = getTargetValue(target, {
max,
min,
})
return current.value
}
const inc = (delta = 1) => {
setValue(c => c + delta)
}
const dec = (delta = 1) => {
setValue(c => c - delta)
}
const set = (value: ValueParam) => {
setValue(value)
}
const reset = () => {
setValue(initialValue)
}
return [
readonly(current),
{
inc,
dec,
set,
reset,
},
]
}
export default useCounter
总结:Composition API的优点:将组件 UI逻辑 和状态逻辑解耦,让组件得到更大的复用性