在Nodejs中是通過回調函數控制異步過程的,但是當多個事件之間相互依賴,或多個事件一起協作時,就會導致函數嵌套過深的情況。如下是一個在項目中顯示文章詳情頁的的代碼,我們看到這個函數嵌套了七八層。這樣的嵌套很容易因爲缺少括號發生錯誤。另外,當一個回調函數中出現錯誤,整個程序就會退出,並提示錯誤信息。
//查看文章詳細信息
app.get("/detail/:author/:title",function(req,res){
var isAgree = false;
var isColl = false;
var isAttention = false;
console.log(req.session.user);
Post.getOne({author: req.params.author,title: req.params.title}, function (err, post) {
if (err) {
req.flash('error', err);
console.log(err);
}
//判斷是否已點贊
if ( req.session.user && post.agree &&post.agree.indexOf(req.session.user.email)>=0 ) {
isAgree = true;
}
//判斷是否已收藏
if ( req.session.user && post.postcoll.indexOf(req.session.user.email)>=0 ) {
isColl = true;
}
User.getOne({email:req.params.author},function(err,author_detail){
if( req.session.user && req.session.user.attention && req.session.user.attention.indexOf(req.params.author) >=0){
isAttention = true;
}
console.log(isAttention);
//獲取作者的頭像(暱稱的問題)
Post.getTen({tags:{$in:post.tags}},1,{pv:-1},function(err, posts, totle,userImg){
if ( err ){
console.log(err);
}
Post.countPost({author:req.session.user.email},function(err,count){
if(err){
console.log(err);
}
//訪問量增加
Post.viewNum( {author: req.params.author,title: req.params.title},function(err){
Post.getArchive({author:req.session.user.email},function(err,docs){
console.log(docs);
res.render('post/showPost', {
title: req.params.title,
post: post,
relate: posts,
cates: docs,
count:count,
user: req.session.user,
author_detail:author_detail,
isAgree : isAgree,
isColl: isColl,
isAttention:isAttention
})
});
})
})
})
})
});
})
爲了解決函數嵌套過深的問題,現有的方案有模塊化、promise方法和generator方法。該文主要介紹generator方法。
generator簡介
generator是ES6新增的一個生成器,是一種特殊的函數。與普通函數不同的是,定義時需要在函數名前添加‘*’,並且在調用Generator函數後,該函數並不執行,返回的也不是函數的運行結果,而是一個指向內部狀態的指針對象。當調用指針對象的next()方法時,指針指向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下開的地方開始執行,直到遇到下一條yield語句(或return語句)爲止。generator是分段進行的,yield語句是暫停執行的標記,而next方法可以恢復執行。
function* generatorFunc(){
var i = 0 ;
while(i < 5 ){
yield i;
i++
}
}
var gene = generatorFunc();//Object {value: 0, done: false}
console.log(gene.next());//Object {value: 1, done: false}
console.log(gene.next());//Object {value: 2, done: false}
console.log(gene.next());//Object {value: 3, done: false}
console.log(gene.next());//Object {value: 4, done: false}
console.log(gene.next());//Object {value: undefined, done: true}
console.log(gene.next());//函數已經結束 無輸出
這裏說一下yield語句,yield語句就是函數的暫停標誌。運行邏輯如下
函數調用next方法,遇到yield語句就暫停執行後面的操作,並將緊跟在yield後的表達式的值作爲next方法返回對象的value值。注意yield語句本身沒有返回值,或者說總返回undefined。next方法可以帶一個參數,該參數會被當做上一條yield語句的返回值.
function* generatorFunc(){ var i = 0 ; while(i < 5 ){ var rest = yield i; console.log(rest); i++ } } var gene = generatorFunc(); console.log(gene.next());//Object {value: 0, done: false} rest輸出undefined console.log(gene.next('ccc'));//Object {value: 0, done: false} rest輸出‘ccc’
- 下一次調用next方法時再繼續向下執行,直到遇到下一個yield語句
- 如果沒有遇到新的yield語句,就一直運行到函數結束,直到return位置,並將return語句後面的表達式的值作爲返回的對象的value值
- 如果該函數沒有return語句,則返回的對象的value屬性爲undefined。
generator結合thunk化函數處理回調地獄
Nodejs中co庫實現了使用generator處理回調函數的問題。KOA框架主要使用的也是generator原理處理的異步函數。這裏介紹一下大概的原理。
我們先創建使用回調函數實現讀取文件的功能
var fs = require("fs");
fs.readFile('path1', function (err, data) {
if (err) throw err;
console.log(data);
fs.readFile('path2', function (err, data) {
if (err) throw err;
console.log(data);
});
});
現在我們藉助co的思想使用generator來改寫它
var fs = require('fs');
co(function* (){
var data1 = yield readFile('test.txt');
console.log(data1);
var data2 = yield readFile('test1.txt');
console.log(data2);
})
// Thunk 化就是將多參數函數,將其替換成單參數只接受回調函數作爲唯一參數的版本
function readFile( path ){
return function(callback){
fs.readFile(path,callback);
}
}
function co(fn){
var gen = fn();
function next(err,data){
var result = gen.next(data);
if(!result.done){
console.log(result);
result.value(next);
}
}
next();
}
讓我們分析一下上面的代碼。co函數的參數需要是一個生成器函數。在co中定義一個next方法,該方法主要調用生成器函數中的next方法。next方法中傳遞的參數即爲生成器函數上一條yield語句的返回值(對應data1和data2)。result中的value爲next方法返回的值(包含value和done兩個值,當done爲true時,表示已經函數執行完成)。當函數沒執行完成時調用value中的函數。value中的函數是被thunk化的,只接受回調函數。
上面代碼有兩個關鍵點,一個是readfile函數的thunk化,還有就是co函數。
有一點需要注意,co中需要流程控制的函數,都必須要Thunk化或者Promise化。所謂Thunk化就是將多參數函數替換隻接收回調函數的單參數函數。原生API不支持Thunk化,所以就有了thunkify這個庫將原生api thunk化。
爲什麼要thunk化呢,利用generator來實現異步回調的實質就是把next()放入回調函數中。thunk化之後可以得到一個只接收callback的函數。
generator結合promise對象解決回調地獄問題
promise對象其實是一個用來處理異步操作的延遲對象。promise對象有三種狀態:未完成態、完成態和失敗態,其中未完成態可以轉化爲完成態和失敗態,但是不能逆向轉化也不能相互轉化,即完成態(失敗態)不能轉化爲未完成態和失敗態(完成態)。只有異步的結果可以決定當前是那種狀態,任何其他操作都不能改變這種狀態。
讓我們大概瞭解一下promise的基本用法
首先需要創建Promise對象,Promise構造函數接收一個函數作爲參數,該函數的兩個參數分別爲resolve和reject。resolve的作用是當Promise對象的狀態從未完成態變爲完成態的時候調用,reject的作用是當Promise對象從未完成態變爲失敗態的時候調用。
var promise = new Promise(function(resolve,reject){});
Promise實例的then方法可以分別指定resolve和reject轉檯的回調函數
promise.then(function(value){ //success },function(err){ //failure });
下面我們看一下如何使用promise和generator解決回調地獄的問題。先上代碼
var Q = require('q');
var preadFile = function(file){
var deferred = Q.defer();
fs.readFile(file,function(err,data){
if(!err){
deferred.resolve(data);
}else{
deferred.reject(err);
}
console.log(deferred.promise);
})
return deferred.promise;
}
//建立generator生成器函數
var g = function* (){
try{
//執行創建promise對象的函數
var foo = yield preadFile('test.txt');
console.log(foo);
}catch(e){
console.log(e);
}
}
//創建執行generator生成器的函數
function run(generator) {
var it = generator();
//result的值爲執行生成器的next方法返回的值,含有value和done兩個屬性,value爲promise對象
function go (result){
//當result中的done取值爲true時,表示該異步操作已經執行完成。
if(result.done){
return result.value;
}
//設置promise對象的then方法
return result.value.then(function(data){
return go(it.next(data));
},function(error){
return go(it.throw(error));
})
}
go(it.next());
}
run(g);
從代碼中可以看出。該方法主要分爲三部分,首先創建一個返回promise對象的函數getFoo;創建generator生成器函數,並在yield語句中執行getFoo函數,使generator執行next方法的時候返回promise對象,當接收到promise對象時,給對象添加then方法。最重要的是在then方法中執行nextgenerator的next方法。這些函數中最主要的是run方法,在run方法中運行了控制異步流程的generator方法,並定義了promise的then方法。
最後總結一下:歸根結底,使用generator解決回調地獄問題,需要首先處理一下調用的函數,使函數正確執行後能夠自動執行next方法,並且傳遞執行完方法後的結果。
參考文獻:co.js 異步回調的原理