咱們知道Javascript語言的執行環境是"單線程"。也就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務。javascript
這種模式雖然實現起來比較簡單,執行環境相對單純,可是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。html
爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步和異步。本文主要介紹異步編程幾種辦法,並經過比較,獲得最佳異步編程的解決方案!前端
想閱讀更多優質文章請猛戳GitHub博客java
咱們能夠通俗理解爲異步就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。排在異步任務後面的代碼,不用等待異步任務結束會立刻運行,也就是說,異步任務不具備」堵塞「效應。好比,有一個任務是讀取文件進行處理,異步的執行過程就是下面這樣node
這種不連續的執行,就叫作異步。相應地,連續的執行,就叫作同步git
"異步模式"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。接下來介紹下異步編程六種方法。github
回調函數是異步操做最基本的方法。如下代碼就是一個回調函數的例子:面試
ajax(url, () => { // 處理邏輯 })
可是回調函數有一個致命的弱點,就是容易寫出回調地獄(Callback hell)。假設多個請求存在依賴性,你可能就會寫出以下代碼:ajax
ajax(url, () => { // 處理邏輯 ajax(url1, () => { // 處理邏輯 ajax(url2, () => { // 處理邏輯 }) }) })
回調函數的優勢是簡單、容易理解和實現,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合,使得程序結構混亂、流程難以追蹤(尤爲是多個回調函數嵌套的狀況),並且每一個任務只能指定一個回調函數。此外它不能使用 try catch 捕獲錯誤,不能直接 return。npm
這種方式下,異步任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
下面是兩個函數f1和f2,編程的意圖是f2必須等到f1執行完成,才能執行。首先,爲f1綁定一個事件(這裏採用的jQuery的寫法)
f1.on('done', f2);
上面這行代碼的意思是,當f1發生done事件,就執行f2。而後,對f1進行改寫:
function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); }
上面代碼中,f1.trigger('done')表示,執行完成後,當即觸發done事件,從而開始執行f2。
這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合",有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程。
咱們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作"發佈/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
首先,f2向信號中心jQuery訂閱done信號。
jQuery.subscribe('done', f2);
而後,f1進行以下改寫:
function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
上面代碼中,jQuery.publish('done')的意思是,f1執行完成後,向信號中心jQuery發佈done信號,從而引起f2的執行。
f2完成執行後,能夠取消訂閱(unsubscribe)
jQuery.unsubscribe('done', f2);
這種方法的性質與「事件監聽」相似,可是明顯優於後者。由於能夠經過查看「消息中心」,瞭解存在多少信號、每一個信號有多少訂閱者,從而監控程序的運行。
Promise本意是承諾,在程序中的意思就是承諾我過一段時間後會給你一個結果。 何時會用到過一段時間?答案是異步操做,異步是指可能比較長時間纔有結果的才作,例如網絡請求、讀取本地文件等
這個承諾一旦從等待狀態變成爲其餘狀態就永遠不能更改狀態了,好比說一旦狀態變爲 resolved 後,就不能再次改變爲Fulfilled
let p = new Promise((resolve, reject) => { reject('reject') resolve('success')//無效代碼不會執行 }) p.then( value => { console.log(value) }, reason => { console.log(reason)//reject } )
當咱們在構造 Promise 的時候,構造函數內部的代碼是當即執行的
new Promise((resolve, reject) => { console.log('new Promise') resolve('success') }) console.log('end') // new Promise => end
接下來咱們看幾個例子:
// 例1 Promise.resolve(1) .then(res => { console.log(res) return 2 //包裝成 Promise.resolve(2) }) .catch(err => 3) .then(res => console.log(res))
// 例2 Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error('My Error') }) .catch(() => 1) .then(x => x + 1) .then(x => console.log(x)) //2 .catch(console.error)
// 例3 let fs = require('fs') function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, 'utf8', (err, data) => { if (err) reject(err) resolve(data) }) }) } read('./name.txt') .then(function(data) { throw new Error() //then中出現異常,會走下一個then的失敗回調 }) //因爲下一個then沒有失敗回調,就會繼續往下找,若是都沒有,就會被catch捕獲到 .then(function(data) { console.log('data') }) .then() .then(null, function(err) { console.log('then', err)// then error }) .catch(function(err) { console.log('error') })
Promise不只可以捕獲錯誤,並且也很好地解決了回調地獄的問題,能夠把以前的回調地獄例子改寫爲以下代碼:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))
它也是存在一些缺點的,好比沒法取消 Promise,錯誤須要經過回調函數捕獲。
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣,Generator 最大的特色就是能夠控制函數的執行。
咱們先來看個例子:
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true}
可能結果跟你想象不一致,接下來咱們逐行代碼分析:
咱們再來看個例子:有三個本地文件,分別1.txt,2.txt和3.txt,內容都只有一句話,下一個請求依賴上一個請求的結果,想經過Generator函數依次調用三個文件
//1.txt文件 2.txt
//2.txt文件 3.txt
//3.txt文件 結束
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let it = r() let { value, done } = it.next() value.then(function(data) { // value是個promise console.log(data) //data=>2.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>3.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>結束 }) }) }) // 2.txt=>3.txt=>結束
從上例中咱們看出手動迭代Generator
函數很麻煩,實現邏輯有點繞,而實際開發通常會配合 co
庫去使用。co
是一個爲Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可使用更加優雅的方式編寫非阻塞代碼。
安裝co
庫只需:npm install co
上面例子只需兩句話就能夠輕鬆實現
function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let co = require('co') co(r()).then(function(data) { console.log(data) }) // 2.txt=>3.txt=>結束=>undefined
咱們能夠經過 Generator 函數解決回調地獄的問題,能夠把以前的回調地獄例子改寫爲以下代碼:
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,你能夠輕鬆地達成以前使用生成器和co函數所作到的工做,它有以下特色:
一個函數若是加上 async ,那麼該函數就會返回一個 Promise
async function async1() { return "1" } console.log(async1()) // -> Promise {<resolved>: "1"}
Generator函數依次調用三個文件那個例子用async/await寫法,只需幾句話即可實現
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } async function readResult(params) { try { let p1 = await read(params, 'utf8')//await後面跟的是一個Promise實例 let p2 = await read(p1, 'utf8') let p3 = await read(p2, 'utf8') console.log('p1', p1) console.log('p2', p2) console.log('p3', p3) return p3 } catch (error) { console.log(error) } } readResult('1.txt').then( // async函數返回的也是個promise data => { console.log(data) }, err => console.log(err) ) // p1 2.txt // p2 3.txt // p3 結束 // 結束
若是請求兩個文件,毫無關係,能夠經過併發請求
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function readAll() { read1() read2()//這個函數同步執行 } async function read1() { let r = await read('1.txt','utf8') console.log(r) } async function read2() { let r = await read('2.txt','utf8') console.log(r) } readAll() // 2.txt 3.txt
1.JS 異步編程進化史:callback -> promise -> generator -> async + await
2.async/await 函數的實現,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
3.async/await能夠說是異步終極解決方案了。
(1) async/await函數相對於Promise,優點體如今:
固然async/await函數也存在一些缺點,由於 await 將異步代碼改形成了同步代碼,若是多個異步代碼沒有依賴性卻使用了 await 會致使性能上的下降,代碼沒有依賴性的話,徹底可使用 Promise.all 的方式。
(2) async/await函數對 Generator 函數的改進,體如今如下三點:
Generator 函數的執行必須靠執行器,因此纔有了 co 函數庫,而 async 函數自帶執行器。也就是說,async 函數的執行,與普通函數如出一轍,只要一行。