2.3. 专栏: Promise只能进行异步操作?

在使用Promise.resolve(value) 等方法的时候,如果promise对象立刻就能进入resolve状态的话,那么你是不是觉得 .then 里面指定的方法就是同步调用的呢?

实际上, .then 中指定的方法调用是异步进行的。

  1. var promise = new Promise(function (resolve){
  2. console.log("inner promise"); // 1
  3. resolve(42);
  4. });
  5. promise.then(function(value){
  6. console.log(value); // 3
  7. });
  8. console.log("outer promise"); // 2

执行上面的代码会输出下面的log,从这些log我们清楚地知道了上面代码的执行顺序。

  1. inner promise // 1
  2. outer promise // 2
  3. 42 // 3

由于JavaScript代码会按照文件的从上到下的顺序执行,所以最开始 <1> 会执行,然后是 resolve(42); 被执行。这时候 promise 对象的已经变为确定状态,FulFilled被设置为了 42

下面的代码 promise.then 注册了 <3> 这个回调函数,这是本专栏的焦点问题。

由于 promise.then 执行的时候promise对象已经是确定状态,从程序上说对回调函数进行同步调用也是行得通的。

但是即使在调用 promise.then 注册回调函数的时候promise对象已经是确定的状态,Promise也会以异步的方式调用该回调函数,这是在Promise设计上的规定方针。

因此 <2> 会最先被调用,最后才会调用回调函数 <3>

为什么要对明明可以以同步方式进行调用的函数,非要使用异步的调用方式呢?

2.3.1. 同步调用和异步调用同时存在导致的混乱

其实在Promise之外也存在这个问题,这里我们以一般的使用情况来考虑此问题。

这个问题的本质是接收回调函数的函数,会根据具体的执行情况,可以选择是以同步还是异步的方式对回调函数进行调用。

下面我们以 onReady(fn) 为例进行说明,这个函数会接收一个回调函数进行处理。

mixed-onready.js

  1. function onReady(fn) {
  2. var readyState = document.readyState;
  3. if (readyState === 'interactive' || readyState === 'complete') {
  4. fn();
  5. } else {
  6. window.addEventListener('DOMContentLoaded', fn);
  7. }
  8. }
  9. onReady(function () {
  10. console.log('DOM fully loaded and parsed');
  11. });
  12. console.log('==Starting==');

mixed-onready.js会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。

如果在调用onReady之前DOM已经载入的话

对回调函数进行同步调用

如果在调用onReady之前DOM还没有载入的话

通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用

因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会不同。

为了解决这个问题,我们可以选择统一使用异步调用的方式。

async-onready.js

  1. function onReady(fn) {
  2. var readyState = document.readyState;
  3. if (readyState === 'interactive' || readyState === 'complete') {
  4. setTimeout(fn, 0);
  5. } else {
  6. window.addEventListener('DOMContentLoaded', fn);
  7. }
  8. }
  9. onReady(function () {
  10. console.log('DOM fully loaded and parsed');
  11. });
  12. console.log('==Starting==');

关于这个问题,在 Effective JavaScript第67项 不要对异步回调函数进行同步调用 中也有详细介绍。

  • 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。

  • 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。

  • 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。

  • 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout 等异步API。

Effective JavaScript
— David Herman

前面我们看到的 promise.then 也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式

最后,如果将上面的 onReady 函数用Promise重写的话,代码如下面所示。

onready-as-promise.js

  1. function onReadyPromise() {
  2. return new Promise(function (resolve, reject) {
  3. var readyState = document.readyState;
  4. if (readyState === 'interactive' || readyState === 'complete') {
  5. resolve();
  6. } else {
  7. window.addEventListener('DOMContentLoaded', resolve);
  8. }
  9. });
  10. }
  11. onReadyPromise().then(function () {
  12. console.log('DOM fully loaded and parsed');
  13. });
  14. console.log('==Starting==');

由于Promise保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout 来自己实现异步调用。