Javascript
异步编程前后经历了四个阶段,分别是Callback
阶段,Promise
阶段,Generator
阶段和Async/Await
阶段。Callback
很快就被发现存在回调地狱和控制权问题,Promise
就是在这个时间出现,用以解决这些问题,Promise
并不是一个新事务,而是按照一个规范实现的类,这个规范有不少,如 Promise/A
,Promise/B
,Promise/D
以及 Promise/A
的升级版 Promise/A+
,最终 ES6 中采用了 Promise/A+ 规范。后来出现的Generator
函数以及Async
函数也是以Promise
为基础的进一步封装,可见Promise
在异步编程中的重要性。 前端
关于Promise
的资料已经不少,但每一个人理解都不同,不一样的思路也会有不同的收获。这篇文章会着重写一下Promise
的实现以及笔者在平常使用过程当中的一些心得体会。node
Promise/A+规范主要分为术语、要求和注意事项三个部分,咱们重点看一下第二部分也就是要求部分,以笔者的理解大概说明一下,具体细节参照完整版Promise/A+标准。git
一、
Promise
有三种状态pending
,fulfilled
和rejected
。(为了一致性,此文章称fulfilled
状态为resolved
状态)github
- 状态转换只能是
pending
到resolved
或者pending
到rejected
;- 状态一旦转换完成,不能再次转换。
二、
Promise
拥有一个then
方法,用以处理resolved
或rejected
状态下的值。面试
then
方法接收两个参数onFulfilled
和onRejected
,这两个参数变量类型是函数,若是不是函数将会被忽略,而且这两个参数都是可选的。then
方法必须返回一个新的promise
,记做promise2
,这也就保证了then
方法能够在同一个promise
上屡次调用。(ps:规范只要求返回promise
,并无明确要求返回一个新的promise
,这里为了跟ES6实现保持一致,咱们也返回一个新promise
)onResolved/onRejected
有返回值则把返回值定义为x
,并执行[[Resolve]](promise2, x);onResolved/onRejected
运行出错,则把promise2
设置为rejected
状态;onResolved/onRejected
不是函数,则须要把promise1
的状态传递下去。三、不一样的
promise
实现能够的交互。编程
- 规范中称这一步操做为
promise
解决过程,函数标示为[[Resolve]](promise, x),promise
为要返回的新promise
对象,x
为onResolved/onRejected
的返回值。若是x
有then
方法且看上去像一个promise
,咱们就把x当成一个promis
e的对象,即thenable
对象,这种状况下尝试让promise
接收x
的状态。若是x
不是thenable
对象,就用x
的值来执行promise
。[[Resolve]](promise, x)函数具体运行规则:segmentfault
- 若是
promise
和x
指向同一对象,以TypeError
为据因拒绝执行promise
;- 若是
x
为Promise
,则使promise
接受x
的状态;- 若是
x
为对象或者函数,取x.then
的值,若是取值时出现错误,则让promise
进入rejected
状态,若是then
不是函数,说明x
不是thenable
对象,直接以x
的值resolve
,若是then
存在而且为函数,则把x
做为then
函数的做用域this
调用,then
方法接收两个参数,resolvePromise
和rejectPromise
,若是resolvePromise
被执行,则以resolvePromise
的参数value
做为x
继续调用[[Resolve]](promise, value),直到x
不是对象或者函数,若是rejectPromise
被执行则让promise
进入rejected
状态;- 若是
x
不是对象或者函数,直接就用x
的值来执行promise
。
规范解读第1条,代码实现:数组
class Promise { // 定义Promise状态,初始值为pending status = 'pending'; // 状态转换时携带的值,由于在then方法中须要处理Promise成功或失败时的值,因此须要一个全局变量存储这个值 data = ''; // Promise构造函数,传入参数为一个可执行的函数 constructor(executor) { // resolve函数负责把状态转换为resolved function resolve(value) { this.status = 'resolved'; this.data = value; } // reject函数负责把状态转换为rejected function reject(reason) { this.status = 'rejected'; this.data = reason; } // 直接执行executor函数,参数为处理函数resolve, reject。由于executor执行过程有可能会出错,错误状况须要执行reject try { executor(resolve, reject); } catch(e) { reject(e) } } }
规范解读第2条,代码实现:promise
/** * 拥有一个then方法 * then方法提供:状态为resolved时的回调函数onResolved,状态为rejected时的回调函数onRejected * 返回一个新的Promise */ then(onResolved, onRejected) { // 设置then的默认参数,默认参数实现Promise的值的穿透 onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e }; onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e }; let promise2; promise2 = new Promise((resolve, reject) => { // 若是状态为resolved,则执行onResolved if (this.status === 'resolved') { try { // onResolved/onRejected有返回值则把返回值定义为x const x = onResolved(this.data); // 执行[[Resolve]](promise2, x) resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } } // 若是状态为rejected,则执行onRejected if (this.status === 'rejected') { try { const x = onRejected(this.data); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } } }); return promise2; }
如今咱们就按照规范解读第2条,实现了上述代码,上述代码很明显是有问题的,问题以下浏览器
resolvePromise
未定义;then
方法执行的时候,promise
可能仍然处于pending
状态,由于executor
中可能存在异步操做(实际状况大部分为异步操做),这样就致使onResolved/onRejected
失去了执行时机;onResolved/onRejected
这两相函数须要异步调用(官方Promise
实现的回调函数老是异步调用的)。解决办法:
resolvePromise
函数;then
方法执行时若是promise
仍然处于pending
状态,则把处理函数进行储存,等resolve/rejec
t函数真正执行的的时候再调用。promise.then
属于微任务,这里咱们为了方便,用宏任务setTiemout
来代替实现异步,具体细节特别推荐这篇文章。好了,有了解决办法,咱们就把代码进一步完善:
class Promise { // 定义Promise状态变量,初始值为pending status = 'pending'; // 由于在then方法中须要处理Promise成功或失败时的值,因此须要一个全局变量存储这个值 data = ''; // Promise resolve时的回调函数集 onResolvedCallback = []; // Promise reject时的回调函数集 onRejectedCallback = []; // Promise构造函数,传入参数为一个可执行的函数 constructor(executor) { // resolve函数负责把状态转换为resolved function resolve(value) { this.status = 'resolved'; this.data = value; for (const func of this.onResolvedCallback) { func(this.data); } } // reject函数负责把状态转换为rejected function reject(reason) { this.status = 'rejected'; this.data = reason; for (const func of this.onRejectedCallback) { func(this.data); } } // 直接执行executor函数,参数为处理函数resolve, reject。由于executor执行过程有可能会出错,错误状况须要执行reject try { executor(resolve, reject); } catch(e) { reject(e) } } /** * 拥有一个then方法 * then方法提供:状态为resolved时的回调函数onResolved,状态为rejected时的回调函数onRejected * 返回一个新的Promise */ then(onResolved, onRejected) { // 设置then的默认参数,默认参数实现Promise的值的穿透 onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e }; onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e }; let promise2; promise2 = new Promise((resolve, reject) => { // 若是状态为resolved,则执行onResolved if (this.status === 'resolved') { setTimeout(() => { try { // onResolved/onRejected有返回值则把返回值定义为x const x = onResolved(this.data); // 执行[[Resolve]](promise2, x) this.resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } // 若是状态为rejected,则执行onRejected if (this.status === 'rejected') { setTimeout(() => { try { const x = onRejected(this.data); this.resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } // 若是状态为pending,则把处理函数进行存储 if (this.status = 'pending') { this.onResolvedCallback.push(() => { setTimeout(() => { try { const x = onResolved(this.data); this.resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallback.push(() => { setTimeout(() => { try { const x = onRejected(this.data); this.resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; } // [[Resolve]](promise2, x)函数 resolvePromise(promise2, x, resolve, reject) { } }
至此,规范中关于then
的部分就所有实现完毕了。
规范解读第3条,代码实现:
// [[Resolve]](promise2, x)函数 resolvePromise(promise2, x, resolve, reject) { let called = false; if (promise2 === x) { return reject(new TypeError('Chaining cycle detected for promise!')) } // 若是x仍然为Promise的状况 if (x instanceof Promise) { // 若是x的状态尚未肯定,那么它是有可能被一个thenable决定最终状态和值,因此须要继续调用resolvePromise if (x.status === 'pending') { x.then(function(value) { resolvePromise(promise2, value, resolve, reject) }, reject) } else { // 若是x状态已经肯定了,直接取它的状态 x.then(resolve, reject) } return } if (x !== null && (Object.prototype.toString(x) === '[object Object]' || Object.prototype.toString(x) === '[object Function]')) { try { // 由于x.then有多是一个getter,这种状况下屡次读取就有可能产生反作用,因此经过变量called进行控制 const then = x.then // then是函数,那就说明x是thenable,继续执行resolvePromise函数,直到x为普通值 if (typeof then === 'function') { then.call(x, (y) => { if (called) return; called = true; this.resolvePromise(promise2, y, resolve, reject); }, (r) => { if (called) return; called = true; reject(r); }) } else { // 若是then不是函数,那就说明x不是thenable,直接resolve x if (called) return ; called = true; resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { resolve(x); } }
这一步骤很是简单,只要按照规范转换成代码便可。
最后,完整的Promise
按照规范就实现完毕了,是的,规范里并无规定catch
、Promise.resolve
、Promise.reject
、Promise.all
等方法,接下来,咱们就看一看Promise
的这些经常使用方法。
catch
方法是对then
方法的封装,只用于接收reject(reason)
中的错误信息。由于在then
方法中onRejected
参数是可不传的,不传的状况下,错误信息会依次日后传递,直到有onRejected
函数接收为止,所以在写promise
链式调用的时候,then
方法不传onRejected
函数,只须要在最末尾加一个catch()
就能够了,这样在该链条中的promise
发生的错误都会被最后的catch
捕获到。
catch(onRejected) { return this.then(null, onRejected); }
catch
在promise
链式调用的末尾调用,用于捕获链条中的错误信息,可是catch
方法内部也可能出现错误,因此有些promise
实现中增长了一个方法done
,done
至关于提供了一个不会出错的catch
方法,而且再也不返回一个promise
,通常用来结束一个promise
链。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
finally
方法用于不管是resolve
仍是reject
,finall
y的参数函数都会被执行。
finally(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; }); };
Promise.all
方法接收一个promise
数组,返回一个新promise2
,并发执行数组中的所有promise
,全部promise
状态都为resolved
时,promise2
状态为resolved
并返回所有promise
结果,结果顺序和promise
数组顺序一致。若是有一个promise
为rejected
状态,则整个promise2
进入rejected
状态。
static all(promiseList) { return new Promise((resolve, reject) => { const result = []; let i = 0; for (const p of promiseList) { p.then(value => { result[i] = value; if (result.length === promiseList.length) { resolve(result); } }, reject); i++; } }); }
Promise.race
方法接收一个promise
数组, 返回一个新promise2
,顺序执行数组中的promise
,有一个promise
状态肯定,promise2
状态即肯定,而且同这个promise
的状态一致。
static race(promiseList) { return new Promise((resolve, reject) => { for (const p of promiseList) { p.then((value) => { resolve(value); }, reject); } }); }
Promise.resolve
用来生成一个rejected
完成态的promise
,Promise.reject
用来生成一个rejected
失败态的promise
。
static resolve(value) { let promise; promise = new Promise((resolve, reject) => { this.resolvePromise(promise, value, resolve, reject); }); return promise; } static reject(reason) { return new Promise((resolve, reject) => { reject(reason); }); }
经常使用的方法基本就这些,Promise
还有不少扩展方法,这里就不一一展现,基本上都是对then
方法的进一步封装,只要你的then
方法没有问题,其余方法就均可以依赖then
方法实现。
面试相关问题,笔者只说一下我司这几年的状况,并不能表明所有状况,参考便可。Promise
是我司前端开发职位,nodejs
开发职位,全栈开发职位,必问的一个知识点,主要问题会分布在Promise
介绍、基础使用方法以及深层次的理解三个方面,问题通常在3-5个,根据面试者回答状况会适当增减。
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。有了Promise
对象,就能够将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操做更加容易。
(固然了也能够简单介绍promise
状态,有什么方法,callback
存在什么问题等等,这个问题是比较开放的)
这个答案不是固定的,能够参考最简实现 Promise,支持异步链式调用
onResolved/onRejected
函数异步调用,错误捕获合理等亮点。JS
中分为两种任务类型:macrotask
和microtask
,其中macrotask
包含:主代码块,setTimeout
,setInterval
,setImmediate
等(setImmediate
规定:在下一次Event Loop
(宏任务)时触发);microtask
包含:Promise
,process.nextTick
等(在node
环境下,process.nextTick
的优先级高于Promise
)Event Loop
中执行一个macrotask
任务(栈中没有就从事件队列中获取)执行过程当中若是遇到microtask
任务,就将它添加到微任务的任务队列中,macrotask
任务执行完毕后,当即执行当前微任务队列中的全部microtask
任务(依次执行),而后开始下一个macrotask
任务(从事件队列中获取)
浏览器运行机制可参考这篇文章
JS
运行机制的理解)Promise.deferred
、Promise.all
、Promise.race
、Promise.resolve
、Promise.reject
等
一、没法取消Promise
,一旦新建它就会当即执行,没法中途取消。
二、若是不设置回调函数,Promise
内部抛出的错误,不会反应到外部。
三、吞掉错误或异常,错误只能顺序处理,即使在Promise
链最后添加catch
方法,依然可能存在没法捕捉的错误(catch
内部可能会出现错误)
四、阅读代码不是一眼能够看懂,你只会看到一堆then
,必须本身在then
的回调函数里面理清逻辑。
(此题目,欢迎你们补充答案)
一、使用async
函数配合await
或者使用generator
函数配合yield
。
二、使用promise.then
经过for
循环或者Array.prototype.reduce
实现。
function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function (promise, task) { return promise.then(() => task).then(pushValue); }, Promise.resolve()); }
promise
的理解程度,又能考察编程逻辑,最后还有bind
和reduce
等方法的运用)async
函数和generator
函数的能够获得20%的分数,能够用promise.then
配合for
循环解决的能够获得60%的分数,配合Array.prototype.reduce
实现的能够获得最后的20%分数。在要中止的promise
链位置添加一个方法,返回一个永远不执行resolve
或者reject
的Promise
,那么这个promise
永远处于pending
状态,因此永远也不会向下执行then
或catch
了。这样咱们就中止了一个promise
链。
Promise.cancel = Promise.stop = function() { return new Promise(function(){}) }
(此题目,欢迎你们补充答案)
catch
在promise
链式调用的末尾调用,用于捕获链条中的错误信息,可是catch
方法内部也可能出现错误,因此有些promise
实现中增长了一个方法done
,done
至关于提供了一个不会出错的catch
方法,而且再也不返回一个promise
,通常用来结束一个promise
链。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
promise
的理解程度)done()
方法代码实现一、链式promise
要返回一个promise
,而不仅是构造一个promise
。
二、合理的使用Promise.all
和Promise.race
等方法。
三、在写promise
链式调用的时候,then
方法不传onRejected
函数,只须要在最末尾加一个catch()
就能够了,这样在该链条中的promise
发生的错误都会被最后的catch
捕获到。若是catch()
代码有出现错误的可能,须要在链式调用的末尾增长done()
函数。
(此题目,欢迎你们补充答案)
至此,我司关于Promise
的一些面试题目就列举完毕了,有些题目的答案是开放的,欢迎你们一块儿补充完善。总结起来,Promise
做为js面试必问部分仍是相对容易掌握并经过的。
Promise做为全部js开发者的必备技能,其实现思路值得全部人学习,经过这篇文章,但愿小伙伴们在之后编码过程当中能更加熟练、更加明白的使用Promise。
http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
http://www.javashuo.com/article/p-aqjjleyx-h.html