深度揭秘 Promise 微任务注册和执行过程

13,634 阅读12分钟
  • 五段代码深入剖析 Promise 的注册微任务和代码执行过程
  • 分析 Promise/A+ 与 webkit( chrome 和 safari 内核) 的 Promise 的实现差异
  • 巩固一下,出道题

Promise 大伙太熟悉了,不过这里不讲大伙都知道的表面简单知识,而是一起来深入剖析 Promise 的注册微任务和执行的完整过程。能正确的使用 Promise 且能做到知其然知其所以然~

我们通常学习 Promise 都是基于 Promises/A+ 的实现。但是我不得不告诉你,本文还将分析该 js 实现和 webkit 的 Promise 的实现差异。具体到代码运行上的差异。

本文整体思路采取 代码例子 + 剖析讲解 的方式来解读,做到人人能懂,人人能理解的目的。

毫不夸张,如果全部读懂本文,那么 Promise 的注册和执行过程都将所向披靡,深入你的骨髓,你就是 Promise 大神! ~~~~~~~

前言

本文已代码解读的方式来学习整个过程。这里提供了五段代码,如果你都能理解清楚,完全正确的说出 output 过程,那么厉害大牛如你,我在这里给你竖个大拇指,祝贺你对 Promise 的执行过程已经了如指掌。

当然可能你也未必真正了解核心,能正确的理解和解释这个过程,不妨看看题目的解释。
当然如果是和我一样的菜鸟,那么我们就一起来看看吧~

看答案前,先自己默默算下输出结果吧。

第一段代码

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
  .then(() => {
    console.log("外部第一个then");
    return new Promise((resolve, reject) => {
      console.log("内部promise");
      resolve();
    })
    .then(() => {
    console.log("内部第一个then");
    })
    .then(() => {
    console.log("内部第二个then");
    });
  })
  .then(() => {
    console.log("外部第二个then");
  });

这个输出还是比较简单的,外部第一个 new Promise 执行,执行完 resolve ,然后执行外部第一个 then 。外部第一个 then 方法里面 return 一个 Promise,这个 return ,代表 外部的第二个 then 的执行需要等待 return 之后的结果。当然会先执行完内部两个 then 之后,再执行 外部的第二个 then ,机智如你,完全正确。

output:
外部promise
外部第一个then
内部promise
内部第一个then
内部第二个then
外部第二个then

这是第二段代码

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
  .then(() => {
    console.log("外部第一个then");
    new Promise((resolve, reject) => {
      console.log("内部promise");
      resolve();
    })
      .then(() => {
        console.log("内部第一个then");
      })
      .then(() => {
        console.log("内部第二个then");
      });
  })
  .then(() => {
    console.log("外部第二个then");
  });

这段代码和第一段代码就相差一个 return ,然而结果确是不一样的。

那这个怎么理解呢?

我们核心要看 then 的回调函数是啥时候注册的,我们知道,事件机制是 “先注册先执行”,即数据结构中的 “队列” 的模式,first in first out。那么重点我们来看下他们谁先注册的。

外部的第二个 then 的注册,需要等待 外部的第一个 then 的同步代码执行完成。 当执行内部的 new Promise 的时候,然后碰到 resolve,resolve 执行完成,代表此时的该 Promise 状态已经扭转,之后开始内部的第一个 .then 的微任务的注册,此时同步执行完成。我们知道需要执行的动作是一个微任务,那么自然要先执行完同步任务,比如如下:

new Promise((resolve, reject) => {
    resolve();
    console.log(111);
})
.then(() => {
    consle.log(222);
})

这个代码显然优先输出执行 1111,再执行 222。 因为 222 的输出是微任务的执行,111 是同步执行。

同理回到上面的代码,内部的 resolve 之后,当然是先执行内部的 new Promise 的第一个 then 的注册,这个 new Promise 执行完成,立即同步执行了后面的 .then 的注册。

然而这个内部的第二个 then 是需要第一个 then 的的执行完成来决定的,而第一个 then 的回调是没有执行,仅仅只是执行了同步的 .then 方法的注册,所以会进入等待状态。

这个时候,外部的第一个 then 的同步操作已经完成了,然后开始注册外部的第二个 then,此时外部的同步任务也都完成了。同步操作完成之后,那么开始执行微任务,我们发现 内部的第一个 then 是优先于外部的第二个 then 的注册,所以会执行完内部的第一个 then 之后,然后注册内部的第二个 then ,然后执行外部的第二个 then ,然后再执行内部的第二个 then。

output:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then

我们发现,这里显然是执行完一个 then ,接着会注册该 then 之后的下一个 then,按照任务队列的原理,我们可以发现,内外 then 是交替执行,然后交替注册的。所以才会出现输出内外交替内容。
另外,我这里所说的 then 的注册,是指微任务队列的注册,并不是 .then 的方法的执行,实际上 .then 方法的执行,我们可以理解为仅仅只是初始化而已。如果看过源码的会知道,.then 的执行确实是同步的,内部是再开启一个 new Promise ,但是由于上一个状态未流转,该 then 并不会此时注册到微任务队列中,而是会等待上一个的执行完成,所以我们把 .then 没注册微任务就理解成尚没执行是没有问题的。

