• 作者:老汪软件技巧
  • 发表时间: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类型支持,实现懒加载