# Promise
Promise的一些常识及注意点。
# 1、基础
主要用于解决异步问题,比传统的回调函数和事件, 更加合理。
Promise的两个特点
1、Promise对象的状态不受外界影响
1)pending 初始状态
2)fulfilled 成功状态
3)rejected 失败状态
Promise 有以上三种状态,只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态
2、Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可逆,只能由 pending变成fulfilled或者由pending变成rejected。也就是说, 如果改变已经发生了, 你再对Promise添加回调函数, 也会立即得到结果(当然这个回调函数的触发是在微任务中)。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise的三个缺点
1)无法取消Promise,一旦新建它就会立即执行,无法中途取消 2)如果不设置回调函数,Promise内部抛出的错误,不会反映到外部, 吞掉错误
try {
new Promise(resolve => {
console.log(1);
throw new Error('error');
console.log(2);
});
} catch (error) {
console.log('catch');
console.error(error);
}
// 如果不设置回调函数,这个错误就不会被捕获到,上面的try catch 是捕获不到的,必须添加catch回调
3)当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
# 2、基础使用
没啥好说的, 以一个最简单的XHR实例举例即可
function fetch(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
const status = xhr.status;
if (status === 0 || (status >= 200 && status < 400)) {
resolve(xhr.responseText);
} else {
reject(status);
}
}
};
xhr.send();
});
}
fetch('http://www.xxx.com/xxx')
.then(res => {})
.catch(error => {});
TIP
注意点:Promise新建后就会立即执行,也就是说Promise的构造函数是同步执行的
resolve或者reject之后的代码如果没有return, 还是会继续执行
一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外。
# 3、resolve的参数是另外一个Promise
const p1 = new Promise(resolve => {
resolve('p1');
});
const p2 = new Promise(resolve => {
resolve(p1);
});
p2.then(res => {
console.log(res);
});
上述代码,p2的状态由p1来决定
# 4、then、catch
关于这两个的用处就不说, then的链式调用也没什么好说的
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
调用reject()
或者抛出一个错误都可以被catch捕获
const p = new Promise(() => {
throw new Error('test');
});
p.catch(error => {
console.log(error);
});
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个
catch
语句捕获。
const p = new Promise(resolve => {
resolve(1);
});
p.then(
res => {
console.log(res);
throw new Error('error');
},
error => {
console.error(1, error);
}
);
p.then(res => {
console.log(res);
throw new Error('error');
}).catch(error => {
console.error(2, error);
});
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。
# 5、内部错误会被吞掉
WARNING
跟传统的try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
const p = new Promise(resolve => {
resolve(x + 2); // ReferenceError: x is not defined
});
p.then(res => {
console.log('1');
});
console.log('2')
内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程、终止脚本执行。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”, 如果想捕获这种错误,可以使用 unhandledrejection
。
// 浏览器
window.addEventListener('unhandledrejection', (event) => {
const { reason } = event;
console.log(event);
});
// Node.js
process.on('unhandledRejection', function (err, p) {
throw err;
});
一个测试题, 下面哪个catch可以捕获到抛出的错误
try {
const p = new Promise(resolve => {
resolve(2);
setTimeout(function () {
throw new Error('1');
}, 0);
});
p.then(res => {
console.log(res);
}).catch(error => {
console.log('error1');
console.error(error);
});
} catch (error) {
console.log('error2');
console.error(error);
}
确实一个都抓不到, 无论是Promise的catch 还是 try...catch 都抓不到下一轮宏任务抛出的错误
# 6、Promise.prototype.finally
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
finally的实现
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
# 7、Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 只有所有的实例都是fullfilled状态, 才会返回。
使用没什么问题,我们自己实现一下
const all = function (promises) {
if (!promises || promises.length === 0) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
let count = 0;
let result = new Array(promises.length);
promises.forEach((promise, index) => {
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
promise
.then(res => {
result[index] = res;
count++;
if (count === promises.length) {
resolve(result);
}
})
.catch(error => {
reject(error);
});
});
});
};
# 8、Promise.race()
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
实现一个简单版的rance
const race = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
promise
.then(res => {
resolve(res);
})
.catch(error => {
reject(error);
});
});
});
};
使用race封装接口请求超时机制
function fetchApi(url, timeout) {
return Promise.race([
fetch(url),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('request timeout'));
}, timeout);
})
]);
}
# 9、Promise.allSettled()
用来确定一组异步操作是否都结束了(不管成功或失败)。
简单自己实现一个, 其实不使用finally也可以的, 思路简单的。
const allSettled = function (promises) {
return new Promise((resolve, reject) => {
let count = 0;
let result = [];
promises.forEach(promise => {
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
promise
.then(res => {
result.push(res);
})
.catch(error => {
result.push(error);
})
.finally(() => {
count++;
if (count === promises.length - 1) {
resolve(result);
}
});
});
});
};
# 10、Promise.any()
只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。
# 11、打破Promise链式调用
打破Promise链式调用:抛出一个错误或者返回 一个Promise.reject 都行
const p = new Promise((resolve) => {
resolve(1);
});
p.then((res) => {
console.log(res);
return 2;
})
.then((res) => {
console.log(res);
// throw new Error('need break');
// return Promise.reject('need break');
return 4;
})
.then((res) => {
console.log(res);
return 5;
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log('finally');
});
# 12、resolve后面的代码还会执行吗
如果没有return, resolve后面的代码仍然会执行, 只不过此时Promise的状态已经变成fullfilled而已。
# 13、微任务
Promise本身是同步执行函数,当执行到resolve或者reject时, 会把then回调或者catch的任务作为微任务添加到该次宏任务末尾。
所以Promise的回调是在微任务队列处理的,看一个常见的面试题, 说出下面的打印顺序
console.log(1);
Promise.resolve(2).then(res => {
console.log(res);
});
setTimeout(console.log.bind(null, 3)); // setTimeout 的任务会在下一次宏任务循环处理
console.log(4);
// 1 4 2 3 应该没有疑问
# 14、Async/Await
async
函数返回一个Promise实例,return的值会作为then回调的参数。 如果没有return 会默认返回undefined。 如果在函数内部抛出错误throw new Error()
, 这个返回的Promise会变成rejected状态。
搭配await使用,能让代码结构看起来更像是同步的代码,不过需要注意多个promise的串并行问题, 看是使用串行还是并行。 比如下面的伪代码, 总是会在url1
的请求完成之后才会请求url2
的资源, 这就是串行了。
function getData(url) {
return new Promise(resolve => {
fetch(url, res => {
resolve(res);
});
});
}
async function getAllData() {
const res1 = await getData('url1');
const res2 = await getData('url2');
}
// 如果想改成并行发出请求, 可以改成下面的样子, 当然也可以基于业务需求使用Promise.all或者Promise.race等工具
async function getAllData() {
const p1 = getData('url1');
const p2 = getData('url2');
const res1 = await p1;
const res2 = await p2;
}
// 这样子两个getData的函数都是同步并行执行的。
# 15、在TS中使用Promise
主要是如何定义Promise回调函数的参数类型, 也就是then回调的参数类型, 2个办法
通过构造函数的泛型
function foo(): Promise<number> { return new Promise(resolve => { resolve(1); }); } // 可以推断res的类型是number foo().then(res => { console.log(res.toFixed(2)); });
通过定义resolve函数的类型
function foo() { return new Promise((resolve: (val: number) => void) => { resolve(1); }); } // 可以明确知道res的类型是number foo().then(res => { console.log(res.toFixed(2)); });
# 16、Promise.resolve()
Promise.resolve()
静态方法以给定值“解决(resolve)”一个 [Promise
]。如果该值本身就是一个 Promise,那么该 Promise 将被返回;如果该值是一个 [thenable]对象,Promise.resolve()
将调用其 then()
方法及其两个回调函数;否则,返回的 Promise 将会以该值兑现。
const promise1 = Promise.resolve(123);
promise1.then((value) => {
console.log(value);
// Expected output: 123
});
简而言之,Promise.resolve()
返回一个 Promise 对象,其最终状态取决于另一个 Promise 对象、thenable 对象或其他值。
实际上,大部分解决逻辑是由 Promise()
构造函数传递的 resolve
函数 (opens new window)实现的,简单概括如下:
- 如果传入的是一个非 thenable (opens new window) 对象的值,则返回的 Promise 对象将以该值兑现。
- 如果传入的是一个 thenable 对象,则通过传入一对解决函数作为参数调用该 thenable 对象的
then
方法后得到的状态将作为返回的 Promise 对象的状态。(但是因为原生的 Promise 直接通过Promise.resolve()
调用,而不创建封装对象,所以不会在原生 Promise 上调用then
方法。)如果resolve
函数接收到另一个 thenable 对象,则会再次进行解决,以确保 Promise 对象的最终兑现值永远不会是 thenable 对象。
# 16.1 resolve 另一个 promise
Promise.resolve()
方法会重用已存在的 Promise
实例。如果它正在解决一个原生的 Promise,它将返回同一 Promise 实例,而不会创建一个封装对象。
const original = Promise.resolve(33);
const cast = Promise.resolve(original);
cast.then((value) => {
console.log(`值:${value}`);
});
console.log(`original === cast ? ${original === cast}`);
// 按顺序打印:
// original === cast ? true
// 值:33
# 16.2 resolve thenable 对象并抛出错误
resolve thenable 对象
const p1 = Promise.resolve({
then: (resolve, reject) => {
resolve('已兑现!');
}
});
console.log(p1 instanceof Promise); // true,thenable 对象被转换为一个 Promise 对象
p1.then(
v => {
console.log(v); // "已兑现!"
},
e => {
// 不会被调用
}
);
Thenable 在回调之前抛出异常
// Promise 被拒绝
const thenable = {
then(resolve) {
throw new TypeError('抛出异常');
resolve('Resolving');
}
};
const p2 = Promise.resolve(thenable);
p2.then(
v => {
// 不会被调用
},
e => {
console.error(e); // TypeError: 抛出异常
}
);
Thenable 在回调 Promise 被解决之后抛出异常
const thenable2 = {
then(onFulfilled) {
onFulfilled('解决');
throw new TypeError('Throwing');
}
};
const p3 = Promise.resolve(thenable2);
p3.then(
v => {
console.log(v); // "解决"
},
e => {
// 不会被调用
}
);