回调地狱

  1. function loadScript(src, cb) {
  2. const script = document.createElement("script");
  3. script.src = src;
  4. // 这里约定第一个参数是处理error
  5. script.onload = () => cb(null, script);
  6. script.onerror = () => cb(new Error(`Script load error for ${src}`));
  7. document.head.appendChild(script);
  8. }
  9. loadScript("./1.js", (err, script) => {
  10. if (err) {
  11. throw Error(err);
  12. return;
  13. }
  14. console.log("文件加载完毕啦~", script);
  15. loadScript("./2.js", (err, script) => {
  16. if (err) {
  17. throw Error(err);
  18. return;
  19. }
  20. console.log("第二个文件加载完毕!", script);
  21. });
  22. });
  • 回调函数默认第一个参数是error
  • 嵌套的回调函数过多的时候,就会出现”回调地狱”

    Promise

    基本使用

    Promise为我们解决了什么问题? 在传统的异步编程中,如果异步之间存在依赖关系,就需要通过层层嵌套回调的方式满足这种依赖,如果嵌套层数过多,可读性和可以维护性都会变得很差,产生所谓的“回调地狱”,而 Promise 将嵌套调用改为链式调用,增加了可阅读性和可维护性。也就是说,Promise 解决的是异步编码风格的问题。 那 Promise 的业界实现都有哪些呢? 业界比较著名的实现 Promise 的类库有 bluebird、Q、ES6-Promise。

  • Promise 本质是一个状态机,每个 Promise 有三种状态:pending、fulfilled 以及rejected。状态转变只能是pending —> fulfilled 或者 pending —> rejected,fulfilled和rejected会成为settled。状态转变不可逆。

  • then 方法可以被同一个 promise 调用多次。
  • then 方法必须返回一个 promise。规范2.2.7中规定, then 必须返回一个新的 Promise
  • 值穿透 => 链式调用
  • finally中没有参数
  • catch相当于.then(null,()=>{})
  1. var p = new Promise(function(resolve, reject) {
  2. if(/* good condition */) {
  3. resolve('Success!');
  4. }
  5. else {
  6. reject('Failure!');
  7. }
  8. });
  9. p.then(function(data) {
  10. /* do something with the result */
  11. }).catch(function(error) {
  12. /* error :( */
  13. }).finally(()=>{
  14. /* cleanup */
  15. })
  1. promise.then(f1).catch(f2);
  2. promise.then(f1, f2);
  3. 二者相同吗?
  4. 不相同:如果 f1 中出现 error,那么在这儿它会被 .catch 处理,而f2不会。
  1. function loadScript(src) {
  2. return new Promise((resolve, reject) => {
  3. const script = document.createElement("script");
  4. script.src = src;
  5. script.onload = () => resolve(script);
  6. script.onerror = () =>
  7. reject(new Error(`Script load error for ${src}`));
  8. document.head.appendChild(script);
  9. });
  10. }
  11. let promise = loadScript(
  12. "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"
  13. );
  14. promise
  15. .then((script) => console.log(`${script.src} is loaded!`))
  16. .then((script) => loadScript("./1.js"))
  17. .then((script) => loadScript("./2.js"))
  18. .then((script) => console.log("全部加载完毕~!"))
  19. .catch((err) => console.log(err));
  20. //https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js is loaded!
  21. // Another handler...
  • 为了使用loadScript.then这种形式,loadScript需要返回Promise,并且在onload和onerror中分别resolve和reject

    1. new Promise((resolve, reject) => {
    2. setTimeout(() => resolve(1), 1000);
    3. })
    4. .then((result) => {
    5. console.log(result); // 1
    6. return new Promise((resolve, reject) => {
    7. setTimeout(() => resolve(result * 2), 1000);
    8. });
    9. })
    10. .then((result) => {
    11. console.log(result); // 2
    12. return new Promise((resolve, reject) => {
    13. setTimeout(() => resolve(result * 2), 1000);
    14. });
    15. })
    16. .then((result) => {
    17. console.log(result); // 4
    18. return result * 2;
    19. })
    20. .then((result) => console.log(result));
  • 为了使链式可以延续,好的做法是每个then之后,都返回Promise

    错误处理-catch

    1. new Promise((resolve, reject) => {
    2. // hahah();
    3. resolve(22);
    4. })
    5. .then(() => {
    6. throw new Error("Go Wrong");
    7. })
    8. .then(() => {
    9. reject("reject");
    10. })
    11. .catch((err) => {
    12. console.log(err);
    13. throw new Error("Wrong Again");
    14. })
    15. .then(() => console.log("catch之后的then被执行"))
    16. .catch((err) => {
    17. console.log("再次捕获", err);
    18. });
  • JS错误、reject、throw的错误都会被catch捕获

  • promise链中catch捕获之后的then还会继续执行
  • promise链中一旦某个then发生错误,紧跟着该处的then(catch之前)不会执行
  • catch中再次抛出错误会被下一个catch捕获
  • 如果没有catch错误,脚本会被杀死,不会继续往下执行。通过unhandledrejection可以捕获到该类错误
  • Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”
  1. new Promise(function(resolve, reject) {
  2. setTimeout(() => {
  3. throw new Error("Whoops!");
  4. }, 1000);
  5. }).catch(alert);
  6. 函数代码周围有个“隐式的 try..catch”。所以,所有同步错误都会得到处理。
  7. 这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。因此,promise 无法处理它。
  8. //============正确姿势===============
  9. new Promise(function (resolve, reject) {
  10. setTimeout(() => {
  11. try {
  12. throw new Error("Whoops!");
  13. } catch (err) {
  14. console.log("try catch 捕获")
  15. }
  16. }, 1000);
  17. }).catch((err) => console.log("promise catch 捕获"));
  18. //"try catch 捕获"
  1. new Promise((resolve, reject) => {
  2. // hahah();
  3. reject("Go Wrong");
  4. })
  5. .catch((err) => {
  6. console.log(err);
  7. })
  8. .then(() => console.log("继续执行"));
  9. console.log("last execute");
  10. //======================================
  11. // promise reject错误
  12. window.addEventListener("unhandledrejection", (e) => {
  13. console.log("unhandledrejection", e);
  14. });
  15. new Promise((resolve, reject) => {
  16. // hahah();
  17. reject("Go Wrong");
  18. })
  19. console.log("last execute");

