深刻前端-JavaScript異步編程

JavaScript的執行機制在上篇文章中進行了深刻的探討,那麼既然是一門單線程語言,如何進行良好體驗的異步編程呢

回調函數Callbacks

當程序跑起來時,通常狀況下,應用程序(application program)會時常經過API調用庫裏所預先備好的函數。可是有些庫函數(library function)卻要求應用先傳給它一個函數,好在合適的時候調用,以完成目標任務。這個被傳入的、後又被調用的函數就稱爲回調函數(callback function)。node

什麼是異步

"調用"在發出以後,這個調用就直接返回了,因此沒有返回結果。換句話說,當一個異步過程調用發出後,調用者不會馬上獲得結果。而是在"調用"發出後,"被調用者"經過狀態、通知來通知調用者,或經過回調函數處理這個調用。異步調用發出後,不影響後面代碼的執行。
簡單說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。
在異步執行的模式下,每個異步的任務都有其本身一個或着多個回調函數,這樣當前在執行的異步任務執行完以後,不會立刻執行事件隊列中的下一項任務,而是執行它的回調函數,而下一項任務也不會等當前這個回調函數執行完,由於它也不能肯定當前的回調合適執行完畢,只要引它被觸發就會執行,es6

地獄回調階段

異步最先的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。可是回調函數有一個很常見的問題,就是回調地獄的問題
下面這幾種都屬於回調ajax

  • 事件回調
  • Node API
  • setTimeout/setInterval中的回調函數
  • ajax 請求

異步回調嵌套會致使代碼難以維護,而且不方便統一處理錯誤,不能 try catch會陷入回調地獄express

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});

ajax(url, () => {
    // 處理邏輯
    ajax(url1, () => {
        // 處理邏輯
        ajax(url2, () => {
            // 處理邏輯
        })
    })
})

Promise解決地獄回調階段

Promise 必定程度上解決了回調地獄的問題,Promise 最先由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。編程

  • Promise存在三個狀態(state)pending、fulfilled、rejected
  • pending(等待態)爲初始態,並能夠轉化爲fulfilled(成功態)和rejected(失敗態)
  • 成功時,不可轉爲其餘狀態,且必須有一個不可改變的值(value)
  • 失敗時,不可轉爲其餘狀態,且必須有一個不可改變的緣由(reason)
  • new Promise((resolve, reject)=>{resolve(value)}) resolve爲成功,接收參數value,狀態改變爲fulfilled,不可再次改變。
  • new Promise((resolve, reject)=>{reject(reason)}) reject爲失敗,接收參數reason,狀態改變爲rejected,不可再次改變。
  • 如果executor函數報錯 直接執行reject();

Promise 是一個構造函數,new Promise 返回一個 promise對象

const promise = new Promise((resolve, reject) => {
       // 異步處理
       // 處理結束後、調用resolve 或 reject
});

then方法註冊 當resolve(成功)/reject(失敗)的回調函數

// onFulfilled 參數是用來接收promise成功的值,
// onRejected 參數是用來接收promise失敗的緣由
//兩個回調返回的都是promise,這樣就能夠鏈式調用
promise.then(onFulfilled, onRejected);
const promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled' 
}, reason => { // onRejected 不會被調用
    
})

then方法的鏈式調用

Promise對象的then方法返回一個新的Promise對象,所以能夠經過鏈式調用then方法。then方法接收兩個函數做爲參數,第一個參數是Promise執行成功時的回調,第二個參數是Promise執行失敗時的回調。兩個函數只會有一個被調用,函數的返回值將被用做建立then返回的Promise對象。這兩個參數的返回值能夠是如下三種狀況中的一種:promise

  • return 一個同步的值 ,或者 undefined(當沒有返回一個有效值時,默認返回undefined),then方法將返回一個resolved狀態的Promise對象,Promise對象的值就是這個返回值。
  • return 另外一個 Promise,then方法將根據這個Promise的狀態和值建立一個新的Promise對象返回。
  • throw 一個同步異常,then方法將返回一個rejected狀態的Promise, 值是該異常。

解決層層回調問題

//對應上面第一個node讀取文件的例子
function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});
//對應第二個ajax請求例子
ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

能夠看到,Promise在必定程度上其實改善了回調函數的書寫方式,最明顯的一點就是去除了橫向擴展,不管有再多的業務依賴,經過多個then(...)來獲取數據,讓代碼只在縱向進行擴展;另一點就是邏輯性更明顯了,將異步業務提取成單個函數,整個流程能夠看到是一步步向下執行的,依賴層級也很清晰,最後須要的數據是在整個代碼的最後一步得到。
因此,Promise在必定程度上解決了回調函數的書寫結構問題,但回調函數依然在主流程上存在,只不過都放到了then(...)裏面,和咱們大腦順序線性的思惟邏輯仍是有出入的。瀏覽器

Promise缺點

  • 沒法取消 Promise
  • 當處於pending狀態時,沒法得知目前進展到哪個階段
  • 錯誤不能被 try catch

生成器Generators/ yield

什麼是Generator

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣
Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。形式上,Generator 函數是一個普通函數,可是有兩個特徵。app

  • 一是,function關鍵字與函數名之間有一個星號;
  • 二是,函數體內部使用yield表達式,定義不一樣的內部狀態

Generator調用方式

Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而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 }

瞭解Co

能夠看到上個例子當中咱們須要一步一步去調用next這樣也會很麻煩,這時咱們能夠引入co來幫咱們控制
Co是一個爲Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可使用更加優雅的方式編寫非阻塞代碼。
Co 函數庫約定,yield 命令後面只能是 Thunk 函數或 Promise 對象,而 async 函數的 await 命令後面,能夠跟 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。
說白了就是幫你自動執行你的Generator不用手動調用nextasync

解決異步問題

咱們能夠經過 Generator 函數解決回調地獄的問題,能夠把以前的回調地獄例子改寫爲以下代碼:

const co = require('co');
co(
function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
).then(data => {
    //code
}).catch(err => {
    //code
});
function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

終極解決方案Async/ await

async 函數是Generator 函數的語法糖,是對Generator作了進一步的封裝。

Async特色

  • 當調用一個 async 函數時,會返回一個 Promise 對象。
async function async1() {
      return "1"
    }
    console.log(async1()) // -> Promise {<resolved>: "1"}
  • 當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;
  • 當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值。
  • async 函數中可能會有 await 表達式,這會使 async 函數暫停執行,等待 Promise 的結果出來,而後恢復async函數的執行並返回解析(resolved)。
  • 內置執行器。 Generator 函數的執行必須靠執行器,因此纔有了 co 函數庫,而 async 函數自帶執行器。也就是說,async 函數的執行,與普通函數如出一轍,只要一行。
  • 更廣的適用性。co 模塊約定,yield 命令後面只能是 Thunk 函數或 Promise對象。而 async 函數的 await 命令後面則能夠是 Promise 或者 原始類型的值(Number,string,boolean,但這時等同於同步操做)

await特色

  • await 操做符用於等待一個Promise 對象。它只能在異步函數 async function 中使用。
  • [return_value] = await expression;
  • await 表達式會暫停當前 async function 的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回調的resolve函數參數做爲 await 表達式的值,繼續執行 async function。
  • 若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常緣由拋出。
  • 另外,若是 await 操做符後的表達式的值不是一個 Promise,則返回該值自己。

重點:遇到 await 表達式時,會讓 async 函數 暫停執行,等到 await 後面的語句(Promise)狀態發生改變(resolved或者rejected)以後,再恢復 async 函數的執行(再以後 await 下面的語句),並返回解析值(Promise的值)

爲何await能夠暫停執行並等到Promise的狀態改變再恢復執行呢

promise就是作這件事的 , 它會自動等到Promise決議之後的返回值,resolve(...)或者reject(...)均可以。
async內部會在promise.then(callback),回調函數裏調用 next()... (還有用Thunk的, 也是爲了作這個事的);

Async執行方式

簡單說 , async/awit 就是對上面gennerator自動化流程的封裝 , 讓每個異步任務都是自動化的執行 , 當第一個異步任務readFile(A)執行完如上一點說明的, async內部本身執行next(),調用第二個任務readFile(B);

這裏引入ES6阮一峯老師的例子
const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};


async function read() {
    await readFile(A);//執行到這裏中止往下執行,等待readFile內部resolve(data)後,再往下執行
    await readFile(B);
    await readFile(C);
    //code
}

//這裏可用於捕獲錯誤
read().then((data) => {
    //code
}).catch(err => {
    //code
});

參考文章

相關文章
相關標籤/搜索