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 和 allSettled 的用法:(这三个方法都是将若干个 Promise 实例,包装成一个新的 Promise 实例)
- all 接收一个 promise 对象数组,在所有异步操作执行完且全部成功的时候才执行 then 回调,只要有一个失败,就执行 catch 回调(只对第一个失败的promise 对象执行)。
- race 也接收一个 promise 对象数组,不同的是,哪个最先执行完,对应的那个对象就执行 then 或 catch 方法( then 或 catch 只执行一次)。
- allSettled 同样接收一个 promise 对象数组。当所有的 promise 对象都解决时(无论是 resolve 还是 reject ),才执行 then 回调,它带来了“我只要兑现所有承诺,我不在乎结果”。
/* 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; // } // })()