【typescript工具库】用函数增强的方法把你的工具函数组合起来

1,165 阅读5分钟

大家好,我是Electrolux。今天这个文章讲一讲我ts工具库的一个利器-函数增强

什么是函数增强

我也不知道这个词有没有,没有就当作造了一个词语出来吧哈哈。这玩意跟函数劫持和函数化编程的compose的概念差不多。可以用于修改和扩展现有的 JavaScript 函数。但是区别在于compose基本上是属于嵌套关系。由于嵌套关系,因此常常比较难以理解。

这个函数方法是我从我的工具库里面抽离出来的一个函数。区别于传统的compose,我这里引用了优先级这一个概念进行扁平化处理。这个方法允许你劫持一个函数,或者进行多个函数在同一平面的组合。添加额外的逻辑,然后返回一个经过增强的新函数

它包括以下几个东西:

  • 一个目标函数:这是你想要增强的函数。
  • 一个或多个增强函数:这些函数添加额外的逻辑或修改目标函数的行为。
  • 一个优先级概念:用于确定增强函数的执行顺序。

实现函数增强

我们将使用 TypeScript 来实现一个简单的函数增强器。这个函数增强器包含以下核心部分:

  • fnType 接口:用于定义函数类型,包括优先级和函数本身。
  • enhance 类:主要的增强器类,用于管理目标函数和增强函数。
  • addFunction 方法:用于添加增强函数和它们的优先级。
  • next 方法和 asyncNext 方法:用于执行函数增强器中的所有函数,根据优先级排序执行。
/**
 * @des 可以看作是函数增强 | 保存劫持函数 | 添加逻辑 | 返回enhance后的增强逻辑
 */
interface fnType {
    priority: number;
    fn: Function
}
class enhance {
    fnArray: Array<fnType>
    enhance: Record<any, any>
    /**
     * @des 被劫持的函数默认优先级是0
     */
    constructor(targetFn: Function) {
        this.fnArray = [{
            priority: 0,
            fn: targetFn
        }]
        /**
         * @des 函数共享的变量
         */
        this.enhance = {
​
        }
    }
    addFunction(enhanceFn: Function, priority: number) {
        this.fnArray?.push({
            fn: enhanceFn,
            priority
        })
    }
    // 同步
    next(arg: any) {
        this.fnArray = this.fnArray?.sort((a, b) => {
            return a.priority - b.priority
        })
        for (let i = 0; i < this.fnArray?.length; i++) {
            this.fnArray[i].fn.call(this, arg)
        }
    }
    // 异步
    async asyncNext(arg: any) {
        this.fnArray = this.fnArray?.sort((a, b) => {
            return a.priority - b.priority
        })
        for (let i = 0; i < this.fnArray?.length; i++) {
            let res = await this.fnArray[i].fn.call(this, arg)
            if(res == "stop"){
                break
            }
        }
    }
}

简单聊一下这些函数吧

在这个示例中,我们首先创建了一个异步函数 sleepA,然后创建了一个名为 hello 的增强函数。接着,我们创建了一个函数增强器 enhancer,将 console.log 作为目标函数,并使用 addFunction 方法将 hello 函数添加到增强器中。

最后,我们使用 asyncNext 方法执行增强器中的函数,并查看增强器的状态。

作为开发者和使用者,需要注意下面几点

  • 执行顺序: 增强函数的执行顺序由优先级决定,低优先级的函数将首先执行。
  • 上下文绑定: 确保在增强函数中正确绑定 this 上下文,以访问增强器的状态。
  • 异步处理: 如果增强函数包含异步操作,使用 asyncNext 来确保它们按顺序执行。

我们来一段helloworld 吧

函数埋点 | 增强普通函数

统计 某一个函数执行次数,这里以前我们都是使用闭包来统计,现在我们可以 取到 变量enhance 来获取几个函数共同的 变量。下面的示例中我用console.log作为 需要增强的普通函数并且统计他执行的次数

async function hello(this: enhance) {
    this.enhance.count = this.enhance.count ??  0
    this.enhance.count=this.enhance.count+1
    console.log("执行次数:",this.enhance.count)
}
​
let ftest = new enhance(globalThis.console.log)
ftest.addFunction(hello, 1)
ftest.asyncNext(`\x1B[92m 我是第一次输出 \x1B[0m`)
ftest.asyncNext(`\x1B[92m 我是第二次输出 \x1B[0m`)
ftest.asyncNext(`\x1B[92m 我是第三次输出 \x1B[0m`).then(()=>{
    console.log("三次输出完成:",ftest.enhance)
})

