Skip to content

How to make a Promise and so on

下文添添补补后,分三个大块~

  • ① 如何实现一个 promise.
  • ② async/await 的本质
  • ③ babel 对于 async/await 的处理

如何实现一个Promise

“First, look into the mirror.”

“Second, promise yourself that you would lose 10 pounds before this Summer.”

哈哈哈开玩笑啦!我一直在想Promise是怎么实现的呢,尤其是其链式调用。执行.then的时候,是返回this,再往thenFunction里去push一系列要处理的函数;还是返回一个new Promise就好了呢~带着这个疑问,我…仍然写不出来…哭TAT。

不过还好!被我发现了别人腻害的剖析!置顶感谢@剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类 。按照他的流程过了一遍,大概理解Promise的神奇功效啦!故特此记录!

首先回答一下我的主要矛盾:.then()返回了什么?

关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(详情)对象,所以在我们的实现中,也让then返回一个新的Promise对象。

另外,原文中还提到,”每个Promise对象都可以在其上多次调用then方法,而每次调用then返回的Promise的状态取决于那一次调用then时传入参数的返回值,所以then不能返回this,因为then每次返回的Promise的结果都有可能不同。” 恩,所以.then()返回的必定是一个new Promise。

其次还要铭记在脑海中的是Promise的初始状态为pending,它可以由此状态转换为fulfilled/resolved或者rejected,一旦状态确定,就不可以再次转换为其它状态。这是Promise的标准所称的状态,我开始觉得这只是并没有什么用的说辞而已,实际上这句话是应该要体现在代码里的。它给Promise起到了一个标记兼指挥的作用。

于是,按照原文的说明,蹭蹭蹭的就可以造出一个简要Promise啦!以下是只带resolve版以及我随便取了个Poppy名字(꒪⌓꒪)的custom Promise:

按上述demo,在二阶then并存在异步的情况下执行步骤如下:

① 定义阶段

执行promise1.then=>在promise1.then()中,定义promise2,将promise2要执行的内容暂存=>执行promise2.then=>在promise2.then()中,定义promise3,将promise3要执行的内容暂存

② 执行阶段

promise1处理完毕,执行promise1中暂存的promise2=>promise2处理完毕,执行promise2中暂存的promise3

所以,其实.then造成暂停的错觉,归根到底还是由回调函数来完成的。Promise会先将回调函数暂存起来,等回调函数之前的步骤全部处理完毕后再call它。.then中返回new Promise的好处也很明显,一个then相当于一个promise,在then中实现了前一个promise对后一个promise的控制。

所以我弄清楚了以后还蛮高兴的,不过也差不多止步于此了…原文中还额外探讨了以下几项:

※值的穿透:这点很简单,没返回值将入参后抛即可。

※如何停止一个promise(即在之前出现错误时阻止后面一系列then的执行)。原文给了个很有创意的答案是引入一个永不去resolve的promise~

Promise.cancel = Promise.stop = function() {  
  return new Promise(function(){})  
}  

new Promise(function(resolve, reject) {  
  resolve(42)  
}).then(function(value) {  
   // "Big ERROR!!!"  
   return Promise.stop()  
}).catch().then() 

※promise链上返回的最后一个promise错了怎么办?promise里的所有错误都被catch了所以并不会被爆出来呀。原文:我们可以在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。

以上。最后再分享一个生活小窍门之《巧用Promise设置fetch的timeout》:


function request(data, config) {  

  const fetchPromise = fetch(...).then(r => r.json).catch(err => throw new Error(err))  
  const abortPromise = new Promise((resolve, reject) => setTimeout(reject, 30000, 'timeout'))  

  return Promise.race(\[fetchPromise, abortPromise\])  
}  

try {  
   const res = await request(data, config)  
} catch (err) {  
    // timeout啦  
}  

update: async/await的背后

async/await不仅仅只是promise的语法糖那么简单喔。

async/await的背后是promise + generator

function\* foo(){  
  let res1 = yield fetch('...');  
  let res2 = yield fetch('...');  
}  

const gen = foo();  
gen.next().value.then(res1 =>{  
  return gen.next().value;  
}).then(res => console.log('res2'))  

其实下半部分执行生成器的代码往往会被封装成一个函数,名为“执行器”,比如co。

generator的背后是协程

协程有两大特点:

  • 运转在线程上。一个线程可以有多个协程,但同时只能有一个owner执行权。
  • 协程的调用不由操作系统、而由用户控制。

站在协程的角度拆解如上代码,可以描述为:

  • 通过调用foo生成了一个协程,命名为 gen。
  • 在父线程中调用 gen.next,把主线程的控制权交给 gen 协程。
  • gen 协程执行 yield,暂停 gen 协程的执行,并返回第一个请求结果给父协程。
  • 父协程通过 promise.then,等待结果返回。
  • 结果返回后父协程再通过 gen.next,将控制权交给 gen 协程继续执行下个请求。

再来看async/await

所以可以这么理解它:

