JavaScript – Promise

前言

我学 Promise 的时候, 那时还没有 es6. 曾经还自己实现过. 但时隔多年, 现在 es6 的 promise 已经很完善了.

这篇作为一个简单的复习. (毕竟我已经 1 年多没有写 JS 了...)

以前写过相关的文章:

Javascript Promise 学习(上)

Javascript Promise 学习 (中)

$q 就是angular 的promise

angular2 学习笔记 ( Rxjs, Promise, Async/Await 的区别 )

 

参考

阮一峰 – Promise 对象

 

Promise 解决的问题

什么是异步和回调 callback

JS 是单线程, 无法并发处理事情, 但是它可以异步. 比如发 http request, request 通过网卡发出去后, CPU 就去做别的事情, 而不是傻傻等网卡回复.

当 response 回来以后, 网卡通知 CPU, 这时再把任务接回来. 这个动作就叫 callback. 就是你别等我, 我好了会通知你.

callback 的写法长这样

const callback = () => {
  console.log('timeout');
};
setTimeout(callback, 2000);

setTimeout 是一个异步函数, 调用后, 游览器会用另一个线程去计算时间, 主线程继续处理其它代码. 时间到, 主线程会被通知, 然后运行后续 (callback) 的代码.

大概是这个概念. 其它的异步函数包括 Ajax, FileReader 等等 (通常涉及到磁盘 IO, 网路请求都会是异步的. 因为做这些事情的时候不需要 CPU).

回调地狱

callback 的写法一旦嵌套就会变成很丑, unreadable.

比如, 我想写一个

delay 3 秒,

运行 console 'a'

再 delay 2 秒

运行 console 'b'

再 delay 1 秒

运行 console 'c'

写出来长这样:

setTimeout(() => {
  console.log('a');
  setTimeout(() => {
    console.log('b');
    setTimeout(() => {
      console.log('c');
    }, 1000);
  }, 2000);
}, 3000);

丑不丑? Promise 就是用来解决丑这个问题的. 它可以把嵌套的回调 "打平" flat

 

Promise 基本用法

Promise 的核心是封装了异步函数的调用, 和 callback 的写法, 记住这个点.

promise 使用是这样的

const promise = new Promise((resolve, _reject) => {
  setTimeout(() => {
    resolve('return value 123');
  }, 3000);
});
promise.then((returnValue) => {
  console.log('returnValue', returnValue); // return value 123
});

分 2 个阶段看待

初始化 Promise

Promise 是一个 class. 实例化它会等到 promise 对象.

实例化时, 需要传入一个函数. 函数里面封装了要执行的异步代码.

比如上面的 setTimeout. 或者是 Ajax, FileReader 等等都行.

resolve 是一个 callback 代理, 当异步完成以后. 我们调用 resolve 告知 Promise 异步完成了. 并且返回异步函数的返回值 (比如 Ajax 后的 response data)

注册 callback

注册 callback 是通过 promise 对象来实现的. 调用 .then 函数把 callback 传进去

callback 会在 resolve 的时候被执行, 并且获得异步函数的返回值.

注: 有没有返回值都是 ok 的.

意义何在? 

像这样把异步函数和 callback wrap 起来, 意义何在呢? 如果只是 1 个 callback 那么没有什么太大的意义.

记得, Promise 要解决的是嵌套的 callback (回调地狱)

 

.then Combo

我们把上面的 setTimeout 用 Promise 封装一下

function delayAsync(delayTime: number): Promise<void> {
  const promise = new Promise<void>((resolve, _reject) => {
    setTimeout(() => {
      resolve();
    }, delayTime);
  });
  return promise;
}

delayAsync(3000).then(() => {
  console.log('a');
});

相等于

setTimeout(() => {
  console.log('a');
}, 3000);

return promise + .then combo

如果要再嵌套一个 delay, 你可能会认为是这样写

delayAsync(3000).then(() => {
  console.log('a');
  delayAsync(2000).then(() => {
    console.log('b');
  });
});

虽然这个也可以跑, 但是正确的用法不是这样. 而是这样

delayAsync(3000)
  .then(() => {
    console.log('a');
    return delayAsync(2000);
  })
  .then(() => {
    console.log('b');
  });

在第一个 then 里, 我们返回了另一个 promise 对象.