再看第三段代码

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
  .then(() => {
    console.log("外部第一个then");
    let p = new Promise((resolve, reject) => {
      console.log("内部promise");
      resolve();
    })
    p.then(() => {
        console.log("内部第一个then");
      })
    p.then(() => {
        console.log("内部第二个then");
      });
  })
  .then(() => {
    console.log("外部第二个then");
  });

这段代码的差异,就是内部的 Promise 的代码的写法变了,不再是链式调用。

这里怎么理解呢?

这里在执行内部的 new Promise 的 resolve 执行完成之后(扭转了该 Promise 的状态),new Promise 之后的两个同步 p.then 是两个执行代码语句,都是同步执行,自然是会同步注册完。

两种方式的最主要的区别是:

  • 链式调用的注册是前后依赖的 比如上面的外部的第二个 then 的注册,是需要外部的第一个的 then 的执行完成。
  • 变量定义的方式,注册都是同步的 比如这里的 p.then 和 var p = new Promise 都是同步执行的。

所以这里的代码执行就比较清晰了,内部都执行完成之后(因为都优先于外部的第二个 then 的注册),再执行外部的第二个 then :

output:
外部promise
外部第一个then
内部promise
内部第一个then
内部第二个then
外部第二个then

第四段代码

let p = new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
p.then(() => {
    console.log("外部第一个then");
    new Promise((resolve, reject) => {
      console.log("内部promise");
      resolve();
    })
      .then(() => {
        console.log("内部第一个then");
      })
      .then(() => {
        console.log("内部第二个then");
      });
  })
p.then(() => {
    console.log("外部第二个then");
  });

这段代码中,外部的注册采用了非链式调用的写法,根据上面的讲解,我们知道了外部代码的 p.then 是并列同步注册的。所以代码在内部的 new Promise 执行完,p.then 就都同步注册完了。

内部的第一个 then 注册之后,就开始执行外部的第二个 then 了(外部的第二个 then 和 外部的第一个 then 都是同步注册完了)。然后再依次执行内部的第一个 then ,内部的第二个 then。

output:
外部promise
外部第一个then
内部promise
外部第二个then
内部第一个then
内部第二个then

我相信,如果能看懂上面的四段代码之后,对 Promise 的执行和注册非常了解了。

如果还是不太懂,麻烦多看几遍,相信你一定能懂~~~~~~~~

核心思想:

Promise 的 then 的 注册微任务队列 和 执行 是分离的。
注册 : 是完全遵循 JS 和 Promise 的代码的执行过程。
执行 : 先 同步,再 微任务 ,再 宏观任务。

只有分开理解上述,才能真正理解它们的执行顺序~~~~~~~~~~~~~~~~

第五段代码

经过上面仔细深度文字解析之后,我相信你会豁然开朗了。
再来一道巩固的题目:

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
  .then(() => {
    console.log("外部第一个then");
    new Promise((resolve, reject) => {
      console.log("内部promise");
      resolve();
    })
      .then(() => {
        console.log("内部第一个then");
      })
      .then(() => {
        console.log("内部第二个then");
      });
    return new Promise((resolve, reject) => {
      console.log("内部promise2");
      resolve();
    })
      .then(() => {
        console.log("内部第一个then2");
      })
      .then(() => {
        console.log("内部第二个then2");
      });
  })
  .then(() => {
    console.log("外部第二个then");
  });

这段代码,其实就是结合了第一道题目和第二道题目综合而成。
外部的第二个 then ,依赖于内部的 return 的执行结果,所以会等待 return 执行完成。
内部的第一段 new Promise 变成和内部的第二段 new Promise 的交替输出了,理解方式和第二段代码一样。

output:
外部promise
外部第一个then
内部promise
内部promise2
内部第一个then
内部第一个then2
内部第二个then
内部第二个then2
外部第二个then

Promise/A+ 和 webkit 的 Promise 的实现差异

我们知道 ES6 的 Promise 是需要考虑向下兼容的,开发当中往往没有用系统内核的 Promise ,而是使用 npm install promise 来引入的。那么 promise 的 js 实现和浏览器的实现是完全一致的吗?

按照上面的四段代码的解析,我们理解到了,Promise 的 then 的执行,是依赖于上一个 then 的执行完成之后,即 resolve 状态之后,才开始注册到微任务队列中的。

我们一起看一道题目,这里的区别是 then 返回了一个 Promise.resolve();

