第16章异步编程node
随着计算机的不断发展,用户对计算机应用的要求愈来愈高,须要提供更多、更智能、响应速度 更快的功能。这就离不开异步编程的话题。同时,随着互联网时代的崛起,网络应用要求可以支 持更多的并发量,这显然也要用到大量的异步编程。那么从这节课开始,咱们会学习到底什么是 异步编程,以及在JS中如何实现异步编程。编程
本章咱们将学习以下内容:安全
・什么是异步编程。网络
•回调和Promise。多线程
•生成器 Generator。并发
• ES7中的异步实现Async和Await。异步
16-1异步编程概述异步编程
16-1-1什么是异步编程?函数
咱们先来看看到底什么是异步。提到异步就不得不提另一个概念:同步。那什么又叫同步呢。 不少初学者在刚接触这个概念时会想固然的认为同步就是同时进行。显然,这样的理解是错误 的,咱不能按字面意思去理解它。同步,英文全称叫作Synchronization。它是指同一时间只能作 —件事,也就是说一件事情作完了才能作另一件事。学习
好比我们去火车站买票,假设窗口只有1个,那么同一时间只能处理1我的的购票业务,其他的需 要进行排队。这种one by one的动做就是同步。这种同步的状况其实有不少,任何须要排队的情 况均可以理解成同步。那若是在程序中呢,咱们都知道代码的执行是一行接着一行的,好比下面 这段代码:
let ary = [];
for(let i = 0;i < 100;i++){
ary[i] = i;
}
console.log(ary);
这段代码的执行就是从上往下依次执行,循环没执行完,输出的代码就不会执行,这就是典型的 同步。在程序中,绝大多数代码都是同步的。
同步操做的优势在于作任何事情都是依次执行,井井有理,不会存在你们同时抢一个资源的问
题。你想一想,若是火车站取消排队机制,那么你们势必会争先恐后去抢着买票,形成的结果就是 秩序大乱,甚至可能引起一系列安全问题。若是代码不是同步执行的又会发生什么呢?有些代码 须要依赖前面代码执行后的结果,但如今你们都是同时执行,那结果就不必定能获取到。并且这 些代码可能在对同一数据就进行操做,也会让这个数据的值出现不肯定的状况。
固然同步也有它的缺点。因为是依次进行,假如其中某一个步骤花的时间比较长,那么后续动做 就会等待它的完成,从而影响效率。
不过,在有些时候咱们仍是但愿可以在效率上有所提高,也就是说可让不少操做同时进行。这 就是另一个概念:异步。假设火车站有10我的须要买票,如今只有1个窗口提供服务,若是平 均每一个人耗费5分钟,那么总共须要50分钟才能办完全部人的业务。火车站为了提升效率,加开 了 9个窗口,如今一共有10个窗口提供服务,那么这10我的就能够同时办理了,总共只须要5分 钟,他们全部人的业务均可以办完。这就是异步带来的优点。
16-1-2异步的实现
像刚才例子中开多个窗口的方式称为多线程。线程能够理解成一个应用程序中的执行任务,每一个 应用程序至少会有一个线程,它被称为主线程。若是你想实现异步处理,就能够经过开启多个线 程,这些线程能够同时执行。这是异步实现的一种方式。不过这种方式仍是属于阻塞式的。
什么叫作阻塞式呢。你想一想,开10个窗口能够知足10我的同时买票。可是如今有100我的呢?不 可能再开90个窗口吧,因此每一个窗口实际上仍是须要排队。也就是说虽然我能够经过开启多个线 程来同时执行不少任务,可是每一个任务中的代码仍然是同步的。当某个任务的代码执行时间过 长,也只会影响到当前线程的代码,而其余线程的代码不会受到影响。
假设如今火车站不想开那么多窗口,仍是只有1个窗口提供服务,那如何可以提升购票效率呢? 咱们能够这样作,把购票的流程分为两步,第一步:预约及付款。第二步:取票。其中,第一步 可让购票者在网上操做。第二步到火车站的窗口取票。这样,最耗时的工做已经提早完成,不 须要排队。到火车站时,虽然只有1个窗口,1次也只能接待1我的,可是取票的动做很快,平均 每一个人耗时不到1分钟,10我的也就不到10分钟就能够处理完成。这样既提升了效率,又少开了 窗口。这也是一种异步的实现。咱们能够看到,开1个窗口,就至关于只有1个线程。而后把耗时 的一些操做分红两部分,先把快速能作完的事情作了,这样保证它不会阻塞其余代码的运行。剩 下耗时的部分再单独执行。这就是单线程阻塞式的异步实现机制。
16-1-3 JS中的异步实现
咱们知道JS引擎就是以单线程的机制来运行代码。那么在JS代码中想要实现异步就只有采用单
线程非阻塞式的方式。好比下面这段代码:
console.log("start");
setTimeout(function(){ console.log("timeout");
},5000);
console.log("end");
这段代码先输出一个字符串"start",而后用时间延迟函数,等到5000秒钟后输出"timeout",在代 码的最后输出"end"。最后的执彳丁结果是:
start
end
//等待5秒后
timeout
从结果能够看到end的输出并无等待时间函数执行完,实际上setTimeout就是异步的实现。代 码的执行流程以下:
首先执行输出字符串"start",而后开始执行setTimeout函数。因为它是一个异步操做,因此它会 被分为两部分来执行,先调用setTimeout方法,而后把要执行的函数放到一个队列中。代码继续 往下执行,当把全部的代码都执行完后,放到队列中的函数才会被执行。这样,全部异步执行的 函数都不会阻塞其余代码的执行。虽然,这些代码都不是同时执行,可是因为任何代码都不会被 阻塞,因此执行效率会很快。
你们认真看这个图片,而后思考一个问题:当setTimeout执行后,何时开始计时的呢?因为 单线程的缘由,不可能在setTimeout后就开始执行,由于一个线程同一时间只能作一件事情。执 行后续代码的同时就不可能又去计时。那么只多是在全部代码执行完后才开始计时,而后5秒 后执行队列中的回调函数,是这样吗?咱们用一段代码来验证下:
console.log("start");
setTimeout(function(){
console.log("timeout");
},5000);
for(let i = 0;i <= 500000;i++){
console.log("i:",i);
}
console.log("end");
这段代码在以前的基础上加了一个循环,循环次数为50万次,而后每次输出i的值。这段循环是 比较耗时的,从实际运行来看,大概须要14秒左右(具体时间可自行测算)。这个时间已经远远 大于setTimeout的等待时间。按照以前的说法,应该先把全部同步的代码执行完,而后再执行异 步的回调方法,结果应该是:
start
i:1
(...)//一直输出到500000
//耗时 14秒左右
end
//等待5秒后
timeout
但实际的运行结果是:
start
i:1
(...)//一直输出到500000
//耗时 14秒左右
end
//没有等待
timeout
从结果能够看UsetTimeout的计时应该是早就开始了,可是JS是单线程运行,那谁在计时呢?要 解释这个问题,你们必定要先搞明白一件事。JS的单线程并非指整个JS引擎只有1个线程。它 是指运行代码只有1个线程,可是它还有其余线程来执行其余任务。好比时间函数的计时、AJAX 技术中的和后台交互等操做。因此,实际状况应该是:JS引擎中执行代码的线程开始运行代码,
当执行到异步方法时,把异步的回调方法放入到队列中,而后由专门计时的线程开始计时。代码 线程继续运行。若是计时的时间已到,那么它会通知代码线程来执行队列中对应的回调函数。当 然,前提是代码线程已经把同步代码执行完后。不然须要继续等待,就像这个例子中同样。
最后,你们必定要注意一件事情,因为执行代码只有1个线程,因此在任何同步代码中出现死循 环,那么它后续的同步代码以及异步的回调函数都没法执行,好比:
console.log("start");
setTimeout(function(){
console.log("timeout"); },5000); console.log("end"); for(;;){}
timeout用于也不会输出,由于执行代码的线程已经陷入死循环中。
16-2 Promise实现异步
前面一讲中咱们了解了什么是异步,以及JS中实现异步的原理。这一节我们将学习JS中实现异 步的具体方法。前面咱们已经看到了一个用setTimeout实现的异步操做:
console.log("start");
setTimeout(function(){
console.log("timeout");
},5000);
console.log("end");
16-2-1回调函数
在调用setTimeout函数时咱们传递了一个函数进去,这个函数并无当即被调用,而是在5秒后 被调用。这种函数也被称为回调函数(关于回调函数请参看前面的内容)。因为JS中的函数是一 等公民,它和其余数据类型同样,能够做为参数传递也能够做为返回值返回,因此常常可以看到 回调函数使用。
回调地狱
在异步实现中,回调函数的使用是不可避免的。以前我不是讲过吗,JS的异步是单线程非阻塞式 的。它将一个异步动做分为两步,第一步执行异步方法,而后代码接着往下执行。而后在后面的 某个时刻调用第二步的回调函数,完成后续动做。有的时候,咱们但愿在异步操做中加入同步的 行为。好比,我想打印4句话,可是每句话都在前一句话的基础上延迟2秒输出。代码以下:
setTimeout(function(){
console.log("first");
setTimeout(function(){
console.log("second");
setTimeout(function(){
console.log("third");
setTimeout(function(){
console.log("fourth");
},2000);
},2000);
},2000);
},2000);
这段代码可以实现想要的功能,可是总以为哪里不对。若是输出的内容愈来愈多,嵌套的代码也 会增多。那不管是编写仍是阅读起来都会很恐怖。形成这种状况的罪魁祸首就是回调函数。由于
你想在前面的异步操做完成后再进行接下来的动做,那只能在它的回调函数中进行,这样就会越 套越多,代码愈来愈来复杂,俗称"回调地狱"。
16-2-2 Promise
为了解决这个问题,在ES6中加入了一个新的对象Promise。Promise提供了一种更合理、更强大 的异步解决方案。接下来咱们来看看它的用法。
new Promise(function(resolve,reject){
//dosomething
});
首先须要建立一个Promise对象,该对象的构造函数中接收一个回调函数,回调函数中能够接收 两个参数,resolve和reject。注意,这个回调函数是在Promise建立后就会调用。它实际上就是异 步操做的第一步。那第二步操做再在哪里作呢? Promise把两个步骤分开了,第二步经过Promise 对象的the n方法实现。
let pm = new Promise(function(resolve,reject){
//dosomething
});
console.log("go on");
pm.then(function(){ console.log("异步完成");
});
不过要注意的是,then方法的回调函数不是说只要then方法一调用它就会调用,而是在Promise 的回调函数中经过调用resolve触发的。
let pm = new Promise(function(resolve,reject){
resolve();
});
console.log("go on");
pm.then(function(){ console.log("异步完成");
});
实际上Promise实现异步的原理和以前纯用回调函数的原理是同样的。只是Promise的作法是显示 的将两个步骤分开来写。then方法的回调函数一样会先放入队列中,等待全部的同步方法执行完 后,同时Promise中的resolve也被调用后,该回调函数才会执行。
调用resolve时还能够把数据传递给then的回调函数。
let pm = new Promise(function(resolve,reject){ resolve("this is data");
});
console.log("go on"); pm.then(function(data){
console.log("异步完成",data);
});
效果:
Jie-Xie:desktop Jie$ node 1
go on
异步完成this is data
reject是出现错误时调用的方法。它触发的不是then中的回调函数,而是catch中的回调函数。比 如:
let err = false;
let pm = new Promise(function(resolve,reject){
if(!err){
resolve("this is data");
}else{ reject("fail");
}
}); console.log("go on"); pm.then(function(data){ console.log("异步完成",data);
});
pm.catch( function(err){ console.log("出现错误",e rr);
});
下面,我把刚才时间函数的异步操做用Promise实现一次。固然,其中setTimeout仍是须要使 用,只是在它外面包裹一个Promise对象。
let pm = new Promise(function(resolve,reject){ setTimeout( function(){
resolve();
},2000);
});
console.log("go on");
pm.then(function(){ console.log("异步完成");
});
效果和以前同样,可是代码复杂了很多,感受有点画蛇添足。接下来作作同步效果。
let timeout = function(time){ return new Promise(function(resolve,reject){ setTimeout( function(){
resolve(); },time);
});
}
console.log("go on"); timeout(2000).then(function(){ console.log("first"); return timeout(2000);
}).then(function(){ console.log("second"); return timeout(2000);
}).then(function(){ console.log("third");