image.png
image.png

静态方法

Promise 类有 5 种静态方法:

  1. Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。
  2. Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:
    • status: “fulfilled” 或 “rejected”
    • value(如果 fulfilled)或 reason(如果 rejected)。
  3. Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果。
  4. Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。
  5. Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。
    1. Promise.all([])
    2. promise.race([])
    3. promise.allSettled([])
    4. Promise.resolve()
    5. Promise.reject()
    1. Promise.all([
    2. "First Value",
    3. new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
    4. new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
    5. new Promise((resolve,reject) => setTimeout(() => reject(new Error('Whoops!')), 1000)), // 3
    6. ]).then((value) => console.log(value)).catch(err=> console.log(err)); //
    1. Promise.allSettled([
    2. "First Value",
    3. new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
    4. new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
    5. new Promise((resolve,reject) => setTimeout(() => reject(new Error('Whoops!')), 1000)), // 3
    6. ]).then((value) => console.log(value)).catch(err=> console.log(err)); //
    image.png
  • 如果非Promise,数值会被原样透传

    Generator

常规函数只会返回一个单一值(或者不返回任何值)。
而 Generator 可以按需一个接一个地返回(“yield”)多个值

执行 Generator 函数会返回一个遍历器对象,(也就是说可以for of、扩展运算符……)。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

基本使用

  1. function* generateSequence() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. }
  6. let generator = generateSequence();
  7. console.log(generator);
  8. let sequence = [0,...generateSequence()];
  9. console.log(sequence); // [0,1,2] 注意没有3
  10. for (let value of generator) {
  11. console .log(value); // 1,然后是 2
  12. }
  13. console.log(generator.next()); // 1
  14. console.log(generator.next()); // 2
  15. console.log(generator.next()); // 3

异步generator

Generator可以通过yield和next进行暂停和执行,是异步编程的一种解决方案。

  1. //模拟请求
  2. const fetchData = (data) =>
  3. setTimeout((val) => console.log(val), 1000, data + 1);
  4. function* generatorFetch() {
  5. yield fetchData(1000);
  6. yield fetchData(2000);
  7. yield fetchData(3000);
  8. }
  9. const gen = generatorFetch();
  10. gen.next(); //1001
  11. gen.next(); //2001
  12. gen.next(); //3001

这段代码非常像同步操作,除了加上了yield命令

  1. function* generatorFunction() {
  2. let ask1 = yield '2 + 2 = ?'; // ask1: 4
  3. let ask2 = yield '3 * 3 = ?'; // ask2: 9
  4. }
  5. const generator = generatorFunction();
  6. const g1 = generator.next().value; // "2 + 2 = ?"
  7. const g2 = generator.next(4).value; // "3 * 3 = ?"
  8. const g3 = generator.next(9).done; // true
  9. //错误捕获
  10. try {
  11. generator.throw(new Error('The answer is not found in my database')); // (2)
  12. } catch (err) {
  13. console.log(err);
  14. }

image.png

手写 Generator 函数

  1. // 简易版
  2. // 定义生成器函数,入参是任意集合
  3. function webCanteenGenerator(list) {
  4. var index = 0;
  5. var len = list.length;
  6. return {
  7. // 定义 next 方法
  8. // 记录每次遍历位置,实现闭包,借助自由变量做迭代过程中的“游标”
  9. next: function() {
  10. var done = index >= len; // 如果索引还没有超出集合长度,done 为 false
  11. var value = !done ? list[index++] : undefined; // 如果 done 为 false,则可以继续取值
  12. // 返回遍历是否完毕的状态和当前值
  13. return {
  14. done: done,
  15. value: value
  16. }
  17. }
  18. }
  19. }
  20. var canteen = webCanteenGenerator(['道路千万条', '安全第一条', '行车不规范']);
  21. canteen.next();
  22. canteen.next();
  23. canteen.next();
  24. // {done: false, value: "道路千万条"}
  25. // {done: false, value: "安全第一条"}
  26. // {done: false, value: "行车不规范"}
  27. // {done: true, value: undefined}

https://mp.weixin.qq.com/s/b-4wgsYmTYYF0NxrnA8pIg

Generator 缺陷:

  • 1.函数外部无法捕获异常
  • 2.多个 yield 会导致调试困难

async 函数对 Generator 函数改进如下:

  • 1.内置执行器
  • 2.更好的语义
  • 3.更广的适用性
  • 4.返回值是 Promise

    Async/Await

    基本使用

    async/await 做的事情就是将 Generator 函数转换成 Promise,说白了,async 函数就是 Generator 函数的语法糖,await 命令就是内部 then 命令的语法糖

async 确保了函数返回一个 promise,也会将非 promise 的值包装成promise
关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果

  1. async function f() {
  2. let promise = new Promise((resolve, reject) => {
  3. setTimeout(() => resolve("done!"), 1000)
  4. });
  5. let result = await promise; // 等待,直到 promise resolve (*)
  6. alert(result); // "done!"
  7. }
  8. f();
  9. f().then(val=>console.log(val))

相比于 promise.then,await 只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。

