Promise基本上现在不管是大厂还是小厂,promise 已经成为了面试必考知识点;关于 Promise,想必大家都又所了解,可是又掌握了多少,真正面试的时候,又能有多少把握呢?

常见面试题

首先,我们以常见的 Promise 面试题为切入点,我们看看面试官们都爱考什么:

  • Promise 的出现 解决了什么问题 ?

  • Promise 的基础特征 ?

  • Promise 常用的 API 有哪些 ?

  • 能不能手写一个符合 Promise/A+ 规范的 Promise?

  • then 的链式调用 & 值穿透特性原理

  • Promise 有什么缺陷,可以如何解决?

这几个问题由浅入深,我们一个一个来看:

Promise 出现的原因

Promise 出现以前,在我们处理多个异步请求嵌套时,代码往往是这样的。。。

1
2
3
4
5
6
7
8
9
let fs = require('fs')

fs.readFile('./name.txt','utf8',function(err,data){
fs.readFile(data, 'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data);
})
})
})

为了拿到回调的结果,我们必须一层一层的嵌套,可以说是相当恶心了。而且基本上我们还要对每次请求的结果进行一系列的处理,使得代码变的更加难以阅读和难以维护,这就是传说中臭名昭著的 回调地狱, 产生 回调地狱 的原因归结起来有两点:

  1. 嵌套调用,第一个函数的输出往往是第二个函数的输入;

  2. 处理多个异步请求并发,开发时往往需要同步请求最终的结果。

原因分析出来后,那么问题的解决思路就很清晰了:

  1. 消灭嵌套调用:通过 Promise 的链式调用可以解决 .then()

  2. 合并多个任务的请求结果:使用 Promise.all 获取合并多个任务的错误处理。

Promise 正是用一种更加友好的代码组织方式,解决了异步嵌套的问题。

我们来看看上面的例子用 Promise 实现是什么样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let fs = require('fs')

function read(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
resolve(data);
})
})
}

read('./name.txt').then((data)=>{
return read(data)
}).then((data)=>{
return read(data)
}).then((data)=>{
console.log(data);
},err=>{
console.log(err);
})

臃肿的嵌套变得线性多了有木有?没错,他就是我们的异步神器 Promise

让我们再次回归刚才的问题,Promise 为我们解决了什么问题?在传统的异步编程中,如果异步之间存在依赖关系,就需要通过层层嵌套回调的方式满足这种依赖,如果嵌套层数过多,可读性和可以维护性都会变得很差,产生所谓的 “回调地狱”,而 Promise 将嵌套调用改为链式调用,增加了可阅读性和可维护性。也就是说,Promise 解决的是异步编码风格的问题。

Promise 基本特征

promise 有三个状态:pendingfulfilled,or rejected

  1. new promise 时, 需要传递一个 executor() 执行器,执行器立即执行;

  2. executor 接受两个参数,分别是 resolvereject

  3. promise 的默认状态是 pending

  4. promise 只能从 pendingrejected, 或者从 pendingfulfilled,状态一旦确认,就不会再改变;

  5. promise 必须有一个 then 方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected

  6. then 方法的执行结果也会返回一个 Promise 对象。因此我们可以进行 then 的链式执行,这也是解决回调地狱的主要方式。

  7. 如果调用 then 时,promise 已经成功,则执行 onFulfilled,参数是 promisevalue

  8. 如果调用 then 时,promise 已经失败,那么执行 onRejected, 参数是 promisereason

  9. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调 onRejected

下面就通过例子进一步讲解。

1
2
3
4
5
6
7
8
9
// 构建 Promise
var promise = new Promise(function (resolve, reject) {
if (/* 异步操作成功 */) {
resolve(data);
} else {
/* 异步操作失败 */
reject(error);
}
});
  • resolve 函数的作用:在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

  • reject 函数的作用:在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 实例生成以后,可以用 then 方法指定 resolved 状态和 reject 状态的回调函数。

1
2
3
4
5
6
7
promise.then(onFulfilled, onRejected);

promise.then(function(data) {
// do something when success
}, function(error) {
// do something when failure
});

then 方法中指定的回调函数,将在当前脚本所有同步任务执行完才会执行。如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var promise = new Promise(function(resolve, reject) {
// 构造函数内部 同步执行
console.log('before resolved');
resolve();
console.log('after resolved');
});

promise.then(function() {
// 异步执行
console.log('resolved');
});

console.log('outer');

-------output-------
before resolved
after resolved
outer
resolved

