這裡深入探討下Javascript的非同步程式設計技術。 (P.S. 本文較長,請準備好瓜子可樂:D)
#一. Javascript非同步程式設計簡介
##至少在語言層級上,Javascript是單線程的,因此異步編程對其尤為重要。 拿nodejs來說,外殼是一層js語言,這是使用者操作的層面,在這個層次上它是單執行緒運行的,也就是說我們不能像Java、Python這類語言在語言級別使用多線程能力。取而代之的是,nodejs編程中大量使用了非同步程式技術,這是為了高效使用硬件,同時也可以不造成同步阻塞。不過nodejs在底層實作其實還是用了多執行緒技術,只是這一層使用者對使用者來說是透明的,nodejs幫我們做了幾乎全部的管理工作,我們不用擔心鎖或其他多執行緒程式設計會遇到的問題,只管寫我們的非同步程式碼就好。二. Javascript非同步程式設計方法
ES 6以前:* 回呼函數* 事件監聽(事件發布/訂閱)
* Promise物件
PS:如要執行以下例子,請安裝node v0.11以上版本,在命令列下使用node [檔案名稱.js] 的形式來運行,有部分程式碼需要開啟特殊選項,會在具體例子裡說明。
1.回呼函數
回呼函數在Javascript中非常常見,一般是需要在一個耗時操作之後執行某個操作時可以使用回呼函數。 example 1://一个定时器function timer(time, callback){ setTimeout(function(){ callback(); }, time); } timer(3000, function(){ console.log(123); })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.txt2 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:
1 //callback hell 2 3 doSomethingAsync1(function(){ 4 doSomethingAsync2(function(){ 5 doSomethingAsync3(function(){ 6 doSomethingAsync4(function(){ 7 doSomethingAsync5(function(){ 8 // code... 9 });10 });11 });12 });13 });
1 try{2 //do something may cause exception..3 }4 catch(e){5 //handle exception...6 }example 4:
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 }
透過觀察以上4個例子,可以發現一個問題,在回呼函數嵌套層數不深的情況下,程式碼還算容易理解和維護,一旦嵌套層數加深,就會出現「回調金字塔」的問題,就就像example 4一樣,如果這裡面的每個回呼函數中又包含了許多業務邏輯的話,整個程式碼區塊就會變得非常複雜。從邏輯正確性的角度來說,上面這幾種回呼函數的寫法沒有任何問題,但是隨著業務邏輯的增加和趨於復雜,這種寫法的缺點馬上就會暴露出來,想要維護它們實在是太痛苦了,這就是「回調地獄(callback hell)」。
一個衡量回調層次複雜度的方法是,在example 4中,假設doSomethingAsync2要發生在doSomethingAsync1之前,我們需要忍受多少重構的痛苦。 example 5:
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");example 6:
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 });
你覺得會輸出什麼?答案是undefined。我們嘗試讀取一個不存在的文件,當然會引發異常,但是最外層的try/catch語句卻無法捕捉這個異常。這是非同步程式碼的執行機制導致的。
Tips: 為什麼非同步程式碼回呼函數中的例外無法被最外層的try/catch語句捕獲?#非同步呼叫一般分為兩個階段,提交請求和處理結果,這兩個階段之間有事件循環的調用,它們屬於兩個不同的事件循環(tick),彼此沒有關聯。 非同步呼叫一般以傳入callback的方式來指定非同步操作完成後要執行的動作。而異步調用本體和callback屬於不同的事件循環。try/catch語句只能捕捉當次事件循環的異常,對callback無能為力。
也就是說,一旦我們在非同步呼叫函數中扔出一個非同步I/O請求,非同步呼叫函數立即返回,此時,這個非同步呼叫函數和這個非同步I/O請求沒有任何關係。2.事件監聽(事件發布/訂閱)
example 7:###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 });######這種使用事件監聽處理的非同步程式設計方式很適合一些需要高度解耦的場景。例如在先前遊戲服務端專案中,當人物屬性改變時,就需要寫入到持久層。解決方案是先寫一個訂閱方,訂閱'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 });
上面这个例子,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.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 })
如果在catch方法中发生了异常:
example 10:
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 x+1;26 })27 .then(function(data){28 console.log("completed");29 })
我们可以在catch方法后再加catch方法来捕获这个x+1的异常:
example 11:
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 x+1;26 })27 .catch(function(err){28 console.log("error caught: " + err);29 })30 .then(function(data){31 console.log("completed");32 })
如果几个异步调用有关联,但它们不是顺序式的,是可以同时进行的,我们很直观地会希望它们能够并发执行(这里要注意区分“并发”和“并行”的概念,不要搞混)。
Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.all([p1,p2,p3]);
Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象实例。
p的状态由p1、p2、p3决定,分两种情况:
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
一个具体的例子:
example 12:
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 var promises = [1, 2].map(function(fileno){16 return read('./text' + fileno + '.txt');17 });18 19 Promise.all(promises)20 .then(function(contents){21 console.log(contents);22 })23 .catch(function(err){24 console.log("error caught: " + err);25 })
Promise.race()
Promise.race()也是将多个Promise实例包装成一个新的Promise实例:
var p = Promise.race([p1,p2,p3]);
上述代码中,p1、p2、p3只要有一个实例率先改变状态,p的状态就会跟着改变,那个率先改变的Promise实例的返回值,就传递给p的返回值。如果Promise.all方法和Promise.race方法的参数不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。
example 13:
1 var http = require('http'); 2 var qs = require('querystring'); 3 4 var requester = function(options, postData){ 5 var promise = new Promise(function(resolve, reject){ 6 var content = ""; 7 var req = http.request(options, function (res) { 8 res.setEncoding('utf8'); 9 10 res.on('data', function (data) {11 onData(data);12 });13 14 res.on('end', function () {15 resolve(content);16 });17 18 function onData(data){19 content += data;20 }21 });22 23 req.on('error', function(err) {24 reject(err);25 });26 27 req.write(postData);28 req.end();29 });30 31 return promise;32 }33 34 var promises = ["柠檬", "苹果"].map(function(keyword){35 var options = {36 hostname: 'localhost', 37 port: 9200, 38 path: '/meiqu/tips/_search',39 method: 'POST' 40 };41 42 var data = {43 'query' : {44 'match' : {45 'summary' : keyword46 }47 }48 };49 data = JSON.stringify(data);50 return requester(options, data);51 });52 53 Promise.race(promises)54 .then(function(contents) {55 var obj = JSON.parse(contents);56 console.log(obj.hits.hits[0]._source.summary);57 })58 .catch(function(err){59 console.log(err); 60 });
有时候需将现有对象转换成Promise对象,可以使用Promise.resolve()。
如果Promise.resolve方法的参数,不是具有then方法的对象(又称thenable对象),则返回一个新的Promise对象,且它的状态为fulfilled。
如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。
example 14:
1 var p = Promise.resolve('Hello');2 3 p.then(function (s){4 console.log(s)5 });
Promise.reject()
Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。
example 15:
1 var p = Promise.reject('出错了');2 3 p.then(null, function (s){4 console.log(s)5 });
上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。
3.Generator函数
Generator函数是协程在ES 6中的实现,最大特点就是可以交出函数的执行权(暂停执行)。
注意:在node中需要开启–harmony选项来启用Generator函数。
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
协程的运行方式如下:
第一步:协程A开始运行。
第二步:协程A执行到一半,暂停,执行权转移到协程B。
第三步:一段时间后,协程B交还执行权。
第四步:协程A恢复执行。
上面的协程A就是异步任务,因为分为两步执行。
比如一个读取文件的例子:
example 16:
1 function asnycJob() {2 // ...其他代码3 var f = yield readFile(fileA);4 // ...其他代码5 }
asnycJob函数是一个协程,yield语句表示,执行到此处执行权就交给其他协程,也就是说,yield是两个阶段的分界线。协程遇到yield语句就暂停执行,直到执行权返回,再从暂停处继续执行。这种写法的优点是,可以把异步代码写得像同步一样。
看一个简单的Generator函数例子:
example 17:
1 function* gen(x){ 2 var y = yield x + 2; 3 return y; 4 } 5 6 var g = gen(1); 7 var r1 = g.next(); // { value: 3, done: false } 8 console.log(r1); 9 var r2 = g.next() // { value: undefined, done: true }10 console.log(r2);
上述代码中,调用Generator函数,会返回一个内部指针(即遍历器)g,这是Generator函数和一般函数不同的地方,调用它不会返回结果,而是一个指针对象。调用指针g的next方法,会移动内部指针,指向第一个遇到的yield语句,上例就是执行到x+2为止。
换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段。
Generator函数的数据交换和错误处理
next方法返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,这是向Generator函数体内输入数据。
example 18:
1 function* gen(x){ 2 var y = yield x + 2; 3 return y; 4 } 5 6 var g = gen(1); 7 var r1 = g.next(); // { value: 3, done: false } 8 console.log(r1); 9 var r2 = g.next(2) // { value: 2, done: true }10 console.log(r2);
Generator函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
example 19:
1 function* gen(x){ 2 try { 3 var y = yield x + 2; 4 } 5 catch (e){ 6 console.log(e); 7 } 8 return y; 9 }10 11 var g = gen(1);12 g.next();13 g.throw('error!'); //error!
example 20:
1 var fs = require('fs'); 2 var thunkify = require('thunkify'); 3 var readFile = thunkify(fs.readFile); 4 5 var gen = function* (){ 6 var r1 = yield readFile('./text1.txt', 'utf8'); 7 console.log(r1); 8 var r2 = yield readFile('./text2.txt', 'utf8'); 9 console.log(r2);10 };11 12 //开始执行上面的代码13 var g = gen();14 15 var r1 = g.next();16 r1.value(function(err, data){17 if (err) throw err;18 var r2 = g.next(data);19 r2.value(function(err, data){20 if (err) throw err;21 g.next(data);22 });23 });
在深入讨论Generator函数之前我们先要知道Thunk函数这个概念。
求值策略(即函数的参数到底应该何时求值)
(1) 传值调用
(2) 传名调用
Javascript是传值调用的,Thunk函数是编译器“传名调用”的实现,就是将参数放入一个临时函数中,再将这个临时函数放入函数体,这个临时函数就叫做Thunk函数。
举个栗子就好懂了:
example 21:
1 function f(m){ 2 return m * 2; 3 } 4 var x = 1; 5 f(x + 5); 6 7 //等同于 8 var thunk = function (x) { 9 return x + 5;10 };11 12 function f(thunk){13 return thunk() * 2;14 }
JavaScript语言的Thunk函数
在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
example 22:
1 var fs = require('fs'); 2 3 // 正常版本的readFile(多参数版本) 4 fs.readFile(fileName, callback); 5 6 // Thunk版本的readFile(单参数版本) 7 var readFileThunk = Thunk(fileName); 8 readFileThunk(callback); 9 10 var Thunk = function (fileName){11 return function (callback){12 return fs.readFile(fileName, callback);13 };14 };
example 23:
1 var Thunk = function(fn){2 return function (){3 var args = Array.prototype.slice.call(arguments);4 return function (callback){5 args.push(callback);6 return fn.apply(this, args);7 }8 };9 };
使用上面的转换器,生成fs.readFile的Thunk函数。
example 24:
1 var readFileThunk = Thunk(fs.readFile);2 readFileThunk('./text1.txt', 'utf8')(function(err, data){3 console.log(data);4 });
可以使用thunkify模块来Thunk化任何带有callback的函数。
我们需要借助Thunk函数的能力来自动执行Generator函数。
下面是一个基于Thunk函数的Generator函数执行器。
example 25:
1 //Generator函数执行器 2 3 function run(fn) { 4 var gen = fn(); 5 6 function next(err, data) { 7 var result = gen.next(data); 8 if (result.done) return; 9 result.value(next);10 }11 12 next();13 }14 15 run(gen);
example 26:
1 var fs = require('fs'); 2 var thunkify = require('thunkify'); 3 var readFile = thunkify(fs.readFile); 4 5 var gen = function* (){ 6 var f1 = yield readFile('./text1.txt', 'utf8'); 7 console.log(f1); 8 var f2 = yield readFile('./text2.txt', 'utf8'); 9 console.log(f2);10 var f3 = yield readFile('./text3.txt', 'utf8');11 console.log(f3);12 };13 14 function run(fn) {15 var gen = fn();16 17 function next(err, data) {18 var result = gen.next(data);19 if (result.done) return;20 result.value(next);21 }22 23 next();24 }25 26 run(gen); //自动执行
yield *语句
普通的yield语句后面跟一个异步操作,yield *语句后面需要跟一个遍历器,可以理解为yield *后面要跟另一个Generator函数,讲起来比较抽象,看一个实例。
example 27:
1 //嵌套异步操作流 2 var fs = require('fs'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 var gen = function* (){ 7 var f1 = yield readFile('./text1.txt', 'utf8'); 8 console.log(f1); 9 10 var f_ = yield * gen1(); //此处插入了另外一个异步流程11 12 var f2 = yield readFile('./text2.txt', 'utf8');13 console.log(f2);14 15 var f3 = yield readFile('./text3.txt', 'utf8');16 console.log(f3);17 };18 19 var gen1 = function* (){20 var f4 = yield readFile('./text4.txt', 'utf8');21 console.log(f4);22 var f5 = yield readFile('./text5.txt', 'utf8');23 console.log(f5);24 }25 26 function run(fn) {27 var gen = fn();28 29 function next(err, data) {30 var result = gen.next(data);31 if (result.done) return;32 result.value(next);33 }34 35 next();36 }37 38 run(gen); //自动执行
1
4
5
2
3
也就是说,使用yield *可以在一个异步操作流程中直接插入另一个异步操作流程,我们可以据此构造可嵌套的异步操作流,更为重要的是,写这些代码完全是同步风格的。
目前业界比较流行的Generator函数自动执行的解决方案是co库,此处也只给出co的例子。顺带一提node-fibers也是一种解决方案。
顺序执行3个异步读取文件的操作,并依次输出文件内容:
example 28:
1 var fs = require('fs'); 2 var co = require('co'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 co(function*(){ 7 var files=[ 8 './text1.txt', 9 './text2.txt',10 './text3.txt'11 ];12 13 var p1 = yield readFile(files[0]);14 console.log(files[0] + ' ->' + p1);15 16 var p2 = yield readFile(files[1]);17 console.log(files[1] + ' ->' + p2);18 19 var p3 = yield readFile(files[2]);20 console.log(files[2] + ' ->' + p3);21 22 return 'done';23 });
example 29:
1 var fs = require('fs'); 2 var co = require('co'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 co(function* () { 7 var files = ['./text1.txt', './text2.txt', './text3.txt']; 8 var contents = yield files.map(readFileAsync); 9 10 console.log(contents);11 });12 13 function readFileAsync(filename) {14 return readFile(filename, 'utf8');15 }
ES 7中的async和await
async和await是ES 7中的新语法,新到连ES 6都不支持,但是可以通过Babel一类的预编译器处理成ES 5的代码。目前比较一致的看法是async和await是js对异步的终极解决方案。
async函数实际上是Generator函数的语法糖(js最喜欢搞语法糖,包括ES 6中新增的“类”支持其实也是语法糖)。
配置Babel可以看:配置Babel
如果想尝个鲜,简单一点做法是执行:
1 sudo npm install --global babel-cli
async_await.js代码如下:
1 var fs = require('fs'); 2 3 var readFile = function (fileName){ 4 return new Promise(function (resolve, reject){ 5 fs.readFile(fileName, function(error, data){ 6 if (error){ 7 reject(error); 8 } 9 else {10 resolve(data);11 }12 });13 });14 };15 16 var asyncReadFile = async function (){17 var f1 = await readFile('./text1.txt');18 var f2 = await readFile('./text2.txt');19 console.log(f1.toString());20 console.log(f2.toString());21 };22 23 asyncReadFile();
输出:
1
2
相关文章:
深入理解JavaScript编程中的同步与异步机制_基础知识
以上是Javascript非同步程式設計知識點系統講解的詳細內容。更多資訊請關注PHP中文網其他相關文章!