强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

  1. async function async1() {
  2. console.log("async1 start");
  3. await async2();
  4. console.log("async1 end");
  5. }
  6. //等价于;
  7. async function async1() {
  8. console.log("async1 start");
  9. Promise.resolve(async2()).then(() => {
  10. console.log("async1 end");
  11. });
  12. }

我们可以用async包裹匿名函数

  1. (async () => {
  2. let response = await fetch('/article/promise-chaining/user.json');
  3. let user = await response.json();
  4. ...
  5. })();

错误处理:一般用try/catch来捕获

  1. async function f() {
  2. try {
  3. await Promise.reject(new Error("Whoops!"));
  4. } catch (err) {
  5. console.log(err);
  6. }
  7. }
  8. f();

手写

尝试通过 Babel[8] 官网转换一下上述代码,可以看到其核心就是 _asyncToGenerator 方法。

  1. //模拟请求
  2. const fetchData = (data) =>
  3. new Promise((resolve) =>
  4. setTimeout(
  5. (val) => {
  6. console.log(val);
  7. resolve(val);
  8. },
  9. 1000,
  10. data + 1
  11. )
  12. );
  13. async function fetchResult() {
  14. const result1 = await fetchData(1);
  15. const result2 = await fetchData(result1);
  16. const result3 = await fetchData(result2);
  17. }
  18. fetchResult();
  19. ==================================================
  20. // 1.传入参数是generator
  21. // 2.返回promise
  22. // 3.自动调用generator.next()
  23. function asyncToGenerator(genFunction) {
  24. return function (...args) {
  25. const gen = genFunction.apply(this, args);
  26. return new Promise((resolve, reject) => {
  27. function step(key, arg) {
  28. let genResult;
  29. try {
  30. genResult = gen[key](arg); //相当于执行generator.next(args);
  31. } catch (err) {
  32. return reject(err);
  33. }
  34. const { value, done } = genResult;
  35. if (done) {
  36. return resolve(value);
  37. }
  38. return Promise.resolve(value).then(
  39. (val) => {
  40. step('next', val);
  41. },
  42. (err) => {
  43. step('throw', err);
  44. }
  45. );
  46. }
  47. step('next');
  48. });
  49. };
  50. }
  51. function* fetchResult() {
  52. const data = yield fetchData('data');
  53. const data2 = yield fetchData(data);
  54. return 'success';
  55. }
  56. const gen = asyncToGenerator(fetchResult);
  57. gen().then((res) => console.log(res));

练习

手写promise

Promise

一步一步按照思路来实现,简易版:

  1. const PENDING = 'pending';
  2. const FULLFILLED = 'fullfiled';
  3. const REJECTED = 'rejected';
  4. class MyPromise {
  5. constructor(executor) {
  6. this.status = PENDING;
  7. this.value = undefined;
  8. this.reason = undefined;
  9. const resolve = (val) => {
  10. if (this.status === PENDING) {
  11. this.status = FULLFILLED;
  12. this.value = val;
  13. }
  14. };
  15. const reject = (reason) => {
  16. if (this.status === PENDING) {
  17. this.status = REJECTED;
  18. this.reason = reason;
  19. }
  20. };
  21. try {
  22. executor(resolve, reject);
  23. } catch (err) {
  24. reject(err);
  25. }
  26. }
  27. then(onFullfilled, onRejected) {
  28. if (this.status === FULLFILLED) {
  29. onFullfilled(this.value);
  30. }
  31. if (this.status === REJECTED) {
  32. onRejected(this.reason);
  33. }
  34. }
  35. }
  36. new MyPromise((resolve, reject) => {
  37. resolve('第一个处理的数值');
  38. }).then(
  39. (val) => {
  40. console.log(val);
  41. },
  42. (reason) => {
  43. console.log(reason);
  44. }
  45. );

ok,看起来的是work了,但是如果将第41行换成setTimeout的时候,发现不符合预期。

  1. new MyPromise((resolve, reject) => {
  2. // 传入一个异步操作
  3. setTimeout(() => {
  4. resolve('第一个处理的数值');
  5. }, 100);
  6. }).then(
  7. (val) => {
  8. console.log(val);
  9. },
  10. (reason) => {
  11. console.log(reason);
  12. }
  13. );
  14. //没有任何输出

原因是什么?因为setTimeout是宏任务,不会立刻执行,then的时候,还是PENDING状态
所以在then中需要判断下,当前状态是 pending,我们需要先将成功和失败的回调分别存放起来,在executor()的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。

  1. const PENDING = 'pending';
  2. const FULLFILLED = 'fullfiled';
  3. const REJECTED = 'rejected';
  4. class MyPromise {
  5. constructor(executor) {
  6. this.status = PENDING;
  7. this.value = undefined;
  8. this.reason = undefined;
  9. this.onResolvedCallbacks = []; //**********************
  10. this.onRejectedCallbacks = []; //**********************
  11. const resolve = (val) => {
  12. if (this.status === PENDING) {
  13. this.status = FULLFILLED;
  14. this.value = val;
  15. this.onResolvedCallbacks.forEach((fn) => fn());//**********************
  16. }
  17. };
  18. const reject = (reason) => {
  19. if (this.status === PENDING) {
  20. this.status = REJECTED;
  21. this.reason = reason;
  22. this.onRejectedCallbacks.forEach((fn) => fn());//**********************
  23. }
  24. };
  25. try {
  26. executor(resolve, reject);
  27. } catch (err) {
  28. reject(err);
  29. }
  30. }
  31. then(onFullfilled, onRejected) {
  32. if (this.status === FULLFILLED) {
  33. onFullfilled(this.value);
  34. }
  35. if (this.status === REJECTED) {
  36. onRejected(this.reason);
  37. }
  38. //**********************
  39. if (this.status === PENDING) {
  40. this.onResolvedCallbacks.push(() => onFullfilled(this.value));
  41. this.onRejectedCallbacks.push(() => onRejected(this.reason));
  42. }
  43. }
  44. }
  45. new MyPromise((resolve, reject) => {
  46. setTimeout(() => {
  47. resolve('第一个处理的数值');
  48. }, 100);
  49. }).then(
  50. (val) => {
  51. console.log(val);
  52. },
  53. (reason) => {
  54. console.log(reason);
  55. }
  56. );

