JavaScript中异步编程问题能够说是基础中的重点,也是比较难理解的地方。首先要弄懂的是什么叫异步?javascript
咱们的代码在执行的时候是从上到下按顺序执行,一段代码执行了以后才会执行下一段代码,这种方式叫同步(synchronous)执行,也是咱们最容易理解的方式。可是在某些场景下:java
上面这些场景可能很是耗时,并且时间不定长,这时候这些代码就不该该同步执行了,先执行能够执行的代码,在将来的某个时间再来执行他们的handler,这就是异步。node
经过这篇文章咱们来了解几个知识点:git
先作些准备工做,补一补一些很是重要的前置的概念。es6
一个程序(program)至少包含一个进程(process),一个进程至少包含一个线程(thread)。github
进程有如下特色:web
线程有如下特色:面试
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分能够同时执行。但操做系统并无将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。 这就是进程和线程的重要区别。
画张图来简单描述下:
全部的程序都要交给CPU实现计算任务,可是CPU一个时间点只能处理一个任务。这时若是多个程序在运行,就涉及到了《操做系统原理》中重要的线程调度算法,线程是CPU轮转的最小单位,其余上下文信息用所在进程中的。ajax
进程是资源的分配单位,线程是CPU在进程内切换的单位。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器一般由如下常驻线程组成:算法
Javascript是单线程的,那么为何Javascript要是单线程的?
这是由于Javascript这门脚本语言诞生的使命所致:JavaScript为处理页面中用户的交互,以及操做DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。若是JavaScript是多线程的方式来操做这些UI DOM,则可能出现UI操做的冲突; 若是Javascript是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操做一个DOM,一个负责修改一个负责删除,那么这个时候就须要浏览器来裁决如何生效哪一个线程的执行结果。固然咱们能够经过锁来解决上面的问题。但为了不由于引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。
这时候再理解阻塞非阻塞就好理解了,对于异步任务,单线程的JavaScript若是什么也不干等待异步任务结束,这种状态就是阻塞的;若是将异步消息放到一边,过会再处理,就是非阻塞的。
请求不能当即获得应答,须要等待,那就是阻塞;不然能够理解为非阻塞。
生活中这种场景太常见了,上厕所排队就是阻塞,没人直接上就是非阻塞。
由于JavaScript是单线程的,每一个时刻都只能一个事件,因此JavaScript中的同步和异步事件就有了一个奇妙的执行顺序。
JavaScript在运行时(runtime)会产生一个函数调用栈,先入栈的函数先被执行。可是有一些任务是不须要进入调用栈的,这些任务被加入到消息队列中。当函数调用栈被清空时候,就会执行消息队列中的任务(任务总会关联一个函数,并加入到调用栈),依次执行直至全部任务被清空。因为JavaScript是事件驱动,当用户触发事件JavaScript再次运行直至清空全部任务,这就是事件循环。
函数调用栈中的任务永远优先执行,调用栈无任务时候,遍历消息队列中的任务。消息队列中的任务关联的函数(通常就是callback)放入调用栈中执行。
举两个例子:异步请求
function ajax (url, callback){ var req = new XMLHttpRequest(); req.onloadend = callback; req.open('GET', url, true); req.send(); }; console.log(1); ajax('/api/xxxx', function(res){ console.log(res); }); console.log(2);
一个开发常常遇到的业务场景,异步请求一个数据,上述过程用图表示:
图中三条线分别表示函数执行的调用栈,异步消息队列,以及请求所依赖的网络请求线程(浏览器自带)。执行顺序:
console.log(1);
。ajax
方法,方法里面配置XMLHttpRequest
的回调函数,并交由线程执行异步请求。console.log(2);
。function(res){console.log(res);}
。定时器任务:
console.log(1); setTimeout(function(){ console.log(2); }, 100); setTimeout(function(){ console.log(3); }, 10); console.log(4); // 1 // 4 // 3 // 2
跟上面的例子很像,只不过异步请求变成了定时器,上述代码的指向过程图:
执行顺序以下:
console.log(1);
。setTimeout
向消息队列添加一个定时器任务1。setTimeout
向消息队列添加一个定时器任务2。console.log(4);
。console.log(3);
。console.log(2);
。经过上面例子能够很好理解,就像工做中你正在作一件事情,这时候领导给你安排一个不着急的任务,你停下来跟领导说'等我忙完手里的活就去干',而后把手里的活干完去干领导安排的任务。全部任务完成至关于完成了一个事件循环。
macrotask 和 microtask 都是属于上述的异步任务中的一种,分别是一下 API :
setTimeout
, setInterval
, setImmediate
, I/O, UI renderingprocess.nextTick
(node), Promises
, Object.observe
(废弃), MutationObserver
setTimeout
的 macrotask ,和 Promise
的 microtask 有什么不一样呢:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // "script start" // "script end" // "promise1" // "promise2" // "setTimeout"
这里的运行结果是Promise
的当即返回的异步任务会优先于setTimeout
延时为0的任务执行。
缘由是任务队列分为 macrotasks 和 microtasks,而Promise
中的then
方法的函数会被推入 microtasks 队列,而setTimeout
的任务会被推入 macrotasks 队列。在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。
因此上面实现循环的顺序:
并发咱们应该常常听过,跟他相似的一个词叫并行。
并发:多个进程在一台处理机上同时运行,一个时间段内处理多件事情,宏观上比如一我的边唱边跳,微观上这我的唱一句跳一步。(能够类比时间片轮转法,多个线程同时占用一个CPU,外部看来能够并发处理多个线程)
并行:多态拥有相同处理能力的处理机在同时处理不一样的任务,比如广场上多个大妈同时再调广场舞。(多个CPU同时处理多个线程任务)
在JavaScript中,由于其是单线程的缘由,因此决定了其每时刻只能干一件事情,事件循环是并发在JavaScript单线程中的一种处理方式。
可是在平常开发中咱们确定见过,同时发送多个请求。这种状况下多个网络线程和js线程共同占用一个CPU,就是并发。
虽然已经理解了JavaScript中运行异步任务的过程,可是这样显然对开发不友好,由于咱们一般并不知道异步任务在什么时候结束。因此前人开发了多种处理异步的方法。每种方法咱们都从三个角度考虑其优缺点:
一种最多见的处理异步问题的方法,将异步任务结束时候要干的事情(回调函数)做为参数传给他,等任务结束时候运行回调函数。咱们经常使用的$.ajax()
和setTimeout
都属于这种方式,可是这样的问题很明显:多个异步任务按顺序执行很是恐怖。
// 著名的回调金字塔 asyncEvent1(()=>{ asyncEvent2(()=>{ asyncEvent3(()=>{ asyncEvent4(()=>{ .... }); }); }); });
上面这种状况很是难以维护,在早期Node项目中常常出现这种状况,有人对上面小改动:
function asyncEvent1CB (){ asyncEvent2(asyncEvent2CB); } function asyncEvent2CB (){ asyncEvent3(asyncEvent3CB); } function asyncEvent3CB (){ asyncEvent4(asyncEvent4CB); } function asyncEvent4CB () { // ... } asyncEvent1(asyncEvent1CB);
这样讲回调函数分离出来,逻辑清晰了一些,可是仍是很明显:方法调用顺序是硬编码,耦合性仍是很高。并且一旦同时发送多个请求,这多个请求的回调函数执行顺序很难保证,维护起来很是麻烦。
这就是回调函数的弊端:
虽然回调函数这种方式问题不少,可是不能否认的是在ES6以前,他就是处理异步问题广泛较好的方式,并且后面不少方式仍然基于回调函数。
JavaScript是事件驱动,任务的执行不取决代码的顺序,而取决于某一个事件是否发生。DOM中有大量事件如onclick
,onload
,onerror
等等。
$('.element1').on('click', function(){ console.log(1); }); $('#element2').on('click', function(){ console.log(2); }); document.getElementById('#element3').addEventListener('click', function(){ console.log(3); }, false);
例如上面这段代码 你没法预知输出结果,由于事件触发没法被预知。跟这个很像的还有订阅者发布者模式:
github上有个有意思的小demo。注册在发布者里面的回调函数什么时候被触发取决于发布者什么时候发布事件,这个不少时候也是不可预知的。
回调函数与事件监听的区别:
不过事件监听也存在问题:
promise出场了,当年理解promise花了我很多功夫。Promise确实跟前二者很不同,简单说下promise。
promise更详细的内容能够看阮一峰老师的文章。
Promise对于异步处理已经十分友好,大多生产环境已经在使用,不过仍有些缺点:
中文翻译成'生成器',ES6中提供的一种异步编程解决方案,语法行为与传统函数彻底不一样。简单来讲,我能够声明一个生成器,生成器能够在执行的时候暂停,交出函数执行权给其余函数,而后其余函数能够在须要的时候让该函数再次运行。这与以前的JavaScript听起来彻底不一样。
详细的内容参考阮一峰老师的文章,这里咱们来据几个例子,正常的ajax调用写法看起来以下:
// 使用setTimeout模拟异步 function ajax (url, cb){ setTimeout(function(){ cb('result'); }, 100); } ajax('/api/a', function(result){ console.log(result); }); // 'result'
一旦咱们想要多个异步按顺序执行,简直是噩梦。这里使用generator处理异步函数利用了一个特色:调用next()
函数就会继续执行下去,因此利用这个特色咱们处理异步原理:
yield
出去。next()
将生成器继续进行下去。咱们对上面的例子加以改进:
// 使用setTimeout模拟异步 function ajax (url, cb){ setTimeout(function(){ cb(url + ' result.'); }, 100); } function ajaxCallback(result){ console.log(result); it.next(result); } function* ajaxGen (){ var aResult = yield ajax('/api/a', ajaxCallback); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b', ajaxCallback); console.log('bResult: ' + bResult); } var it = ajaxGen(); it.next(); // /api/a result. // aResult: /api/a result. // /api/b result. // bResult: /api/b result.
运行下上面代码,能够看到控制台输出结果竟然跟咱们书写的顺序同样!咱们稍加改动:
// 使用setTimeout模拟异步 function ajax (url, cb){ setTimeout(function(){ cb(url + ' result.'); }, 100); } function run (generator) { var it = generator(ajaxCallback); function ajaxCallback(result){ console.log(result); it.next(result); } it.next(); }; run(function* (cb){ var aResult = yield ajax('/api/a', cb); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b', cb); console.log('bResult: ' + bResult); });
简单几下改造即可以生成一个自执行的生成器函数,同时也完成了异步场景同步化写法。generator的核心在于:同步,异步,回调三者分离,遇到异步交出函数执行权,再利用回调控制程序生成器继续进行。上面的run函数只是一个简单的实现,业界已经有CO这样成熟的工具。实际上开发过程当中一般使用generator搭配Promise实现,再来修改上面的例子:
// 使用setTimeout模拟异步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ resolve(url + ' result.'); }, 100); }); } function run (generator) { var it = generator(); function next(result){ var result = it.next(result); if (result.done) return result.value; result.value.then(function(data){ console.log(data); next(data); }); } next(); }; run(function* (){ var aResult = yield ajax('/api/a'); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b'); console.log('bResult: ' + bResult); });
使用Promise来代替callback,理解上花费点时间,大大提升了效率。上面是一种常见,以前我用过generator实现多张图片并发上传,这种状况下利用generator控制上传上传数量,达到断断续续上传的效果。
进化到generator这一步能够说是至关智能了,不管是单个异步,多个按顺序异步,并发异步处理都十分友好,可是也有几个问题:
有没有更简便的方法?
理解了上面的generator,再来理解async/await就简单多了。
ES2017 标准引入了 async 函数,使得异步操做变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。
再看一遍上面的例子,而后修改上面的例子用async/await:
// 使用setTimeout模拟异步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ console.log(url + ' result.'); resolve(url + ' result.'); }, 100); }); } async function ajaxAsync () { var aResult = await ajax('/api/a'); console.log('aResult: ' + aResult); var bResult = await ajax('/api/b'); console.log('bResult: ' + bResult); } ajaxAsync();
能够明显的看到,async/await写法跟generator最后一个例子很像,基本上就是使用async/await关键字封装了一个自执行的run方法。
async
函数对 Generator 函数的改进,体如今如下四点。
- 内置执行器:Generator 函数的执行必须靠执行器,因此才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数如出一辙,只要一行。
- 更好的语义:
async
和await,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。- 更广的适用性:
co
模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,能够是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)。- 返回值是 Promise:
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用then方法指定下一步的操做。
这里async/await不作深刻介绍,详情移步阮一峰老师的博客。
一个很不经常使用的api,可是是一个异步编程的方法,跟以上几种又不太同样。
你可能会遇到一个很是耗时的计算任务,若是在js线程里运行会形成页面卡顿,这时使用web worker,将计算任务丢到里面去,等计算完成再以事件监听的方式通知主线程处理,这是一个web work的应用场景。在这时候,浏览器中是有多个线程在处理js的,worker同时能够在建立子线程,实现js'多线程'。web worker的文档。实战的话看这篇。
与前面几种方法不一样的是,咱们绞尽脑汁想把异步事件同步化,可是web worker却反其道而行,将同步的代码放到异步的线程中。
目前,web worker一般用于页面优化的一种手段,使用场景:
<canvas>
或者<video>
元素中获取的数据,能够把图像分割成几个不一样的区域而且把它们推送给并行的不一样Workers来作计算。JavaScript中的异步编程方式目前来讲大体这些,其中回调函数这种方式是最简单最多见的,Promise是目前最受欢迎的方式。前四种方式让异步编码模式使咱们可以编写更高效的代码,而最后一种web worker则让性能更优。这里主要是对异步编程流程梳理,前提知识点的补充,而对于真正的异步编程方式则是以思考分析为主,使用没有过多介绍。最后补充一个链接:JavaScript异步编程常见面试题,帮助理解。