Vue3新特性
Vue3新特性
- 1、Composition API
- 1.1 什么是 Composition API
- 1.2 常用 Composition API
- 1.2.1 setup
- 1.2.2 ref
- 1.2.3 reactive
- 1.2.4 computed
- 1.2.5 watchEffect、watchPostEffect、watchSyncEffect
- 1.2.6 watch
- 2、生命周期
- 2.1 Vue3生命周期钩子
- 2.2 vue2 和 vue3 关于生命周期的对比
- 3、异步组件
- 4、自定义指令
- 5、Teleport
- 6、自定义 Hooks
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) // 2plusOne.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))
// -> 输出 0count.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 xxxxxxexport const vDrag: Directive = {mounted(el) {//el,当前domel.draggable = true;el.addEventListener("dragstart", () => {console.log("dragstart");});},
};
vFocus组件
import { Directive } from "vue";// mixin xxxxxxexport const vFocus: Directive = {//tsmounted(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 } = optionslet target = valif (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 } = optionsconst 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逻辑 和状态逻辑解耦,让组件得到更大的复用性