已经解决了以上问题了,Promise最核心的是 值穿透
具体如何实现呢?思考一下,如果每次调用 then 的时候,我们都重新创建一个 promise 对象,并把上一个 then 的返回结果传给这个新的 promise 的 then 方法,不就可以一直 then 下去了么?

  1. const PENDING = 'pending';
  2. const FULLFILLED = 'fullfiled';
  3. const REJECTED = 'rejected';
  4. class MyPromise {
  5. constructor(executor) {
  6. this.status = PENDING;
  7. this.value = undefined;
  8. this.reason = undefined;
  9. this.onResolvedCallbacks = [];
  10. this.onRejectedCallbacks = [];
  11. const resolve = (val) => {
  12. if (this.status === PENDING) {
  13. this.status = FULLFILLED;
  14. this.value = val;
  15. this.onResolvedCallbacks.forEach((fn) => fn());
  16. }
  17. };
  18. const reject = (reason) => {
  19. if (this.status === PENDING) {
  20. this.status = REJECTED;
  21. this.reason = reason;
  22. this.onRejectedCallbacks.forEach((fn) => fn());
  23. }
  24. };
  25. try {
  26. executor(resolve, reject);
  27. } catch (err) {
  28. reject(err);
  29. }
  30. }
  31. then(onFullfilled, onRejected) {
  32. return new Promise((resolve, reject) => { //*****************
  33. //模拟事件循环中then的微任务
  34. setTimeout(() => { //*****************
  35. if (this.status === FULLFILLED) {
  36. const result = onFullfilled(this.value);
  37. resolve(result); //*****************
  38. // resolvePromise(promise2, x, resolve, reject);
  39. }
  40. }, 0);
  41. if (this.status === REJECTED) {
  42. const reason = onRejected(this.reason);
  43. reject(reason);
  44. }
  45. if (this.status === PENDING) {
  46. this.onResolvedCallbacks.push(() => {
  47. resolve(onFullfilled(this.value));
  48. });
  49. this.onRejectedCallbacks.push(() => {
  50. const reason = onRejected(this.reason);
  51. reject(reason);
  52. });
  53. }
  54. });
  55. }
  56. }
  57. new MyPromise((resolve, reject) => {
  58. // resolve(2321);
  59. setTimeout(() => {
  60. resolve('第一个处理的数值');
  61. }, 100);
  62. })
  63. .then(
  64. (val) => {
  65. console.log(val);
  66. return '第二个被处理的值';
  67. },
  68. (reason) => {
  69. console.log(reason);
  70. }
  71. )
  72. .then((val) => console.log('值穿透', val));

值穿透已经初步支持。
关于then有很多规范,如下:
then 方法是 Promise 的核心,这里做一下详细介绍。

  1. promise.then(onFulfilled, onRejected)

一个 Promise 的then接受两个参数: onFulfilled和onRejected(都是可选参数,并且为函数,若不是函数将被忽略)

  • onFulfilled 特性:
    • 当 Promise 执行结束后其必须被调用,其第一个参数为 promise 的终值,也就是 resolve 传过来的值
    • 在 Promise 执行结束前不可被调用
    • 其调用次数不可超过一次
  • onRejected 特性
    • 当 Promise 被拒绝执行后其必须被调用,第一个参数为 Promise 的拒绝原因,也就是reject传过来的值
    • 在 Promise 执行结束前不可被调用
    • 其调用次数不可超过一次
  • 调用时机
    onFulfilledonRejected 只有在执行环境堆栈仅包含平台代码时才可被调用(平台代码指引擎、环境以及 promise 的实施代码)
  • 调用要求
    onFulfilledonRejected 必须被作为函数调用,如果不是函数,忽略。后面的then中可以获取之前的值。promise.then().then(),那么其后面的 then 依旧可以得到之前 then 返回的值,这就是所谓的值的穿透
  • 多次调用then方法可以被同一个promise调用多次
    • promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
    • promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
  • 返回then方法会返回一个Promise,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(详情)对象,所以在我们的实现中,也让then返回一个新的Promise对象。
  1. promise2 = promise1.then(onFulfilled, onRejected);
  • 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
  • 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
  • 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的拒因
    • 不论 promise1 被 reject 还是被 resolve , promise2 都会被 resolve,只有出现异常时才会被 rejected
      每个Promise对象都可以在其上多次调用then方法,而每次调用then返回的Promise的状态取决于那一次调用then时传入参数的返回值,所以then不能返回this,因为then每次返回的Promise的结果都有可能不同。
  1. const resolvePromise = (promise2, x, resolve, reject) => {
  2. // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1
  3. if (promise2 === x) {
  4. return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  5. }
  6. // Promise/A+ 2.3.3.3.3 只能调用一次
  7. let called;
  8. // 后续的条件要严格判断 保证代码能和别的库一起使用
  9. if ((typeof x === 'object' && x != null) || typeof x === 'function') {
  10. try {
  11. // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1
  12. let then = x.then;
  13. if (typeof then === 'function') {
  14. // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3
  15. then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
  16. if (called) return;
  17. called = true;
  18. // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
  19. resolvePromise(promise2, y, resolve, reject);
  20. }, r => {
  21. // 只要失败就失败 Promise/A+ 2.3.3.3.2
  22. if (called) return;
  23. called = true;
  24. reject(r);
  25. });
  26. } else {
  27. // 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4
  28. resolve(x);
  29. }
  30. } catch (e) {
  31. // Promise/A+ 2.3.3.2
  32. if (called) return;
  33. called = true;
  34. reject(e)
  35. }
  36. } else {
  37. // 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4
  38. resolve(x)
  39. }
  40. }

