要我能用得这么熟,javascript
那前端出师了哈。前端
http://foio.github.io/javascript-asyn-pattern/java
改天一个一个亲测一下。node
Javascript语言是单线程的,没有复杂的同步互斥;可是,这并无限制它的使用范围;相反,借助于Node,Javascript已经在某些场景下具有通吃先后端的能力了。近几年,多线程同步IO的模式已经在和单线程异步IO的模式的对决中败下阵来,Node也所以得名。接下来咱们深刻介绍一下Javascript的杀手锏,异步编程的发展历程。git
让咱们假设一个应用场景:一篇文章有10个章节,章节的数据是经过XHR异步请求的,章节必须按顺序显示。咱们从这个问题出发,逐步探求从粗糙到优雅的解决方案。github
在那个年代,javascript仅限于前端的简单事件处理,这是异步编程的最基本模式了。 好比监听dom事件,在dom事件发生时触发相应的回调。ajax
element.addEventListener('click',function(){ //response to user click });
好比经过定时器执行异步任务。编程
setTimeout(function(){ //do something 1s later }, 1000);
可是这种模式注定没法处理复杂的业务逻辑的。假设有N个异步任务,每个任务必须在上一个任务完成后触发,因而就有了以下的代码,这就产生了回调黑洞。json
doAsyncJob1(function(){ doAsyncJob2(function(){ doAsyncJob3(function(){ doAsyncJob4(function(){ //Black hole }); }) }); });
针对上文的回调黑洞问题,有人提出了开源的promise/A+规范,具体规范见以下地址:https://promisesaplus.com/。promise表明了一个异步操做的结果,其状态必须符合下面几个要求:后端
一个Promise必须处在其中之一的状态:pending, fulfilled 或 rejected.
若是是pending状态,则promise能够转换到fulfilled或rejected状态。
若是是fulfilled状态,则promise不能转换成任何其它状态。
若是是rejected状态,则promise不能转换成任何其它状态。
promise有then方法,能够添加在异步操做到达fulfilled状态和rejected状态的处理函数。
promise.then(successHandler,failedHandler);
而then方法同时也会返回一个promise对象,这样咱们就能够链式处理了。
promise.then(successHandler,failedHandler).then().then();
MDN上的一张图,比较清晰的描述了Pomise各个状态之间的转换。
假设上文中的doAsyncJob都返回一个promise对象,那咱们看看如何用promise处理回调黑洞:
doAsyncJob1().then(function(){ return doAsyncJob2();; }).then(function(){ return doAsyncJob3(); }).then(function(){ return doAsyncJob4(); }).then(//......);
这种编程方式是否是清爽多了。咱们最常用的jQuery已经实现了promise规范,在调用$.ajax时能够写成这样了:
var options = {type:'GET',url:'the-url-to-get-data'}; $.ajax(options).then(function(data){ //success handler },function(data){ //failed handler });
咱们可使用ES6的Promise的构造函数生成本身的promise对象,Promise构造函数的参数为一个函数,该函数接收两个函数(resolve,reject)做为参数,并在成功时调用resolve,失败时调用reject。以下代码生成一个拥有随机结果的promise。
var RandomPromiseJob = function(){ return new Promise(function(resolve,reject){ var res = Math.round(Math.random()*10)%2; setTimeout(function(){ if(res){ resolve(res); }else{ reject(res); } }, 1000) }); } RandomPromiseJob().then(function(data){ console.log('success'); },function(data){ console.log('failed'); });
jsfiddle演示地址:http://jsfiddle.net/panrq4t7/
promise错误处理也十分灵活,在promise构造函数中发生异常时,会自动设置promise的状态为rejected,从而触发相应的函数。
new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).then(undefined,function(data){ console.log(data.message); });
其中then(undefined,function(data)能够简写为catch。
new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).catch(function(data){ console.log(data.message); });
jsfiddle演示地址:http://jsfiddle.net/x696ysv2/
promise的功能毫不仅限于上文这种小打小闹的应用。对于篇头提到的一篇文章10个章节异步请求,顺序展现的问题,若是使用回调处理章节之间的依赖逻辑,显然会产生回调黑洞; 而使用promise模式,则代码形式优雅并且逻辑清晰。假设咱们有一个包含10个章节内容的数组,并有一个返回promise对象的getChaper函数:
var chapterStrs = [ 'chapter1','chapter2','chapter3','chapter4','chapter5', 'chapter6','chapter7','chapter8','chapter9','chapter10', ]; var getChapter = function(chapterStr) { return get('<p>' + chapterStr + '</p>', Math.round(Math.random()*2)); };
下面咱们探讨一下如何优雅高效的使用promise处理这个问题。
顺序promise主要是经过对promise的then方法的链式调用产生的。
//按顺序请求章节数据并展现 chapterStrs.reduce(function(sequence, chapterStr) { return sequence.then(function() { return getChapter(chapterStr); }).then(function(chapter) { addToPage(chapter); }); }, Promise.resolve());
这种方法有一个问题,XHR请求是串行的,没有充分利用浏览器的并行性。网络请求timeline和显示效果图以下:
查看jsfiddle演示代码: http://jsfiddle.net/81k9nv6x/1/
Promise类有一个all方法,其接受一个promise数组:
Promise.all([promise1,promise2,...,promise10]).then(function(){ });
只有promise数组中的promise所有兑现,才会调用then方法。使用Promise.all,咱们能够并发性的进行网络请求,并在全部请求返回后在集中进行数据展现。
//并发请求章节数据,一次性按顺序展现章节 Promise.all(chapterStrs.map(getChapter)).then(function(chapters){ chapters.forEach(function(chapter){ addToPage(chapter); }); });
这种方法也有一个问题,要等到全部数据加载完成后,才会一次性展现所有章节。效果图以下:
查看jsfiddle演示代码:http://jsfiddle.net/7ops845a/
其实,咱们能够作到并发的请求数据,尽快展现知足顺序条件的章节:即前面的章节展现后就能够展现当前章节,而不用等待后续章节的网络请求。基本思路是:先建立一批并行的promise,而后经过链式调用then方法控制展现顺序。
chapterStrs.map(getChapter).reduce(function(sequence, chapterStrPromise) { return sequence.then(function(){ return chapterStrPromise; }).then(function(chapter){ addToPage(chapter); }); }, Promise.resolve());
效果以下:
查看jsfiddle演示代码:http://jsfiddle.net/fuog1ejg/
这三种模式基本上归纳了使用Pormise控制并发的方式,你能够根据业务需求,肯定各个任务之间的依赖关系,从而作出选择。
ES6中已经实现了promise规范,在新版的浏览器和node中咱们能够放心使用了。对于ES5及其如下版本,咱们能够借助第三方库实现,q(https://github.com/kriskowal/q)是一个很是优秀的实现,angular使用的就是它,你能够放心使用。下一篇文章准备实现一个本身的promise。
异步编程的一种解决方案叫作"协程"(coroutine),意思是多个线程互相协做,完成异步任务。随着ES6中对协程的支持,这种方案也逐渐进入人们的视野。Generator函数是协程在 ES6 的实现.
让咱们先从三个方面了解generator。
在普通函数名前面加*号就能够生成generator函数,该函数返回一个指针,每一次调用next函数,就会移动该指针到下一个yield处,直到函数结尾。经过next函数就能够控制generator函数的执行。以下所示:
function *gen(){ yield 'I'; yield 'love'; yield 'Javascript'; } var g = gen(); console.log(g.next().value); //I console.log(g.next().value); //love console.log(g.next().value); //Javascript
next函数返回一个对象{value:'love',done:false},其中value表示yield返回值,done表示generator函数是否执行完成。这样写有点low?试试这种语法。
for(var v of gen()){ console.log(v); }
next()函数中能够传递参数,做为yield的返回值,传递到函数体内部。这里有点tricky,next参数做为上一次执行yeild的返回值。理解“上一次”很重要。
function* gen(x){ var y = yield x + 1; yield y + 2; return 1; } var g = gen(1); console.log(g.next()) // { value: 2, done: false } console.log(g.next(2)) // { value: 4, done: true } console.log(g.next()); //{ value: 1, done: true }
好比这里的g.next(2),参数2为上一步yield x + 1 的返回值赋给y,从而咱们就能够在接下来的代码中使用。这就是generator数据传递的基本方法了。
经过generator函数返回的指针,咱们能够向函数内部传递异常,这也使得异步任务的异常处理机制获得保证。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); console.log(g.next()); //{ value: 3, done: false } g.throw('error'); //error
仍然使用本文中的getChapter方法,该方法返回一个promise,咱们看一下如何使用generator处理异步回调。gen方法在执行到yield指令时返回的result.value是promise对象,而后咱们经过next方法将promise的结果返回到gen函数中,做为addToPage的参数。
function *gen(){ var result = yield getChapter('I love Javascript'); addToPage(result); } var g = gen(); var result = g.next(); result.value.then(function(data){ g.next(data); });
gen函数的代码,和普通同步函数几乎没有区别,只是多了一条yield指令。
jsfiddle地址以下:http://jsfiddle.net/fhnc07rq/3/
虽然gen函数自己很是干净,只须要一条yield指令便可实现异步操做。可是我却须要一堆代码,用于控制gen函数、向gen函数传递参数。有没有更规范的方式呢?其实只须要将这些操做进行封装,co库为咱们作了这些(https://github.com/tj/co)。那么咱们用generator和co实现上文的逐步加载10个章节数据的操做。
function *gen(){ for(var i=0;i<chapterStrs.length;i++){ addToPage(yield getChapter(chapterStrs[i])); } } co(gen);
jsfiddle演示地址:http://jsfiddle.net/0hvtL6e9/
这种方法的效果相似于上文中提到“顺序promise”,咱们能不能实现上文的“并发promise,渐进式”呢?代码以下:
function *gen(){ var charperPromises = chapterStrs.map(getChapter); for(var i=0;i<charperPromises.length;i++){ addToPage(yield charperPromises[i]); } } co(gen);
jsfiddle演示地址: http://jsfiddle.net/gr6n3azz/1/
经历过复杂性才能达到简单性。咱们从最开始的回调黑洞到最终的generator,愈来愈复杂也愈来愈简单。
===================
function *gen() { yield 'I'; yield 'love'; yield 'Javascript'; } var g = gen(); console.log(g.next().value); console.log(g.next().value); console.log(g.next().value); function *gen1(x) { var y = yield x + 1; yield y + 2; return 1; } var g1 = gen1(3); console.log(g1.next()); console.log(g1.next(10)); console.log(g1.next());