由于JS运行环境是单线程的,即一次只能完成一个任务,所以多任务时需要排队。异步可以理解为改变执行顺序的操作,异步任务必须在同步任务执行结束之后,从任务队列中依次取出执行。
js常见的异步方法有四种:
1,回调函数callback
函数B作为函数A的入参,且函数A执行了函数B,此时我们把函数A叫做回调函数。(ajax、setTimeout、dom事件回调等都是回调函数)
例如:
function A(callback) { console.log("A"); callback();//函数A执行了函数B } function B() { console.log("B"); } A(B);//函数B作为函数A的入参
function A(callback){ console.log("A"); callback("param B");//函数A执行了函数"B",给函数"B"入参"param B" } A((val) => { //函数"B"作为函数A的入参,"B"有一个入参val console.log(val);//输出"param B" })
缺点:高耦合,结构混乱,回调函数不能使用 try catch 捕获错误,不能直接 return
2,Promise
当回调的层级较少时,回调函数可以满足我们的需求,但是当层级越来越多,回调越来越多,例如吃饭,要吃饭就要先做饭,要做饭就要先淘米,要淘米就要先买米,要买米就要去菜场(或者APP),去菜场就要坐车,坐车就要准备公交卡……如此形成回调地狱!(下一篇再说回调地狱吧)
Promise很好地解决了回调地狱的问题,它包含三种状态:pending、fulfilled(resolved)、rejected。pending是promise的初始状态,resolved表示执行完成且成功的状态,rejected表示执行完成且失败的状态。三个状态不可逆转。
Promise本身是同步,then的内容是异步:
let p = new Promise((resolve,reject) => { console.log("promise本身是同步"); resolve("then是异步"); }).then((res) => { console.log(res); }) console.log("想不到吧");
输出:
then接收两个回调函数,一个表示成功,一个表示失败:
let p = new Promise((resolve,reject) => { console.log("promise本身是同步"); reject("then是异步"); }).then((res) => { console.log(res); },(err) => { console.log(err); }) console.log("想不到吧");
第二个回调也可以使用catch:
let p = new Promise((resolve,reject) => { console.log("promise本身是同步"); reject("catch是异步"); }).then((res) => { console.log(res); }).catch((err) => { console.log(err); }) console.log("想不到吧")
promise解决回调地狱的问题的方法是链式调用:
function testP(val) { return new Promise((resolve, reject) => { resolve(val); }); } testP("0").then(res1 => { console.log(res1); //输出0 return testP("1"); }).then(res2 => { console.log(res2); //输出1 return testP("2"); }).then(res3 => { console.log(res3); //输出2 return testP("3"); }).catch(err => { console.log(err); });
首先testP("0")创建了一个promise,进入pending状态,当resove后进入fulfilled状态,第一个then中输出0并返回一个promise,进入pending状态,当resove后进入fulfilled状态,第二个then中输出1并返回一个promise......以此类推。
我们修改函数testP:
function testP(filename){ return new Promise((resolve,reject) => { fs.readFile(filename, (err, data) => { if(err) { reject(err); }else { resolve(data); } }) }) } testP(url).then(res1 => { return testP(url1); }).then(res2 => { return testP(url2); }).then(res3 => { return testP(url3); }).catch(err => { console.log(err); });
testP返回一个promise,then中传入回调函数,上文我们得出结论promise本身是同步,then是异步,then延迟传入回调函数,此为回调函数延迟绑定。
继续改写testP:
function testP(filename){ return new Promise((resolve,reject) => { fs.readFile(filename, (err, data) => { if(err) { reject(err); }else { resolve(data); } }) }) } let step1 = testP(url).then(res => { return testP(url1); }).catch((err) => { console.log(err); }) let step2 = step1.then((res) => { return testP(url2) }).catch((err) => { console.log(err); }) let step3 = step2.then((res) => { return testP(url3) }).catch((err) => { console.log(err); }) step3.then((res) => {}).catch((err) => { console.log(err); })
将每一个then拆开,step1/step2/step3都是上文链式调用的内部返回,在step1/step2/step3之后都可以继续完成之后的链式调用,此为返回值穿透。
在step1/step2/step3中都进行了catch捕获异常(即rejected状态),链式调用时,rejected状态的异常信息会一直往后传递,直到被catch接收到,所以不用频繁的catch,此为错误冒泡。
缺点:无法取消 Promise。
3,Generator函数(ES6)
Generator 是一个可以暂停执行(分段执行)的函数,函数名前面要加星号,是一个状态机,封装了多个内部状态。
function *myGenerator() { yield 'hello'; yield 'world'; yield 'wawawa'; return 'amazing'; } let mg = myGenerator();
调用myGenerator()但并不会执行,它返回一个指向函数内部的指针对象。调用对象的next()方法使指针指向下一个,每次next()会从上一次暂停的地方继续执行,直到遇到yield或者return,Generator分段执行,yield是标记暂停的地方,next表示恢复执行,每次next返回的是一个对象,包含value和done,value的值是yield表达式后面的值,done表示是否执行完毕。
输出:
缺点:手动迭代。
4,async/await(ES7)
基于Promise实现,使异步代码看起来更像同步代码。
async修饰符加在函数前面,返回一个promise,可以使用then添加回调函数。
await后跟着一个promise或者一个原始类型的值(会自动转成立即 resolved 的 Promise 对象),等待resolve的结果。任何一个await后的Promise发生reject,整个aysnc都会中断,需要try{}catch(err){}来捕获错误。
async function mysw(){ return 1; } mysw();//返回一个promise,其中PromiseResult是1
async function mysw(){ return new Promise((resolve,reject) => { resolve(1) }); } mysw(); //同上
async function mysw(){ await 1; } mysw(); //返回一个promise,其中PromiseResult是undefined
async function mysw(){ let a = await 1; //1会转成resolve(1)的promise console.log(a); } mysw(); //输出1,返回PromiseResult是1的promise
async function mysw(){ let a = await new Promise((resolve) => { //同上 resolve(1) }); console.log(a); } mysw();//同上
async function mysw(){ let a = await new Promise((resolve) => { resolve(1) }); console.log(a) return 1; } mysw(); //输出1,返回一个PromiseResult值为1的promise
async function mysw(){ let a = await new Promise((resolve) => { resolve("a"); }); console.log(a); return 1; } mysw().then((res) => { console.log(res);//输出a,1,返回一个PromiseResult为undefined的promise });
async function mysw(){ try { let a = await new Promise((resolve) => { resolve(x); }); console.log(a); } catch(err) { console.log(err); } return 1; } mysw();//ReferenceError: x is not defined
看起来async/await和Generator用法相似,把星号换成async,把yield换成await,它们区别在于:
1,async函数自带执行器
2,语义清楚
3,async函数的返回值是 Promise 对象,Generator 函数的返回值是 Iterator 对象
4,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
await语句后面的内容需要等待await的内容执行完才能执行(宏任务除外),可以把await当成是then的语法糖,await之后的内容就相当于then里的回调函数,是异步,根据先微后宏的顺序继续执行(事件循环在后面讲哈)