Skip to content

Promise使用的各种技巧

大厂面试中Promise是常见的一类编程笔试题, 其核心技巧在于巧用闭包和Promise链

本文讨论使用函数式编程, 通过λ演算来生成新的async函数, 在尽可能不修改已有代码的前提下就可以为现有的async函数添加新特性

INFO

我们先构造一个async函数来辅助测试:
time: 返回的promisetime秒后fulfill
result: resolve时的result, reject时的reason
isResolved: 为true时, 返回的promise被resolve, 否则被rejected

typescript
async function asyncRun(time: number, result: any = time, isResolve: boolean = true): Promise<number> {
  console.log(`${Date().toString()} - start:  ${result}`)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isResolve) {
        console.log(`${Date().toString()} - resolve:  ${result}`);
        resolve(result);
      } else {
        console.log(`${Date().toString()} - reject:  ${result}`);
        reject(result);
      }
    }, 1000 * time)
  });
}

可以手动控制的Promise

TIP

利用闭包透传resolve reject方法

RUN ME

typescript
interface PromiseHandle<T> {
  resolve: (value: T) => void;
  reject: (reason?: any) => void;
}

function getControlledPromise<T = unknown>(): { promise: Promise<T>, handle: PromiseHandle<T>} {
  let handle: PromiseHandle<T>;
  const promise = new Promise<T>((resolve, reject) => {
    handle = { resolve, reject };
  });
  return { promise, handle: handle! };
}

// TESTING CODE!!!!
const { promise, handle } = getControlledPromise<number>();
promise.then(r => console.log(r))
handle.resolve(1);

cancel - 让一个async函数可以被取消

TIP

利用闭包透传reject方法, 手动cancel

RUN ME

typescript
type CancellablePromise<T> = Promise<T> & {
  cancel: (reason?: any) => void
}

function cancellable<T extends (...args: any[]) => Promise<any>>(f: T): (...args: Parameters<T>) => CancellablePromise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  return (...args: Parameters<T>): CancellablePromise<R> => {
    let rejectFn: (reason?: any) => void;
    let promise: Promise<R> = new Promise((resolve, reject) => { 
      rejectFn = reject;
      f.apply(null, args).then(resolve).catch(reject);
    });
    (promise as CancellablePromise<R>).cancel = function(reason?: any) {
      rejectFn(reason);
    };
    return (promise as CancellablePromise<R>);
  };
}

// TESTING CODE!!!!
let f = cancellable(asyncRun);
const promise = f(5);
promise.then((res) => console.log('success: ', res)).catch(r => console.log('fail: ', r));
setTimeout(() => {
  promise.cancel('1');
}, 3000)

timeout - 让一个async函数超时

TIP

通过setTimeout设置定时器来reject, 被rejectpromise无法再次被resolve, 被resolvepromise也无法再次被reject

RUN ME

typescript
function timeout<T extends (...args: any[]) => Promise<any>>(f: T, time: number, reason?: any | ((args: Parameters<T>) => any)): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  return (...args: Parameters<T>): Promise<R> => new Promise((resolve, reject) => {
    setTimeout(() => reject(reason && typeof reason === 'function' ? reason.call(null, args) : reason), time);
    f(...args).then(resolve);
  });
}

// TESTING CODE!!!
const f = timeout(asyncRun, 3000, (args: any[]) => `Arguemnts: ${args.join()}`);
f(5).then((res) => console.log('success: ', res)).catch(r => console.log('fail: ', r));

retry - 让一个async函数可以自动重试

TIP

递归调用可以简化代码

RUN ME

typescript
function retry<T extends (...args: any[]) => Promise<any>>(f: T, times: number): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  if (times < 0) {
    times = 0;
  }
  return (...args: Parameters<T>): Promise<R> => new Promise((resolve, reject) => { 
      function run(reason?: any) {
        if (times-- >= 0) {
          f.apply(null, args).then(resolve).catch(run)
        } else {
          reject(reason);
        }
      }
      return run();
    });
}

