前情提要
Promise从入门到手写:juejin.cn/post/693968…
上一篇文章主要分为四个部分:
Promise
介绍Promise
特点Promise
使用Promise
手写
如果有需要得可以点击上面的链接,原本这篇文章我个人认为是比较完整的,但是经过同事,以及掘金评论区的小伙伴的反馈,我决定再对之前的文章进行一些补全。
本篇文章内容如下:
Promise补充
:包括Promise A+规范 | 用类的方式对Promise进行手写Generator
: Generator介绍与用法 | Generator与协程 | Generate自动执行(执行器)async/await
:async/await用法 | 对比Generator的改进项知识补充
:Thunk函数 | co模块介绍与源码私货
:结束语 | 属于我风格的经典文章结束闲扯淡
ok,那我们正式开始我们的旅程吧~
Promise补充
Promise A+规范
在前文我们已经实现过Promise了,后面我们还会用类的方式再写一次,但是我貌似并没有去介绍Promise A+规范官网。
大家其实只需要去关注上面链接的第二章。第二章分为三个部分:
Promise States
即 Promise的状态The then Method
即 then方法The Promise Resolution Procedure
即 Promise的处理过程
在这里我搜索到了一个Promise A+规范
的中英文对照翻译我也在这里做一个简单总结:
Promise
本质是一个状态机,且状态只能为以下三种:Pending
(等待)、Fulfilled
(成功)、Rejected
(失败),状态的变更是单向且不可逆的,只能从Pending
->Fulfilled
或Pending
->Rejected
。then
方法接收两个可选参数,分别对应状态改变时触发的回调。then
方法返回一个promise
。then
方法可以被同一个promise
调用多次。
当然,其实Promise A+规范
还有很多细节,我的总结其实很不完整,比如官网的第二章第三节,Promise处理过程我很难去简单总结,而且上面那个中英文对照翻译的很好,大家有时间建议看一看,其实网络上很多关于Promise A+规范
的介绍,也其实像我这般简单说说,具体细节还是要去看官网或者翻译中英文对照翻译的~
而且其实按照完整的
Promise A+
规范,其实之前的代码在一些细节上不能完全通过Promise A+
规范。
Promise手写(类的方式)
变动和之前文章变动不大。但是有一点要注意,像resolve
,reject
,all
,race
是类的静态方法,所以要加上static
关键字。
关于各个方法的写法,思路,在上一篇文章有完整的介绍,如果有不懂的,可以去看看之前的文章。相信可以解答大部分问题。
//Promise类的写法 class Promise { // 构造方法 constructor(executor) { //保存promise状态 this.promiseState = 'pending'; //保存promise结果 this.promiseResult = null; //用于保存异步回调函数列表 this.callbackList = []; const resolve = val => { // 状态只能修改一次 if (this.promiseState !== 'pending') return; // 1. 要去修改Promise对象的状态([[promiseState]]),置为成功fulfilled this.promiseState = 'fulfilled'; // 2. 要去修改Promise对象的状态([[promiseResult]]) this.promiseResult = val; setTimeout(() => { // 调用成功的回调【callbackList存起来的】 for (let callback of this.callbackList) { callback.onResolved(val); } }) } const reject = err => { // 状态只能修改一次 if (this.promiseState !== 'pending') return; // 1. 要去修改Promise对象的状态([[promiseState]]),置为失败rejected this.promiseState = 'rejected'; // 2. 要去修改Promise对象的状态([[promiseResult]]) this.promiseResult = err; setTimeout(() => { // 调用失败的回调【callbackList存起来的】 for (let callback of this.callbackList) { callback.onRejected(err); } }) } // 为什么要加try catch 是因为,throw err也相当于调用reject了【前面说过没看过的去补课】 try { /* * 同步执行执行器函数 * 执行器函数接收两个参数,一个是resolve,一个是reject */ executor(resolve, reject); } catch (err) { reject(err); } } // then方法 then(onResolved, onRejected) { const self = this; //处理异常穿透 并且为onResolved,onRejected设置默认值。因为这两个参数可以都不传 if (typeof onRejected !== 'function') { onRejected = err => { throw err; } } if (typeof onResolved !== 'function') { onResolved = val => val; } // then方法会返回Promise return new Promise((resolve, reject) => { // 对返回值的处理进行封装 const handleCallback = (callback) => { // 如果回调函数中抛出错误,则reject try { // 需要依据回调的返回结果确定then方法的返回值 // 现在的this会指向return的promise对象,所以使用self const res = callback(self.promiseResult); if (res instanceof Promise) { //如果回调返回结果是个Promise res.then(val => { resolve(val); }, err => { reject(err); }) } else { // 返回结果不是Promise resolve(res); } } catch (err) { reject(err); } } //调用回调函数 if (this.promiseState === 'fulfilled') { setTimeout(() => { handleCallback(onResolved); }) } if (this.promiseState === 'rejected') { setTimeout(() => { handleCallback(onRejected); }) } /* * 如果是pending状态,则异步任务,在改变状态的时候去调用回调函数 * 所以要保存回调函数 * 因为promise实例阔以指定多个回调,于是采用数组 */ if (this.promiseState === 'pending') { this.callbackList.push({ onResolved: () => { handleCallback(onResolved); }, onRejected: () => { handleCallback(onRejected); } }) } }) } // catch方法 catch(onRejected) { // 我们可以直接使用then方法实现 return this.then(undefined, onRejected); } // resolve方法 static resolve(val) { //返回值的情况在前文说过,可以在 Promise的使用一章找到 return new Promise((resolve, reject) => { if (val instanceof Promise) { val.then(val => { resolve(val); }, err => { reject(err); }); } else { resolve(value); } }) } // reject方法 static reject(err) { //返回值的情况在前文说过,可以在 Promise的使用一章找到 return new Promise((resolve, reject) => { reject(err); }) } // all方法 static all(promiseList) { let count = 0; let res = []; const length = promiseList.length; return new Promise((resolve, reject) => { for (let i = 0; i < length; i++) { promiseList[i].then(val => { count++; res[i] = val; if (count === length) { resolve(res); } }, err => { reject(err); }); } }) } // race方法 static race(promiseList) { const length = promiseList.length; //谁先完成谁就决定结果! return new Promise((resolve, reject) => { for (let i = 0; i < length; i++) { promiseList[i].then(val => { resolve(val); }, err => { reject(err); }); } }) } }
Generator
引子
其实我们写多了Promise
,其实我们也会发现一个问题,Promise
虽然解决了回调地狱的问题,但是我们作为开发者,比如我这种有着半年开发经验的资深前端工程师(手动狗头),其实也遇到了很多比较复杂的情况,导致代码中有很多难读的then方法引起的嵌套结构。
吐槽:当然,虽然很多导致难于理解的原因是历史因素或者开发者的随意导致的。当时从结果出发,我还是不太喜欢会出现嵌套的
Promise.prototype.then()
的异步处理方式。
但是我们还是期望一种技术可以解决then导致的嵌套结构。把异步代码像同步代码一样处理,于是我在开发中最常使用的async/await
在ES7引入了,async/await
提供了以同步代码实现异步任务的能力,使得写异步代码像同步代码一样清晰。
但是,在我们研究async/await
之前,我们需要去研究一下Generator
,因为async
是Generator
的语法糖。
Generator简单介绍
网上关于Generator的用法有很多,大家可以看更详细的文档,我不会在此详细介绍用法~抱歉啦
Generator
是ECMAScript 2015
即ES6
提出的规范。
首先遇事不决,先说用法:
function*
这种声明方式(function关键字后跟一个星号)会定义一个生成器函数(generator function
),它返回一个Generator
对象。
我们直接对MDN上面的例子加以修改:
function* generator(i) { console.log('第一次') yield i; console.log('第二次') yield i + 10; console.log('第三次') } const c = generator(1);
为了比较清晰,我直接把执行过程的打印结果截图:
我们仔细观察这个图片,我们不难发现几件事情:
- 首先就是
function*
这种声明方式定义的生成器函数返回一个对象。这个对象我们直观的发现里面有一个叫做[[GeneratorState]]
的属性,里面代表着这个生成器对象的状态。 Generator.prototype.next()
方法会返回一个由yield
表达式生成的值,并使得函数执行到该yield
的位置并暂停。该值是一个对象,有value
和done
两个属性,value
是返回结果,即yield
表达式后面那个表达式的值,done
代表生成器对象是否已经油尽灯枯,即生成器对象是否遍历结束。[伙伴们,油尽灯枯这个词是不是用的很妙~]
到这里,其实小伙伴们一定会好奇,js是单线程执行的,生成器是如何让函数暂停,之后又恢复的捏,那我们就不得不提一下协程
了。
Generator与协程
- 什么是协程?
协程是一种比线程更加轻量级的存在,协程处在线程的环境中,一个线程可以存在多个协程,可以将协程理解为线程中的一个个任务。不像进程和线程,协程并不受操作系统的管理,而是被具体的应用程序代码所控制。
- 生成器函数是如何让函数暂停,之后又恢复的呢?
一个线程一次只能执行一个协程。比如当前执行 A 协程,另外还有一个 B 协程,如果想要执行 B 的任务,就必须在 A 协程中将JS 线程的控制权转交给 B协程,那么现在 B 执行,A 就相当于处于暂停的状态。
总之,大概的流程就是这样:
协程A
开始执行协程A
暂停,协程B
开始执行协程B
执行完,交给协程C
执行协程C
执行完,执行权交回给协程A
继续执行
Generate自动执行(执行器)
ok,那我们现在去做一件事,我们利用generator
把异步的任务封装成一个方法,之后自动的去按顺序(即类似同步方式)去进行调用。
function* asyncFun() { let resA = yield PromiseA //promise对象 let resB = yield PromiseB //promise对象 let resC = yield PromiseC //promise对象 }
如上面代码所示,我们把我们要依次执行的异步任务按照同步任务的写法写了出来,现在我们要去写一个方法,让他们自动按顺序执行~
function asyncToSyncAndRun(gen){ var g = gen(); //此时g为生成器对象 function next(data){ var result = g.next(data); //注意:前面说过 result的结构,result是一个对象,里面的value对应yield后表达式的返回值 //所以result.value是一个Promise对象 if (result.done) return result.value;//如果遍历结束,return //未遍历结束,就把下一个next执行放在现在的Promise对象的回调中去 result.value.then(function(data){ next(data); }); } next();//触发next方法~ } //自动执行 asyncToSyncAndRun(asyncFun)
于是,通过上面的工具方法asyncToSyncAndRun
,并借助generator
,我们可以把异步任务已同步任务的写法串联起来~