Javascript的异步编程可以说在日常的前端业务开发中举足轻重,常见的异步编程有:回调函数、事件监听、Promise、Generator、Async/await。
那么:
- 同步编程和异步编程有啥区别呢?
- 回调地狱有哪些方法可以解决呢?
- Promise内部究竟有几种状态?
- Promise是如何解决回调地狱的问题?
- Generator执行后返回什么?
- Async/await的方式比Promise和Generatir好在哪里?
同步和异步
同步:就是在执行某段代码时,在该代码没有得到返回结果前,其它代码是阻塞的无法执行,但是一旦执行完成拿到返回值后,就可以执行其它代码了。
异步:就是当某段代码执行异步过程调用发出后,这段代码不会立即得到返回结果,而是挂起在后台执行。在异步调用发出后,一般通过回调函数处理这个调用后才能拿到结果。
前面知道Javascript是单线程的,如果JS都是同步代码执行可能会造成阻塞。如果使用就不会造成阻塞,就不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。
那么JS异步编程的实现方式是如何发展的呢?
早些年为了实现JS的异步编程,一般采用回调函数的方式,如:比较典型的事件回调,但是使用回调函数来实现存在一个很常见的问题,就是回调地狱。看下面的代码像不像俄罗斯套娃。
fs.readFile(a,"utf-8",(err,data)=>{
fs.readFile(b,"utf-8",(err,data)=>{
fs.readFile(c,"utf-8",(err,data)=>{
fs.readFile(d,"utf-8",(err,data)=>{
....
})
})
})
})
常见的异步编程的场景有:
- ajax请求的回调
- 定时器中的回调
- 事件回调
- Node.js中的一些方法回调
异步回调如果层级很少,可读性和代码的维护性暂时还是可以接受的,但是当层级变多后就会陷入回调地狱。
Promise
为了解决回调地狱的问题,社区提出了Promise的解决方案,ES6又将其写入语言标准,采用Promise的实现方式在一定程度上解决了回调地狱的问题。
Promise简单理解就是一个容器,里面保存了某个未来才会结束的事件的结果。从语法而言,Promise是一个可以获取异步操作消息的对象。Promise具有三个状态:
- 待定状态pending:初始状态,既没有被完成,也没有被拒绝
- 已完成fulfilled:操作成功完成
- 已拒绝rejected:操作失败
关于Promise的状态切换,如果想深入研究,可以学习『有限状态机』知识点。
待定状态的Promise对象执行的话,最后要么通过一个值完成,要么就是通过一个原因拒绝。当待定状态改成为完成或拒绝状态时,我们可以使用Promise.then的形式进行链式调用。因为最后Promise.prototype.then和Promise.prototype.catch方法返回的是一个Promise,所以它们可以继续被链式调用。
Promise是如何结局回调地狱问题的?
- 解决多层嵌套问题
- 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性
Promise主要利用三大技术来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡
Promise.all
Promise.all(iterable)可以传递一个可迭代对象作为参数,此方法对于汇总多个Promise的结果很有用,在es6中可以将多个Promise.all异步请求并行操作。当所有结果成功返回时按照顺序返回成功,当其中一个方法失败则进入失败方法。
Promise.all(iterable);
使用Promise.all解决上面的异步编程问题。
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,"utf-8",(err,data)=>{
if(err) return err;
resolve(data);
})
})
}
read(A).then(data=>{
return read(B);
}).then(data=>{
return read(C);
}).then(data=>{
return read(D);
}).catch(reason=>{
console.log(reason);
})
我们看到上面使用Promise的使用对回调地狱的解决有所提升,但是依旧不是很好维护,对此有了新的方法。
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,"utf-8",(err,data)=>{
if(err) return err;
resolve(data);
})
})
}
//通过Promise.all可以实现多个异步并行执行,同一时刻获取最终解决的问题
Promise.all([read(A),read(B),read(C)]).(data=>{
console.log(data)
}).catch(reason=>{
console.log(reason);
})
Promise.allSettled
Promise.allSettled的语法和Promise.all类似,都是接受一个可迭代对象作为参数,返回一个新的Promise。当Promise.allSettled全部处理完毕后,我们可以拿到每个Promise的状态,而不管其是否处理成功。
Promise.allSettled(iterable);
Promise.any
Promise.any也是接收一个可迭代对象作为参数,any方法返回一个Promise。只要参数Promise实例有一个变成fulfilled状态,最后any返回的实例就会变成fullfiled状态;如果所有参数Promise实例都变成rejected状态,最后any返回的实例就会变成rejected状态。
Promise.race
Promise.race接收一个可迭代对象作为参数,race方法返回一个Promise,只要参数之中有一个实例率先改变状态,则race方法的返回状态就跟着改变。
Promise方法 | 作用 |
---|---|
all | 参数所有返回结果都为成功才返回 |
allSettled | 参数无论返回结果是否成功,都返回每个参数执行状态 |
any | 参数中只要有一个成功,就返回该成功的执行结果 |
race | 返回最先执行成功的参数的执行结果 |
Generator
Generator生成器是es6的新关键词,Generator是一个带星号的函数,可以配合yield关键字来暂停或执行函数。
Generator最大的特点就是可以交出函数的执行权,Generator函数可以看作是异步任务的容器,需要暂停的地方使用yield语法进行标注。
function* gen(){
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
t.next(1);//第一调用next函数时,传递的参数无效,因此无法打印结果
t.next(2);//2
t.next(3);//3
t.next(4);//4
t.next(5);//5
上面代码中,调用gen()后程序会被阻塞住,不会执行任何语句;而调用g.next()后程序会继续执行,直到遇到yield关键词时执行暂停;一直执行next方法,最后返回一个对象,其存在两个属性:value和done。
yield也是es6的关键词,配合Generator执行以及暂停,yield关键词最后返回一个迭代器对象,该对象有value和done两个属性,value表示返回的值,done便是当前是否完成。
function* gen(){
yield 1;
yield* gen2();
yield 4;
}
function* gen2(){
yield 2;
yield 3;
}
const g = gen();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
运行结果:
那么,Generator和异步编程有着什么联系呢?泽呢么才能将Generator函数按照顺序一次执行完毕呢?
thunk函数
thunk函数的基本思路就是接收一定的参数,会产生触定制化的函数,最后使用定制化的函数去完成想要实现的功能。
const isType = type => {
return obj => {
return Object.prototype.toString.call(obj) === `[object ${type}]`;
}
}
const isString = isType("string");
const isArray = isType("Array");
isString("yichuan");//true
isArray(["red","green","blue"]);//true
const readFileThunk = filename=>{
return callback=>{
fs.readFile(filename,callback);
}
}
const gen = function* (){
const data1 = yield readFileThunk("a.txt");
console.log(data1.toString());
const data2 = yield readFileThunk("b.txt");
console.log(data2.toString());
}
const g = gen();
g.next().value((err,data1)=>{
g.next(data1).value((err,data2)=>{
g.next(data2);
})
})
我们可以看到上面的代码还是像俄罗斯套娃,理解费劲,我们进行优化以下:
function fun(get){
const next = (err,data)=>{
const res = gen.next(data);
if(res.done) return;
res.value(next);
}
next();
}
run(g);
co函数库是用于处理Generator函数的自动执行,核心原理是前面讲到的通过和thunk函数以及Promise对象进行配合,包装成一个库。
Generator函数就是一个异步操作的容器,co函数接收Generator函数作为参数,并最后返回一个Promise对象。在返回的Promise对象中,co先检查参数gen是否为Generator函数。如果是就执行函数,如果不是就直接返回,并将Promise对象的状态改为resolved。co将Generator函数的内部指针对象的next方法包装成onFulfilled函数,主要是为了能够捕获到抛出的错误。关键在于next,他会反复调用自身。
const co = require("co");
const g = gen();
co(g).then(res=>{
console.log(res);
})
Async/await
JS异步编程从最开始的回调函数的方式演化到使用Promise对象,再到Generator+co函数的方式,每次都有一些改变但是都不彻底。async/await被称为JS中异步终极解决方案,既能够像Generator+co函数一样用同步方式阿里写异步代码,又能够得到底层的语法支持,无需借助任何第三方库。
async是Generator函数的语法糖,async/await的优点是代码清晰,可以处理回调的问题。
function testWait(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log("testWait");
resolve();
},1000);
})
}
async function testAwaitUse(){
await testWait();
console.log("hello");
return "yichuan";
}
//输出顺序依次是:testWait hello yichuan
console.log(testAwaitUse());
异步编程方式小结
JS异步编程方式 | 简单总结 |
---|---|
回调函数 | 最拉胯的异步编程方式 |
Promise | es6新增语法,解决回调地狱问题 |
Generator | 和yield配合使用,返回的是迭代器 |
async/await | 二者配合使用,async返回的是Promise对象,await控制执行顺序 |
参考文章
- 《Javascript核心原理精讲》
- 《Javascript高级程序设计》
- 《你不知道的Javascrtipt》
- 《JS 异步编程六种方案》
写在最后
本文主要介绍了Javascript的最重要的知识点之一,也是之后开发工作中经常要接触的概念,常用的异步编程方式有:回调函数、Promise、Generator和async/await。频繁使用回调函数会造成回调地狱,Promise的出现就是解决回调地狱的,但是Promise的链式函数也有长,对于出现了async/await的终极解决方案。