异步编程简介- promise
异步编程简介- promise
异步编程历史
前端异步编程经历callback、promise、generate、async/await几个阶段。
目前在简单场景使用回调,步骤比较多的场景使用promise和async/await,generate昙花一现,由于其api不易理解并且不易于使用而很少使用。
Promise
简介
promise目的:异步编程解决回调地狱,让程序开发者编写的异步代码具有更好的可读性。
promise规范规定了一种异步编程解决方案的API。规范规定了promise对象的状态和then方法。
promise是这种异步编程的解决方案的具体实现。
状态特性用来让使用promise的用户可以及时通知promise任务执行结果。
then特性让使用promise的用户可以控制执行完一个任务后执行下一个任务。
(使用回调进行异步编程的话,都是用户手动控制的,使用promise的话,只需要告诉promise:“我要执行什么任务”、“我执行的任务结束了”、“然后我要做什么”)
promise语法
promise对象
new Promise对象时候传入函数,函数立即执行,函数接收resolve、reject参数,调用resolve或reject时候会改变promise状态。状态改变后不会再变化。
promise状态
pending
fullfilled
rejected
未调用resolve或者reject时候处于pending状态,调用resolve后处于fullfilled状态,调用reject后处于rejected状态。如果在pending状态时候,执行任务抛出错误,则变成reject状态。
状态变化后,会执行通过then注册的回调。执行顺序和调用then方法的顺序相同。
调用then方法时候,如果状态是pending则注册回调,等到状态改变时候执行,如果状态已经改变则执行相应的回调。
const p = new Promise((resolve, reject) => {
resolve('test');
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
p.then(
data => console.log(2, 'resolve', data),
data => console.log(2, 'reject', data)
);
// 执行结果
1 "resolve" "test"
2 "resolve" "test"
const p = new Promise((resolve, reject) => {
throw new Error('test-error');
// 由于抛出错误,promise状态已经改变为rejected,再调用resolve将不会改变promise状态
resolve('test');
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
p.then(
data => console.log(2, 'resolve', data),
data => console.log(2, 'reject', data)
);
// 执行结果
1 "reject" Error: test-error
2 "reject" Error: test-error
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test');
}, 1000);
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
// 执行结果
1s后打印 `1 "resolve" "test"`
promise对象的方法:then、catch、all、race
then
then方法接受两个参数,onFulfilled(状态变为fullfilled的回调)和onRejected(状态变为rejected的回调)。返回一个新的promise对象,返回的promise对象的状态与then的参数(onFulfilled、onRejected)和onFulfilled、onRejected方法中返回的值有关。
- then方法不传参数
如果不传参数,则then方法返回的promise和调用then的promise的状态一致。
更具体地,如果没有onFullfilled参数并且promise的状态为fullfilled,那么then方法返回的promise和调用then方法的promise状态一致;如果没有onRejected参数并且promise状态为rejected,那么then方法返回的promise和调用then方法的promise状态一致。
可以简单地理解:如果上一个promise不处理,那就下一个promise处理。
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
reject Error: test
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
undefined, () => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve test
- 回调不返回值
无论onFullfilled中还是onRejected中,不返回值(即默认返回undefined),则then返回的新promise的状态变为fullfilled,值为undefined。
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve undefined
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {},
() => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve undefined
- 返回普通值
无论onFullfilled中还是onRejected中,返回普通值,则then返回的新promise的状态变为fullfilled,值为这个值。普通值指的是,非promise对象、非thenable对象(含有then方法的对象)。
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {return 'a'},
() => {return {b: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve a
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {return 'a'},
() => {return {b: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
)
// 执行结果
resolve {b: 1}
- 返回promise
无论onFullfilled中还是onRejected中,返回一个promise对象,则以该promise的任务和状态返回新的promise。
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {},
() => {return Promise.resolve('yes');}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve yes
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {return Promise.reject('error');},
() => {return {a: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
reject error
- 返回thenable
无论onFullfilled中还是onRejected中,返回一个thenable对象,则调用该对象的then方法,该then方法接收两个参数resolvePromise和rejectPromise,如果then中调用了resolvePromise,则返回的promise状态置为fullfilled,如果then中调用了rejectPromise,或者then中抛出异常,则返回的Promise状态置为rejected,在调用resolvePromise或者rejectPromise之前,返回的promise处于pending状态。
var p = new Promise((r) => {throw new Error('test')});
p
.then(
() => ({then: function(resolvePromise, rejectPromise) {resolvePromise('resolvePromise')}}),
e => ({then: function(resolvePromise, rejectPromise) {rejectPromise('rejectPromise')}})
)
.then(
data => console.log('resolve', data),
e => console.log('reject', e)
);
// 执行结果
reject rejectPromise
var p = new Promise((r) => {throw new Error('test')});
p
.then(
() => ({then: function(resolvePromise, rejectPromise) {}}),
e => ({then: function(resolvePromise, rejectPromise) {}})
)
.then(
data => console.log('resolve', data),
e => console.log('reject', e)
);
// 执行结果
promise 处于pending状态
var p = new Promise((r) => {throw new Error('test')});
p
.then(
() => {return {then: function(resolvePromise, rejectPromise) {resolve('resolvePromise')}}},
e => {return {then: function(resolvePromise, rejectPromise) {throw new Error('surprise')}}}
)
.then(
data => console.log('resolve', data),
e => {console.error('reject', e)}
);
// 执行结果
reject Error: surprise
- 抛出错误
无论onFullfilled中还是onRejected中,抛出错误,则以rejected为状态返回新promise。
var p = new Promise(resolve => {resolve('test')});
p
.then(
() => {throw new Error('1')},
e => {return true}
)
.then(
data => console.log('resolve', data),
e => {console.error('reject', e)}
);
// 执行结果
reject Error: 1
var p = new Promise((r) => {throw new Error('test')});
p
.then(
() => {return true},
e => {throw new Error('2')}
)
.then(
data => console.log('resolve', data),
e => {console.error('reject', e)}
);
// 执行结果
reject Error: 2
catch
catch方法和then方法的reject回调用法相同,如果这时候任务处于rejected状态,则直接执行catch,catch的参数就是reject的reason;如果任务处于pending状态,则注册catch回调,等到状态变成rejected时候再执行。【阮一峰教程示例】
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数
——《ES6入门教程》 阮一峰
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
all
Promise.all方法用于多个异步任务执行,当所有任务都正常完成时候,再做后面处理的场景。
Promise.all方法接收一个promise数组作为参数,返回一个promise,当参数的数组中的所有promise都resolve时候,返回的promise才会resolve;而若有一个参数的数组中的promise reject,返回的promise就会reject。
Promise.all方法返回的promise的then的第一个参数onFullfilled回调的参数也是一个数组,对应参数中的数组promise resolve的结果。
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.all([p1, p2])
.then(
([result1, result2]) => {console.log('resolve', result1, result2);}
);
// 执行结果
resolve 1 2
const p1 = Promise.reject(1);
const p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.all([p1, p2])
.then(
([result1, result2]) => {console.log('resolve', result1, result2);},
e => console.log('reject', e)
);
// 执行结果
reject 1
race
Promise.race方法用于多个异步任务执行,当有其中一个任务完成或失败时候,就执行后续处理的场景。
Promise.race接收一个promise数组作为参数,返回一个新的promise。当参数数组中其中一个promise resolve或者reject,返回的promise就相应地改变状态。
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.race([p1, p2])
.then(
data => {console.log('resolve', data);},
e => {console.log('reject', e);}
);
// 执行结果
reject 1
allSettled
Promise.allSettled用于多个异步任务都结束(完成或者失败)时候,再执行后续任务的场景。
Promise.allSettled接收一个promise数组作为参数,返回一个promise。当参数数组中所有promise状态改变后,返回的promise变为fullfilled状态。
返回的promise的onFullfilled参数接收一个结果数组作为参数,数组对应Promise.allSettled传入的promise数组。结果数组每个元素是一个对象,格式固定:{status, value, reason},标识状态、resolve返回值、reject原因。
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.allSettled([p1, p2])
.then(
data => {console.log('resolve', data);},
);
// 执行结果
resolve [{status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]
常见面试题
有3个异步任务,如何让他们顺序执行?
const task1 = () => {
return new Promise((resolve, reject) => {
resolve(1);
});
};
const task2 = () => {
return new Promise((resolve, reject) => {
resolve(2);
});
};
const task3 = () => {
return new Promise((resolve, reject) => {
resolve(3);
});
};
// 顺序执行3个任务
task1()
.then(task2)
.then(task3);
async/await
Promise虽然解决了回调地狱问题,但是缺点是有不少的样板代码,并且写代码时候还是通过then注册回调方式
async、await是语法糖,可以让开发者以写同步代码的形式写异步逻辑。
语法
如果方法中有await,方法需要加async修饰符。await后面跟一个promise。await表达式结果是promise resolve的值。
const task = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log('1');
resolve('2');
}, 1000);
});
};
async function test() {
console.log(0);
const res = await task();
console.log(res);
}
test();
// 执行结果
0
1
2
async方法返回一个promise。其resolve的值就是async方法中return的值。
async function task1() {
return 'test';
}
task1()
.then(console.log);
// 执行结果
test
如果await后面返回的promise reject掉,需要用try catch语句捕获这个reject
const task = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('test-reject');
}, 1000);
});
};
async function test() {
try {
const res = await task();
}
catch (e) {
console.log('error', e);
}
}
test();
// 执行结果
error test-reject
手写Promise
实现promise
实现的思路可以参考从零开始手写Promise
完整代码如下
// https://zhuanlan.zhihu.com/p/144058361
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(executor) {
var _this = this
this.state = PENDING; //状态
this.value = undefined; //成功结果
this.reason = undefined; //失败原因
this.onFulfilled = [];//成功的回调
this.onRejected = []; //失败的回调
function resolve(value) {
if(_this.state === PENDING){
_this.state = FULFILLED
_this.value = value
_this.onFulfilled.forEach(fn => fn(value))
}
}
function reject(reason) {
if(_this.state === PENDING){
_this.state = REJECTED
_this.reason = reason
_this.onRejected.forEach(fn => fn(reason))
}
}
try {
executor(resolve, reject);
}
catch (e) {
reject(e);
}
}
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
Promise.prototype.then = function (onFulfilled, onRejected) {
//_this是promise1的实例对象
var _this = this;
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
var promise2 = new Promise((resolve, reject) => {
if (_this.state === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2, x, resolve, reject);
}
catch (error) {
reject(error)
}
});
}
else if (_this.state === REJECTED) {
setTimeout(()=>{
try {
let x = onRejected(_this.reason);
resolvePromise(promise2, x ,resolve, reject);
}
catch (error) {
reject(error);
}
});
}
else if(_this.state === PENDING) {
_this.onFulfilled.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2, x, resolve, reject);
}
catch (error) {
reject(error);
}
});
});
_this.onRejected.push(() => {
setTimeout(() => {
try {
let x = onRejected(_this.reason);
resolvePromise(promise2, x ,resolve, reject);
}
catch (error) {
reject(error);
}
})
});
}
});
return promise2;
};
function resolvePromise(promise2, x, resolve, reject){
if (promise2 === x) {
reject(new TypeError('Chaining cycle'))
}
if (x && typeof x === 'object' || typeof x === 'function') {
let used;
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {
if (used) return;
used = true
resolvePromise(promise2, y, resolve, reject)
},
r =>{
if (used) return;
used = true;
reject(r);
}
);
}
else {
if (used) return;
used = true;
resolve(x);
}
}
catch(e) {
if (used) return;
used = true;
reject(e);
}
}
else {
resolve(x);
}
}
module.exports = Promise;
可以通过测试工具验证自己实现Promise的完整性:Promise测试工具 promises-aplus-tests,这个工具提供872个测试用例,完全通过后就能够说明实现的Promise符合规范。
promises-aplus-tests的使用方法:
- 安装promises-aplus-tests
- 导出自己实现的Promise
- 编写test脚本,运行脚本即可测试Promise
var promisesAplusTests = require("promises-aplus-tests");
var adapter = require('./promise');
promisesAplusTests(adapter, function (err) {
// All done; output is in the console. Or check `err` for number of failures.
});
实现Promise.all、Promise.race、Promise.any、Promise.allSettled
这3个方法都接受一个promise数组作为参数,返回一个promise
它们区别是:
● Promise.any() 有一个promise成功就算成功,全部promise失败才算失败,成功的话返回成功的结果,全部失败的话,抛出’AggregateError: All promises were rejected’。
● Promise.all() 全部promise成功才算成功,一个promise就算失败,成功的话,返回成功的数据数组,失败的话抛出最先失败的promise的reason。
● Promise.race() 最先的promise完成则返回,promise结果和最先完成的promise一致。
● Promise.allSettled()等所有promise都完成返回,返回的是一个结果数组,结果反映了状态和成功信息/错误原因。
function all(arr) {
return new Promise((resolve, reject) => {
let isComplete = false;
const resolveDataList = new Array(arr.length).fill(undefined);
const onFullfilled = (data, i) => {
if (isComplete) {
return;
}
resolveDataList[i] = data;
if (resolveDataList.every(item => item !== undefined)) {
resolve(resolveDataList);
}
};
const onRejected = reason => {
if (isComplete) {
return;
}
isComplete = true;
reject(reason);
}
arr.forEach((promise, index) => {
promise.then(
data => {onFullfilled(data, index)},
onRejected
);
});
});
}
function race(arr) {
return new Promise((resolve, reject) => {
let isComplete = false;
const onFullfilled = data => {
if (isComplete) {
return;
}
isComplete = true;
resolve(data);
};
const onRejected = reason => {
if (isComplete) {
return;
}
isComplete = true;
reject(reason);
};
arr.forEach(promise => {
promise.then(
onFullfilled, onRejected
);
});
});
}
function any(arr) {
return new Promise((resolve, reject) => {
let isComplete = false;
const rejectDataList = new Array(arr.length).fill(undefined);
const onFullfilled = data => {
if (isComplete) {
return;
}
isComplete = true;
resolve(data);
};
const onRejected = (reason, i) => {
if (isComplete) {
return;
}
rejectDataList[i] = reason;
if (rejectDataList.every(item => item !== undefined)) {
reject('AggregateError: All promises were rejected');
}
}
arr.forEach((promise, index) => {
promise.then(
onFullfilled,
reason => {onRejected(reason, index);}
);
});
});
}
function allSettled(list) {
return new Promise((resolve, reject) => {
const result = new Array(list.length).fill(undefined);
const onStatusChange = () => {
if (result.filter(Boolean).length === list.length) {
resolve(result);
}
}
list.forEach((promise, index) => {
promise.then(
data => {
result[index] = {status: 'fullfilled', data};
onStatusChange();
},
reason => {
result[index] = {status: 'rejected', reason};
onStatusChange();
}
);
});
});
}
实现并发请求控制
import axios from 'axios';
function multiRequest(urls, maxNum) {
return new Promise((resolve, reject) => {
const requestURLS = [...urls];
const result = {};
let urlPool = [];
const createTask = (url) => {
urlPool.push(url);
const onComplete = (error, res) => {
if (urlPool.length === 0) {
resolve(urls.map(url => result[url]));
}
result[url] = error || res;
urlPool = urlPool.filter(value => value !== url);
if (urlPool.length < maxNum && requestURLS.length) {
createTask(requestURLS.pop());
}
};
axios.get(url)
.then(
res => {
onComplete(null, res);
},
error => {
onComplete(error);
}
);
};
while (requestURLS.length && urlPool.length < maxNum) {
createTask(requestURLS.pop());
}
});
}
promise面试题:说出代码执行结果
说明
先来看一个字节面试的真题
console.log(1);
new Promise(resolve => {
resolve();
console.log(2);
}).then(() => {
console.log(3);
})
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
这是一个经典的promise的代码执行结果的面试题,下面我们说明一下这类题目应该如何解答,
在前端面试中,promise的代码执行结果是常出现的一个题目。这个题目主要考察应聘者对promise的理解和对事件循环的理解。
实际上只要掌握几个简单的规则,这种题目就可以轻而易举地解答。
通常这类题目的代码中,主要会出现promise/async-await、setTimeout/setInterval。其中async-await是promise语法糖,所以逻辑是一样的,setInterval和setTimeout的回调都是放在宏任务队列中,规则相同。因此只要掌握了promise和setTimeout的执行规则,就能够解答这类题目。
规则
我们需要掌握5条规则就可以解答这类题目。这5条规则涉及两个知识点:promise和事件循环。
promise
1. new Promise时候马上执行
new Promise(resolve => {
console.log('test');
resolve();
});
上面的代码执行时候,会立刻打印"test",所以Promise实例化时候传入的回调是同步执行的
2. resolve或者reject之后状态不再改变,但后面代码会执行
new Promise(resolve => {
resolve();
console.log('test');
reject();
});
上面的Promise在回调中执行了resolve、打印"test"、reject,这3个方法都会执行,但是reject不会生效,因为执行resolve后promise状态已经变为fullfilled。
事件循环
-
promise的then中的回调放入微任务队列,setTimeout放入宏任务队列
promise.catch的回调一样,也会放入微任务队列。 -
调用栈中代码执行完之后,先取微任务队列中的任务执行,直到微任务队列为空
-
微任务队列为空,取宏任务队列中的一个任务开始执行,然后重复上一步,直到宏任务队列为空
可以简单描述为:一个宏任务 + 所有微任务 ->一个宏任务 + 所有微任务……,循环往复。
解题思路
根据上面的规则,我们可以总结解题的思路是
- 先列出调用栈、宏任务队列、微任务队列及初始状态:调用栈是当前的代码,最开始宏任务队列和微任务队列为空。
- 遇到new Promise,直接执行Promise的函数参数
- 遇到resolve/reject,改变状态
- 遇到promise.then/promise.catch,放入微任务队列、遇到setTimeout放入宏任务队列
- 调用栈执行完后,从微任务队列取任务放到调用栈中执行
- 微任务队列执行完后,从宏任务队列取一个放到调用栈执行,然后执行上一步,直到宏任务队列为空
下面我们通过最开始提到的题目来演示一下如何操作
范例
题目:说出下面代码执行结果:
console.log(1);
new Promise(resolve => {
resolve();
console.log(2);
}).then(() => {
console.log(3);
})
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
解题过程
-
初始状态
代码都在调用栈中 -
第一步
执行当前调用栈,先打印1,然后执行new Promise,打印2,然后将.then回调放到微任务队列,将setTimeout回调放到宏任务队列,然后打印5,调用栈为空
打印1,2,5 -
第二步
查看微任务队列,取出promise.then的回调放入调用栈中执行,执行完后调用栈为空。
打印3 -
第三步
微任务队列为空,所以查找宏任务队列中的setTimeout回调,放入调用栈中,执行完后为空。
打印4 -
结束
调用栈为空,执行结束
下面通过几个面试真题加深理解吧。
真题
1、说出代码执行结果
const promise = new Promise((resolve,reject)=>{
console.log(1);
resolve();
console.log(2);
reject()
})
setTimeout(()=>{console.log(5)},0)
promise.then(()=>{console.log(3)})
.then(()=>{console.log(6)})
.catch(()=>{console.log(7)})
console.log(4)
解析
答案是1,2,4,3,6,5
解析
首先new Promise时候打印1和2,因为new Promise时候会立即执行传入的方法
然后后面代码都是异步代码,先将setTimeout的回调加入宏任务队列,再把promise.then放入到微任务队列,然后直接执行最后一句,打印4
这样宏任务代码执行完了,接下来开始执行微任务队列中的任务,由于promise resolve,因为promise resolve之后状态不会再改变,因此不会执行到reject的对调,所以打印3和6
微任务队列为空,再到宏任务队列中查找任务,找到setTimeout回调执行,打印5
调用栈、宏任务队列、微任务队列都为空,代码执行结束。
2、说出代码执行结果
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve();
}, 0);
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
解析
3, 7, 4, 1, 2, 5
解析
首先定义first
然后执行first,然后执行new Promise传入的方法,先打印3
又new Promise,执行其中传入的方法,打印7
执行setTimeout,将回调放入宏任务队列
执行resolve(1),将内部promise状态置为fullfilled,值为1
执行resolve(2),将外部promise状态置为fullfilled,值为2
执行内部promise.then方法,将回调加入微任务队列
执行first().then,即外部的promise,将回调加入到微任务队列
调用栈为空,开始从微任务队列拿取任务,首先拿到内部promise的回调,打印其值1
然后从微任务队列中拿取外部的promise的回调,打印其值2
此时微任务队列为空,开始从宏任务队列中拿取任务,即setTimeout回调,打印5。
调用栈,宏任务队列和微任务队列都为空,执行结束。