JavaScript的執行機制在上篇文章中進行了深刻的探討,那麼既然是一門單線程語言,如何進行良好體驗的異步編程呢
當程序跑起來時,通常狀況下,應用程序(application program)會時常經過API調用庫裏所預先備好的函數。可是有些庫函數(library function)卻要求應用先傳給它一個函數,好在合適的時候調用,以完成目標任務。這個被傳入的、後又被調用的函數就稱爲回調函數(callback function)。node
"調用"在發出以後,這個調用就直接返回了,因此沒有返回結果。換句話說,當一個異步過程調用發出後,調用者不會馬上獲得結果。而是在"調用"發出後,"被調用者"經過狀態、通知來通知調用者,或經過回調函數處理這個調用。異步調用發出後,不影響後面代碼的執行。
簡單說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。
在異步執行的模式下,每個異步的任務都有其本身一個或着多個回調函數,這樣當前在執行的異步任務執行完以後,不會立刻執行事件隊列中的下一項任務,而是執行它的回調函數,而下一項任務也不會等當前這個回調函數執行完,由於它也不能肯定當前的回調合適執行完畢,只要引它被觸發就會執行,es6
異步最先的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。可是回調函數有一個很常見的問題,就是回調地獄的問題
下面這幾種都屬於回調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 最先由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。編程
const promise = new Promise((resolve, reject) => { // 異步處理 // 處理結束後、調用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 不會被調用 })
Promise對象的then方法返回一個新的Promise對象,所以能夠經過鏈式調用then方法。then方法接收兩個函數做爲參數,第一個參數是Promise執行成功時的回調,第二個參數是Promise執行失敗時的回調。兩個函數只會有一個被調用,函數的返回值將被用做建立then返回的Promise對象。這兩個參數的返回值能夠是如下三種狀況中的一種: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(...)裏面,和咱們大腦順序線性的思惟邏輯仍是有出入的。瀏覽器
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣
Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。形式上,Generator 函數是一個普通函數,可是有兩個特徵。app
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 }
能夠看到上個例子當中咱們須要一步一步去調用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 函數是Generator 函數的語法糖,是對Generator作了進一步的封裝。
async function async1() { return "1" } console.log(async1()) // -> Promise {<resolved>: "1"}
重點:遇到 await 表達式時,會讓 async 函數 暫停執行,等到 await 後面的語句(Promise)狀態發生改變(resolved或者rejected)以後,再恢復 async 函數的執行(再以後 await 下面的語句),並返回解析值(Promise的值)
promise就是作這件事的 , 它會自動等到Promise決議之後的返回值,resolve(...)或者reject(...)均可以。
async內部會在promise.then(callback),回調函數裏調用 next()... (還有用Thunk的, 也是爲了作這個事的);
簡單說 , 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 });