vue3学习小札之(三):逻辑复用
引子
可以前往专栏阅读该系列其他文章:传送门
之前的文章包含了 vue3 的基础知识、组件的深入介绍。
本期文章,将主要介绍 vue3 中如何实现代码封装,逻辑复用
。
P.S.
vue3 取消了mixin,别问,问就是不好用:)
接下来我们将从三个方面(组合式函数、自定义指令、插件
)学习如何在 vue3 中实现逻辑复用。
组合式函数
在实际开发中,我们都会封装工具类函数,但是这类封装一般都是无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。 相比之下,有状态逻辑,负责管理随时间而变化的状态。
在上一篇文章介绍作用域插槽
的时候,说到一种场景:无渲染组件
。
无渲染组件内部其实就是封装了有状态逻辑
,然后将动态数据通过作用域插槽的方式,上抛给消费者组件。从而实现了,组件内部仅负责逻辑处理,而布局、样式等处理都交给了消费者组件。
这种方式固然可行,但是会带来额外的组件实例的性能开销
。
所以,组合式函数的出现就应运而生了。
“组合式函数”(Composables)
是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
当我们无需考虑视图布局
,只是纯逻辑复用时,推荐用组合式函数替代无渲染组件。
组合式函数的约定和最佳实践
在学习编写组合式函数前,先介绍下 vue3 对于组合式函数的一些规范和推荐
。
命名
组合式函数约定用驼峰命名法命名,并以“use”
作为开头。
react自定义hooks:???
输入参数
组合式函数可接收 ref 参数,所以当我们在编写组合式函数时,最好在内部对入参做兼容 ref 处理
。unref()
工具函数会对此非常有帮助:
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
而当组合式函数在接收一个 ref 参数,并且会随之产生响应式 effect
,那么我们就需要针对这个 ref 参数添加侦听器
:
可以使用 watch()
显式地监听此 ref;
也可以在 watchEffect()
中调用 unref()
(针对ref进行解包)来进行正确的追踪。
返回值
推荐组合式函数始终返回一个包含多个 ref 的普通的非响应式
对象。
目的是为了外部调用组合式函数后,对返回值进行解构
为 ref 之后仍可以保持响应性
。
// x 和 y 是两个 ref
const { x, y } = useMouse()
但是将返回值解构为 ref 之后,需要用 .value 获取值,所以我们可以针对组合式函数返回的非响应式对象返回值,做一次响应式处理
,这样内部的 ref 就会自动进行解包,然后我们就可以通过对象属性
的形式来使用组合式函数中返回的状态:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
副作用
如果在组合式函数中执行副作用
(例如:添加 DOM 事件监听器或者请求数据),请注意:
确保在 onUnmounted()
时清理副作用。
如果是同步
形式挂载的侦听器副作用,基本是不需要手动清除的,
如果是异步
形式挂载的,就需要通过调用侦听器的返回值函数,进行手动清除。
使用限制
组合式函数在 <script setup>
或 setup()
钩子中,应始终被同步地调用。
在某些场景下,也可以在像 onMounted()
这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,
只有能确认当前组件实例,才能够:
将组合式函数内的生命周期钩子
注册到该组件实例
上;
将组合式函数内的计算属性和监听器
注册到该组件实例
上,以便在该组件被卸载时停止监听,避免内存泄漏。
P.S.
后续介绍组合式 API 的时候会讲到一个叫做 effect 作用域
的知识点。里面会阐述响应式副作用
(即计算属性和侦听器)的一些细节,其中就涉及到了组件卸载时,停止监听的内容。
通过例子学习编写组合式函数
解下来通过官网的两个例子,学习下如何编写组合式函数。 具体的细节,都写在代码的注释部分。
例子一:鼠标跟踪器
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
// 一个组合式函数可以调用一个或多个其他的组合式函数。
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
// 需要手动清除副作用
onUnmounted(() => target.removeEventListener(event, callback))
}
组件中调用该组合式函数,如下
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
tip:
每一个调用 useMouse()
的组件实例会创建其独有的 x
、y
状态拷贝,因此他们不会互相影响
。
例子二:有异步状态的组合式函数
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
// 同时可以接收静态的 URL 字符串和 URL 字符串的 ref
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// 在请求之前重设状态...
data.value = null
error.value = null
// unref() 解包可能为 ref 的值,针对入参做 ref 处理
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
// 通过 isRef()检测 URL 是否为一个动态 ref
if (isRef(url)) {
// 若输入的 URL 是一个 ref,那么启动一个响应式的请求
// 该 effect 会立刻执行一次,并在此过程中将 URL 的 ref 作为依赖进行跟踪
// 当 URL 的 ref 发生改变时触发回调函数,数据就会被重置,并重新请求
watchEffect(doFetch)
} else {
// 否则只请求一次
// 避免监听器的额外开销
doFetch()
}
return { data, error }
}
组件中调用:
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
自定义指令
前面的文章,已经介绍了两种在 Vue 中重用代码的方式:组件和组合式函数。
组件
是主要的构建模块
组合式函数
则侧重于有状态的逻辑
而这一章节要介绍的自定义指令
,主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
// 类似组件生命周期钩子
// 钩子函数会接收到指令所绑定元素作为其参数
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
在没有使用 <script setup>
的情况下,自定义指令需要通过 directives
选项注册
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
将一个自定义指令全局注册到应用层级
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('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) {}
}
实际开发中,我们一般只会用到mounted
和 updated
这两个钩子函数。
并且当这两个钩子的处理逻辑一样时,vue 提供了一个简写方式
<div v-color="color"></div>
// 直接用一个函数来定义指令
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
钩子参数
除了 el
外,其他参数都是只读的,不要更改
它们。
若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现
el
指令绑定到的元素
,可以用于直接操作 DOM
binding
一个对象
,包含以下属性:
value
传递给指令的值
oldValue
之前传递给指令的值
仅在 beforeUpdate
和 updated
中可用
arg
传递给指令的参数 (如果有的话)
modifiers
一个包含修饰符的对象 (如果有的话)
instance
使用该指令的组件实例
dir
指令的定义对象
vnode
代表绑定元素的底层 VNode
prevNode
代表之前的渲染中,指令所绑定元素的 VNode
仅在 beforeUpdate
和 updated
钩子中可用
注意
当在组件上使用自定义指令时
,它会始终应用于组件的根节点,和透传 attributes 类似。
但是不同于透传 attributes 在多根节点组件
上的表现,自定义指令不能通过 v-bind="$attrs"
来传递给一个明确的根元素。
所以不推荐
在组件上使用自定义指令。
插件
插件 (Plugins)
是一种能为 Vue 添加全局功能
的工具代码。
一个插件可以是一个拥有 install()
方法的对象
,也可以直接是一个安装函数本身。
vue实例方法 app.use()
源码内部会对传入的第一个参数进行类型判断。
app.use()
还可以接收第二个参数:一个对象形式的可选的选项 options
。该参数会被传递给插件内部的安装函数。
安装函数
会接收到安装它的应用实例和传递给 app.use()
的额外选项 options
作为参数。
// 定义一个插件
const myPlugin = {
install(app, options) {
// 配置此应用
}
}
import { createApp } from 'vue'
const app = createApp({})
// 挂载插件
app.use(myPlugin, {
/* 可选的选项 */
})
通过一个例子简单介绍插件的开发
一个简单的 i18n
(国际化 (Internationalization) 的缩写) 插件。
插件的实现:
插件的安装函数内部主要是针对 app 实例
,进行一些全局
的注入
// plugins/i18n.js
export default {
install: (app, options) => {
// 在这里编写插件代码
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
// 将插件接收到的 `options` 参数提供给整个应用,
// 让任何组件都能使用这个翻译字典对象
app.provide('i18n', options)
}
}
插件的安装:
用于查找的翻译字典对象
应当在插件被安装时作为 app.use()
的额外参数被传入
import i18nPlugin from './plugins/i18n'
app.use(i18nPlugin, {
greetings: {
hello: 'Bonjour!'
}
})
插件安装后在业务代码中的使用:
这样,表达式 $translate('greetings.hello')
就会在运行时被替换为 Bonjour!
了
<h1>{{ $translate('greetings.hello') }}</h1>
// 插件中的 provide 就会成为全局的依赖注入
<script setup>
import { inject } from 'vue'
const i18n = inject('i18n')
console.log(i18n.greetings.hello)
</script>
插件发挥作用的常见场景
通过 app.component()
和 app.directive()
注册一到多个全局组件或自定义指令;
通过 app.provide()
使一个资源可被注入进整个应用;
向 app.config.globalProperties
中添加一些全局实例属性或方法。
例如 vue-router,就是一个官方提供的,包含上述三种功能的插件。
总结
本篇文章主要介绍了 vue3 中如何实现代码的逻辑的复用
。
组合式函数
是利用 vue3 组合式 API
来封装和复用有状态逻辑
,侧重于纯逻辑
。
当纯逻辑无法满足场景,需要加入一些布局样式的实现时,无渲染组件
更适合。
自定义指令
是为了重用涉及普通元素的底层 DOM 访问的逻辑,一般是在需要通过直接的 DOM 操作
才能实现功能的场景下,才会使用的一种技术。
插件
侧重于全局的注入
,这种注入包括了:全局组件、全局自定义指令、全局资源、全局的实例属性和方法
。
以上,就是 vue3 使用中会用到的逻辑复用的知识点,我们可以在实际开发中,按需使用。
转载自:https://juejin.cn/post/7135707176521678885