JavaScript异步类型
- 延迟类型:setTimeout、setInterval、setImmediate
- 监听事件:监听 new Image 加载状态、监听 script 加载状态、监听 iframe 加载状态、Message
- 带有异步功能类型: Promise、ajax、Worker、async/await
需要说明的是,在 ES6 之前,JavaScript 语言本身没有异步,延迟类型、监听类型的异步都是由宿主提供的,并非语言的核心部分。
JavaScript常用异步编程
Promise
Promise 对象用于表示一个异步操作的最终状态,及结果值。
Promise有几个特点:
- 对象的状态不受外界影响,有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。只有异步操作的结果可以决定当前是哪种状态,其他操作无法改变。
- 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态改变只可能是:pending -> fulfilled 或 pending -> rejected
- 实例化后,会立即执行一次。所以一般将其用函数包裹起来,使用的时候调用一次。
- 如果执行后的回调也要做一些异步操作,可以无限的.then下去,当然要保证有返回值
方法:
- 对象方法 reject、resolve、all、race、allSettled(ES2020)
- 原型方法 then、catch、finally(ES9)
function promiseTest(n,msg) {
return new Promise((resolve,reject)=>{
setTimeout(function () {
console.log(`执行第${n}个任务`);
msg.code && resolve(msg.text); // 当认为成功的时候,调用resolve函数
!msg.code && reject(msg.text); // 当认为失败的时候,调用reject函数
},n*500)
});
}
let pro = promiseTest(1,{code:true,text:"返回的数据1"});
/* 没有catch,每个then里两个回调函数,此时第一个为成功的回调,第二个为失败的回调 */
pro.then((data)=>{
console.log(data); // 执行成功结果在这里
// return promiseTest(2,{code:true,text:"返回的数据2"});
return promiseTest(2,{code:false,text:"失败的数据"});
},(err)=>{
console.log(err); // 执行失败的结果在这里
}).then((data)=>{console.log(data)},(err)=>{console.log(err)});
观察 then 和 catch 的用法:
- 在多次 then 后最后跟一个 catch,可以捕获所有的异常
/* 多个then和一个catch */
pro.then((data)=>{
console.log(data);
return promiseTest(2,{code:false,text:"失败的数据"});
}).then((data)=>{
console.log(data)
}).catch((err,data)=>{
console.log("失败了",err);
});
all 和 rece 的用法:(这两个方法都是将若干个 Promise 实例,包装成一个新的 Promise 实例)
- all 接收一个 promise 对象列表.在所有异步操作执行完且全部成功的时候才执行 then 回调,只要有一个失败,就执行 catch 回调(只对第一个失败的promise 对象执行)。
- race 也接收一个 promise 对象列表,不同的是,哪个最先执行完,对应的那个对象就执行 then 或 catch 方法( then 或 catch 只执行一次)。
/* all的用法 */
Promise.all([
promiseTest(1,{code:true,text:"返回的数据1"}),
promiseTest(2,{code:false,text:"返回的数据2"}),
promiseTest(3,{code:false,text:"返回的数据3"})
]).then((res)=>{console.log("全部成功",res)}).catch((err)=>{console.log("失败",err);});
/* race的用法 */
Promise.race([
promiseTest(1,{code:false,text:"返回的数据1"}),
promiseTest(2,{code:false,text:"返回的数据2"}),
promiseTest(3,{code:true,text:"返回的数据3"})
]).then((res)=>{console.log("成功",res)}).catch((err)=>{console.log("失败",err);});
Generator
Generator 叫做生成器,通过 function* 关键字来定义的函数称之为生成器函数(generator function),它总是返回一个 Generator 对象。生成器函数在执行时能暂停,又能从暂停处继续执行。调用一个生成器并不会立马开始执行里面的语句,而是返回这个生成器的 迭代对象( iterator )。
Generator 对象有3个方法,都有一样的返回值 { value, done } 【与 Python 生成器的用法一样】
- .next(value) 返回一个由yield表达式生成的值。(value 为向生成器传递的值)
- .return(value) 该方法返回给定的值并结束生成器。(value 为需要返回的值)
- .throw(exception) 该方法用来向生成器抛出异常,并恢复生成器的执行。(exception 用于抛出的异常)
生成器的作用:
可以和 Promise 组合使用。减少代码量,写起来更方便。在没有 Generator 时,写 Promise 会需要很多的 then,每个 then 内都有不同的处理逻辑。现在,我们将所有的逻辑写进一个生成器函数(或者在生成器函数内用 yield 进行函数调用),Promise 的每个 then 内调用同一个函数即可。
定义生成器:
function add(a,b) {
console.log("+");
return a+b;
}
function cut(a,b) {
console.log("-");
return a-b;
}
function mul(a,b) {
console.log("*");
return a*b;
}
function division(a,b) {
console.log("/");
return a/b;
}
function* compute(a, b) {
yield add(a,b);
yield cut(a,b);
let value = yield mul(a,b);
console.log("value",value); // 第三次调用.next()时无法为value赋值,需要第四次调用才能为其赋值
yield mul(a,b);
yield division(a,b);
}
使用生成器:
// 执行一下这个函数得到 Generator 实例,调用next()方法执行,遇到yield暂停
let generator = compute(4, 2);
function promise() {
return new Promise((resolve, reject) => {
let res = generator.next();
if(res.value > 5)
{
resolve("OK");
}else
{
reject("小于5")
}
});
}
let proObj = promise();
proObj.then((data)=>{
console.log(data);
let res = generator.next();
console.log("Promise res1",res);
}).then((data)=>{
let res = generator.next();
// let res = generator.return();
console.log("Promise res2",res);
}).then((data)=>{
let res = generator.next("qwe"); // 第四次next()时,向生成器传数据
console.log("Promise res3",res)
}).catch((err)=>{
console.log("出错",err);
});
Generator 函数的特点:
- 最大特点就是可以交出函数的执行权(暂停执行)。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
- 可以将 yield 关键字使得生成器函数可以与外接交流:可以将内部的值传到外界,也可以将外接的值传入
yield 和 yield* :
- 生成器函数在执行过程中,遇到 yield 会暂停执行,并返回一个值
- yield* 表达式用于委托给另一个 generator 函数(即可以将当前生成器函数的执行权交给另一个生成器函数)或 可迭代对象
function* g1() {
yield 2;
yield 3;
yield 4;
}
function* g2() {
yield 1;
yield* g1();
yield 5;
yield* ["a", "b"];
yield* "cd";
}
var iterator = g2();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: "a", done: false }
console.log(iterator.next()); // { value: "b", done: false }
console.log(iterator.next()); // { value: "c", done: false }
console.log(iterator.next()); // { value: "d", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
async/await
优点:简洁,节约了不少代码
- async 函数就是 Generator 函数的语法糖。要将 Generator 函数转换成 async 函数,只需将 * 替换成 async ,yield 替换成 await 即可
- 被 async 修饰的函数,总会返回一个 Promise 对象。如果代码中返回值不是 promise 或者没有返回值,也会被包装成 promise 对象
- await 只能在 async 函数内使用。它是一个操作符,等待一个函数或表达式。经过该操作符处理后,输出一个值。
如果在异步函数中,每个任务都需要上个任务的返回结果,可以这么做:
function takeLongTime(n) {
return new Promise((resolve,reject) => {
setTimeout(() => {resolve(n + 200)}, n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
如果这几个任务没有关联,可以这样做:
async function doIt() { // 函数执行耗时2100ms
console.time("doIt");
await step1(300).catch((err)=>{console.log(err)}); // 异常处理
await step1(800);
await step1(1000);
console.timeEnd("doIt");
}
doIt();
当然,最好这样做:
async function doIt() { // 函数执行耗时1000ms
console.time("doIt");
const time1Pro = step1(300);
const time2Pro = step1(800);
const time3Pro = step1(1000);
await time1Pro;
await time2Pro;
await time3Pro;
console.timeEnd("doIt");
}
或
async function doIt() { // 函数执行耗时1000ms
console.time("doIt");
const [ time1Pro, time2Pro, time3Pro ] = await Promise.all([step1(300), step1(800), step1(1000)])
console.timeEnd("doIt");
}
doIt();
注意:
- async/await 并没有脱离 Promise,它的出现能够更好地协同 Promise 工作。
- 怎么体现更好地协同?它替代了then catch的写法。使得等待 promise 值的操作更优雅,更容易阅读和书写。
- 函数仅仅加上 async 并没有意义,它仍然是同步函数,只有与 await 结合使用,它才会变成异步函数。
- 这需要精准理解 await。它在等待的时候并没有阻塞程序,此函数也不占用 CPU 资源,使得整个函数做到了异步执行。当 async 函数在执行的时候,第一个 await 之前的代码都是同步执行的。
- doIt() 函数内部是串行执行的,但它本身是异步函数。
- 在这个异步函数内,可能会做很多操作 ABC,他们有执行的先后顺序。这时你可能会想,A、B、C之间没有关联,他们之间可以是并行执行的,并不需要串行,那怎么办?
- 【错误想法】这样想没错,但是没必要。因为他们已经存在于异步函数内了,所有的操作已经是异步的。在同样的环境情景下,底层执行的效率是相同的,并不见得因为A和B之间互相异步而提高效率。
- 【正确想法】这样想是有必要的。参照两个 doIt() ,调用的函数返回 promise 对象,前者是依次生成 promise 对象(依次执行任务),依次等待返回结果。等待总时长取决于所有任务执行时间之和。后者则是同时生成 promise 对象(同时执行任务),依次等待。等待总时长取决于耗时最长的任务。后者的 CPU 运用率更高。
- async 函数内任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。为了不中断后面的操作,我们可以将 await 语句放在 try ... catch 结构内,或者在 await 后面的 Promise 对象跟一个 catch 方法。
- 错误处理。最标准的方法是使用 try...catch 语句,但是它不仅会捕捉到 promise 的异常,还会将所有出现的异常捕获。因此,可以使用 .catch ,只会捕获 promise 相关的异常。
关于错误处理,可以这样做:
function takeLongTime(n) {
return new Promise((resolve,reject) => {
setTimeout(() => {resolve(n + 200)}, n);
}).then(data=>[data,null]).catch(err=>[null,err]);
}
async doIt(){
let [data, err] = await takeLongTime(1000);
console.log(data, err);
}
另外,async函数有多种使用形式:
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
const foo = async () => {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
异步生成器函数
即异步函数和生成器函数的结合体:async function*() {}。它就是 Generator 和 async-await 的完美结合,支持两者的用法和特性。
以前我以为,async-await 可以完全代替 Generator ,但其实不然,前者的优点在于更优雅地处理异步操作,后者能够支持函数内外进行数据交流。
异步生成器函数会返回一个异步迭代器,这个异步迭代器有两种使用方式:
- 通过 for await of 遍历得到值,非常方便
- 通过循环 .next() 得到
两种方式又有不同:
- 前者不能得到异步生成器内 return 的值,后者可以
- 前者不能给 yield 传值,后者可以通过 .next() 方法传值
- 除此之外,可以将后者看成前者的手动实现
如何进一步理解异步生成器呢?其实可以看成是为异步函数提供了一种异步返回、多次返回的机制。在非异步生成器函数中,return 只能有一个,且是函数结束的标志。而异步生成器函数就可以做到:间断地返回多个值,不同的返回值之间可以有同步操作也可以有异步操作。这正是集 Generator 和 async-await 的优点于一身,有利于解耦,有利于逻辑的分离。
关于异步迭代器的遍历顺序:完全按照 yield 的顺序来,没有变化。不会因为哪个耗时短而改变顺序。await 也是一样,多个 await 相互之间的顺序是固定的,无法调整,在这里只能串行执行。
关于性能:对于 ES6(+) 本身来说,以上所有的异步方式性能都 OK,但在真实的生产环境中都要由 babel 编译成 ES5 语法,结果会导致代码体积增加,执行过程中会执行另外一段代码,总体性能会低一些。
实验代码:
const asyncFunc1 = () => new Promise((resolve, reject) => {
setTimeout(() => { resolve("async-1") }, 1000);
});
const asyncFunc2 = () => new Promise((resolve, reject) => {
setTimeout(() => { resolve("async-2") }, 1500);
});
const asyncGenerator = async function* () {
const promise1 = asyncFunc1(); // 1000ms
const promise2 = asyncFunc2(); // 1500ms
const res1 = await promise1;
const res2 = await promise2;
yield res1
yield res2;
// const a = yield res1;
// const b = yield res2;
return "这是异步生成器返回值";
};
const iter = asyncGenerator();
const array = [];
/* 通过 for await of 遍历 */
(async () => {
console.time("记时");
for await (const i of iter) {
array.push(i);
console.timeLog("记时");
console.log("遍历", i);
}
console.timeEnd("记时");
console.log("遍历结果", array);
})()
/* 通过循环 .next() 获得 */
// (async() => {
// console.log("手动循环.next()循环")
// while(true) {
// const next = iter.next("next传值");
// console.log("得到next", next);
// const { value, done } = await next;
// console.log(value, done);
// if (done) break;
// }
// })()
来源:oschina
链接:https://my.oschina.net/u/4293959/blog/3410046