输出结果如下

我是第一次输出 
我是第二次输出
我是第三次输出
执行次数: 1
执行次数: 2
执行次数: 3
三次输出完成: { count: 3 }

可以看到我们的console.log 是比较优雅的被我们劫持到了

函数记忆化 | 增强 promise异步

这是我之前写的 函数记忆的工具方法,我们要将他改造成扁平化的函数增强。当然我认为这种闭包的方式足够精简,但是用函数记忆化似乎更方便各个函数更加明确的进行组合。下面是未改造前面的代码

function memoizeAsync<T>(
  func: (...args: any[]) => Promise<T>
): (...args: any[]) => Promise<T> {
  const cache: Map<string, Promise<T>> = new Map();
​
  return async (...args: any[]): Promise<T> => {
    const key = JSON.stringify(args);
​
    if (cache.has(key)) {
      console.log("触发记忆")
      return cache.get(key)!;
    }
​
    const resultPromise = func(...args);
    cache.set(key, resultPromise);
​
    try {
      const result = await resultPromise;
      return result;
    } catch (error) {
      cache.delete(key);
      throw error;
    }
  };
}
​
  
async function sleep(time:number): Promise<any> {
  return new Promise((resolve) => {
      setTimeout(() => {
          resolve(time)
      }, time);
  })
}
 
let sleepTest = memoizeAsync(sleep)
console.log(sleepTest(1000))
console.log(sleepTest(5000))
console.log(sleepTest(5000))

那么我们来用函数增强实现我们常用的函数吧

interface EnhancePromise extends enhance{
    enhance:{
        cache:Map<string, any>
        result:any
    }
}
​
// 假装是 api请求
async function api(this: EnhancePromise,time:number): Promise<any> {
    console.log("触发了api")
    return new Promise((resolve) => {
        setTimeout(() => {
            let res ={id:time}
            this.enhance.cache.set(JSON.stringify(time),res);
            resolve(res)
        }, time);
​
    })
}
​
// 缓存
async function cache(this: EnhancePromise,args:any) {
    this.enhance.cache = this.enhance.cache ?? new Map()
    if(this.enhance.cache.get(JSON.stringify(args))){
        this.enhance.result = this.enhance.cache.get(JSON.stringify(args))
        return "stop"
    }
}
​
// 主函数
async function main(param:any){
    await api.bind(this)(param)
}
​
// 开始函数增强
let ftest = new enhance(main) 
ftest.addFunction(cache, -1)
ftest.asyncNext(1000).then(()=>{
​
}).then(async ()=>{
    ftest.asyncNext(2000)
}).then(()=>{
    ftest.asyncNext(1000).then(()=>{
        console.log("缓存的数据:",ftest.enhance.result)
    })
})
​

可以看到我们一开始 用 main作为主函数 ,主函数的优先级为0,然后设置了一个cache的函数增强方法放在他前面,优先级给一个-1。

最终输出

触发了api
触发了api
缓存的数据: { id: 1000 }

看出来,3次调用模拟api,其中有一次走了缓存,所以输出了两次触发了api

函数参数校验 | 增强业务逻辑

下面再来一个业务逻辑的示例吧

​
​
// 假装是 api请求
async function api(this: enhance,time:number): Promise<any> {
    return new Promise((resolve) => {
        setTimeout(() => {
            let res ={id:time}
            this.enhance.cache.set(JSON.stringify(time),res);
            resolve(res)
        }, time);
​
    })
}
​
// 校验
async function valid(this: enhance,args:any) {
    if(typeof args == "object"){
        return "stop"
    }
}
​
// 主函数
async function main(param:any){
    await api.bind(this)(param)
}
​
// 开始函数增强 | 校验
let ftest = new enhance(main) 
ftest.addFunction(cache, -1)
ftest.asyncNext(1000).then(()=>{
}).then(async ()=>{
    ftest.asyncNext({"id":"11111"})
})
   

小结

  • 首先初始化用enhance代理你的函数
  • 然后用 addFunction 设置优先级 和 需要增强的函数
  • 最后通过 asyncNext 执行函数

最后欢迎大家指正

代码在:github.com/electroluxc…