async异步工具

在es6中的async的语法中,可以参照java并发包实现一些有意思的异步工具,辅助在异步场景(一般指请求)下的开发。
由于js是单线程,下面的实现都比java中实现简单 (抛除线程概念)。同时涉及到js的执行机制,宏任务,微任务,asyncpromise相关内容,需要提前具备这些知识。

wait(等待)

异步函数中,等待(相当于java线程中的阻塞)一段时间。

实现代码:

async function wait(time = 0) {
  await new Promise(resolve => setTimeout(resolve, time));
  // 避免转译成return await, 会在一些safari版本里面报错
  return undefined;
}

模拟使用代码:

(async () => {
  console.time();
  await wait(1000);
  console.timeEnd(); // 输出: default: 1.002s
})();

Lock(锁)

模拟java并发包中的Lock类实现锁操作。 保证同一个锁包围的异步代码执行过程中,同一时刻只有一段代码在执行。

该锁不符合html5的的异步锁接口,而是提供一个java异步包中Lock接口的简单实现

推荐使用场景:多个请求执行过程前,保证同一时刻只有一个token失效验证转换操作。

实现代码:

export type Resolve<T = any> = (value: T | PromiseLike<T>) => void;

export type Reject = (reason?: any) => void;

export interface FlatPromise<T = any> {
  promise: Promise<T>;
  resolve: Resolve<T>;
  reject: Reject;
};

interface Key {
  key: number,
  resolve: Resolve,
};

/**
* 创建一个扁平的promise
* @returns Prmise
*/
function flatPromise<T = any>(): FlatPromise<T> {
  const result: any = {};
  const promise = new Promise<T>((resolve, reject) => {
    result.resolve = resolve;
    result.reject = reject;
  });
  result.promise = promise;
  return result as FlatPromise<T>;
}

class Lock {
  
  keys: Key[] = [];
  hasLock: boolean = false;
  idCount: number = 0;
  
  constructor() {
    this.keys = [];
    this.hasLock = false;
    this.idCount = 0;
  }
  
  _pushKey(resolve: Resolve) {
    this.idCount += 1;
    const key: Key = {
      key: this.idCount,
      resolve,
    };
    this.keys.push(key);
    return key;
  }
  
  _removeKey(key: Key) {
    const index = this.keys.findIndex(item => item.key === key.key);
    if (index >= 0) {
      this.keys.splice(index, 1);
    }
  }
  
  /**
  * 获取锁.
  * 如果当前锁已经锁定,那么就阻塞当前操作
  */
  async lock() {
    if (this.keys.length || this.hasLock) {
      const { promise, resolve } = flatPromise();
      this._pushKey(resolve);
      await promise;
      return null;
    }
    
    this.hasLock = true;
    return null;
  }
  
  /**
  * 尝试获取锁.
  * 该函数如果没有指定一个有效的time,则立马返回一个结果:如果获取到锁则为true,反之为false.
  * 如果指定一个有效的time(time=0有效),则返回一个promise对象,改对象返回的结果为是否获取到锁
  * @param time 最长等待时间
  */
  tryLock(time?: number) {
    if (time === undefined ||
        Number.isNaN(Math.floor(time)) || time < 0) {
      if (this.hasLock) {
        return false;
      }
      this.lock();
      return Promise.resolve(true);
    }
    
    if (!this.hasLock && !this.keys.length) {
      this.hasLock = true;
      return Promise.resolve(true);
    }
    
    const asyncFn = async () => {
      const { promise, resolve: res } = flatPromise();
      const key = this._pushKey(res);
      
      setTimeout(() => {
        this._removeKey(key);
        key.resolve(false);
      }, time);
      
      const isTimeout = await promise;
      
      return isTimeout !== false;
    };
    
    return asyncFn();
  }
  
  async lockFn(asyncFn: () => Promise<void>) {
    await this.lock();
    try {
      await asyncFn();
    } finally {
      this.unlock();
    }
  }
  