new Promise((resolve, reject) => {
  console.log('外部promise');
  resolve();
})
  .then(() => {
    console.log('外部第一个then');
    new Promise((resolve, reject) => {
      console.log('内部promise');
      resolve();
    })
      .then(() => {
        console.log('内部第一个then');
        return Promise.resolve();
      })
      .then(() => {
        console.log('内部第二个then');
      })
  })
  .then(() => {
    console.log('外部第二个then');
  })
  .then(() => {
    console.log('外部第三个then');
  })

我们先忽略内部第一个 then 的 return ,按照上面所学习到的,正常理解,我们能得出依然是内外交替注册和运行。

output:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
外部第三个then

此题执行顺序图:

promise

上面我们是使用 Promise 的 js 实现的代码输出的结果。

然而你把这段代码放在 chrome/safari 上跑一下,发现结果不一样,如下是 webkit 内核跑出来的结果。

promise

这个是什么原因呢?
为啥多了一个 return Promise.resolve(),就把外层的 then 都执行完了呢?
要理解这个,我们还是要从注册和执行来区分理解。

在执行输出 “内部第一个 then ”之后,碰到 return Promise.resolve();我们就来分析这个 Promise.resolve();

Promise/A+ 的实现:

执行 return Promise.resolve() ,创建一个 Promise 实例,将 Promise 实例设置为 resolve 状态,这个 Promise.resolve() 是同步的,且该 Promise 已经完成了,所以他并不会影响到其他 then 的注册。所以上述我们分析是完全正确的。
如下是 Promise.resolve 的实现,我们发现,完全是同步的,所以不影响最终结果。

Promise.resolve = function (value) {
  if (value instanceof Promise) return value;
  if (value === null) return NULL;
  if (value === undefined) return UNDEFINED;
  if (value === true) return TRUE;
  if (value === false) return FALSE;
  if (value === 0) return ZERO;
  if (value === '') return EMPTYSTRING;
  if (typeof value === 'object' || typeof value === 'function') {
    try {
      var then = value.then;
      if (typeof then === 'function') {
        return new Promise(then.bind(value));
      }
    } catch (ex) {
      return new Promise(function (resolve, reject) {
        reject(ex);
      });
    }
  }
  return valuePromise(value);
};

Promise 的 浏览器(webkit)的实现:

执行 return Promise.resolve() ,创建一个 Promise 实例,执行 resolve ,此时将该 Promise 的 resolve 的 value(这里是undefined) 进入微任务队列,将该 Promise 的状态扭转为 resolve。然后接着执行了之前注册好的 "外部第二个then" ,然后注册 “外部第三个then” ,接着执行 “内部第一个then” 的 return 的 resolve 的这个 undefined value 的 Promise,执行完成之后,然后注册下一个then ,但是没有下一个 then 了,执行完成,整个 return 任务完成,本次同步任务也执行完成,接着执行注册的 “外部第三个then” ,执行完成之后,注册 “外部第四个then”,此时 ”内部第一个then“ 执行完成,注册 ”内部第二个then”,最后执行完“外部第四个then”,再执行 刚刚注册的“内部第二个then”.

源代码如下:

void Promise::Resolver::Resolve(Handle<Value> value) {
  i::Handle<i::JSObject> promise = Utils::OpenHandle(this);
  i::Isolate* isolate = promise->GetIsolate();
  LOG_API(isolate, "Promise::Resolver::Resolve");
  ENTER_V8(isolate);
  EXCEPTION_PREAMBLE(isolate);
  i::Handle<i::Object> argv[] = { promise, Utils::OpenHandle(*value) };
  has_pending_exception = i::Execution::Call(
      isolate,
      isolate->promise_resolve(),
      isolate->factory()->undefined_value(),
      arraysize(argv), argv,
      false).is_null();
  EXCEPTION_BAILOUT_CHECK(isolate, /* void */ ;);
}
PromiseResolve = function PromiseResolve(promise, x) {
    PromiseDone(promise, +1, x, promiseOnResolve)
}
function PromiseDone(promise, status, value, promiseQueue) {
    if (GET_PRIVATE(promise, promiseStatus) === 0) {
        PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue), status);
        PromiseSet(promise, status, value);
    }
}

有了上面的理解之后,如果外层再加一个 then ,那么也知道结果了,执行完刚刚注册的 “内部第二个then”,之后,开始执行注册的 “外部第五个then”。

promise

巩固一下

结合上面已经学会的 Promise 的执行顺序,你应该能答出如下这道题的答案了吧,如果还不会,可以考虑再仔细看一篇。

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
.then(() => {
    console.log("外部第一个then");
    new Promise((resolve, reject) => {
        console.log("内部promise");
        resolve();
    })
    .then(() => {
        console.log("内部第一个then");
    })
    .then(() => {
        console.log("内部第二个then");
    });
    return new Promise((resolve, reject) => {
        console.log("内部promise2");
        resolve();
    })
    .then(() => {
        console.log("内部第一个then2");
    })
    .then(() => {
        console.log("内部第二个then2");
    });
})
.then(() => {
    console.log("外部第二个then");
});

欢迎关注我的微信公众号:

follow-me