首页 > 代码库 > Javascript异步编程
Javascript异步编程
本文转自:http://www.cnblogs.com/nullcc/p/5841182.html
至少在语言级别上,Javascript是单线程的,因此异步编程对其尤为重要。
拿nodejs来说,外壳是一层js语言,这是用户操作的层面,在这个层次上它是单线程运行的,也就是说我们不能像Java、Python这类语言在语言级别使用多线程能力。取而代之的是,nodejs编程中大量使用了异步编程技术,这是为了高效使用硬件,同时也可以不造成同步阻塞。不过nodejs在底层实现其实还是用了多线程技术,只是这一层用户对用户来说是透明的,nodejs帮我们做了几乎全部的管理工作,我们不用担心锁或者其他多线程编程会遇到的问题,只管写我们的异步代码就好。
二. Javascript异步编程方法
ES 6以前:
* 回调函数
* 事件监听(事件发布/订阅)
* Promise对象
ES 6:
* Generator函数(协程coroutine)
ES 7:
* async和await
PS:如要运行以下例子,请安装node v0.11以上版本,在命令行下使用 node [文件名.js] 的形式来运行,有部分代码需要开启特殊选项,会在具体例子里说明。
1.回调函数
回调函数在Javascript中非常常见,一般是需要在一个耗时操作之后执行某个操作时可以使用回调函数。
example 1:
1 //一个定时器 2 function timer(time, callback){ 3 setTimeout(function(){ 4 callback(); 5 }, time); 6 } 7 8 timer(3000, function(){ 9 console.log(123); 10 })
example 2:
//读文件后输出文件内容 var fs = require(‘fs‘); fs.readFile(‘./text1.txt‘, ‘utf8‘, function(err, data){ if (err){ throw err; } console.log(data); });
example 3:
1 //嵌套回调,读一个文件后输出,再读另一个文件,注意文件是有序被输出的,先text1.txt后text2.txt 2 var fs = require(‘fs‘); 3 4 fs.readFile(‘./text1.txt‘, ‘utf8‘, function(err, data){ 5 console.log("text1 file content: " + data); 6 fs.readFile(‘./text2.txt‘, ‘utf8‘, function(err, data){ 7 console.log("text2 file content: " + data); 8 }); 9 });
example 4:
//callback hell doSomethingAsync1(function(){ doSomethingAsync2(function(){ doSomethingAsync3(function(){ doSomethingAsync4(function(){ doSomethingAsync5(function(){ // code... }); }); }); }); });
通过观察以上4个例子,可以发现一个问题,在回调函数嵌套层数不深的情况下,代码还算容易理解和维护,一旦嵌套层数加深,就会出现“回调金字塔”的问题,就像example 4那样,如果这里面的每个回调函数中又包含了很多业务逻辑的话,整个代码块就会变得非常复杂。从逻辑正确性的角度来说,上面这几种回调函数的写法没有任何问题,但是随着业务逻辑的增加和趋于复杂,这种写法的缺点马上就会暴露出来,想要维护它们实在是太痛苦了,这就是“回调地狱(callback hell)”。
一个衡量回调层次复杂度的方法是,在example 4中,假设doSomethingAsync2要发生在doSomethingAsync1之前,我们需要忍受多少重构的痛苦。
回调函数还有一个问题就是我们在回调函数之外无法捕获到回调函数中的异常,我们以前在处理异常时一般这么做:
example 5:
1 try{ 2 //do something may cause exception.. 3 } 4 catch(e){ 5 //handle exception... 6 }
在同步代码中,这没有问题。现在思考一下下面代码的执行情况:
example 6:
1 var fs = require(‘fs‘); 2 3 try{ 4 fs.readFile(‘not_exist_file‘, ‘utf8‘, function(err, data){ 5 console.log(data); 6 }); 7 } 8 catch(e){ 9 console.log("error caught: " + e); 10 }
你觉得会输出什么?答案是undefined。我们尝试读取一个不存在的文件,这当然会引发异常,但是最外层的try/catch语句却无法捕获这个异常。这是异步代码的执行机制导致的。
Tips: 为什么异步代码回调函数中的异常无法被最外层的try/catch语句捕获?
异步调用一般分为两个阶段,提交请求和处理结果,这两个阶段之间有事件循环的调用,它们属于两个不同的事件循环(tick),彼此没有关联。
异步调用一般以传入callback的方式来指定异步操作完成后要执行的动作。而异步调用本体和callback属于不同的事件循环。
try/catch语句只能捕获当次事件循环的异常,对callback无能为力。
也就是说,一旦我们在异步调用函数中扔出一个异步I/O请求,异步调用函数立即返回,此时,这个异步调用函数和这个异步I/O请求没有任何关系。
2.事件监听(事件发布/订阅)
事件监听是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。通常情况下,我们需要考虑哪些部分是不变的,哪些是容易变化的,把不变的部分封装在组件内部,供外部调用,需要自定义的部分暴露在外部处理。从某种意义上说,事件的设计就是组件的接口设计。
example 7:
1 //发布和订阅事件 2 3 var events = require(‘events‘); 4 var emitter = new events.EventEmitter(); 5 6 emitter.on(‘event1‘, function(message){ 7 console.log(message); 8 }); 9 10 emitter.emit(‘event1‘, "message for you");
这种使用事件监听处理的异步编程方式很适合一些需要高度解耦的场景。例如在之前一个游戏服务端项目中,当人物属性变化时,需要写入到持久层。解决方案是先写一个订阅方,订阅‘save‘事件,在需要保存数据时让发布方对象(这里就是人物对象)上直接用emit发出一个事件名并携带相应参数,订阅方收到这个事件信息并处理。
3.Promise对象
ES 6中原生提供了Promise对象,Promise对象代表了某个未来才会知道结果的事件(一般是一个异步操作),并且这个事件对外提供了统一的API,可供进一步处理。
使用Promise对象可以用同步操作的流程写法来表达异步操作,避免了层层嵌套的异步回调,代码也更加清晰易懂,方便维护。
Promise.prototype.then()
Promise.prototype.then()方法返回的是一个新的Promise对象,因此可以采用链式写法,即一个then后面再调用另一个then。如果前一个回调函数返回的是一个Promise对象,此时后一个回调函数会等待第一个Promise对象有了结果,才会进一步调用。
example 8:
1 //ES 6原生Promise示例 2 var fs = require(‘fs‘) 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, ‘utf8‘, function(err, data){ 7 if (err){ 8 reject(err); 9 } 10 resolve(data); 11 }) 12 }); 13 return promise; 14 } 15 16 read(‘./text1.txt‘) 17 .then(function(data){ 18 console.log(data); 19 }, function(err){ 20 console.log("err: " + err); 21 });
以上代码中,read函数是Promise化的,在read函数中,实例化了一个Promise对象,Promise的构造函数接受一个函数作为参数,这个函数的两个参数分别是resolve方法和reject方法。如果异步操作成功,就是用resolve方法将Promise对象的状态从“未完成”变为“完成”(即从pending变为resolved),如果异步操作出错,则是用reject方法把Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),read函数返回了这个Promise对象。Promise实例生成以后,可以用then方法分别指定resolve方法和reject方法的回调函数。
上面这个例子,Promise构造函数的参数是一个函数,在这个函数中我们写异步操作的代码,在异步操作的回调中,我们根据err变量来选择是执行resolve方法还是reject方法,一般来说调用resolve方法的参数是异步操作获取到的数据(如果有的话),但还可能是另一个Promise对象,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,调用reject方法的参数是异步回调用的err参数。
调用read函数时,实际上返回的是一个Promise对象,通过在这个Promise对象上调用then方法并传入resolve方法和reject方法来指定异步操作成功和失败后的操作。
example 9:
1 //原生Primose顺序嵌套回调示例 2 var fs = require(‘fs‘) 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, ‘utf8‘, function(err, data){ 7 if (err){ 8 reject(err); 9 } 10 resolve(data); 11 }) 12 }); 13 return promise; 14 } 15 16 read(‘./text1.txt‘) 17 .then(function(data){ 18 console.log(data); 19 return read(‘./text2.txt‘); 20 }) 21 .then(function(data){ 22 console.log(data); 23 });
在Promise的顺序嵌套回调中,第一个then方法先输出text1.txt的内容后返回read(‘./text2.txt‘),注意这里很关键,这里实际上返回了一个新的Promise实例,第二个then方法指定了异步读取text2.txt文件的回调函数。这种形似同步调用的Promise顺序嵌套回调的特点就是有一大堆的then方法,代码冗余略多。
异常处理
Promise.prototype.catch()
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。
example 9:
1 var fs = require(‘fs‘) 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, ‘utf8‘, function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data); 10 }) 11 }); 12 return promise; 13 } 14 15 read(‘./text1.txt‘) 16 .then(function(data){ 17 console.log(data); 18 return read(‘not_exist_file‘); 19 }) 20 .then(function(data){ 21 console.log(data); 22 }) 23 .catch(function(err){ 24 console.log("error caught: " + err); 25 }) 26 .then(function(data){ 27 console.log("completed"); 28 })
。。。。。。
Javascript异步编程