1. 期约与异步函数
- 内容
- 异步编程
- 期约
- 异步函数
1.1 异步编程
- 在JavaScript是单线程事件循环模式中。异步行为是为了优化因计算量大而时间长的操作。
- 同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行。在程序执行的每一步,都可以推断出程序的状态。
- 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。
- 以往的异步编程模式
- 早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作函数,会造成回调地狱的问题。
- 异步返回值:给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码
- 失败处理:异步操作失败处理在回调模型中也要考虑,因此也有成功回调和失败回调
function double(val){
setTimeout(() => {
setTimeout(console.log,0,val*2 );
}, 1000);
}
double(3)
function double2(val,callback){
setTimeout(() => {
callback(val * 3)
}, 1000);
}
double2(3, (x)=>{
console.log(`返回值: ${x}`)
})
function double(value,success,fail){
setTimeout(() => {
try {
if(typeof value !== 'number'){
throw 'args not number'
}
success(2 * value)
} catch (error) {
fail(error)
}
}, 1000);
}
const successCallBack = (x) => console.log(`Success: ${x}`)
const failCallBack = (e) => console.log(`Fail: ${e}`);
double(2,successCallBack,failCallBack)
double('abc',successCallBack,failCallBack)
1.2 期约
- ECMAScript6新增的引用类型Promise,可以通过new操作符来实例化。创建新期约时需要传入
执行器函数作为参数。
- 期约状态:
- 待定 (pending)
- 兑现 (fulfilled),也称为 解决状态 resolved
- 拒绝 (rejected)
- 期约的状态是不可逆的,只要从待定状态转换为兑现或拒绝,期约的状态就不会改变了。
期约的状态是私有的,不能直接通过JavaScript检测到。这主要是为了避免根据读到的期约状态,以同步的方式处理期约对象。另外,期约的状态也不能被外部JavaScript代码修改。期约故意将异步行为封装起来,从而隔离外部的同步代码
1.2.1 通过执行函数控制期约状态
- 由于期约的状态是私有的,所以只能在内部进行操作。执行器函数主要有两项职责:
初始化期约的异步行为和控制状态的最终转换
- 控制期约状态的转换是通过调用它的两个函数参数实现的。 resolve() reject()
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 1000, p1);
new Promise(() => setTimeout(console.log, 0, "executor"));
setTimeout(console.log, 0, "promise init");
let p2 = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p2);
- Promise.resolve() 也可以实例化一个解决的期约。
- 使用这个静态方法,实际上可以把任何值都转换为一个期约。
- 如果传入的参数是一个期约,那它的行为就类似于一个空包装。
- 静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此可能会有不符合预期的行为。
let p1 = new Promise((resolve, reject) => {
resolve();
});
let p2 = Promise.resolve();
setTimeout(console.log, 0, Promise.resolve(3));
let p3 = Promise.resolve(4);
setTimeout(console.log, 0, p3 === Promise.resolve(p3));
let p4 = Promise.resolve(new Error("foo"));
setTimeout(console.log, 0, p4);
- Promise.reject() 也可以实例化一个拒绝的期约并抛出一个异步错误。
这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获
- 如果给它传入一个期约对象,则这个期约会成为它返回的拒绝期约的理由。
let p = Promise.reject(3);
setTimeout(console.log, 0, p);
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
1.2.2 期约的实例方法
- 实现Thenable接口:在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。这个方法被认为实现了Thenable接口。
- Promise.prototype.then() 是为期约实例添加处理程序的主要方法。这个then()方法接收最多两个参数:onResolved处理程序和onRejected处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入"兑现"和"拒绝"状态时执行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
</html>
<script>
function onResolved(id) {
setTimeout(console.log, 0, id, "resolved");
}
function onRejected(id) {
setTimeout(console.log, 0, id, "rejected");
}
let p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(reject, 3000);
});
p1.then(
() => {
onResolved("p1");
},
() => {
onRejected("p2");
}
);
p2.then(
function () {
onResolved("p2");
},
function () {
onRejected("p2");
}
);
</script>
- Promise.prototype.then()方法返回一个新的期约实例
- 新期约实例基于onResolved处理程序的返回值构建。换句话说,该处理程序的返回值会通过Promise.resolve()包装来生成新期约。
- 如果没有提供这个处理程序,则Promise.resolve()就会包装上一个期约解决之后的值。
- 如果没有显式的返回语句,则Promise.resolve()会包装默认的返回值undefined.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
let p1 = Promise.resolve("foo");
let p2 = p1.then();
setTimeout(console.log, 0, p2);
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3);
setTimeout(console.log, 0, p4);
setTimeout(console.log, 0, p5);
let p6 = p1.then(() => "bar");
let p7 = p1.then(() => Promise.resolve("bar"));
setTimeout(console.log, 0, p6);
setTimeout(console.log, 0, p7);
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
setTimeout(console.log, 0, p8);
setTimeout(console.log, 0, p9);
let p10 = p1.then(() => {
throw "bar";
});
setTimeout(console.log, 0, p10);
let p11 = p1.then(() => Error("zxc"));
setTimeout(console.log, 0, p11);
</script>
</body>
</html>
- onRejected处理程序也与之类似:onRejected处理程序返回的值也会被Promise.resolve()包装。因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
let p1 = Promise.reject("foo");
let p2 = p1.then();
setTimeout(console.log, 0, p2);
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());
setTimeout(console.log, 0, p3);
setTimeout(console.log, 0, p4);
setTimeout(console.log, 0, p5);
let p6 = p1.then(null, () => new Promise(() => {}));
let p7 = p1.then(null, () => Promise.reject());
setTimeout(console.log, 0, p6);
setTimeout(console.log, 0, p7);
let p8 = p1.then(null, () => {
throw "bar";
});
let p9 = p1.then(null, () => Error("zxc"));
setTimeout(console.log, 0, p8);
setTimeout(console.log, 0, p9);
</script>
</body>
</html>
- Promise.prototype.catch() 用于给期约添加拒绝处理程序。这个方法只接收一个参数,onRejected处理程序。其实,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null,onRejected)
<script>
let p1 = Promise.reject();
let onRejected = function (e) {
setTimeout(console.log, 0, "rejected");
};
p1.then(null, onRejected);
p1.catch(onRejected);
let p3 = new Promise(() => {});
let p4 = p3.catch();
setTimeout(console.log, 0, p3);
setTimeout(console.log, 0, p4);
setTimeout(console.log, 0, p3 == p4);
</script>
- Promise.prototype.finally() 用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。主要用于添加清理代码。
<script>
let p1 = Promise.resolve();
let p2 = Promise.reject();
function onFinally(e) {
setTimeout(console.log, 0, "finally");
}
p1.finally(onFinally);
p2.finally(onFinally);
let p3 = new Promise(() => {});
let p4 = p3.finally();
setTimeout(console.log, 0, p4);
</script>
- finally()新期约实例不同于then()或catch()方法返回的实例。在大多数情况下它将表现为父期约的传递。
<script>
let p1 = Promise.resolve('foo')
let p2 = p1.finally(()=> undefined)
let p3 = p1.finally(()=> Promise.resolve())
let p4 = p1.finally(()=> {})
let p5 = p1.finally(()=> 'bar')
let p6 = p1.finally(()=> Error('zxc'))
setTimeout(console.log,0,p2)
setTimeout(console.log,0,p3)
setTimeout(console.log,0,p4)
setTimeout(console.log,0,p5)
setTimeout(console.log,0,p6)
</script>
1.2.3 期约连锁与期约合成
- 期约连锁:把期约串联起来,因为每个期约实例的方法then() catch() finally()都会返回一个新的期约对象。
<script>
let p = new Promise((resolve, reject) => {
console.log("first...");
resolve();
});
p.then(() => {
console.log("second...");
})
.then(() => {
console.log("third...");
})
.then(() => {
console.log("four...");
});
let p1 = new Promise((resolve, reject) => {
console.log("p1 executor");
setTimeout(resolve, 1000);
});
p1.then(
() =>
new Promise((resolve, reject) => {
console.log("p2 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p3 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p4 executor");
setTimeout(resolve, 1000);
})
);
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve("p1 executor")
.then(() => delayedResolve("p2 executor"))
.then(() => delayedResolve("p3 executor"))
.then(() => delayedResolve("p4 executor"));
</script>
- 因为then() catch() finally()都返回期约,所以串联这些方法很直观
let p1 = new Promise((resolve,reject)=>{
console.log('init promise)
reject()
})
p1.catch(()=> console.log('reject handler'))
.then(()=> console.log('resolve handler'))
.finally(()=> console.log('finally handler'))
- Promise.all()和Promise.race() 将多个期约实例组合成一个期约的静态方法,而合成后期约的行为取决于内部期约的行为。
- Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。
- 合成的期约只会在每个包含的期约都解决之后才解决
- 如果至少有一个包含的期约待定,则合成的期约也会待定
- 如果有一个包含的期约拒绝,则合成的期约也会拒绝
- 如果合成的期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序。
- 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期约不会影响最终期约的拒绝理由。
let p = Promise.all([Promise.resolve(), Promise.resolve()]);
let p2 = Promise.all([3, 4]);
let p3 = Promise.all([]);
let p4 = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000)),
]);
setTimeout(console.log, 0, p4);
p4.then(() => setTimeout(console.log, 0, "all resolved()..."));
let p5 = Promise.all([new Promise(() => {})]);
let p6 = Promise.all([
Promise.resolve(),
Promise.resolve(),
Promise.reject(),
]);
let p7 = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4),
]);
p7.then((val) => setTimeout(console.log, 0, val));
let p8 = Promise.all([
Promise.resolve(),
Promise.reject(3),
Promise.reject(4),
]);
p8.catch((reason) => setTimeout(console.log, 0, reason));
- Promise.race() 静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约。
- 无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回心期约。
let p = Promise.race([Promise.resolve(), Promise.resolve()]);
let p2 = Promise.race([3, 4]);
let p3 = Promise.race([]);
let p4 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(resolve, 1000)),
]);
setTimeout(console.log, 0, p4);
let p5 = Promise.race([
new Promise((resolve, reject) => setTimeout(resolve, 1000)),
Promise.reject(4),
]);
setTimeout(console.log, 0, p5);
let p6 = Promise.race([
Promise.resolve(3),
Promise.reject(4),
Promise.resolve(5),
]);
setTimeout(console.log, 0, p6);
1.2.4 期约扩展
- ES6期约实现是可靠的,但它也有不足之处了。如
期约取消和进度追踪.
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
setTimeout(console.log, 0, "delay cancel...");
resolve();
});
});
}
}
const start = document.querySelector("#start");
const end = document.querySelector("#close");
function cancelDelayedResolve(delay) {
setTimeout(console.log, 0, "set delay...");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
setTimeout(console.log, 0, "delayed resolve...");
resolve();
}, delay);
const cancelToken = new CancelToken((cancelCallBack) => {
end.addEventListener("click", cancelCallBack);
});
cancelToken.promise.then(() => clearTimeout(id));
});
}
start.addEventListener('click',()=>cancelDelayedResolve(2000))
1.3 异步期约
- 异步函数,也称为"async/await"语法关键字,以同步方式写的代码能够以异步执行。
let p = new Promise((resolve,reject)=> setTimeout(console.log,1000,3))
p.then(x => console.log(x))
function handler(x){
console.log(x);
}
let p1 = new Promise((resolve,reject)=> setTimeout(console.log,1000,3))
p1.then(handler)
1.3.1 异步函数
- async关键字用于声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。
- 使用 async关键字可以让函数有异步特性,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通函数的正常行为。
- 异步函数如果使用return关键字返回了值,这个值会被Promise.resolve()包装成一个期约对象。如果没有return则返回undefined 异步函数始终返回期约对象。 在函数外部调用这个函数可以得到它返回的期约。
- 在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约。拒绝期约的错误不会被异步函数捕获。
async function foo(){}
let bar = async function(){}
let baz = async ()=>{}
class User {
async name(){}
}
async function foo(){
console.log(1);
}
foo()
console.log(2);
async function test(){
console.log(1);
return 3
}
test().then(console.log)
console.log(2);
1.3.2 await
- 因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用await关键字可以暂停异步函数代码的执行,等待期约解决。
- await 关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。 await关键字尝试解包对象的值,然后将这个值传给表达式,在异步恢复异步函数的执行。
3.await 关键字的用法与JavaScript的一元操作一样,可以单独使用,也可以在表达式中使用。
- await关键字期待一个实现thenable接口的对象,但常规的值也可以。 等待会抛出错误的同步操作,会返回拒绝的期约。单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误,不过,对拒绝的期约使用awiat则会释放错误值。
async function foo() {
let p = new Promise((resolve, reject) =>
setTimeout(console.log, 1000, 3)
);
console.log(await p);
}
foo();
async function test1() {
console.log(await Promise.resolve("test1"));
}
test1();
async function test2() {
return await Promise.resolve("test2");
}
test2().then(console.log);
async function test3() {
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log("test3");
}
test3();
async function test4() {
console.log(1);
await (() => {
throw 3;
})();
}
test4().catch(console.log);
console.log(2);
1.3.3 停止和恢复执行
- async/await中真正起作用的是await。JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await右边的值可用给了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
async function test1(){
console.log(2);
await null
console.log(4);
}
console.log(1);
test1()
console.log(3);
async function test(){
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar(){
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
test()
console.log(3);
bar()
console.log(5);