写在后面
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