Promise 常用 API

  • Promise.resolve() 默认产生一个成功的 promise

    1
    2
    3
    4
    5
    static resolve(data){
    return new Promise((resolve,reject)=>{
    resolve(data);
    })
    }
  • Promise.reject() 默认产生一个失败的 promisePromise.reject 是直接将值变成错误结果。

    1
    2
    3
    4
    5
    static reject(reason){
    return new Promise((resolve,reject)=>{
    reject(reason);
    })
    }
  • Promise.prototype.catch() 用来捕获 promise 的异常,就相当于一个没有成功的 then

    1
    2
    3
    Promise.prototype.catch = function(errCallback){
    return this.then(null,errCallback)
    }
  • Promise.prototype.finally() finally 表示不是最终的意思,而是无论如何都会执行的意思。 如果返回一个 promise 会等待这个 promise 也执行完毕。如果返回的是成功的 promise,会采用上一次的结果;如果返回的是失败的 promise,会用这个失败的结果,传到 catch 中。

    1
    2
    3
    4
    5
    6
    7
    Promise.prototype.finally = function(callback) {
    return this.then((value) => {
    return Promise.resolve(callback()).then(() => value)
    },(reason) => {
    return Promise.resolve(callback()).then(() => {throw reason})
    })
    }
  • Promise.all() 是解决并发问题的,多个异步并发获取最终的结果(如果有一个失败则失败)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    Promise.all = function(values) {

    if (!Array.isArray(values)) {
    const type = typeof values;
    return new TypeError(`TypeError: ${type} ${values} is not iterable`)
    }

    return new Promise((resolve, reject) => {
    let resultArr = [];
    let orderIndex = 0;

    const processResultByKey = (value, index) => {
    resultArr[index] = value;
    if (++orderIndex === values.length) {
    resolve(resultArr)
    }
    }

    for (let i = 0; i < values.length; i++) {
    let value = values[i];
    if (value && typeof value.then === 'function') {
    value.then((value) => {
    processResultByKey(value, i);
    }, reject);
    } else {
    processResultByKey(value, i);
    }
    }

    });
    }

    let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('ok1');
    }, 1000);
    })

    let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject('ok2');
    }, 1000);
    })

    Promise.all([1,2,3,p1,p2]).then(data => {
    console.log('resolve', data);
    }, err => {
    console.log('reject', err);
    })
  • Promise.race() 用来处理多个请求,采用最快的(谁先完成用谁的)。

手写一个符合 Promise/A+ 规范的 Promise

依据 promise 的基本特征,我们试着勾勒下 Promise 的形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
constructor(executor) {
// 默认状态为 PENDING
this.status = PENDING;
// 存放成功状态的值,默认为 undefined
this.value = undefined;
// 存放失败状态的值,默认为 undefined
this.reason = undefined;

// 调用此方法就是成功
let resolve = (value) => {
// 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
}

// 调用此方法就是失败
let reject = (reason) => {
// 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
}

try {
// 立即执行,将 resolve 和 reject 函数传给使用者
executor(resolve,reject)
} catch (error) {
// 发生异常时执行失败逻辑
reject(error)
}
}

// 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}

if (this.status === REJECTED) {
onRejected(this.reason)
}
}
}

写完我们可以测试一下:

1
2
3
4
5
6
7
8
9
10
const promise = new Promise((resolve, reject) => {
resolve('成功');
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)

控制台输出:

1
"success 成功"

现在我们已经实现了一个基础版的 Promise,但是还不要高兴的太早噢,这里我们只处理了同步操作的 promise。如果在 executor()中传入一个异步操作的话呢,我们试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
// 传入一个异步操作
setTimeout(() => {
resolve('成功');
},1000);
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)

执行测试脚本后发现,promise 没有任何返回。

因为 promise 调用 then 方法时,当前的 promise 并没有成功,一直处于 pending 状态。所以如果当调用 then 方法时,当前状态是 pending,我们需要先将成功和失败的回调分别存放起来,在executor()的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。

结合这个思路,我们优化一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 存放成功的回调
this.onResolvedCallbacks = [];
// 存放失败的回调
this.onRejectedCallbacks= [];

let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 依次将对应的函数执行
this.onResolvedCallbacks.forEach(fn=>fn());
}
}

let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 依次将对应的函数执行
this.onRejectedCallbacks.forEach(fn=>fn());
}
}

try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}

then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}

if (this.status === REJECTED) {
onRejected(this.reason)
}

if (this.status === PENDING) {
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
});

// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
this.onRejectedCallbacks.push(()=> {
onRejected(this.reason);
})
}
}
}

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
},1000);
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)

控制台等待 1s 后输出:

1
"success 成功"

ok!大功告成,异步问题已经解决了!

熟悉设计模式的同学,应该意识到了这其实是一个 发布订阅模式,这种收集依赖 -> 触发通知 -> 取出依赖执行的方式,被广泛运用于发布订阅模式的实现。

then 的链式调用&值穿透特性