finally/catch

  1. Promise.prototype.catch = function(errCallback){
  2. return this.then(null,errCallback)
  3. }
  4. Promise.prototype.finally = function(cb) {
  5. return this.then((value)=>{
  6. return Promise.resolve(cb()).then(()=>value)
  7. },(reason)=>{
  8. return Promise.resolve(cb()).then(()=>{throw reason})
  9. })
  10. }

Promise.resolve/reject

  1. static resolve(val){
  2. return new Promise((resolve,reject)=>{
  3. resolve(val);
  4. })
  5. }
  6. static reject(reason){
  7. return new Promise((resolve,reject)=>{
  8. reject(reason);
  9. })
  10. }

Promise.allSettled

  1. if (!Promise.allMySettled) {
  2. Promise.allMySettled = function (promises) {
  3. const convertedPromises = promises.map((p) =>
  4. Promise.resolve(p).then(
  5. (value) => ({ status: "fulfilled", value }),
  6. (reason) => ({ status: "rejected", reason })
  7. )
  8. );
  9. return Promise.all(convertedPromises);
  10. };
  11. }
  12. Promise.allMySettled([
  13. "First Value",
  14. new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
  15. new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
  16. new Promise((resolve, reject) =>
  17. setTimeout(() => reject(new Error("Whoops!")), 1000)
  18. ), // 3
  19. ])
  20. .then((value) => console.log(value))
  21. .catch((err) => console.log(err));

Promise.all

  1. if (!Promise.myAll) {
  2. Promise.myAll = function (promises) {
  3. return new Promise((resolve, reject) => {
  4. let value = [];
  5. let count = 0;
  6. for (let [i, p] of promises.entries()) {
  7. // 传入的参数中如果不是promise
  8. Promise.resolve(p).then(
  9. (val) => {
  10. value[i] = val;
  11. count++;
  12. // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
  13. if (count === promises.length) resolve(value);
  14. },
  15. (err) => reject(err)
  16. );
  17. }
  18. });
  19. };
  20. }
  21. Promise.myAll([
  22. "11111",
  23. new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  24. new Promise((resolve, reject) =>
  25. setTimeout(() => reject(new Error("Whoops!")), 2000)
  26. ),
  27. new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
  28. ])
  29. .then((val) => console.log(val))
  30. .catch((err) => console.log(err));

Promise.race

  1. if (!Promise.myRace) {
  2. Promise.myRace = function (promises) {
  3. return new Promise((resolve, reject) => {
  4. for (let p of promises) {
  5. Promise.resolve(p).then(
  6. (val) => resolve(val),
  7. (err) => reject(err)
  8. );
  9. }
  10. });
  11. };
  12. }
  13. Promise.myRace([
  14. new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  15. new Promise((resolve, reject) =>
  16. setTimeout(() => reject(new Error("Whoops!")), 1000)
  17. ),
  18. new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
  19. ])
  20. .then((val) => console.log(val))
  21. .catch((err) => console.log(err));

Promisify

将回调函数转换成promise
promisify(f):它接受一个需要被 promisify 的函数 f,并返回一个包装(wrapper)函数。

  1. function loadScript(src, callback) {
  2. let script = document.createElement('script');
  3. script.src = src;
  4. script.onload = () => callback(null, script);
  5. script.onerror = () => callback(new Error(`Script load error for ${src}`));
  6. document.head.append(script);
  7. }
  8. // 用法:
  9. // loadScript('path/script.js', (err, script) => {...})
  1. function promisify(f) {
  2. return function (...args) { // 返回一个包装函数(wrapper-function) (*)
  3. return new Promise((resolve, reject) => {
  4. function callback(err, result) { // 我们对 f 的自定义的回调 (**)
  5. if (err) {
  6. reject(err);
  7. } else {
  8. resolve(result);
  9. }
  10. }
  11. args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾
  12. f.call(this, ...args); // 调用原始的函数
  13. });
  14. };
  15. }
  16. // 用法:
  17. let loadScriptPromise = promisify(loadScript);
  18. loadScriptPromise(...).then(...);
  19. 但是如果原始的 f 期望一个带有更多参数的回调 callback(err, res1, res2, ...),该怎么办呢?
  20. 我们可以继续改进我们的辅助函数。让我们写一个更高阶版本的 promisify
  21. 当它被以 promisify(f) 的形式调用时,它应该以与上面那个版本的实现的工作方式类似。
  22. 当它被以 promisify(f, true) 的形式调用时,它应该返回以回调函数数组为结果 resolve promise。这就是具有很多个参数的回调的结果。
  23. // promisify(f, true) 来获取结果数组
  24. function promisify(f, manyArgs = false) {
  25. return function (...args) {
  26. return new Promise((resolve, reject) => {
  27. function callback(err, ...results) { // 我们自定义的 f 的回调
  28. if (err) {
  29. reject(err);
  30. } else {
  31. // 如果 manyArgs 被指定,则使用所有回调的结果 resolve
  32. resolve(manyArgs ? results : results[0]);
  33. }
  34. }
  35. args.push(callback);
  36. f.call(this, ...args);
  37. });
  38. };
  39. }
  40. // 用法:
  41. f = promisify(f, true);
  42. f(...).then(arrayOfResults => ..., err => ...);

