Composition API
在 Vue2 我们定义一个组件,基本模板应该是:
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
如果我们选用 Vue3 的组合式方式呢?
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
可以发现最大的变化是:
组合式的写法,必须要在 script 中指定 setup,这样 Vue 在编译转换等阶段都会采用一种全新的方式进行。
什么是 Composition API?
选项式 API 与 Composition API 属于两种心智模型,选项式就是我们所说的配置式,它通过前期的内容约定,结构规范保证程序健壮性及可读性。组合式显然比选项式灵活,除此以外,组合式实现了 UI 复用与状态逻辑复用的分离。他们在底层编译和转换时区别很大。
选项式 vs 组合式状态与事件
选项式声明的方式
<script>
export default {
data() {
return {
message: 'Hello World!'
}
},
methods: {
reverseMessage() {
this.message = this.message.split('').reverse().join('')
},
notify() {
alert('navigation was prevented.')
}
}
}
</script>
<template>
<h1>{{ message }}</h1>
<!--
绑定一个方法/函数。
这个 @click 语法是 v-on:click 的简写。
-->
<button @click="reverseMessage">Reverse Message</button>
<!-- 也可以写成一个内联表达式语句 -->
<button @click="message += '!'">Append "!"</button>
<!--
Vue 也为一些像 e.preventDefault() 和 e.stopPropagation()
这样的常见任务提供了修饰符。
-->
<a href="https://vuejs.org" @click.prevent="notify">
A link with e.preventDefault()
</a>
</template>
<style>
button, a {
display: block;
margin-bottom: 1em;
}
</style>
组合式方式
<script setup>
import { ref } from 'vue'
const message = ref('Hello World!')
function reverseMessage() {
// 通过其 .value 属性 访问/修改一个 ref 的值。
message.value = message.value.split('').reverse().join('')
}
function notify() {
alert('navigation was prevented.')
}
</script>
<template>
<h1>{{ message }}</h1>
<!--
绑定一个方法/函数。
这个 @click 语法是 v-on:click 的简写。
-->
<button @click="reverseMessage">Reverse Message</button>
<!-- 也可以写成一个内联表达式语句 -->
<button @click="message += '!'">Append "!"</button>
<!--
Vue 也为一些像 e.preventDefault() 和 e.stopPropagation()
这样的常见任务提供了修饰符。
-->
<a href="https://vuejs.org" @click.prevent="notify">
A link with e.preventDefault()
</a>
</template>
<style>
button, a {
display: block;
margin-bottom: 1em;
}
</style>
常用 Composition API
一些常问问题:
说说对 setup 的理解
组合式 API 的入口
ref 和 reactive 有什么区别?
对于响应式而言,
ref
支持对象类型和基本类型,而reactive
只支持对象类型;reactive
如果传入基本类型的参数,这种数据会失去响应式。使用
reactive
时,对响应式对象重新赋值是会失去响应式的。ref
则没有这种问题。ref 内部使用了 reactive
ref()
接受一个参数,参数可以是任意类型
,不管是基本类型还是引用类型ref
都具备响应式,另外ref
要通过.value
进行取值。
//当ref是对象类型的时候,本质上也是一个reactive
ref(obj) === reactive({ value: obj })
ref 和 shallowRef 的区别,以及 reactive 和 shallowReactive 的区别?
shallow 表示浅层,这里均是指的响应值作用在第一层,即
.value
,不过我们可以使用triggerRef(xxx)
来在深层内容变更后,手动触发更新,需要注意的是 shallowReactive 没有对应方法
watchEffect 与 watch 的区别
watch
具备一定的惰性 lazy (wath侦听器首次加载不会执行,只有数据发生变化才会执行)。而watchEffect首次和数据改变都执行
watch
中的参数可以拿到原始值(回调函数的第一个参数)和当前值(回调函数的第二个参数)。而watchEffect
只能获取到当前的值,不能获取到之前的值
watch
可以侦听ref
基础类型的数据,也可以对reactive
类型的数据进行监听(watch的第一个参数要写成函数的形式,函数里面写要监听值或对象的属性)watch需要手动配置监听的数据,而watchEffect是自动监听其内部回调函数使用到的数据。
setup
setup()
钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value
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)
}
}
从这个例子可以清晰的看到,vue 的 setup 函数可以接收两个参数,分别为 props、context。context 中包含属性、插槽、触发事件等内容。
在组件内部可以通过 compose 方法,将内部方法暴露到模板引用上以供父组件使用。
ref
ref
函数和reactive
函数都是用来定义响应式数据;但是reactive
更适合定义复杂的数据类型(json/arr)、ref
适合定义基本数据类型(可接收基本数据类型和对象)
对于ref:
- 函数参数可以是基本数据类型,也可以接受对象类型
- 如果参数是对象类型时,其底层的本质还是reactive,系统会自动根据我们给ref传入的值转换。ref函数只能操作浅层次的数据,把基本数据类型当作自己的属性值;深层次依赖于reactive
- 在template中访问,系统会自动添加.value;在js中需要手动.value
- ref响应式原理是依赖于
Object.defineProperty()
的get()
和set()
的。
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
可以看到,ref 函数
用于定义一个响应式对象,这个对象用于存储 input ref 应用,即为 dom 对象。
reactive
用法同 ref;参数必须是对象或者数组
const obj = reactive({ count: 0 })
obj.count++
computed
用法和ref类似。
computed接收函数作为参数:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
computed接收对象作为参数
这个对象中接受set方法(设置值)和get方法(获取值)
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
watchEffect、watchPostEffect、watchSyncEffect
watchEffect
接收一个回调函数,会在组件渲染时立即调用一次回调,同时自动追踪回调中的响应式依赖,当响应式依赖数据变更时会重新执行回调。
第一个参数,侦听器回调,该回调提供了一个参数
onCleanup
(可清除过期的副作用函数)
第二个为可选参数,是一个配置对象,有以下属性:
flush
:调整回调函数的刷新时机。onTrack / onTrigger
:调试侦听器的依赖。
后两者是前者的语法糖,就是将第二个参数中的 flush
,指定为对应值,分别为:flush?: 'pre' | 'post' | 'sync'
。默认:’pre’
简单使用
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()
watch
侦听一个 getter 函数:
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
侦听一个 ref:
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true }
强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。
const state = reactive({ count: 0 })
watch(
() => state,
(newValue, oldValue) => {
// newValue === oldValue
},
{ deep: true }
)
当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
const state = reactive({ count: 0 })
watch(state, () => {
/* 深层级变更状态所触发的回调 */
})
生命周期
vue2 和 vue3 关于生命周期的对比
Vue2 | Vue3 |
---|---|
beforeCreate | setup,在 beforeCreate 和 created 前,因此一般在组合式 api 中使用它做一些前置处理。 |
created | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
异步组件
什么是异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。换言之,我们的组件可能不再是同步导入或者组件需要等待 Promise
resolve
完成后才被渲染。这样的组件我们称为异步组件
Vue 提供了 defineAsyncComponent
方法
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent ( () => {
return new Promise ( ( resolve, reject ) => { // ...从服务器获取组件
resolve ( /* 获取到的组件 */ )
})
}) // ... 像使用其他一般组件一样使用 `AsyncComp`
defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent ( () => import ( './components/MyComponent.vue' ) )
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用 app.component()
全局注册:
<script setup>
import { defineAsyncComponent } from 'vue'
//通过异步组件的形式定义
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
通常会与 Suspense 配合
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
自定义指令
我们都知道指令是为了增强组件的,我们常见的指令有:v-if、v-show、v-model、v-bind:value、v-on:click 等。
自定义指令需要始终关注以下几个问题:
- 指令的钩子函数,有点类似生命周期函数钩子
- 指令钩子函数中的参数
- 指令的逻辑处理
一个基本的 v-focus 指令
例如,我们想要 input 组件在初始化渲染时,就聚焦,那么我们可以这样:
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
如果用选项式 API,那就这样:
export default {
directives: {
// 在模板中启用 v-focus
focus: {
method(el){
el.foucus()
}
}
}
}
指令钩子
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) {}
}
参数详解
指令的钩子会传递以下几种参数:
-
el
:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
-
vnode
:代表绑定元素的底层 VNode。 -
prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
举例来说,像下面这样使用指令:
<div v-example:foo.bar="baz">
binding
参数会是一个这样的对象:
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
<div v-example:[arg]="value"></div>
这里指令的参数会根据组件的 arg
数据属性响应式地更新。
Teleport
该特性允许你将组件内的某个子组件挂载到任意 HTML 节点上
页面中有一个按钮,当按钮点击时,会弹出 modal。以此为需求 -> 实现方案:
不使用 Teleport
// App.vue
<!--可定制插槽和 CSS 过渡效果的模态框组件。-->
<script setup>
import Modal from './Modal.vue'
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>
<modal :show="showModal" @close="showModal = false">
<template #header>
<h3>custom header</h3>
</template>
</modal>
</template>
然后在同级创建 Modal 组件,相关代码如下:
<script setup>
const props = defineProps({
show: Boolean
})
</script>
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button
class="modal-default-button"
@click="$emit('close')"
>OK</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
transition: opacity 0.3s ease;
}
.modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* 对于 transition="modal" 的元素来说
* 当通过 Vue.js 切换它们的可见性时
* 以下样式会被自动应用。
*
* 你可以简单地通过编辑这些样式
* 来体验该模态框的过渡效果。
*/
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
使用 Teleport
Modal.vue
内容不变,调整 App.vue
的内容,如下:
// App.vue
<script setup>
import Modal from './Modal.vue'
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>
<Teleport to="body">
<!-- 使用这个 modal 组件,传入 prop -->
<modal :show="showModal" @close="showModal = false">
<template #header>
<h3>custom header</h3>
</template>
</modal>
</Teleport>
</template>
Teleport 原理简单介绍
-
Teleport 组件渲染的时候会调用
patch
方法,patch
方法会判断如果 shapeFlag 是一个 Teleport 组件,则会调用它的process
方法。process
方法包含了Teleport组件创建和组件更新的逻辑。 -
Teleport 组件创建
- 首先会在在主视图里插入注释节点或者空白文本节点
- 接着获取目标元素节点
- 最后调用
mount
方法创建子节点往目标元素插入 Teleport 组件的子节点
-
Teleport 组件更新首先会更新子节点,处理 disabled 属性变化的情况,处理 to 属性变化的情况。
-
最后 Teleport 组件挂载会调用
unmount
方法,会判断如果 shapeFlag 是一个 Teleport 组件,则会执行它的 remove 方法。 -
remove 方法 会调用hostRemove方法移除文本节点,然后遍历子节点循环调用 unmount 方法挂载子节点。
自定义 Hooks
自定义 Hook 的价值
我们通过自定义 Hook,可以将组件的状态与 UI 实现分离,虽然这个 api 和早期的 mixin 非常像,但是他的设计思想实在先进太多。
自定义 Hook 示例
假设我们需要封装一个计数器,该计数器用于实现数字的增加或者减少,并且我们可以指定数字可最大和最小值,如果我们使用 vue3 composition 封装,会是怎样的呢?
我们先设想一下使用方法:
<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>
看到了使用方法,我们可以尝试定义这个 hook 函数,这里我们新建一个文件,用于编写 useCounter
相关代码。
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