// TESTING CODE!!!
const f = retry(asyncRun, 2);
f(2, 2, false).then((res) => console.log('success: ', res)).catch(r => console.log('fail: ', r));

limit - 让一个async函数的并发调用受限

假设并发数为5, 一个async函数, 同时调用10次, 实际只调用5次, 每当有一次调用被fulfill后, 再开始执行一次被pending的调用, 直到所有调用完成

TIP

利用手动promise来调度函数调用
利用promise链来返回调用结果

RUN ME

typescript
function limitParallel<T extends (...args: any[]) => Promise<any>>(f: T, limit: number): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  const waitTask = new Array();
  function run(...args: Parameters<T>): Promise<R> {
      limit--;
      return f.apply(null, args).finally(() => {
        limit++;
        waitTask.length && waitTask.shift()();
      });
  }
  return (...args: Parameters<T>): Promise<R> => {
    if (limit > 0) {
      return run(...args);
    } else {
      let resolve: (value: PromiseLike<R>) => void;
      let promise: Promise<R> = new Promise((resolveF) => resolve = resolveF);
      waitTask.push(() => resolve(run(...args)));
      return promise;
    }
  };
}

// TESTING CODE!!!
const fn = limitParallel(asyncRun, 5);
for(let i = 0; i < 10; i++) {
  console.log(`${Date().toString()} - try start:  ${i}`);
  fn(i, i % 2 === 0);
}

throttle - 让一个async函数可以节流控制

async函数的节流控制需要注意的是, 只拿最后一次调用的结果, 例如依次用"a", "ab", "abc"发送搜索请求来获取列表数据, 需要用最后一次"abc"搜索的结果来resolve前两次的结果; 通常我们采用一个标记字段(例如sequence_id)来标记最后一次调用来判断是否是最新结果, 但async函数可以利用promise链来实现

TIP

利用promise链来返回调用结果, 利用闭包来获取最新的调用结果

RUN ME

typescript
function throttle<T extends (...args: any[]) => Promise<any>>(f: T, wait: number): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  let lastPromise: Promise<R>;
  let lastTime: number = 0;
  return (...args: Parameters<T>): Promise<R> => {
    if (Date.now() - lastTime > wait) {
      // 超出上一次节流区间或第一次调用
      lastPromise = f.apply(null, args);
      lastTime = Date.now();
    }
    return lastPromise.then(() => lastPromise);
  };
}

// TESTING CODE!!!
const fn = throttle(asyncRun, 5000);
let i = 0;
let timer = setInterval(() => {
  const expect = i;
  fn(i % 4 ? i + 1 : 20, i).then((res) => console.log(`try to get result: ${expect}, actually: ${res}`)).catch(r => console.log('fail: ', r));
  if (i++ >= 10) {
    clearInterval(timer);
  }
}, 1000);

debounce - 让一个async函数可以防抖控制

TIP

利用promise链来返回调用结果, 利用闭包来获取最新的调用结果

RUN ME

typescript
function debounce<T extends (...args: any[]) => Promise<any>>(f: T, wait: number): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
  type R = Awaited<ReturnType<T>>;
  let lastPromise: Promise<R>;
  let lastTime: number = 0;
  return (...args: Parameters<T>): Promise<R> => {
    if (Date.now() - lastTime > wait) {
      // 超出上一次防抖区间或第一次调用
      lastPromise = f.apply(null, args);  
    }
    lastTime = Date.now();
    return lastPromise.then(() => lastPromise);
  };
}

// TESTING CODE!!!
const fn = debounce(asyncRun, 5000);
let i = 0;
const run = () => setTimeout(() =>{
  const expect = i;
  fn(i + 1, i).then((res) => console.log(`try to get result: ${expect}, actually: ${res}`)).catch(r => console.log('fail: ', r));
  if (i++ < 10) {
    run();
  }
}, 1000 * (i + 1));
run();

Released under the MIT License.