promise链式调用

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promise 实现)

  1. function red() {
  2. console.log("red");
  3. }
  4. function green() {
  5. console.log("green");
  6. }
  7. function yellow() {
  8. console.log("yellow");
  9. }
  10. function light(ms, cb) {
  11. return new Promise((resolve, reject) => {
  12. setTimeout(() => {
  13. cb();
  14. resolve();
  15. }, ms);
  16. });
  17. }
  18. function step() {
  19. Promise.resolve(() => {
  20. return light(1000, red);
  21. })
  22. .then(() => {
  23. return light(2000, green);
  24. })
  25. .then(() => {
  26. return light(3000, yellow);
  27. })
  28. .then(() => {
  29. step();
  30. });
  31. }
  32. step();
  1. class A {
  2. constructor() {
  3. this.list = Promise.resolve();
  4. }
  5. say(message) {
  6. this.list = this.list.then(() => {
  7. console.log(message);
  8. });
  9. return this;
  10. }
  11. delay(ms) {
  12. this.list = this.list.then(()=>{
  13. return new Promise(resolve=>{
  14. setTimeout(()=>{
  15. resolve()
  16. },ms)
  17. })
  18. })
  19. return this
  20. }
  21. }
  22. const a = new A();
  23. a.say("1")
  24. .delay(1000)
  25. .say("2")
  26. .delay(4000)
  27. .say("fsaffa");

实现 mergePromise 函数,把传进去的函数数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

思路:全局定义一个promise实例p
循环遍历函数数组,每次循环更新p,将要执行的函数item通过p的then方法进行串联,并且将执行结果推入data数组
最后将更新的data返回,这样保证后面p调用then方法,如何后面的函数需要使用data只需要将函数改为带参数的函数。

  1. const timeout = ms =>
  2. new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve();
  5. }, ms);
  6. });
  7. const ajax1 = () =>
  8. timeout(2000).then(() => {
  9. console.log("1");
  10. return 1;
  11. });
  12. const ajax2 = () =>
  13. timeout(1000).then(() => {
  14. console.log("2");
  15. return 2;
  16. });
  17. const ajax3 = () =>
  18. timeout(2000).then(() => {
  19. console.log("3");
  20. return 3;
  21. });
  22. const mergePromise = ajaxArray => {
  23. let data = [];
  24. let p = Promise.resolve();
  25. for (let ajax of ajaxArray) {
  26. p = p.then(ajax).then(val => {
  27. data.push(val);
  28. return data;
  29. });
  30. }
  31. return p;
  32. };
  33. mergePromise([ajax1, ajax2, ajax3]).then(data => {
  34. console.log("done");
  35. console.log(data); // data 为 [1, 2, 3]
  36. });

并发

