說一說javascript的異步編程

衆所周知javascript是單線程的,它的設計之初是爲瀏覽器設計的GUI編程語言,GUI編程的特性之一是保證UI線程必定不能阻塞,不然體驗不佳,甚至界面卡死。javascript

所謂的單線程就是一次只能完成一個任務,其任務的調度方式就是排隊,這就和火車站洗手間門口的等待同樣,前面的那我的沒有搞定,你就只能站在後面排隊等着。java

圖片來自網絡

這種模式的好處是實現起來簡單,執行環境相對單純,壞處就是隻要有一個任務耗時很長,後面的任務都會必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使了整個頁面卡在這個地方,其餘任務沒法執行。node

爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。jquery

「同步」就是上面所說的,後面的任務等待上一個任務結束,而後再執行。git

什麼是「異步」?

所謂異步簡單說就是一個任務分紅兩段,先執行一段,轉而執行其餘任務,等作好了準備轉而執行第二段。github

如下是當有ABC三個任務,同步或異步執行的流程圖:npm

同步編程

thread ->|----A-----||-----B-----------||-------C------|
複製代碼

異步:api

A-Start ---------------------------------------- A-End   
           | B-Start ----------------------------------------|--- B-End   
           |   |     C-Start -------------------- C-End      |     |   
           V   V       V                           V         V     V      
  thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
複製代碼

"異步"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。promise

本文簡單梳理總結了JavaScript異步函數的發展歷史以下圖:

圖片來自網絡

  1. 回調函數
  2. Promise
  3. Generator+co
  4. async,await

回調函數Callbacks

彷佛一切應該從回調函數開始談起。

異步JavaScript

在Javascript 中,異步編程方式只能經過JavaScript中的一等公民函數才能完成:這種方式意味着咱們能夠將一個函數做爲另外一個函數的參數,在這個函數的內部能夠調用被傳遞進來的函數(即回調函數)。

這也正是回調函數誕生的緣由:若是你將一個函數做爲參數傳遞給另外一個函數(此時它被稱爲高階函數),那麼在函數內部, 你能夠調用這個函數來完成相應的任務。

回調函數沒有返回值(不要試圖用return),僅僅被用來在函數內部執行某些動做。

看下面的例子:

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});
複製代碼

這裏只是作4步,嵌套了4層回調,若是更多步驟呢?顯然這樣的代碼只是寫起來比較爽可是缺點也不少。

過分使用回調函數所會遇到的挑戰:

  • 若是不能合理的組織代碼,很是容易形成回調地獄(callback hell),這會使得你的代碼很難被別人所理解。
  • 不能捕獲異常 (try catch 同步執行,回調函數會加入隊列,沒法捕獲錯誤)
  • 沒法使用return語句返回值,而且也不能使用throw關鍵字。

也正是基於這些緣由,在JavaScript世界中,一直都在尋找着可以讓異步JavaScript開發變得更簡單的可行的方案。這個時候就出現了promise,它解決了上述的問題。

Promise

Promise 的最大優點是標準化,各種異步工具庫都按照統一規範實現,即便是async函數也能夠無縫集成。因此用 Promise 封裝 API 通用性強,用起來簡單,學習成本低。

一個Promise表明的是一個異步操做的最終結果。

Promise意味着[許願|承諾]一個尚未完成的操做,但在將來會完成的。與Promise最主要的交互方法是經過將函數傳入它的then方法從而獲取得Promise最終的值或Promise最終拒絕(reject)的緣由。要點有三個:

  • 遞歸,每一個異步操做返回的都是promise對象
  • 狀態機:三種狀態轉換,只在promise對象內部能夠控制,外部不能改變狀態
  • 全局異常處理

1)定義

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, thenif (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});
複製代碼

每一個Promise定義都是同樣的,在構造函數裏傳入一個匿名函數,參數是resolve和reject,分別表明成功和失敗時候的處理。

2) 調用

promise.then(function(text){
    console.log(text)// Stuff worked!
    return Promise.reject(new Error('我是故意的'))
}).catch(function(err){
    console.log(err)
})
複製代碼

它的主要交互方式是經過then函數,若是Promise成功執行resolve了,那麼它就會將resolve的值傳給最近的then函數,做爲它的then函數的參數。若是出錯reject,那就交給catch來捕獲異常就行了。

咱們能夠經過調用promise的示例,瞭解一下propmise的一些原理及特性:

普通調用實例:

let fs = require('fs');
let p = new Promise(function(resolve,reject){
  fs.readFile('./1.txt','utf8',(err,data)=>{
      err?reject(err):resolve(data);
  })
})

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
複製代碼

1.promise實例能夠屢次調用then方法

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
p.then((data)=>{console.log(data)},(err)=>{console.log(err)});

複製代碼

2.promise實例能夠支持then方法的鏈式調用,jquery實現鏈式是經過返回當前的this。可是promise不能夠經過返回this來實現。由於後續經過鏈式增長的then不是經過原始的promise對象的狀態來決定走成功仍是走失敗的。

p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
複製代碼

3.只要then方法中的成功回調和失敗回調,有返回值(包括undefiend),都會走到下個then方法中的成功回調中,而且把返回值做爲下個then成功回調的參數傳進去。