  /**
  * 释放锁
  */
  unlock() {
    if (this.keys.length === 0 && this.hasLock === true) {
      this.hasLock = false;
      return;
    }
    
    if (this.keys.length === 0) {
      return;
    }
    
    const index = Math.floor(Math.random() * this.keys.length);
    const key = this.keys[index];
    this._removeKey(key);
    key.resolve(undefined);
  }
  
  toString() {
    return `${this.keys.length}-${this.hasLock}`;
  }
}

模拟使用代码:

function delay(callback: () => void, time: number) {
  return new Promise<void>((resolve) => setTimeout(() => {
    callback();
    resolve(undefined);
  }, time));
}

(async () => {
  const lock = new Lock();
  const syncResult: string[] = [];
  const unSyncResult: string[] = [];
  const withLockAsync = async () => {
    await lock.lock();
    
    await delay(() => {
      syncResult.push('1');
    }, Math.random() * 10);
    
    await delay(() => {
      syncResult.push('2');
    }, Math.random() * 10);
    
    await delay(() => {
      syncResult.push('3');
    }, Math.random() * 10);
    
    lock.unlock();
  };
  
  const withoutLockAsync = async () => {
    await delay(() => {
      unSyncResult.push('1');
    }, Math.random() * 3);
    
    await delay(() => {
      unSyncResult.push('2');
    }, Math.random() * 3);
    
    await delay(() => {
      unSyncResult.push('3');
    }, Math.random() * 3);
  };
  
  const taskList = [];
  for (let i = 0; i < 10; i += 1) {
    taskList.push(withLockAsync(), withoutLockAsync());
  }
  await Promise.all(taskList);
  
  // 输出1,2,3,1,2,3...
  // 证明withLockAsync函数中的代码同一时刻只有一个执行,不会被打算
  console.log(syncResult);
  // 输出的值不一定按照1,2,3,1,2,3...
  // 证明在普通的async函数中,await后的代码会被打乱
  console.log(unSyncResult);
})();

Semaphore(信号量)

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

推荐使用场景:一般用于流量的控制,特别是公共资源有限的应用场景。例如控制同一时刻请求的连接数量,假设浏览器的请求连接数上限为10个,多个异步并发请求可以使用Semaphore来控制请求的异步执行个数最多为10个。

简单实现代码:

class Semaphore {
  constructor(permits) {
    this.permits = permits;
    this.execCount = 0;
    this.waitTaskList = [];
  }
  
  async acquire() {
    const that = this;
    this.execCount += 1;
    if (this.execCount <= this.permits) {
      // 为了保证按照调用顺序执行
      // 如果有等待的,那么先执行等待的,当前的挂起
      // 没有则快速通过
      if (that.waitTaskList.length !== 0) {
        const waitTask = this.waitTaskList.pop();
        waitTask();
        await new Promise((resolve) => {
          that.waitTaskList.push(resolve);
        });
      }
      return;
    }
    await new Promise((resolve) => {
      that.waitTaskList.push(resolve);
    });
  }
  
  release() {
    this.execCount -= 1;
    if (this.execCount < 0) {
      this.execCount = 0;
    }
    if (this.waitTaskList.length === 0) {
      return;
    }
    const waitTask = this.waitTaskList.pop();
    waitTask();
  }
}

模拟一个复杂的页面中复杂的请求场景:

(async () => {
  const semaphore = new Semaphore(5);
  
  let doCount = 0;
  let currntCount = 0;
  const request = async (id) => {
    await semaphore.acquire();
    currntCount++;
    console.log(`执行请求${id}, 正在执行的个数${currntCount}`);
    await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));
    semaphore.release();
    currntCount--;
    doCount++;
    console.log(`执行请求${id}结束,已经执行${doCount}次`);
  };
  const arr = new Array(10).fill(1);
  setTimeout(() => {
    // 依次执行10个请求
    arr.forEach((_, index) => {
      request(`1-${index}`);
    });
  }, 300)
  // 随机触发10个请求
  let index = 0;
  const timerId = setInterval(() => {
    if (index > 9) {
      clearInterval(timerId);
      return;
    }
    request(`2-${index}`);
    index++;
  }, Math.random() * 300 + 300);
  // 同时执行10个请求
  await Promise.all(arr.map(async (_, index) => {
    await request(`3-${index}`);
  }));
  // 等待上面的内容全部执行, 资源释放后,在执行4个请求
  setTimeout(() => {
    const lastArr = new Array(4).fill(1);
    lastArr.forEach((_, index) => {
      request(`4-${index}`);
    });
  }, 5000);
})();