有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = [‘http://example.com/1.jpg‘, …., ‘http://example.com/8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。

但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个

  1. const urls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  2. function load(url) {
  3. return new Promise((resolve, reject) => {
  4. setTimeout(() => {
  5. resolve(url);
  6. }, (Math.random() * 10000) >> 0);
  7. });
  8. }
  9. function limitLoad(urls, handler, limit) {
  10. const queue = urls.slice(0);
  11. const promises = queue.splice(0, limit).map((url, idx) => {
  12. return handler(url).then(() => idx);
  13. });
  14. let p = Promise.race(promises);
  15. for (let i = 0; i < queue.length; i++) {
  16. p = p.then((idx) => {
  17. promises[idx] = handler(queue[i]).then(() => idx);
  18. return Promise.race(promises);
  19. });
  20. }
  21. }
  22. limitLoad(urls, load, 3);
  23. =============超时重试========
  24. let urls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  25. urls = urls.map((v) => ({
  26. url: v,
  27. retry: 0,
  28. }));
  29. function load(urlInfo) {
  30. return new Promise((resolve, reject) => {
  31. setTimeout(() => {
  32. if (Math.random() > 0.5) {
  33. resolve(urlInfo);
  34. } else {
  35. reject(urlInfo);
  36. }
  37. }, (Math.random() * 3000) >> 0);
  38. });
  39. }
  40. function limitLoad(urls, handler, limit, retryNum) {
  41. const queue = urls.slice(0);
  42. while (limit) {
  43. exec(queue.shift());
  44. limit--;
  45. }
  46. function exec(urlInfo) {
  47. handler(urlInfo)
  48. .then(() => {
  49. const curUrlInfo = queue.shift();
  50. if (curUrlInfo) {
  51. exec(curUrlInfo);
  52. }
  53. })
  54. .catch((res) => {
  55. if (res.retry >= retryNum) {
  56. count++;
  57. const curUrlInfo = queue.shift();
  58. exec(curUrlInfo);
  59. } else {
  60. exec({
  61. ...res,
  62. retry: ++res.retry,
  63. });
  64. }
  65. });
  66. }
  67. }
  68. limitLoad(urls, load, 3, 3);
  1. const urls = [
  2. 'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',
  3. 'https://www.kkkk1000.com/images/getImgData/gray.gif',
  4. 'https://www.kkkk1000.com/images/getImgData/Particle.gif',
  5. 'https://www.kkkk1000.com/images/getImgData/arithmetic.png',
  6. 'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',
  7. 'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',
  8. 'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',
  9. 'https://www.kkkk1000.com/images/wxQrCode2.png'
  10. ];
  11. function loadImg(url) {
  12. return new Promise((resolve, reject) => {
  13. const ImageName = url.split(/\//g)[5] || url.split(/\//g)[4];
  14. const img = new Image();
  15. img.onload = function () {
  16. console.log('一张图片加载完成', ImageName);
  17. resolve(ImageName);
  18. };
  19. img.onerror = reject;
  20. img.src = url;
  21. });
  22. }

解法一

  1. function fetchLimit(urls, limit, handler) {
  2. while (limit--) {
  3. loop();
  4. }
  5. function loop() {
  6. if (urls.length > 0) {
  7. handler(urls.shift()).then(loop);
  8. }
  9. }
  10. }
  11. fetchLimit(urls, 3, loadImg);

解法二

  1. let mapLimit = (list, limit, asyncHandle) => {
  2. //这5个异步请求中无论哪一个先执行完,都会继续执行下一个list项
  3. let loop = (urls) => {
  4. return asyncHandle(urls.shift())
  5. .then(()=>{
  6. // 数组还未迭代完,递归继续进行迭代
  7. if (urls.length!==0) return loop(urls)
  8. else return '这个坑位finish';
  9. })
  10. };
  11. let asyncList = []; // 正在进行的所有并发异步操作
  12. while(limit--) {
  13. asyncList.push( loop([...list]) );
  14. }
  15. return Promise.all(asyncList); // 所有并发异步操作都完成后,本次并发控制迭代完成
  16. }
  17. mapLimit(urls,5,loadImg).then((res)=>console.log(res))

解法三

1.用 Promise.race来实现,先并发请求3个图片资源,这样可以得到 3 个 Promise实例,组成一个数组promises
2.然后不断的调用 Promise.race 来返回最快改变状态的 Promise,从数组(promises )中删掉这个 Promise 对象实例
3.再加入一个新的 Promise实例,直到全部的 url 被取完。

  1. function limitLoad(urls, handler, limit) {
  2. // 对数组做一个拷贝
  3. const urlList = [...urls]
  4. let promises = [];
  5. //并发请求到最大数,得到3个promise实例 [p1,p2,p3]
  6. promises = urlList.splice(0, limit).map((url, index) => {
  7. // 这里返回的 index 是任务在 promises 的脚标,
  8. //用于在 Promise.race 之后找到完成的任务脚标
  9. return handler(url,index).then(() => {
  10. return index
  11. });
  12. });
  13. (async function loop() {
  14. //Promise.race(promises).then(x=>{
  15. // promises[x] = handler(urlList.shift()).then(()=>loop())
  16. //});
  17. let p = Promise.race(promises);
  18. for (let i = 0; i < urlList.length; i++) {
  19. p = p.then((res) => {
  20. promises[res] = handler(urlList[i]).then(() => {
  21. return res
  22. });
  23. return Promise.race(promises)
  24. })
  25. }
  26. })()
  27. }
  28. limitLoad(urls, loadImg, 3)
  1. let urls = ['http://dcdapp.com', …];
  2. /*
  3. *实现一个方法,比如每次并发的执行三个请求,如果超时(timeout)就输入null,直到全部请求完
  4. *batchGet(urls, batchnum=3, timeout=3000);
  5. *urls是一个请求的数组,每一项是一个url
  6. *最后按照输入的顺序返回结果数组[]
  7. */
  1. async function batchFetchData(urls, limit = 3, timeout = 3000) {
  2. let ret = [];
  3. while (urls.length > 0) {
  4. var preList = urls.splice(0, limit);
  5. let requestList = preList.map((url) => {
  6. return request(url, timeout);
  7. });
  8. const result = await Promise.allsettled(requestList);
  9. ret.concat(
  10. result.map((item) => {
  11. if (item.status === 'rejected') {
  12. return null;
  13. } else {
  14. return item.value;
  15. }
  16. })
  17. );
  18. }
  19. return ret;
  20. }
  21. function request(url, timeout) {
  22. return new Promise((resolve, reject) => {
  23. setTimeout(() => {
  24. reject();
  25. }, timeout);
  26. // ajax发送请求
  27. ajax({ url }, (data) => {
  28. resolve(data);
  29. });
  30. });
  31. }
  32. // urls为一个不定长的数组
  33. batchFetchData(['http1', 'http2', 'http3']);
  1. /*
  2. 可以批量请求数据,所有的 URL 地址在 urls 参数中,
  3. 同时可以通过 max 参数控制请求的并发度,当所有请
  4. 求结束之后,需要执行 callback 回调函数。发请求的
  5. 函数可以直接使用 fetch 即可
  6. */
  1. function fetchLimit(sourceUrls, limit, cb) {
  2. const urls = [...sourceUrls];
  3. while (limit--) {
  4. exec();
  5. }
  6. const finished = 0;
  7. function exec() {
  8. const url = urls.splice(urls);
  9. if (finished >= urls.length) {
  10. return cb();
  11. }
  12. fetch(url)
  13. .then((res) => {
  14. finished++;
  15. exec();
  16. })
  17. .catch((err) => {
  18. exec();
  19. });
  20. }
  21. }
  22. =============================================
  23. function fetchLimit(urls, limit, cb) {
  24. let currentIdx = 0; //当前请求地址的数组索引
  25. let finished = 0; //已经完成的请求数量
  26. for (let i = 0; i < limit; i++) {
  27. loop();
  28. }
  29. function loop() {
  30. console.log("finished", finished, "currentIdx", currentIdx);
  31. // 当所有请求完成之后执行cb
  32. if (finished === urls.length) {
  33. return cb();
  34. }
  35. //当超出urls
  36. if (currentIdx > urls.length) {
  37. limit = 0;
  38. }
  39. if (limit > 0) {
  40. fetch(urls[currentIdx])
  41. .then((res) => {
  42. // do your logic
  43. console.log(currentIdx, urls[currentIdx]);
  44. finished++;
  45. loop();
  46. })
  47. .catch((err) => {
  48. // do your logic
  49. loop();
  50. });
  51. }
  52. currentIdx++;
  53. }
  54. }
  55. const callback = () => {
  56. console.log("全部请求完毕");
  57. };
  58. fetchLimit(urls, 3, callback);
  1. /**
  2. *
  3. * @param { Array } urls 请求地址数组
  4. * @param { Number } max 最大并发请求数
  5. * @param { Function } callback 回调地址
  6. */
  7. function parallelFetch(urls, max, callback) {
  8. // 如果当前环境不支持 fetch , 则提示程序无法正常运行
  9. if (!window.fetch || "function" !== typeof window.fetch) {
  10. throw Error("当前环境不支持 fetch 请求,程序终止");
  11. }
  12. // 如果参数有误,则提示输入正确的参数
  13. if (!urls || urls.length <= 0) {
  14. throw Error("urls is empty: 请传入正确的请求地址");
  15. }
  16. const _urlsLength = urls.length; // 请求地址数组的长度
  17. const _max = max || 1; // 保证最大并发值的有效性
  18. let _currentIndex = 0; // 当前请求地址的索引
  19. let _maxFetch = max <= _urlsLength ? max : _urlsLength; // 当前可以正常请求的数量,保证最大并发数的安全性
  20. let _finishedFetch = 0; // 当前完成请求的数量,用于判断何时调用回调
  21. console.log(`开始并发请求,接口总数为 ${_urlsLength} ,最大并发数为 ${_maxFetch}`);
  22. // 根据最大并发数进行循环发送,之后通过状态做递归请求
  23. for (let i = 0; i < _maxFetch; i++) {
  24. fetchFunc();
  25. }
  26. // 请求方法
  27. function fetchFunc() {
  28. // 如果所有请求数都完成,则执行回调方法
  29. if (_finishedFetch === _urlsLength) {
  30. console.log(`当前一共 ${_urlsLength} 个请求,已完成 ${_finishedFetch} 个`)
  31. if ("function" === typeof callback) return callback();
  32. return false;
  33. }
  34. // 如果当前请求的索引大于等于请求地址数组的长度,则不继续请求
  35. if (_currentIndex >= _urlsLength) {
  36. _maxFetch = 0;
  37. }
  38. //如果可请求的数量大于0,表示可以继续发起请求
  39. if (_maxFetch > 0) {
  40. console.log( `当前正发起第 ${_currentIndex + 1 } 次请求,当前一共 ${_urlsLength} 个请求,已完成 ${_finishedFetch} 个,请求地址为:${urls[_currentIndex]}`);
  41. // 发起 fetch 请求
  42. fetch(urls[_currentIndex])
  43. .then((res) => {
  44. // TODO 业务逻辑,正常的逻辑,异常的逻辑
  45. // 当前请求结束,正常请求的数量 +1
  46. _maxFetch += 1;
  47. _finishedFetch += 1;
  48. fetchFunc();
  49. })
  50. .catch((err) => {
  51. // TODO 异常处理,处理异常逻辑
  52. // 当前请求结束,正常请求的数量 +1
  53. _maxFetch += 1;
  54. _finishedFetch += 1;
  55. fetchFunc();
  56. });
  57. // 每次请求,当前请求地址的索引 +1
  58. _currentIndex += 1;
  59. // 每次请求,可以正常请求的数量 -1
  60. _maxFetch -= 1;
  61. }
  62. }
  63. }
  64. let urls = [];
  65. for (let i = 0; i < 100; i++) {
  66. urls.push(`https://jsonplaceholder.typicode.com/todos/${i}`);
  67. }
  68. const max = 10;
  69. const callback = () => {
  70. console.log("我请求完了");
  71. };
  72. parallelFetch(urls, max, callback);
  1. import asyncPool from 'tiny-async-pool';
  2. const timeout = (i) =>
  3. new Promise((resolve) => setTimeout(() => resolve(i), i));
  4. async function asyncPool(poolLimit, array, iteratorFn) {
  5. const ret = []; // 存储所有的异步任务
  6. const executing = []; // 存储正在执行的异步任务
  7. for (const item of array) {
  8. // 调用iteratorFn函数创建异步任务
  9. const p = Promise.resolve().then(() => iteratorFn(item, array));
  10. ret.push(p); // 保存新的异步任务
  11. // 当poolLimit值小于或等于总任务个数时,进行并发控制
  12. if (poolLimit <= array.length) {
  13. // 当任务完成后,从正在执行的任务数组中移除已完成的任务
  14. const e = p.then(() => executing.splice(executing.indexOf(e), 1));
  15. executing.push(e); // 保存正在执行的异步任务
  16. if (executing.length >= poolLimit) {
  17. await Promise.race(executing); // 等待较快的任务执行完成
  18. }
  19. }
  20. }
  21. return Promise.all(ret);
  22. }
  23. (async () => {
  24. const results = await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
  25. console.log(2222, results);
  26. })();

超时中断

  1. const promise = new Promise((resolve, reject) => {
  2. setTimeout(() => {
  3. // 模拟的接口调用 ajax 超时设置
  4. resolve("请求成功");
  5. }, 3000);
  6. });
  7. function fetchWithTimeout(fetchPromise, timeout = 2000) {
  8. const timePromise = new Promise((resolve, reject) => {
  9. setTimeout(() => {
  10. reject("请求超时timeout");
  11. }, timeout);
  12. });
  13. return Promise.race([fetchPromise, timePromise]);
  14. }
  15. fetchWithTimeout(promise, 1000)
  16. .then(val => console.log(val))
  17. .catch(err => console.log(err));


Reference

https://mp.weixin.qq.com/s/b-4wgsYmTYYF0NxrnA8pIg
https://juejin.cn/post/6850037281206566919#heading-5
https://github.com/sisterAn/blog/issues/13
https://github.com/sisterAn/JavaScript-Algorithms/issues/85
https://segmentfault.com/a/1190000022705474