然后再第一个 then 之后 combo 了另一个 then.

这样的写法就成功的把嵌套的回调 "打平" 了.

Promise 内部实现原理

其实没有必要懂底层逻辑, 会用就可以了. 简单了解一下到时可以啦.

Promise 对象的 then 负责注册 callback. 同时它返回另一个 promise. 你可以把它理解为一个 child promise (连续几个 .then 就变成了一个 promise chain)

callback 除了可以返回普通的 value 也可以返回一个 promise 对象.

当返回 promise 对象, Promise 就会等待这个 promise 对象 resolve 才执行 callback.

Promise 内部就是维护着 promise chain 和所以 callback 的执行顺序. 这样就做到了 "打平" 的写法了.

感悟

Promise 在 es6 之前就有了, 在 JS 的语法基础上, 通过封装实现另一种调用方式, 让代码更好写, 更好读.

jQuery 也是有这种 feel. 还有 Fluent Builder 模式 也是这样. 都是很聪明的实现.

 

Promise 的执行顺序

console.log('1');
const promise = new Promise<void>((resolve) => {
  console.log('2');
  resolve();
  console.log('3');
});
promise.then(() => {
  console.log('5');
});
console.log('4');

new Promise 传入的函数会马上被执行 (里面通常会调用异步函数, 但并没有强制, 你也可以直接调用 resolve 返回的)

上面我刻意搞了一个同步执行的情况, resolve 虽然马上被执行了, 但是 callback 并没有马上被执行.

一直等到 console.log(4) 完了以后 callback 才被执行.

也就是说任何 promise 的 callback 都会被押后执行, 即使 resolve 没有被异步调用. 这个是唯一需要特别注意的.

after resolve 依然执行代码 ?

best practice 的话, resolve 之后就不应该执行代码了.

刻意习惯性的在 resolve 前加上 return, 确保后续没有执行代码. (不然挺乱的)

const promise = new Promise<void>((resolve) => {
  console.log('2');
  return resolve();
});

 

Reject and Catch

上面提到的例子都是 succeed 的情况. 其实 Promise 还有一个强项, 那就是解决异步函数和回调 catch error 的问题.

异步的 catching error 问题

同步代码很容易 catch error

try {
  throw new Error('erorr');
} catch (e) {
  console.log('error 123');
}

换成异步的话

try {
  setTimeout(() => {
    throw new Error('error');
  }, 2000);
} catch (e) {
  console.log('error 123'); // won't be call
}

这时就无法 catch 到 error 了.

Promise Reject & Catch

const promise = new Promise<void>((resolve, reject) => {
  if (Math.random() >= 0.5) {
    resolve();
  } else {
    reject();
  }
});

promise
  .then(() => {
    console.log('ok');
  })
  .catch(() => {
    console.log('error');
  });

除了 resolve, 还有一个 reject 函数用来告知 Promise 异步函数搞糟了.

除了 .then, 还有一个 .catch 用来捕获 reject 的返回.

它的逻辑是 resolve 就去找 .then 的回调. reject 就去找 .catch 的回调.

此外, 除了 reject, throw 也是可以被 catch 到,

delayAsync(3000)
  .then(() => {
    throw new Error('error');
    return delayAsync(2000);
  })
  .then(() => {
    console.log('b');
  })
  .catch(() => {
    console.log('catch');
  });

在第一个 then 回调中使用了 throw, 最后一个 catch 也是可以捕获到的哦. 只要在 promise chain 中, Promise 就会去找得到.

另外, promise chain 中, resolve 和 reject 是可以换来换去的. 在 catch 回调中也可以继续返回 resolve promise, 然后变回 succeed 进入下一个 .then 回调.

before and after

小心坑

上面提到,在 Promise 里面是可以使用 throw 的。

function doSomethingAsync(): Promise<void> {
  return new Promise((resolve, reject) => {
    throw new Error('test');
  });
}

上面这样是 ok 的,不适用 reject 也可以,但是下面这样就不行

function doSomethingAsync(): Promise<void> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      throw new Error('test');
    }, 3000);
  });
}

因为 throw 被 setTimeout wrap 起来了(注:不只是 setTimeout,只要 wrap 起来作用域跑掉就不可以了)。这种情况下就必须使用 reject 才行。

 

.finally

.then for resolve

.catch for reject

