# 9、 异步问题大全
# 一、单线程
(1)单线程的概念
如果大家熟悉java,应该都知道,java是一门多线程语言,我们常常可以利用java的多线程处理各种各样的事,比如说文件上传,下载等,而JavaScript是否也可以支持多线程呢?
答案是否定的,JavaScript是一门单线程的语言,因此,JavaScript在同一个时间只能做一件事,单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务,比如说下面这段代码
// 同步代码
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
fun1();
fun2();
// 输出
1
2
很容易看出1、2依次输出,思考:如果是读取文件,或者数据获取呢?还是要依次加载吗?
(2)js为什么是单线程
js的单线程和他的用途有关,js是浏览器的脚本语言,主要用来实现与用户的交互,利用javascript可以实现对Dom进行操作,如果是多线程的话,同时删除和新增一个Dom,浏览器会不知所措
# 二、同步任务和异步任务
(1)为什么有同步任务和异步任务的区别?
因为浏览器在执行任务的时候一次只能执行一个,当遇到耗时很久的任务时,后面的任务只能等前一个加载完才会执行,无疑会严重影响用户体验。所以js设计,当遇到读取文件或者请求数据等需要长时间耗时的任务时会先挂载到任务队列中等待,先去执行后面的任务,等请求完毕再回头执行挂起的任务。
(2)同步任务:主线程上执行排队的任务,打开网站,元素的渲染就是同步任务
(3) 异步任务:不进入主线程,在任务队列中排队,当任务队列通知主线程可以开始了,该任务才会进入主线程执行,像打开网站中图片,音乐的加载就是异步任务。
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
function fun3() {
console.log(3);
}
fun1();
setTimeout(function(){
fun2();
},0);
fun3();
// 输出
1
3
2
有了异步,就算有耗时较长的任务,也不会影响后面的执行。
(4)异步机制
JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念
异步任务不进入主线程,会先进入任务队列,任务队列是先进先出的数据结构,也是一个事件队列,当主线程有空时,就会读取任务队列,排在前面的任务会优先处理,如果该任务指定了回调函数,主任务在执行的时候也会执行这个回调函数。
单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等到,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环
异步机制包含以下几个步骤:
(1)所有同步任务都在主线程上执行,行成一个执行栈
(2)主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
(4)主线程不断的重复上面的第三步
# 三、异步编程
那么,怎么才能实现异步编程,写出性能更好的代码呢,下面有几种方式
(1) 回调函数(2)Promise (3) Generator(2)Async await
下面依次介绍
# 四、回调函数
先看一个ajax小栗子
var req = new XMLHttpRequest();
req.open("GET",url);
req.send(null);
req.onreadystatechange=function(){}
req.send()就是Ajax向服务器发送请求, req.onreadystatechange() 就是事件回调,借由浏览器的HTTP请求线程发起对服务器的请求,得到相应之后触发请求完成事件,将回调函数推到事件队列中等待执行。
另外 setTimeout和为元素绑定监听事件都是相同的道理
总结:
回调函数优点:简单、容易理解和部署
缺点:不利于代码阅读和维护、流程混乱,而且每个任务执行指定一个回调函数,各部分高耦合
# 五、Promise
一直以来js处理异步都是使用callback方式,随着js开发模式的日益成熟,CommonJs顺势提出Promise,而后ES6将其纳入规范
- 含义: Promise是异步编程的一种解决方案,
- 优点: 相比传统回调函数和事件更加合理和优雅,Promise是链式编程(例子),有效的解决了令人头痛的回调地狱问题,Promise的结果有成功和失败两种状态,只有异步操作的结果,可以决定当前是哪一种状态,外界的任何操作都无法改变这个状态 基本用法:
//ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
const p = new Promise(function(resolve,reject){
if(success){
resolve('成功的结果')
}else{
reject('失败的结果')
}
})
p.then(function (res) {
// 接收resolve传来的数据,做些什么
},function (err) {
// 接收reject传来的数据,做些什么
})
p.catch(function (err) {
// 接收reject传来的数据或者捕捉到then()中的运行报错时,做些什么
})
p.finally(function(){
// 不管什么状态都执行
})
- 常用API
- resolve 返回异步操作成功的结果
- reject 返回异步操作失败的结果
- then 执行Promise状态是成功的操作
- catch 执行Promise状态是失败的操作
- finally 不管Promise状态是成功或失败都执行的操作
Promise.all
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3])
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
# Promise包括以下几个规范
- 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
- 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
- promise必须实现
then
方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致 - then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用,同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象
检测浏览器是否支持Promise,可以在其控制台打印
typeof(Promise)==="function" // true 支持
简单的栗子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败
function test(resolve,reject){
const timeOut = Math.random() *2
console.log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
console.log('call resolve()...');
resolve('200 OK');
}
else {
console.log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}
test()
函数只关心自身的逻辑,并不关心具体的resolve
和reject
将如何处理结果。
new Promise(test).then(function (result) {
console.log('成功:' + result);
}).catch(function (reason) {
console.log('失败:' + reason);
});
可见:Promise最大的好处是在异步执行的流程中,把执行的代码和处理结果的代码,清晰的分离了
# 串行执行异步任务
job1.then(job2).then(job3).catch(handleError);
// 0.5秒后返回input*input的计算结果:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}
// 0.5秒后返回input+input的计算结果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}
var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(123);
});
p.then(multiply)
.then(add)
.then(multiply)
.then(add)
.then(function (result) {
log('Got value: ' + result);
});
Log:
// start new Promise...
// calculating 123 x 123...
// calculating 15129 + 15129...
// calculating 30258 x 30258...
// calculating 915546564 + 915546564...
// Got value: 1831093128
# 并行执行异步任务
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()
实现如下:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()
实现:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
# 思考:Promise到底解决了什么问题 ?
1、什么是Promise? Promise 是异步编程的一种解决方案
2、Promise的优点?
- 在异步执行的流程中,将执行代码和处理代码处理结果的代码清晰的分离了
- 解决了回调地狱的问题
- 可以执行更多的异步操作:如多个异步任务并行处理(Promise.all),多个异步任务串行处理(Promise.race)
3、Promise如何解决这两个问题
解决可读性问题:清楚的记录异步任务的执行步骤和逻辑
解决信任问题(同步执行、调用过早的问题):Promise有这些特征:只能决议一次,决议值只能有一个,决议之后无法改变。任何then中的回调也只会被调用一次。Promise的特征保证了Promise可以解决信任问题。
原理是:Promise反转了控制反转,普通的回调函数将成功后的操作写到了回调函数中,这些操作由第三方操作,而Promise将成功后的回调放到了.then中,由Promise精准控制
对于回调过晚或没有调用的问题,Promise本身不会回调过晚,只要决议了,它就会按照规定运行。至于服务器或者网络的问题,并不是Promise能解决的,一般这种情况会使用Promise的竞态APIPromise.race加一个超时的时间:
function timeoutPromise(delay) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject("Timeout!");
}, delay);
});
}
Promise.race([doSomething(), timeoutPromise(3000)])
.then(...)
.catch(...);
# 六、Generator函数
Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行。
# 1、generator函数
function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
console.log(h.next());//{ value: 'hello', done: false }
console.log(h.next());//{ value: 'generator', done: false }
console.log(h.next());//{ value: 'undefined', done: true }
*2、Generator 函数与*迭代器(Iterator)
特征:
- 一是,function关键字与函数名之间有一个星号;
- 二是,函数体内部使用yield表达式,定义不同的内部状态;
- 三是,通过next方法获取每段状态的返回结果,上面分成4次执行了Gennrator函数,第一次,获取第一个yield函数的返回结果并暂 停,done:false,代表函数内的状态还没有执行结束;第二次,同理;第三次,获取return 的返回结果,done:true表示函数内的状态已经执行完毕;第四次,函数内已经没有可以执行的状态,所有返回undfined,同时告诉函数内的状态已经执行完毕;如果函数内没有return,在第三次时返回undfined,done:true表示函数内的状态已经执行完毕
迭代器接口的对象都可以for-of实现遍历
function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
for(var value of h){
console.log(value);//"hello","generator"
}
总结Generator它的特点,一句话:可以随心所欲的交出和恢复函数的执行权,yield交出执行权,next()恢复执行权。我们举几个应用场景的实例。
下面我们利用饭店肚包鸡的制作过程来说明,熊大去饭店吃饭,点了只肚包鸡,然后就美滋滋的玩着游戏等着吃鸡。这时后厨就开始忙活了,后厨只有一名大厨,还有若干伙计,由于大厨很忙,无法兼顾整个制作过程,需要伙计协助,于是根据肚包鸡的制作过程做了如下的分工。
肚包鸡的过程:准备工作(宰鸡,洗鸡,刀工等)->炒鸡->炖鸡->上料->上桌
大厨很忙,负责核心的工序:炒鸡,上料
伙计负责没有技术含量,只有工作量的打杂工序:准备工作,炖鸡,上桌
1、协程:
//大厨的活
function* chef(){
console.log("fired chicken");//炒鸡
yield "worker";//交由伙计处理
console.log("sdd ingredients");//上料
yield "worker";//交由伙计处理
}
//伙计的活
function* worker(){
console.log("prepare chicken");//准备工作
yield "chef";//交由大厨处理
console.log("stewed chicken");
yield "chef";//交由大厨处理
console.log("serve chicken");//上菜
}
var ch = chef();
var wo = worker();
//流程控制
function run(gen){
var v = gen.next();
if(v.value =="chef"){
run(ch);
}else if(v.value =="worker"){
run(wo);
}
}
run(wo);//从伙计开始执行
// 打印结果
// prepare chicken
// fired chicken
// stewed chicken
// sdd ingredients
// serve chicken
2、异步编程
Generator函数,官方给的定义是"Generator函数是ES6提供的一种异步编程解决方案"。我认为它解决异步编程的两大问题
- 回调地狱
- 异步流控
接下来用普通方法制作
setTimeout(function(){
console.log("prepare chicken");
setTimeout(function(){
console.log("fired chicken");
setTimeout(function(){
console.log("stewed chicken");
....
},500)
},500)
},500)
用setTimeout方法来模拟异步过程,这种层层嵌套就是回调地狱,就是回调地狱,Promise就是解决这种回调的解决方案,有兴趣的可以作为练习,用Promise修改这个例子。
我们用Generator来实现:
//准备
function prepare(sucess){
setTimeout(function(){
console.log("prepare chicken");
sucess();
},500)
}
//炒鸡
function fired(sucess){
setTimeout(function(){
console.log("fired chicken");
sucess();
},500)
}
//炖鸡
function stewed(sucess){
setTimeout(function(){
console.log("stewed chicken");
sucess();
},500)
}
//上料
function sdd(sucess){
setTimeout(function(){
console.log("sdd chicken");
sucess();
},500)
}
//上菜
function serve(sucess){
setTimeout(function(){
console.log("serve chicken");
sucess();
},500)
}
//流程控制
function run(fn){
const gen = fn();
function next() {
const result = gen.next();
if (result.done) return;//结束
// result.value就是yield返回的值,是各个工序的函数
result.value(next);//next作为入参,即本工序成功后,执行下一工序
}
next();
};
//工序
function* task(){
yield prepare;
yield fired;
yield stewed;
yield sdd;
yield serve;
}
run(task);//开始执行
// 打印结果
// prepare chicken
// fired chicken
// stewed chicken
// sdd ingredients
// serve chicken
我们分析下执行过程:
1、每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。
2、首次执行next()方法,gen.next()的value,即result.value返回的是prepare函数对象,执行result.value(next),即执行prepare(next);prepre执行完成后,继续调用其入参的next,即下一步工序,
3、以此类推,完成整个工序的实现。
从上面例子看,task方法将各类工序"扁平化",解决了层层嵌套的回调地狱;run方法,使各个工序同步执行,实现了异步流控。
# 七、Async 、 await
1、Async
async function helloAsync(){
return "helloAsync";
}
console.log(helloAsync())*//Promise {: "helloAsync"}*
申明async方法比较简单,只需要在普通的函数前加上"async"关键字即可。我们执行下这个函数,发现并没有返回字符串"helloAsync",而是通过Promise.resolved()将字符串封装成了一个Promise对象返回。
既然是返回的Promise对象,我们就是用then方法来处理。
async function helloAsync(){
return "helloAsync";
}
helloAsync().then(v=>{
console.log(v);//"helloAsync"
})
2、Await
在Generator章节中我们熟悉了yield关键字,yield关键字只能使用在Generator函数中,同样,await关键字也不能单独使用,是需要使用在async方法中。 await字面意思是"等待",那它是在等什么呢?它是在等待后面表达式的执行结果。
function testAwait(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("testAwait");
resolve();
}, 1000);
});
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
使用await 会阻塞到这里,等待testAwait的执行结果完成后在进行下面的操作
执行下,1s后打印了下面的日志。
当await后面不是Promise的时候
function testAwait(){
setTimeout(function(){
console.log("testAwait");
}, 1000);
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync()
// helloAsync
// testAwait
await后面跟着非promise的时候,await等待函数或者直接量的返回而不是执行结果
当Promise执行rejected这种状态的时候,
function testAwait(){
return Promise.reject("error");
}
async function helloAsync(){
await testAwait();
console.log("helloAsync"); //没有打印
}
helloAsync().then(v=>{
console.log(v);
}).catch(e=>{
console.log(e); //"error"
})
当rejected时候出错了,导致接下来的内容不会打印
如果想打印怎么办?
使用try .. catch在函数内部捕获异常。
function testAwait(){
return Promise.reject("error");
}
async function helloAsync(){
try{
await testAwait();
}catch(e){
console.log("this error:"+e)//this error:error
}
console.log("helloAsync"); //helloAsync
}
helloAsync().then(v=>{
}).catch(e=>{
console.log(e);//没有打印
});
异常被try...catch捕获后,继续执行下面的代码,没有导致中断。
三、应用场景
上面说到,await可以阻塞主函数,直到后面的Promise对象执行完成。这个特性就能很轻松的解决按顺序控制异步操作,即我们前一章节讲的异步流程的问题。
在Generator章节的肚包鸡的制作过程的实例,我们用async/await来重写这个例子,并比较下两者实现的区别。
//准备
function prepare(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("prepare chicken");
resolve();
},500)
});
}
//炒鸡
function fired(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("fired chicken");
resolve();
},500)
});
}
//炖鸡
function stewed(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("stewed chicken");
resolve();
},500)
});
}
//上料
function sdd(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("sdd chicken");
resolve();
},500)
});
}
//上菜
function serve(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("serve chicken");
resolve();
},500)
});
}
async function task(){
console.log("start task");
await prepare();
await fired();
await stewed();
await sdd();
await serve();
console.log("end task");
}
task()
1、首先每个制作异步过程封装成Promise对象。
2、利用await阻塞原理,实现每个制作的顺序执行。
相比较Generator实现,无需run流程函数,完美的实现了异步流程。
四、总结
从Promise到Generator,再到async,对于异步编程的解决方案越来越完美,这就是ES6不断发展的魅力所在。
# 八、Generator函数与async函数对比
优点: 相比Generator函数,async函数有如下四点改进
- 内置执行器: Generator 函数的执行必须靠next()进行每一次的模块执行,async自带执行器,只需要和普通函数一样调用即可执行
- 更好的语义async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 返回值是Promise: async函数的返回值是 Promise 对象,可以用then方法指定下一步的操作;而且async函数完全可以看做多个异步函数的操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖,即Promise.all()的用法
- **更广的适用性:**相较于Generator函数async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
参考资料: