promise、generator、async的簡單應用

javascript的運行機制是單線程處理,即只有上一個任務完成后,才會執行下一個任務,這種機制也被稱為“同步”。

“同步”的最大缺點,就是如果某一任務運行時間較長,其后的任務就無法執行。這樣會阻塞頁面的渲染,導致頁面加載錯誤或是瀏覽器不響應進入假死狀態。

如果這段任務采用“異步”機制,那么它就會等到其他任務運行完后再執行,不會阻塞線程。

一、es6之前實現異步的方式:

最常用的方法是采用回調函數,但普通的回調函數并不能實現異步效果:

(1)同步回調
function testFn(data, callback){
  callback(data);
}
testFn('0', function(data2){
  for (var i=data2; i<300000000; i++);
  console.log(1);
});

console.log(2);

等一秒鐘左右for循環結束,控制臺輸出1后,才會輸出2。這是因為 testFn 的回調函數 callback 是同步執行,所以for循環會阻塞后面的任務。

(2)異步回調

想要實現回調的異步執行,必須要借助js的其它方法,例如將上面 testFn 的回調函數放到延時器中調用,延遲的時間設置為0:

function testFn(data, callback){
  setTimeout(function(){
    callback(data)
  },0);
}

這樣控制臺會先輸出2,接著大約一秒鐘后再輸出1,可見回調函數并沒有阻塞后面的任務,實現了異步效果。

