- 作者:老汪软件技巧
- 发表时间:2024-10-03 21:01
- 浏览量:
问题提出:
下面是一个指令对象的 demo,这里只是举个例子,不需要关注实际会不会产生问题:
// 这是最开始对自定义 data 的使用
import { type Directive } from 'vue'
export const enterButtons: Directive = (() => {
const data = {
locked: false
}
return {
// 全局共享 data,会导致问题!
updated(el: HTMLInputElement, binding) {
// 将会多次触发,导致问题!
el.onkeydown = (e) => {
if (data.locked) return
data.locked = true
if (e.key === 'Enter') binding.value.ref.click()
}
el.onkeyup = () => {
data.locked = false
}
}
}
})()
解决方案:
下面是我的 directives.ts 源码:
import { type ObjectDirective } from 'vue'
class AutoInitMapextends WeakKey, V> extends WeakMap {
createDefaultValue: () => V
constructor(createDefaultValue: () => V) {
super()
this.createDefaultValue = createDefaultValue
}
get(key: K): V {
// 如果 get 时键值对不存在则自动设置默认值
if (!super.has(key)) super.set(key, this.createDefaultValue())
return super.get(key)
}
}
const useSeparatedDirectives = (
storeSetup: () => T,
callback: (
store: T
) => ObjectDirective & { bindingMounted?: ObjectDirective['updated'] }
) => {
const isBindingMountedMap = new AutoInitMap(() => false) // el -> 其挂载是否完成 (boolean)
// middleObj 表示要返回的指令对象,
// 实际上它在这里只是用来获取键,最后再将其钩子函数全部自动指向真实对象的钩子函数
const middleObj = callback(storeSetup())
let newCallback = callback // 指令对象的工厂函数
const objMap = new AutoInitMap(() => newCallback(storeSetup()))
// 如果定义了 bindingMounted 钩子函数,则要将其加入 updated 钩子函数
if ('bindingMounted' in middleObj) {
// 将 bindingMounted 内容与 updated 内容合并为新的 updated
const newlUpdated = (
oldBindingMounted: ObjectDirective['updated'],
oldUpdated: ObjectDirective['updated'],
...args: Parameters'updated' ]>>
) => {
const el = args[0] // 指令所在的元素
const binding = args[1] // 指令绑定的内容
// 如果挂载未完成
if (!isBindingMountedMap.get(el)) {
if (
(() => {
// 数组模式
if (Array.isArray(binding.value)) {
if (binding.value.every((ins) => ins)) return true
} /* 对象模式 */ else if ('nodes' in binding.value) {
if (binding.value.nodes) return true
} /* 单例模式 */ else {
if (binding.value) return true
}
return false
})()
) {
// 挂载完成标记并触发 bindingMounted 回调
isBindingMountedMap.set(el, true)
oldBindingMounted!(...args)
}
}
// 如果定义了 updated,那么也要触发
if (oldUpdated) oldUpdated(...args)
}
// 修正 middleObj 的 keys
delete middleObj.bindingMounted
middleObj.updated = () => {}
// 更新 newCallback
newCallback = (data) => {
const res = callback(data)
const oldBindingMounted = res.bindingMounted
delete res.bindingMounted
const oldUpdated = res.updated
res.updated = (...args) =>
newlUpdated(oldBindingMounted, oldUpdated, ...args)
return res
}
}
// 修改 middleObj 的钩子函数,使其自动转发到不同 el 对应的内部指令对象的对应钩子函数
for (const key in middleObj) {
;(middleObj as any)[key] = (...args: any[]) => {
;(objMap.get(args[0]) as any)[key](...args)
}
}
return middleObj
}
bindingMounted 钩子的实现就不细说了,因为我不会说,这个只是实现起来比较麻烦,但是思路难点在于分离闭包
分离闭包大体思路如下:在不同处使用的指令都会触发实际在 main.ts 中注册的 middleObj 的各个生命周期回调函数,在这些回调函数中可以根据传入的 el 不同,触发其单独的对应钩子函数
使用示例:
首先定义 enterButton 指令对象:
export const enterButton = useSeparatedDirectives(
() => {
const locked = false
let cnt = 0
const logCnt = () => console.log(cnt++)
return {
locked,
logCnt
}
},
(data) => {
return {
bindingMounted(el: HTMLInputElement, binding) {
console.log('mouted: ', el, binding.value)
el.addEventListener('keydown', (e) => {
if (data.locked) return
data.locked = true
if (e.key === 'Enter') {
e.preventDefault()
binding.value.click()
data.logCnt()
}
})
el.addEventListener('keyup', () => (data.locked = false))
}
}
}
)
然后在 main.ts 注册:
import { enterButton } from './utils/directives'
app.directive('enterButton', enterButton)
App.vue 测试:
<template>
<input type="text" v-enterButton="buttonRef1" />
<button @click="console.log('button1')" ref="buttonRef1">button>
<input type="text" v-enterButton="buttonRef2" />
<button @click="console.log('button2')" ref="buttonRef2">button>
template>
<style scoped>style>
运行输出:
首先挂载完成正确输出
mouted:
mouted:
分别在两个 input 输入框中按下两次 enter 键输出:button10button11button20button21可以看到他们的内部数据已经是正确分离的了
bindingMounted 支持的所有元素绑定方式:
后记
已经实现ts类型支持,实现懒加载