执行结果:

执行请求3-0, 正在执行的个数1
执行请求3-1, 正在执行的个数2
执行请求3-2, 正在执行的个数3
执行请求3-3, 正在执行的个数4
执行请求3-4, 正在执行的个数5
执行请求3-2结束,已经执行1次
执行请求2-1, 正在执行的个数5
执行请求3-3结束,已经执行2次
执行请求2-0, 正在执行的个数5
...
执行请求3-8结束,已经执行29次
执行请求3-6结束,已经执行30次
执行请求4-0, 正在执行的个数1
执行请求4-1, 正在执行的个数2
执行请求4-2, 正在执行的个数3
执行请求4-3, 正在执行的个数4
执行请求4-3结束,已经执行31次
执行请求4-0结束,已经执行32次
执行请求4-2结束,已经执行33次
执行请求4-1结束,已经执行34次

CountDownLatch(倒计时闭锁)和CyclicBarrier(循环栅栏)

在es的async中,一般情景的CountDownLatch可以直接用Promise.all替代。

使用场景:复杂的Promise.all需求场景,支持在离散的多个异步函数中灵活使用(本人还没有碰到过),从而摆脱Promise.all在使用时需要一个promiseiterable类型的输入。

代码实现:

class CountDownLatch {
  
  constructor(count) {
    this.count = count;
    this.waitTaskList = [];
  }
  
  countDown() {
    this.count--;
    if (this.count <= 0) {
      this.waitTaskList.forEach(task => task());
    }
  }
  
  // 避免使用关键字,所以命名跟java中不一样
  async awaitExec() {
    if (this.count <= 0) {
      return;
    }
    const that = this;
    await new Promise((resolve) => {
      that.waitTaskList.push(resolve);
    });
  }
}

模拟使用代码:

(async () => {
  const countDownLatch = new CountDownLatch(10);
  
  const request = async (id) => {
    console.log(`执行请求${id}`);
    await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));
    console.log(`执行请求${id}结束`);
    countDownLatch.countDown();
  };
  
  const task = new Array(10).fill(1);
  // 后续的代码等同于
  // await Promise.all(task.map(async (_, index) => {
  //   await request(index);
  // }));
  task.forEach((_1, index) => {
    request(index);
  });
  await countDownLatch.awaitExec();
  console.log('执行完毕');
})();

CyclicBarrier抛除线程相关概念后,核心功能就是一个可以重复使用的CountDownLatch,这里就不实现了。

异步函数同步等待依次执行

如果需要对多个异步任务进行依次等待执行,可以按照下面代码实现:

function syncExec(taskList: (() => PromiseLike<void>)[]): PromiseLike<void> {
  if (!taskList.length) return Promise.resolve(undefined);
  let promise = taskList[0]();
  for (let i = 1; i < taskList.length; i += 1) {
    promise = promise.then(() => taskList[i]());
  }
  return promise;
}

模拟使用代码(使用时比Promise.all复杂,需要传入一个返回promise的函数):

(async () => {
  const array = new Array(10).fill(1);
  const taskList = array.map((_1, index) => {
    return async () => {
      const rnd = Math.random() * 100;
      await new Promise((resolve) => setTimeout(resolve, rnd));
      console.log(`执行任务${index}`);
    };
  });

  await syncExec(taskList);
})();

输出的结果会按照顺序打印。

更多

如果你需要更加复杂的异步任务编排工具,可以尝试学习和使用gulp的undertaker和webpack的tapable。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容