除了使用定時器,實現異步執行的方法還有:事件監聽(例如點擊事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。

二、promise、generator和async/await:

es6 引入了 generator 和 promise,es7 又增加了async/await。這三種函數有一個重要的作用,就是解決回調函數的異步執行和嵌套問題。

(因為網絡上介紹這三種方法的文章很多,所以這里只介紹個人覺得相對常用的知識點)
(一)普通的回調嵌套:

定義一個函數,要求只有在獲取兩個數據(例如"data1"和"data2")后,才會執行“console.log”任務,通過對回調函數進行嵌套就可以達到目的:

function Test1(data, callback){
    if(data){          // 本例是同步,要實現異步可以添加延時器
        callback(data);
    };
};

Test1("data1",function(){        
    Test1("data2",function(data2){
        console.log(data2);     // 如果不傳數據就不會執行回調函數
    });
});

回調函數的執行需要依賴上一個函數,這樣的缺點是如果有多個回調函數,就需要嵌套很多層。這會使代碼的可讀性較差,并增加調試和維護的難度。

(二)promise

Promise是一個構造函數,它接收一個匿名函數作為參數:

new Promise(function(resolve, reject){
  resolve(console.log(1));
  reject(console.log(2));
  console.log(3);
})

console.log(4);
// 輸出的順序為1、2、3、4 

1、因為 promise 是構造函數(帶有屬性或方法的函數就叫構造函數),所以必須使用 new 實例化才能調用。而作為參數的匿名函數不需要額外調用就能執行。

2、匿名函數只能有 resolve 和 reject 兩個參數。結合if判斷使用的話,resolve 是條件正確時執行的方法,reject 則是錯誤時執行的方法。

3、將某一任務直接放到 resolve、reject 方法里,或者放到匿名函數里并不能實現異步效果。

function Test2(){
  return new Promise(function(resolve, reject){
    resolve(1);                     
  });
};

Test2().then(function(num){
  console.log(num);
  return num;
}).then(function(num2){
  console.log(num2);
})
console.log(2);      
// 數字輸出的順序是2、1、1

4、為了防止匿名函數的自動執行,需要再定義一個函數,并將 promise 函數作為該函數的返回值。

5、將回調函數寫在 then 或 catch 方法里才能實現異步,then 接受的是 resolve 方法傳遞的數據,catch 則對應的是 reject。

6、resolve 只能向第一個 then 里的函數傳遞數據,后面 then 里的函數只能通過前一個 then 里函數的返回值獲取參數。

function pro1(){
  return new Promise(function(resolve, reject){
    resolve(1);
  });
};

function pro2(){
  return new Promise(function(resolve, reject){
  });
};

Promise.all([ pro1(), pro2() ]).then().catch();
Promise.race([ pro1(), pro2() ]).then().catch();

7、all方法的作用:只有 pro1 和 pro2 都執行 resolve,Promise 才會執行 then 方法。如果其中有一個函數執行 reject,那么Promise 就執行catch方法。

8、race方法的作用:pro1 和 pro2 中只要有一個狀態發生改變,Promise的狀態就跟著發生改變,不論是resolve還是reject。

promise最大的作用

(一)能把嵌套改成鏈式調用。對上面的普通嵌套進行改造:

function Test2(data){
  return new Promise(function(resolve, reject){
    if(data){
      resolve(data);
    }
  });
};

Test2(1).then(function( data1 ){
  console.log( data1 );
  return 2;     
}).then(function( data2 ){
  console.log( data2 );
})

console.log(3);
// 輸出結果為3、1、2

實現效果:
(1)先輸出3,表示 then 方法里的回調函數是異步執行。
(2)如果調用 Test2 時不傳數字“1”,就不會執行 resolve 方法,這樣即使第一個 then 方法里的函數有返回值“2”,也不會執行第二個 then 方法,實現了需求。

(二)解決 ajax 不能傳值給外部變量的問題
ajax 在 success 獲取的數值無法傳遞給外部變量,除非設置為同步模式,而 promise 的 resolve 方法可以解決這個問題。

設一個外部變量 outsideData:

var outsideData ;
function TestAjax(data){
  return new Promise(function(resolve, reject){
    $.ajax({
        url: 'xxx.php';
        type: 'GET',
        datatype: 'json',
        data: ' ',
        success: function(res){
          resolve(res.data)
        }
    });
  });
};

TestAjax().then(function( data ){
  outsideData = data;
})
(三)async/await

先說 async/await,是因為 async 函數是 Generator 函數的語法糖,容易理解,而且和 promise 函數也有很大的關聯。

用 async function 定義一個函數:

async function Test(){      
  return 1;         
}
Test().then(function(num){
  console.log(num);      
});
console.log( Test() );

得到的結果是

1、async function 返回的是一個 promise 對象,所以該函數可以調用 then 方法,并因此實現異步執行效果。

async function Test(){      
  await console.log( 1 );
  console.log( 2 );
  await console.log( 3 );
}
Test();
console.log( 4 );
// 結果為1、4、2、3

2、await 可以代替 then 方法,以實現異步效果。

3、但第一個 await 是同步執行。不論跟的是 promise,還是其他的函數或方法。

4、第一個 await 后面的任務,不論有沒有 await 都是異步執行。但是如果要在 async 函數里再放一個函數,那前面就必須添加 await,否則會報錯。

function proFn(){
  return new Promise(function(resolve, reject){
    resolve(2);
  });
};

async function Test(){
  var data1 = await proFn();
  console.log(1);
  console.log(data1);
};
Test();
console.log(3);
// 結果為3、1、2

5、對于第一個 await,最好的方法是使其等于一個變量,然后對這個變量進行處理。

6、如果 await 后面跟的是 promise ,那么匿名函數必須執行 resolve 方法,否則 await 后面的任務就無法執行。

async/await 最大的作用就是替代 promise 的 then 方法:
function proFn(data){
  return new Promise(function(){
    if(data){
      resolve(data);
    };
  });
};

async function Test(){
  var data1 = await proFn(1);
  var data1 = await proFn(2);
  console.log(data1);
  console.log(data2);
};
Test();

console.log(3);
// 結果為3、1、2

(1)async/await 將鏈式調用變得更加簡化。
(2)async/await 傳遞參數的方式也比 then 簡單,不需要通過 return 來傳遞參數。

(四)generator

和 promise、async/await 不同,generator 本身并不具有異步執行的功能。它在異步中的主要應用,是管理異步回調的執行流程。

generator 函數的特征是使用 * 和關鍵詞 yield:

function* Gen(){
  yield 1;
}
var runGen = Gen();
console.log(runGen.next());  
// 輸出結果為 {value: 1, done: false};

1、函數名后面加小括號并不能調用 generator 函數,只是創建了一個指針對象,next 方法才會執行函數。

2、next 方法返回的對象帶有兩個屬性,分別是“done”和“value”。done 的值表示 generator 函數是否運行完。value 對應的是 yield 后面的值。

function* Gen (num){
  var num2 = yield console.log(num);
  yield console.log(num2);
}
var runGen = Gen(1); 
runGen.next(2);
runGen.next(3);
// 結果輸出1和3。                      

3、next 方法可以傳遞參數,該參數是上一個 yield 表達式的值。
之所以沒有輸出“2”,就是因為第一個 next 方法并沒有與之對應的"上一個 yield",所以傳參無效。

generator 函數管理流程的應用:

使用 generator 管理異步函數,需要用到三個知識點,thunk函數、next方法得到的done屬性、遞歸。

1、thunk函數
普通的多參數函數在調用時,需要一次性傳入多個數據。
thunk 函數則是把多個參數拆開,使得在調用時,數據可以分開傳入。

實現方法是定義一個函數,并且該函數的返回值也是一個函數,這樣就能將參數拆開放在兩個函數里:

// 普通的多參數函數:
function Test(data, callback){};
// 調用普通函數:
Test(data, callback);


// thunk函數:
function Test(data){
 return function(callback){
   callback(); 
 }
}
// 調用thunk函數:
var runThunk = Test(data);
runThunk(callback);

將 callback 參數提取出來,放在作為返回值的匿名函數里,在調用該函數時 callback 和 data 所對應的數據就可以分兩步傳入。

2、通過 done 屬性控制流程:

generator 函數代替“嵌套”去控制流程的思路,就是通過上一個 yield 的執行情況,來決定下一個 next 方法是否執行,這需要用到 done 屬性:

function Test(){
  setTimeout(function(){
    console.log(1);
  },0);     
}
function* Gen(){
  yield Test();
  yield console.log(2);
}

var runGen = Gen();
var genObj = runGen.next();

if(!genObj.done){
  runGen.next();
}
// 結果為2、1

(1)直接給 next 方法外面添加 if 判斷的缺點是,假如某個 yield 后面跟的是異步函數,那么其他 yield 所對應的非異步任務就會優先執行。

如果必須保證前一個任務運行完后,才會執行下一步,就需要把 next 方法放到 value 屬性里:

function Test2(){
  return function(callback){
      console.log(3);
      callback();
  }
}
function* Gen2(){
  yield Test2();
  yield console.log(4);
}

var runGen2 = Gen2();
var genObj2 = runGen2.next();

genObj2.value( function(){
  runGen2.next();
} );
// 得到的結果是 3、4

(2)想在 value 里使用 next 方法,需要將 value 變成一個函數,這就要用到 thunk 函數。而 value 后面加一個小括號,就能調用作為返回值的函數了。

(3)如果把 next 方法直接放到 value 里,那么 next 方法得到的結果會被當成 value 的參數,先輸出。所以需要給 next 方法外面再包一層函數。

下面的函數就是最終形態:

function Test3(){
  return function(callback){
    setTimeout(function(){
       console.log(5);
       callback();
    },0)  
  }
}
function* Gen3(){
  yield Test3();
  yield console.log(6);
}

var runGen3 = Gen3();
var genObj3 = runGen3.next();

genObj3.value( function(){
  if(genObj3.done) return;
  runGen3.next();
} );
// 結果為5、6
3、使用遞歸自動執行 generator 函數:

首先看看手動執行 generator 的例子:

// 為了簡潔,本例并沒有使用異步
function Thunk(num){
  return function(callback){
    console.log(num);
    callback();          
  }; 
};

function* Gen(){
  yield Thunk(1);
  yield Thunk(2);
}

var runGen = Gen();

var runNext = runGen.next();
if(runNext.done) return;
runNext.value(function(){
  
  var runNext2 = runGen.next();
  if(runNext.done) return;
  runNext2.value(function(){
   
  });
});
// 結果輸出1、2

假如存在多個 yield,就需要寫很多 next,這會令代碼變得臃腫。通過觀察可以看出使用 next 方法的部分存在很大的重復性,所以可以使用遞歸(也就是函數內部調用自身)對其進行改造。

var  runGen = Gen();

function next(err, data) {
  var runNext = runGen.next(data);
  if (runNext.done) return;
  runNext.value(next);          
}
next();

5、修改 promise 的鏈式調用

function Test(num){
  return new Promise( function(resolve, reject){
    resolve(num);
  } );
}

function* Gen(){
  yield Test(1);
  yield Test(2);
}

var runGen = Gen();
function next(){
  var genObj = runGen.next();
                
  if(genObj.done) return;
  genObj.value.then(function(num){
    console.log(num);
    next();
  });
}
next();

console.log(3);
// 得到的結果為 3、1、2

總結:

關于es6的研究到此就告一段落了。個人覺得“類”和“箭頭函數”是一定要掌握的,因為這兩點能簡化代碼結構。至于異步,從例子的長短也能看出,generator 沒必要了解很深,還是交給 promise 和 ansyc/await 吧。

三、參考:

1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺點)
2、http://blog.csdn.net/tywinstark/article/details/48447135 (15樓回復)
3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js實現異步的方法)
4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js運行機制)
5、http://www.nowamagic.net/librarys/veda/detail/787 (瀏覽器假死原因)
6、https://segmentfault.com/a/1190000003096984 (異步回調的缺點)
7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回調函數嵌套的缺點)
8、https://segmentfault.com/q/1010000002577322 (回調函數如何實現異步)
9、http://es6.ruanyifeng.com/#docs/promise (promise知識點)
10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一個函數里)
11、https://segmentfault.com/a/1190000007535316(await知識點)
12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是異步)
13、http://es6.ruanyifeng.com/#docs/generator (generator知識點)
14、http://www.liaoxuefeng.com/wiki/ (generator函數的調用)
15、http://www.lxweimin.com/p/87183851756f (promise使用例子)
16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax獲取的數值)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容