async == 一个通过异步执行隐式返回 Promise 作为结果的函数,并运行在协程里

await == 隐性创建了一个 Promise 对象。 => 在创建该对象后,JavaScript 引擎会将对象内的任务提交给微任务队列,并将该对象返回给父协程。=> 父协程调用 Promise.then,注册 将主线程控制权交给 async 函数运行的协程,并传递 value 的回调事件,等待 await 后的异步语句执行完毕,触发该回调。=> async 函数的协程重新拿回控制权,继续执行后续语句。

uupdate: how does babel compile async/await?

那么如何写 async/await 的polyfill呢?我们可以通过 babel-playground 看看babel是怎么做的。

这是很简单的源代码:

 async function lalaland(){  
  const a = await fetch('//dorami')  
  console.log(a)  
} 

async/await = Promise + Generator. 而 Generator 在es2016中才得到支持。所以换言之,我们可以把编写 polyfill 的过程分为 async/await to es2016 和 async/await to es2015 两个阶段。

to es2016

在这个阶段,还可以使用Generator. 以下是babel吐出的代码:

  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    // ③即 调用gen.next()
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    // ④.1 如果gen执行完毕 流程结束
    resolve(value);
  } else {
    // ④.2 否则等这一次yield的promise执行完毕后,在then回调里继续执行_next 即再一次调用asyncGeneratorStep,前往下一次。
    // 注意:由于_next函数已经默认给asyncGeneratorStep传入了唯一的resolve入参,所以这里不用再手动传入resolve了
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    // ①返回了一个promise
    return new Promise(function (resolve, reject) {
      // 迭代器函数即是绑定了 await 函数入参的 await 主体部分
      var gen = fn.apply(self, args);

      // 对于执行generator的包装
      function _next(value) {
        // 这里传入的resolve会终结①这个promise
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      // ②首次执行自动调用asyncGeneratorStep
      _next(undefined);
    });
  };
}

function lalaland() {
  return _lalaland.apply(this, arguments);
}

function _lalaland() {
  // await 函数的主体部分被包成了Generator
  _lalaland = _asyncToGenerator(function* () {
    const a = yield fetch('//dorami')
    console.log(a);
  });
  return _lalaland.apply(this, arguments);
}

如上述代码所示,关键部分是 _asyncToGeneratorasyncGeneratorStep 这两段操作。

前者包裹住了 async 函数里的整个执行体,并且返回了 Promise。

后者则实现了对于执行体的 迭代器.next 的轮番调用,在调用结束后 resolve 了整个流程。

换句话说,这也是对 async/await 现代实现的代码佐证。

to es2015

在这个版本里,Generator 还未受支持,所以关键问题变成了「如何polyfill一个Genearator」?

这次 babel 生成代码中的,异步主体部分依然没变,只是 _lalaland 的主体发生了改变。

原:

function _lalaland() {
  // await 函数的主体部分被包成了Generator
  _lalaland = _asyncToGenerator(function* () {
    const a = yield fetch('//dorami')
    console.log(a);
  });
  return _lalaland.apply(this, arguments);
}

现:

function _lalaland() {
  _lalaland = _asyncToGenerator(
    /*#__PURE__*/
    regeneratorRuntime.mark(function _callee() {
      var a;
      return regeneratorRuntime.wrap(function _callee$(_context) {
        while (1) {
          switch (_context.prev = _context.next) {
            case 0:
              // 指向接下来要执行到 case 2去啦
              _context.next = 2;
              return fetch('//dorami');

            case 2:
              // 将变量 a 保存起来
              a = _context.sent;
              console.log(a);

            case 3:
            case "end":
              return _context.stop();
          }
        }
      }, _callee);
    }));
  return _lalaland.apply(this, arguments);
}

这里用到了 regeneratorRuntime, 可以安装 babel-polyfill 导入所有 runtime 函数或者仅安装 @babel/plugin-transform-regenerator

在没看 regeneratorRuntime 之前,先从结果猜一猜它背后做了哪些事情?

  • regeneratorRuntime.mark: 第一个入参是 generator 函数被转换过的内容,第二个入参是generator函数的函数名。其出参肯定是一个符合 gen 生成器标准的对象,即拥有 next ..这些属性。
  • regeneratorRuntime.wrap: 入参 _callee$ 函数接受 _context 入参。浏览全文,发觉 _context 起到关键指引作用,至少拥有 _context.next (用来存储指定代码走向) 和 _context.sent (用来存储当前 yield 返回值) 这两个属性,以及 _context.stop() (用来停止执行) 这个方法。

然后抱着这样的猜想,可以再去看源代码了。

(内心os:只是没想到源代码有洋洋洒洒500行阿…那就先未完待续了 TAT)

to be continued..

Refs

剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

让fetch也可以timeout

浏览器工作原理与实践: 浏览器中的页面循环系统 @李兵

How are generators transpiled to ES5

【推荐】Ben Nadel: Using ES6 Generators And Yield To Implement Asynchronous Workflows In JavaScript