javascript 更好的替代“承诺”-ing?

qf9go6mv  于 2023-04-28  发布在  Java
关注(0)|答案(1)|浏览(164)

假设我正在编写以下函数:

async function foo(axe: Axe): Promise<Sword> {
  // ...
}

它的用法是这样的:

async function bar() {
  // get an axe somehow ...

  const sword = await foo(axe);

  // do something with the sword ...
}

到目前为止,一切顺利。问题是,为了实现foo,我想调用下面的“回调风格的异步”函数。我不能改变它的签名(它是一个库/模块):

/* The best way to get a sword from an axe!
 * Results in a null if axe is not sharp enough */
function qux(axe: Axe, callback: (sword: Sword) => void);

我发现的最好的方法是"promisify" qux:

async function foo(axe: Axe): Promise<Sword> {
  return new Promise<Sword>((resolve, reject) => {
    qux(axe, (sword) => {
      if (sword !== null) {
        resolve(sword);
      } else {
        reject('Axe is not sharp enough ;(');
      }
    });
  });
}

它工作,但我希望我能做一些更直接/可读性。在某些语言中,您可以创建一个类似promise的对象(我在这里称之为Assure),然后在其他地方显式地设置其值。就像这样:

async function foo(axe: Axe): Promise<Sword> {
  const futureSword = new Assure<Sword>();

  qux((sword) => {
    if (sword !== null) {
      futureSword.provide(sword);
    } else {
      futureSword.fail('Axe is not sharp enough ;(');
    }
  });

  return futureSword.promise;

这在语言本身中是可能的吗?或者我需要使用一个库/模块,比如deferred

更新1:额外动机

为什么人们更喜欢第二种解决方案而不是第一种?由于回调链接。
如果我想在foo中执行多个步骤,而不仅仅是调用qux,该怎么办?如果这是同步代码,它可能看起来像这样:

function zim(sling: Sling): Rifle {
  const bow = bop(sling);
  const crossbow = wug(bow);
  const rifle = kek(crossbow); 
  return rifle;
}

如果这些函数是异步的,promisify-ing会给予我这个:

async function zim(sling: Sling): Promise<Rifle> {
  return new Promise<Rifle>((resolve, reject) => {
    bop(sling, (bow) => {
      wug(bow, (crossbow) => {
        kek(crossbow, (rifle) => {
          resolve(rifle);
        });
      });
    });
  );
}

使用Assure,我可以这样做:

async function zim(sling: Sling): Promise<Rifle> {
  const futureBow = new Assure<Bow>();
  bop(sling, (bow) => futureBow.provide(bow));

  const futureCrossbow = new Assure<Crossbow>();
  wug(await futureBow, (crossbow) => futureCrossbow.provide(crossbow));

  const futureRifle = new Assure<Rifle>();
  kek(await futureCrossbow, (rifle) => futureRifle.provide(rifle));
 
  return futureRifle;
}

我发现这更易于管理,因为我不需要跟踪嵌套的作用域,也不需要担心计算的顺序。如果函数有多个参数,则差异会更大。

反射

也就是说,我同意嵌套调用的版本是优雅的,因为我们不需要声明所有这些临时变量。
在写这个问题的时候,我有了另一个想法,关于如何与JavaScript的精神保持一致:

function zim(sling: Sling): Rifle {
  const bow = await new Promise((resolve, reject) => { bop(sling, resolve); });
  const crossbow = await new Promise((resolve, reject) => { wug(bow, resolve); });
  const rifle = await new Promise((resolve, reject) => { kek(crossbow, resolve); }); 
  return rifle;
}

...这开始看起来很像从Nodejs使用util.promisify。如果回调遵循错误优先的约定就好了。..但在这一点上,似乎有理由实现一个朴素的myPromisify,它 Package 了promisify并处理我所拥有的回调类型。

jhdbpxl9

jhdbpxl91#

Assure可以很简单地在TypeScript中实现:

class Assure<T, U = unknown> {
    public promise: Promise<T>;
    private resolve!: (value: T) => void;
    private reject! : (error: U) => void;

    constructor() {
        this.promise = new Promise((resolve, reject) => {
            this.resolve = resolve;
            this.reject = reject;
        });
    }

    public provide(value: T) {
        this.resolve(value);
    }

    public fail(reason: U) {
        this.reject(reason);
    }
}

带演示的Playground Link
话虽如此,它可能是不需要的。其用法最终基本上类似于使用promise,然而:
1.它不是惯用的。
1.与常规承诺相比,似乎没有带来任何价值。
一个更好的替代方案可能是定义一个函数,可以将回调函数转换为承诺返回函数。这也不需要太多的工作--使用TS,我们甚至可以确保转换后的函数具有正确的类型。下面是主要类型的回调函数的实现:

/* utility types */
/** All but last elements of a tuple: [a, b, c, d] -> [a, b, c] */
type Initial<T extends any[]> = T extends [ ...infer Initial, any ] ? Initial : any[];
/** All but first elements of a tuple: [a, b, c, d] -> [b, c, d] */
type Tail<T extends any[]> = T extends [ any, ...infer Tail ] ? Tail : any[];

/** First elements of a tuple: [a, b, c, d] -> a */
type Head<T extends any[]> = T extends [ infer Head, ...any[] ] ? Head : any;
/** Last elements of a tuple: [a, b, c, d] -> d */
type Last<T extends any[]> = T extends [ ...any[], infer Last ] ? Last : any;

/** First parameter of a function: ( (a, b, c, d) => any ) -> a */
type FirstParameter<T extends (...args: any[]) => void> = Head<Parameters<T>>;
/** Last parameter of a function: ( (a, b, c, d) => any ) -> d */
type LastParameter<T extends (...args: any[]) => void> = Last<Parameters<T>>;
/* /utility types */

/**
 * Converts an asynchronous function to a promise returning one.
 * The function should have a callback with a result as last parameter
 */
function promisifyResultCallback<T extends (...args: any[]) => void>(fn: T) {
    type ResultCallback = LastParameter<T>;
    type ReturnType = FirstParameter<ResultCallback>;

    return function(...args: Initial<Parameters<T>>): Promise<ReturnType> {
        return new Promise((resolve) => {
            fn(...args, resolve);
        });
    }
}

/**
 * Converts an asynchronous function to a promise returning one.
 * The function should have a callback with an error and result as last parameter - error-first style like in Node.js
 */
function promisifyErrorFirstCallback<T extends (...args: any[]) => void>(fn: T) {
    type ResultCallback = LastParameter<T>;
    type ReturnType = LastParameter<ResultCallback>;

    return function(...args: Initial<Parameters<T>>): Promise<ReturnType> {
        return new Promise((resolve, reject) => {
            fn(...args, (err: unknown, x: ReturnType) => {
                if (err) 
                    reject(err);
                
                resolve(x);
            });
        });
    }
}

/**
 * Converts an asynchronous function to a promise returning one.
 * The function should have two callback at the end  for success and error
 */
function promisifyTwoCallbacks<T extends (...args: any[]) => void>(fn: T) {
    type ResultCallback = Last<Initial<Parameters<T>>>; //second to last
    type ReturnType = FirstParameter<ResultCallback>;

    return function(...args: Initial<Initial<Parameters<T>>>): Promise<ReturnType> {
        return new Promise((resolve, reject) => {
            fn(...args, resolve, reject);
        });
    }
}

它允许这样的用法:

declare function onlyResultCallback(a: string, b: number, callback: (resut: boolean) => void): void;
const p_onlyResultCallback = promisifyResultCallback(onlyResultCallback);
//    ^ type is: (a: string, b: number) => Promise<boolean>

declare function errorFirstCallback(a: string, b: number, callback: (err: Error | null, resut: boolean) => void): void;
const p_errorFirstCallback = promisifyErrorFirstCallback(errorFirstCallback);
//    ^ type is: (a: string, b: number) => Promise<boolean>

declare function twoCallbacks(a: string, b: number, onSuccess: (resut: boolean) => void, onError: (err: Error) => void): void;
const p_twoCallbacks = promisifyTwoCallbacks(twoCallbacks);
//    ^ type is: (a: string, b: number) => Promise<boolean>

Playground链接
使用promisify函数,foo()可以简单地实现为:

declare function qux(axe: Axe, callback: (sword: Sword | null) => void): void;

async function foo(axe: Axe): Promise<Sword> {
  const p_qux = promisifyResultCallback(qux);
  const maybeSword = await p_qux(axe);

  if (maybeSword === null)
    throw 'Axe is not sharp enough ;(';
  
  // the maybeSword is narrowed to just a sword since null is eliminated in the `if`
  return maybeSword;
}

Playground链接
这里对p_qux的赋值仅用于演示目的。更惯用的代码可以在函数外进行一次赋值,或者直接使用const maybeSword = await promisifyResultCallback(qux)(axe);
如果真的需要,promising函数可以被改变为允许传递参数,并允许像promisifyResultCallback(qux, axe)这样的用法,但是,这留给读者练习。
使用promise也消除了嵌套问题:

declare function bop( sling   : Sling   , callback: (bow: Bow          ) => void ): void;
declare function wug( bow     : Bow     , callback: (crossbow: Crossbow) => void ): void;
declare function kek( crossbow: Crossbow, callback: (rifle: Rifle      ) => void ): void;

async function zim(sling: Sling): Promise<Rifle> {
  return new Promise<Rifle>((resolve, reject) => {
    bop(sling, (bow) => {
      wug(bow, (crossbow) => {
        kek(crossbow, (rifle) => {
          resolve(rifle);
        });
      });
    });
  });
}

一旦功能被承诺:

const p_bop = promisifyResultCallback(bop);
const p_wug = promisifyResultCallback(wug);
const p_kek = promisifyResultCallback(kek);

可以作为常规的promise处理:

function zim(sling: Sling) {
    return p_bop(sling)
        .then(bow => p_wug(bow))
        .then(crossbow => p_kek(crossbow))
}

function zim(sling: Sling) {
    return p_bop(sling)
        .then(p_wug)
        .then(p_kek)
}

async function zim(sling: Sling) {
    const bow = await p_bop(sling);
    const crossbow = await p_wug(bow);
    const rifle = await p_kek(crossbow);
    return rifle;
}

Playground链接

相关问题