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);
}
如上述代码所示,关键部分是 _asyncToGenerator 和 asyncGeneratorStep 这两段操作。
前者包裹住了 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类
浏览器工作原理与实践: 浏览器中的页面循环系统 @李兵
How are generators transpiled to ES5
【推荐】Ben Nadel: Using ES6 Generators And Yield To Implement Asynchronous Workflows In JavaScript