.finally for resolve or reject. 

就是说不管结果是 succeed 还是 failed 都执行 finally 的回调. 

这个跟 try catch finally 概念差不多.

 

Promise.resolve & Promise.reject

Promise.resolve 是一个方法, 它接受一个值. 返回一个 promise.

console.log('1');
Promise.resolve('value').then((value) => {
  console.log('3', value); // value
});
console.log('2');

通常会这样写有 2 个目的.

delay 操作

当想 delay 一个执行的时候, 通常会使用 setTimeout, 但是 setTimeout 默认是 4ms. 有没有一种方式就把执行押后到当前 stack 运行完呢? 

那就是用 Promise

console.log('1');
setTimeout(() => {
  console.log('4');
});
Promise.resolve().then(() => {
  console.log('3');
});
console.log('2');

同步 / 异步函数

有些函数依据情况来决定是否是异步的. 但即使同步的情况下它依然得返回 Promise, 因为返回接口必须是一样的呀

比如上面的 delay. 当 0 的时候, 不希望用 setTimeout 的话可以这样写.

function delayAsync(delayTime: number): Promise<void> {
  if (delayTime === 0) {
    return Promise.resolve(); // 直接 resolve
  } else {
    const promise = new Promise<void>((resolve, _reject) => {
      setTimeout(() => {
        resolve();
      }, delayTime);
    });
    return promise;
  }
}

它和下面这个写法是等价的

return new Promise(resolve => resolve());

Promise.reject() 和 Promise.resolve() 概念是一样, 我就不说了.

 

Promise all, race, any, allSettled

Promise.all

Promise.all([delayAsync(1000), delayAsync(2000)]).then(([v1, v2]) => {
  console.log('done', [v1, v2]);
});

当所有的 delayAsync 都 resolve 之后触发回调, 并且拿到所有的返回值.

注意: 所以 Promise.all 的成员会依照顺序去执行, 但不会等待前者 resolve. 像上面的例子, delayAsync(2000) 不会等 delayAsync(1000) 1 秒后才执行 (不会等哦). 

当遇到 reject

只要其中一个 reject, catch 马上会被执行, Promise 不会等待其它 all 的成员.

Promise.all([
  delayAsync(1000),
  delayAsync(2000),
  Promise.reject('c'),
  Promise.reject('d'),
])
  .then(([v1, v2]) => {
    console.log('done', [v1, v2]);
  })
  .catch((value) => {
    console.log('failed', value); // value only contain 'c' 因为其中一个 reject 后, catch 马上就执行了, Promise 不会等其成员了
  });

Promise.race

.race 和 .all 的区别是, all 会等到所有成员 succedd 才返回所有值, 而 race 则是第一个 succeed 后直接返回它的 值. 其它的就不等了.

当遇到 reject 的情况就和 .all 一样. 其中一个 reject 立马执行 catch, 不等其它成员.

Promise.any (es2021)

这个方法很新哦.

它和 race 有点像, 主要的区别是面对 reject 的时候.

any 的意思是只要其中一个 succeed 就 ok. 所以当出现 reject 的时候, 它不会像 .all 或者 .race 那样立马去 catch. 只有当全部成员都 reject 才会进入 catch.

比如说, 3 个成员, 第一个 reject, 无所谓, 继续等第二个, 假设等 2 个 succeed resolve, 那么就直接进入 .then 的回调. 返回成员 2 resolve 的值, 不会等待成员 3.

Promise.allSettled (es2020)

allSettled 顾名思义, 就是等所以的 promise 执行完. 它和 .all 不同的地方是, 它不管成员是 succeed 还是 failed.

只要全部有结果以后就会触发 .then 回调, 然后把状态和返回值传进去

Promise.allSettled([delayAsync(1000).then(() => Promise.resolve('value1')), delayAsync(3000).then(() => Promise.reject())]).then(v => {
  console.log('v', v);
}).catch((e) => {
  console.log('e', e); // never run, even all failed
});

无论怎样 catch 都不会被执行哦, 所以不要写 catch.

.then 回调接受的参数, 表示了每个成员的结果.

 

请继续看下一篇 

JavaScript – 用 Generator 运行异步函数 & await async

 

posted @ 2022-05-22 11:52  兴杰  阅读(216)  评论(0编辑  收藏  举报