写在后面
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
的实现以及笔者在日常应用过程中的一些心得体会。
实现Promise
标准解读
Promise/A+标准次要分为术语、要求和注意事项三个局部,咱们重点看一下第二局部也就是要求局部,以笔者的了解大略阐明一下,具体细节参照完整版Promise/A+规范。
1、
Promise
有三种状态pending
,fulfilled
和rejected
。(为了一致性,此文章称fulfilled
状态为resolved
状态)
- 状态转换只能是
pending
到resolved
或者pending
到rejected
;- 状态一旦转换实现,不能再次转换。
2、
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
的状态传递上来。3、不同的
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)函数具体运行规定:
- 如果
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条,代码实现:
/** * 领有一个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
实现的回调函数总是异步调用的)。
解决办法:
- 依据标准解读第3条,定义并实现
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
的这些罕用办法。
Promise其余办法实现
1、catch办法
catch
办法是对then
办法的封装,只用于接管reject(reason)
中的错误信息。因为在then
办法中onRejected
参数是可不传的,不传的状况下,错误信息会顺次往后传递,直到有onRejected
函数接管为止,因而在写promise
链式调用的时候,then
办法不传onRejected
函数,只须要在最开端加一个catch()
就能够了,这样在该链条中的promise
产生的谬误都会被最初的catch
捕捉到。
catch(onRejected) { return this.then(null, onRejected); }
2、done办法
catch
在promise
链式调用的开端调用,用于捕捉链条中的错误信息,然而catch
办法外部也可能呈现谬误,所以有些promise
实现中减少了一个办法done
,done
相当于提供了一个不会出错的catch
办法,并且不再返回一个promise
,个别用来完结一个promise
链。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
3、finally办法
finally
办法用于无论是resolve
还是reject
,finall
y的参数函数都会被执行。
finally(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; }); };
4、Promise.all办法
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++; } }); }
5、Promise.race办法
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); } }); }
6、Promise.resolve办法/Promise.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面试相干
面试相干问题,笔者只说一下我司这几年的状况,并不能代表全副状况,参考即可。Promise
是我司前端开发职位,nodejs
开发职位,全栈开发职位,必问的一个知识点,次要问题会散布在Promise
介绍、根底应用办法以及深层次的了解三个方面,问题个别在3-5个,依据面试者答复状况会适当增减。
1、简略介绍下Promise。
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更正当和更弱小。它由社区最早提出和实现,ES6 将其写进了语言规范,对立了用法,原生提供了Promise
对象。有了Promise
对象,就能够将异步操作以同步操作的流程表达出来,防止了层层嵌套的回调函数。此外,Promise
对象提供对立的接口,使得管制异步操作更加容易。
(当然了也能够简略介绍promise
状态,有什么办法,callback
存在什么问题等等,这个问题是比拟凋谢的)
- 发问概率:99%
- 评分标准:人性化判断即可,此问题个别作为引入问题。
- 加分项:纯熟说出Promise具体解决了那些问题,存在什么毛病,利用方向等等。
2、实现一个简略的,反对异步链式调用的Promise类。
这个答案不是固定的,能够参考最简实现 Promise,反对异步链式调用
- 发问概率:50%(手撸代码题,因为这类题目比拟消耗工夫,一场面试并不会呈现很多,所以呈现频率不是很高,但却是必备常识)
- 加分项:基本功能实现的根底上有
onResolved/onRejected
函数异步调用,谬误捕捉正当等亮点。
3、Promise.then在Event Loop中的执行程序。(能够间接问,也能够出具体题目让面试者答复打印程序)
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
工作(从事件队列中获取)
浏览器运行机制可参考这篇文章
- 发问概率:75%(能够了解为4次面试中3次会问到,顺便能够考查面试者对
JS
运行机制的了解) - 加分项:扩大讲述浏览器运行机制。
4、论述Promise的一些静态方法。
Promise.deferred
、Promise.all
、Promise.race
、Promise.resolve
、Promise.reject
等
- 发问概率:25%(绝对根底的问题,个别在其余问题答复不是很现实的状况下发问,或者为了引出下一个题目而发问)
- 加分项:越多越好
5、Promise存在哪些毛病。
1、无奈勾销Promise
,一旦新建它就会立刻执行,无奈中途勾销。
2、如果不设置回调函数,Promise
外部抛出的谬误,不会反馈到内部。
3、吞掉谬误或异样,谬误只能程序解决,即使在Promise
链最初增加catch
办法,仍然可能存在无奈捕获的谬误(catch
外部可能会呈现谬误)
4、浏览代码不是一眼能够看懂,你只会看到一堆then
,必须本人在then
的回调函数外面理清逻辑。
- 发问概率:25%(此问题作为进步题目,呈现概率不高)
- 加分项:越多越正当越好(网上有很多说法,不一一佐证)
(此题目,欢送大家补充答案)
6、应用Promise进行程序(sequence)解决。
1、应用async
函数配合await
或者应用generator
函数配合yield
。
2、应用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());}
- 发问概率:90%(我司发问概率极高的题目,即能考查面试者对
promise
的了解水平,又能考查编程逻辑,最初还有bind
和reduce
等办法的使用) - 评分标准:说出任意解决办法即可,其中只能说出
async
函数和generator
函数的能够失去20%的分数,能够用promise.then
配合for
循环解决的能够失去60%的分数,配合Array.prototype.reduce
实现的能够失去最初的20%分数。
7、如何进行一个Promise链?
在要进行的promise
链地位增加一个办法,返回一个永远不执行resolve
或者reject
的Promise
,那么这个promise
永远处于pending
状态,所以永远也不会向下执行then
或catch
了。这样咱们就进行了一个promise
链。
Promise.cancel = Promise.stop = function() { return new Promise(function(){}) }
- 发问概率:50%(此问题次要考查面试者罗辑思维)
(此题目,欢送大家补充答案)
8、Promise链上返回的最初一个Promise出错了怎么办?
catch
在promise
链式调用的开端调用,用于捕捉链条中的错误信息,然而catch
办法外部也可能呈现谬误,所以有些promise
实现中减少了一个办法done
,done
相当于提供了一个不会出错的catch
办法,并且不再返回一个promise
,个别用来完结一个promise
链。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
- 发问概率:90%(同样作为出题率极高的一个题目,充沛考查面试者对
promise
的了解水平) - 加分项:给出具体的
done()
办法代码实现
9、Promise存在哪些应用技巧或者最佳实际?
1、链式promise
要返回一个promise
,而不只是结构一个promise
。
2、正当的应用Promise.all
和Promise.race
等办法。
3、在写promise
链式调用的时候,then
办法不传onRejected
函数,只须要在最开端加一个catch()
就能够了,这样在该链条中的promise
产生的谬误都会被最初的catch
捕捉到。如果catch()
代码有呈现谬误的可能,须要在链式调用的开端减少done()
函数。
- 发问概率:10%(出题概率极低的一个题目)
- 加分项:越多越好
(此题目,欢送大家补充答案)
至此,我司对于Promise
的一些面试题目就列举结束了,有些题目的答案是凋谢的,欢送大家一起补充欠缺。总结起来,Promise
作为js面试必问局部还是绝对容易把握并通过的。
总结
Promise作为所有js开发者的必备技能,其实现思路值得所有人学习,通过这篇文章,心愿小伙伴们在当前编码过程中能更加纯熟、更加明确的应用Promise。
参考链接:
http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
https://segmentfault.com/a/1190000016550260