第一個then走成功:
p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
輸出:undefiend
第一個then走失敗:
  p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
輸出:undefiend

複製代碼

4.只要then方法中的成功回調和失敗回調,有一個拋出異常,則都會走到下一個then中的失敗回調中

第一個then走成功:
p.then((data)=>{throw new Err("錯誤")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤
第一個then走失敗:
  p.then((data)=>{console.log(1)},(err)={throw new Err("錯誤")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤

複製代碼

5.成功和失敗 只能走一個,若是成功了,就不會走失敗,若是失敗了,就不會走成功;

6.若是then方法中,返回的不是一個普通值,仍舊是一個promise對象,該如何處理?

答案:它會等待這個promise的執行結果,而且傳給下一個then方法。若是成功,就把這個promise的結果傳給下一個then的成功回調而且執行,若是失敗就把錯誤傳給下一個then的失敗回調而且執行。

7.具有catch捕獲錯誤;若是catche前面的全部then方法都沒有失敗回調,則catche會捕獲到錯誤信息執行他就是用來兜兒底用的

p是一個失敗的回調:
p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('錯誤')}
複製代碼

8.返回的結果和 promise是同一個,永遠不會成功和失敗

var  r  = new Promise(function(resolve,reject){
   return r;
})
r.then(function(){
    console.log(1)
},function(err){
    console.log(err)
})
複製代碼

能夠看到結果一直都是pending狀態

圖片來自網絡

當你沒有現成的Promise時,你可能須要藉助一些Promise庫,一個流行的選擇是使用 bluebird。 這些庫可能會提供比原生方案更多的功能,而且不侷限於Promise/A+標準所規定的特性。

Generator(ECMAScript6)+co

JavaScript 生成器是個相對較新的概念, 它是ES6(也被稱爲ES2015)的新特性。想象下面這樣的一個場景:

當你在執行一個函數的時候,你能夠在某個點暫停函數的執行,而且作一些其餘工做,而後再返回這個函數繼續執行, 甚至是攜帶一些新的值,而後繼續執行。

上面描述的場景正是JavaScript生成器函數所致力於解決的問題。當咱們調用一個生成器函數的時候,它並不會當即執行, 而是須要咱們手動的去執行迭代操做(next方法)。也就是說,你調用生成器函數,它會返回給你一個迭代器。迭代器會遍歷每一箇中斷點。

function* foo () {  
  var index = 0;
  while (index < 2) {
    yield index++; //暫停函數執行,並執行yield後的操做
  }
}
var bar =  foo(); // 返回的實際上是一個迭代器

console.log(bar.next());    // { value: 0, done: false }  
console.log(bar.next());    // { value: 1, done: false }  
console.log(bar.next());    // { value: undefined, done: true }  
複製代碼

更進一步的,若是你想更輕鬆的使用生成器函數來編寫異步JavaScript代碼,咱們可使用 co 這個庫,co是著名的tj大神寫的。

Co是一個爲Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可使用更加優雅的方式編寫非阻塞代碼。

使用co,前面的示例代碼,咱們可使用下面的代碼來改寫:

co(function* (){  
  yield Something.save();
}).then(function() {
  // success
})
.catch(function(err) {
  //error handling
});
複製代碼

你可能會問:如何實現並行操做呢?答案可能比你想象的簡單,以下(其實它就是Promise.all而已):

yield [Something.save(), Otherthing.save()];  
複製代碼

終極解決方案Async/ await

簡而言之,使用async關鍵字,你能夠輕鬆地達成以前使用生成器和co函數所作到的工做。

在這背後,async函數實際使用的是Promise,這就是爲何async函數會返回一個Promise的緣由。

所以,咱們使用async函數來完成相似於前面代碼所完成的工做,可使用下面這樣的方式來從新編寫代碼:

async function save(Something) {  
  try {
    await Something.save(); // 等待await後面的代碼執行完,相似於yield
  } catch (ex) {
    //error handling
  }
  console.log('success');
} 
複製代碼

使用async函數,你須要在函數聲明的最前面加上async關鍵字。這以後,你能夠在函數內部使用await關鍵字了,做用和以前的yield做用是相似的。

使用async函數完成並行任務與yiled的方式很是的類似,惟一不一樣的是,此時Promise.all再也不是隱式的,你須要顯示的調用它:

async function save(Something) {  
    await Promise.all[Something.save(), Otherthing.save()]
}
複製代碼

Async/Await是異步操做的終極解決方案,Koa 2在node 7.6發佈以後,立馬發佈了正式版本,而且推薦使用async函數來編寫Koa中間件。

這裏給出一段Koa 2應用裏的一段代碼:

exports.list = async (ctx, next) => {
  try {
    let students = await Student.getAllAsync();
  
    await ctx.render('students/index', {
      students : students
    })
  } catch (err) {
    return ctx.api_error(err);
  }
};
複製代碼

它作了3件事兒

  • 經過await Student.getAllAsync();來獲取全部的students信息。
  • 經過await ctx.render渲染頁面
  • 因爲是同步代碼,使用try/catch作的異常處理

以後還會分享node的基本概念和eventLoop(宏任務和微任務)

(完)

參考: The Evolution of Asynchronous JavaScript

相關文章
相關標籤/搜索