JavaScript异步类型
- 延迟类型:setTimeout、setInterval、setImmediate
- 监听事件:监听new Image加载状态、监听script加载状态、监听iframe加载状态、Message
- 带有异步功能类型: Promise、ajax、Worker、async/await
JavaScript常用异步编程
Promise
Promise有几个特点:
- 对象的状态不受外界影响,有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。只有异步操作的结果可以决定当前是哪种状态,其他操作无法改变。
- 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态改变只可能是:pending -> fulfilled 或 pending -> rejected
- 实例化后,会立即执行一次。所以一般将其用函数包裹起来,使用的时候调用一次。
- 如果执行后的回调也要做一些异步操作,可以无限的.then下去,当然要保证有返回值
方法:
- 原型方法 all race reject resolve
- 对象方法 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的用法:
- 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),与Python的语法一模一样
生成器有3个方法,都有一样的返回值{value,done}
- .next(value) 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); });
async/await
优点:简洁,节约了不少代码
- 被async修饰的函数,总会返回一个Promise对象。如果代码中返回值不是promise对象,它会将其包装成promise对象的resolved值
- 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"); } doIt();
注意:
- async/await并没有脱离Promise,它的出现能够更好地协同Promise工作。
- 怎么体现更好地协同?它替代了then catch的写法。使得等待promise值的操作更优雅,更容易阅读和书写。
- 函数仅仅加上async并没有意义,它仍然是同步函数,只有与await结合使用,它才会变成异步函数。
- 这需要精准理解await。它在等待的时候并没有阻塞程序,此函数也不占用CPU资源,使得整个函数做到了异步执行。
- doIt()函数内部是串行执行的,但它本身是异步函数。
- 在这个异步函数内,可能会做很多操作ABC,他们有执行的先后顺序。这时你可能会想,A、B、C之间没有关联,他们之间可以是并行执行的,并不需要串行,那怎么办?
- 【错误想法】这样想没错,但是没必要。因为他们已经存在于异步函数内了,所有的操作已经是异步的。在同样的环境情景下,底层执行的效率是相同的,并不见得因为A和B之间互相异步而提高效率。
- 【正确想法】这样想是有必要的。参照两个doIt(),调用的函数返回promise对象,前者是依次生成promise对象(依次执行任务),依次等待返回结果。等待总时长取决于所有任务执行时间之和。后者则是同时生成promise对象(同时执行任务),依次等待。等待总时长取决于耗时最长的任务。后者的CPU运用率更高。
- 错误处理。最标准的方法是使用try...catch语句,但是它不仅会捕捉到promise的异常,还会将所有出现的异常捕获。因此,可以使用.catch,只会捕获promise相关的异常。