我们都知道,promise 的优势在于可以链式调用。在我们使用 Promise 的时候,当 then 函数中 return 了一个值,不管是什么值,我们都能在下一个 then 中获取到,这就是所谓的then 的链式调用。而且,当我们不在 then 中放入参数,例:promise.then().then(),那么其后面的 then 依旧可以得到之前 then 返回的值,这就是所谓的值的穿透。那具体如何实现呢?简单思考一下,如果每次调用 then 的时候,我们都重新创建一个 promise 对象,并把上一个 then 的返回结果传给这个新的 promise 的 then 方法,不就可以一直 then 下去了么?那我们来试着实现一下。这也是手写 Promise 源码的重中之重,所以,打起精神来,重头戏来咯!

有了上面的想法,我们再结合 Promise/A+ 规范梳理一下思路:

  1. then 的参数 onFulfilledonRejected 可以缺省,如果 onFulfilled 或者 onRejected 不是函数,将其忽略,且依旧可以在下面的 then 中获取到之前返回的值;

  2. promise 可以 then 多次,每次执行完 promise.then 方法后返回的都是一个 “新的promise”;

  3. 如果 then 的返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调中;

  4. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调中;

  5. 如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;

  6. 如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;

  7. 如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;

我们将代码补充完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

const resolvePromise = (promise2, x, resolve, reject) => {
// 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// Promise/A+ 2.3.3.3.3 只能调用一次
let called;
// 后续的条件要严格判断 保证代码能和别的库一起使用
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
// 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === 'function') {
// 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
if (called) return;
called = true;
// 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, r => {
// 只要失败就失败 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e)
}
} else {
// 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4
resolve(x)
}
}

class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks= [];

let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}

let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}

try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}

then(onFulfilled, onRejected) {
//解决 onFufilled,onRejected 没有传值的问题
//Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
//因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// 每次调用 then 都返回一个新的 promise Promise/A+ 2.2.7
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
//Promise/A+ 2.2.2
//Promise/A+ 2.2.4 --- setTimeout
setTimeout(() => {
try {
//Promise/A+ 2.2.7.1
let x = onFulfilled(this.value);
// x可能是一个proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
//Promise/A+ 2.2.7.2
reject(e)
}
}, 0);
}

if (this.status === REJECTED) {
//Promise/A+ 2.2.3
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
}

if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
});

this.onRejectedCallbacks.push(()=> {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);
});
}
});

return promise2;
}
}

测试一下:

1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) => {
reject('失败');
}).then().then().then(data=>{
console.log(data);
},err=>{
console.log('err',err);
})

控制台输出:

1
"失败 err"

至此,我们已经完成了 promise 最关键的部分:then 的链式调用和值的穿透。搞清楚了 then 的链式调用和值的穿透,你也就搞清楚了 Promise

Promise 有什么缺陷,可以如何解决?

Promise 虽然跳出了异步嵌套的怪圈,用链式表达更加清晰,但是我们也发现如果有大量的异步请求的时候,流程复杂的情况下,会发现充满了屏幕的 then,看起来非常吃力,而 Async/Await 的出现就是为了解决这种复杂的情况。

优缺点:

  1. 代码简洁,不用 then,避免了嵌套代码

  2. 错误处理,Async/Awaittry/catch 可以同时处理同步和异步错误,在下面的 promise 示例中,try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中。我们需要使用 .catch,这样错误处理代码非常冗余。并且,在我们的实际生产代码会更加复杂。

  3. 中间值不好操作

  4. 调试困难

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const makeRequest = () => {
try {
getJSON().then(result => {
// JSON.parse可能会出错
const data = JSON.parse(result)
console.log(data)
})
// 取消注释,处理异步代码的错误
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
}
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}

Async/Await 的增加,可以让接口按顺序异步获取数据,用更可读,可维护的方式处理回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function demo01() {
return 123;
}
demo01().then(val => {
console.log(val);// 123
});
// async相当于promise, awite相当于等到Promise.resolve()的回调,那resolve()解决了,reject()呢
// Promise.reject 的情况我们可以用 try catch 包裹一下
async function errorDemoSuper() {
try {
let result = await sleep(1000);
console.log(result);
} catch (err) {
console.log(err);
}
}

取个应用的案列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 主页面 校验 code
handleInputInviteCode() {
this.$store.dispatch('walletStore/verifyInviteCode', {
inviteCode: this.inviteCode
}).then(res => {
this.$emit('confirm')
}).catch(error => {
this.$emit('failure')
})
}

// store 中的 action 页面
verifyInviteCode({commit,state,dispatch}, {inviteCode, header, id}) {
return new Promise(async (resolve, reject) => {
try{
const res = await handleRequest.post(`/api/core/invite/activation/${inviteCode}`, {}, header)
if (res.status === 200 && res.data === true){
let form = await handleGetUserInfo(currentWallet)
await commit('changeWallet', form)
resolve(form)
return
}
reject(LANG_DATA[res.code] || res.code)
} catch (e){
reject(e.message || e.code || '激活失败,请检查激活码')
}
})
}