JavaScript屬於單線程語言,即在同一時間,只能執行一個任務。在執行任務時,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。javascript
當咱們向後臺發送一個請求時,主線程讀取 「向後臺發送請求」 這個事件並執行以後,到獲取後臺返回的數據這一過程會有段時間間隔,這時CPU處於空閒階段,直到獲取數據後再繼續執行後面的任務,這就下降了用戶體驗度,使得頁面加載變慢。因而,全部任務能夠分紅兩種:同步任務和異步任務。java
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制,這個過程會不斷重複。"任務隊列"是一個事件的隊列(也能夠理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。node
回調函數,也被稱爲高階函數,是一個被做爲參數傳遞給另外一個函數並在該函數中被調用的函數。看一個在JQuery中簡單廣泛的例子:編程
// 注意: click方法是一個函數而不是變量 $("#button").click(function() { alert("Button Clicked"); });
能夠看到,上述例子將一個函數做爲參數傳遞給了click
方法,click
方法會調用該函數,這是JavaScript中回調函數的典型用法,它在jQuery
中普遍被使用。它不會當即執行,由於咱們沒有在後面加( ),而是在點擊事件發生時纔會執行。json
好比,咱們要下載一個gif,可是不但願在下載的時候阻斷其餘程序,能夠實現以下:api
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto) function handlePhoto (error, photo) { if (error) { console.error('Download error!', error); } else { console.log('Download finished', photo); } } console.log('Download started')
首先聲明handlePhoto
函數,而後調用downloadPhoto
函數並傳遞handlePhoto
做爲其回調函數,最後打印出「Download started」。
請注意,handlePhoto
還沒有被調用,它只是被建立並做爲回調傳入downloadPhoto
。但直到downloadPhoto
完成其任務後才能運行,這可能須要很長時間,具體取決於Internet
鏈接的速度,因此運行代碼後,會先打印出Download started
。數組
這個例子是爲了說明兩個重要的概念:promise
handlePhoto
回調只是稍後存儲一些事情的一種方式;var fs = require('fs'); /** * 若是三個異步api操做的話 沒法保證他們的執行順序 * 咱們在每一個操做後用回調函數就能夠保證執行順序 */ fs.readFile('./data1.json', 'utf8', function(err, data){ if (err) { throw err; } else { console.log(data); fs.readFile('./data2.json', 'utf8', function(err, data){ if (err) { throw err; } else { console.log(data) fs.readFile('./data3.json', 'utf8', function(err, data){ if (err) { throw err; } else { console.log(data); } }) } }) } })
有沒有看到這些以"})"結尾的金字塔結構?因爲回調函數是異步的,在上面的代碼中每一層的回調函數都須要依賴上一層的回調執行完,因此造成了層層嵌套的關係最終造成相似上面的回調地獄。app
var form = document.querySelector('form') form.onsubmit = function formSubmit (submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, function postResponse (err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body }) }
能夠看到,上面的代碼給兩個函數加了描述性功能名稱,使代碼更容易閱讀,當發生異常時,你將得到引用實際函數名稱而不是「匿名」的堆棧跟蹤。異步
如今咱們能夠將這些功能移到咱們程序的頂層:
document.querySelector('form').onsubmit = formSubmit; function formSubmit (submitEvent) { var name = document.querySelector('input').value; request({ uri: "http://example.com/upload", body: name, method: "POST" }, postResponse); } function postResponse (err, response, body) { var statusMessage = document.querySelector('.status'); if (err) return statusMessage.value = err; statusMessage.value = body; }
從新整改代碼結構以後,能夠清晰的看到這段函數的功能。
從上面取出樣板代碼,並將其分紅幾個文件,將其轉換爲模塊。
這是一個名爲formuploader.js
的新文件,它包含了以前的兩個函數:
module.exports.submit = formSubmit; function formSubmit (submitEvent) { var name = document.querySelector('input').value; request({ uri: "http://example.com/upload", body: name, method: "POST" }, postResponse) } function postResponse (err, response, body) { var statusMessage = document.querySelector('.status'); if (err) return statusMessage.value = err; statusMessage.value = body; }
把它們exports
後,在應用程序中引入並使用,這就使得代碼更加簡潔易懂了:
var formUploader = require('formuploader'); document.querySelector('form').onsubmit = formUploader.submit;
處理每一處錯誤,而且回調的第一個參數始終保留用於錯誤:
var fs = require('fs') fs.readFile('/Does/not/exist', handleFile); function handleFile (error, file) { if (error) return console.error('Uhoh, there was an error', error); // otherwise, continue on and use `file` in your code; }
有第一個參數是錯誤是一個簡單的慣例,鼓勵你記住處理你的錯誤。若是它是第二個參數,會更容易忽略錯誤。
除了上述代碼層面的解決方法,還可使用如下更高級的方法,也是另外4種實現異步的方法。可是請記住,回調是JavaScript的基本組成部分(由於它們只是函數),在學習更先進的語言特性以前學習如何讀寫它們,由於它們都依賴於對回調。
訂閱者把本身想訂閱的事件註冊到調度中心,當該事件觸發時候,發佈者發佈該事件到調度中心(順帶上下文),由調度中心統一調度訂閱者註冊到調度中心的處理代碼。
好比有個界面是實時顯示天氣,它就訂閱天氣事件(註冊到調度中心,包括處理程序),當天氣變化時(定時獲取數據),就做爲發佈者發佈天氣信息到調度中心,調度中心就調度訂閱者的天氣處理程序。簡單來講,發佈訂閱模式,有一個事件池,用來給你訂閱(註冊)事件,當你訂閱的事件發生時就會通知你,而後你就能夠去處理此事件。
使用發佈訂閱模式,來修改Ajax
:
xhr.onreadystatechange = function () {//監聽事件 if (this.readyState === 4) { if (this.status === 200) { switch (dataType) { case 'json': { Event.emit('data '+method,JSON.parse(this.responseText)); //觸發事件 break; } case 'text': { Event.emit('data '+method,this.responseText); break; } case 'xml': { Event.emit('data '+method,this.responseXML); break; } default: { break; } } } } }
ES6
將Promise
寫進了語言標準,統一了用法,原生提供了Promise
對象。Promise
,簡單說就是一個容器,裏面保存着一個異步操做的結果。從語法上說,Promise
是一個對象,從它能夠獲取異步操做的消息。
Promise
有3種狀態:pending
(進行中)、fulfilled
(成功)、rejected
(失敗)。
Promise
很重要的兩個特色:
Promise
對象的狀態改變,只有兩種可能:從pending
變爲fulfilled
和從pending
變爲rejected
。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲resolved
(已定型)。const p = new Promise((resolve,reject) => { // resolve在異步操做成功時調用 resolve('success'); // reject在異步操做失敗時調用 reject('error'); }); p.then(result => { console.log(result); }); p.catch(result => { console.log(result); })
ES6
規定,Promise
對象是一個構造函數,用來生成Promise
實例。new
一個Promise
實例時,這個對象的起始狀態就是Pending
狀態,再根據resolve
或reject
返回Fulfilled
狀態 / Rejected
狀態。
前面能夠看到,Promise
實例具備then
方法,因此then
方法是定義在原型對象Promise.prototype
上的,它的做用是爲Promise
實例添加狀態改變時的回調函數。
then
方法返回的是一個新的Promise
實例,所以then
能夠採用鏈式寫法:
getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的別名,用於指定發生錯誤時的回調函數。
getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 處理 getJSON 和 前一個回調函數運行時發生的錯誤 console.log('發生錯誤!', error); });
Promise.all
方法用於將多個Promise
實例,包裝成一個新的Promise
實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,p
的狀態由p1
、p2
、p3
決定,分紅兩種狀況:
p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態纔會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p的回調函數。p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的實例的返回值,會傳遞給p
的回調函數。Promise.race
方法一樣是將多個Promise
實例,包裝成一個新的Promise
實例。不一樣的是,race()
接受的對象中,哪一個對象返回快就返回哪一個對象,若是指定時間內沒有得到結果,就將Promise
的狀態變爲reject
。
const p = Promise.race([ fetch('/resource-that-may-take-a-while'), new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('request timeout')), 5000) }) ]); p .then(console.log) .catch(console.error);
上面代碼中,若是 5 秒以內fetch
方法沒法返回結果,變量p
的狀態就會變爲rejected
,從而觸發catch
方法指定的回調函數。
Promise.resolve('foo') // 等價於 new Promise(resolve => resolve('foo'))
const p = Promise.reject('出錯了'); // 等同於 const p = new Promise((resolve, reject) => reject('出錯了')) p.then(null, function (s) { console.log(s) }); // 出錯了
下面是一個用Promise
對象實現的Ajax
操做的例子:
const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); }); return promise; }; getJSON("/posts.json").then(function(json) { console.log('Contents: ' + json); }, function(error) { console.error('出錯了', error); });
Node 8
提供了兩個工具函數util.promisify
、util.callbackify
用於在回調函數和Promise
之間作方便的切換,咱們也能夠用JavaScript
代碼來實現一下。
function promisify(fn_callback) { //接收一個有回調函數的函數,回調函數通常在最後一個參數 if(typeof fn_callback !== 'function') throw new Error('The argument must be of type Function.'); //返回一個函數 return function (...args) { //返回Promise對象 return new Promise((resolve, reject) => { try { if(args.length > fn_callback.length) reject(new Error('arguments too much.')); fn_callback.call(this,...args,function (...args) { //nodejs的回調,第一個參數爲err, Error對象 args[0] && args[0] instanceof Error && reject(args[0]); //除去undefined,null參數 args = args.filter(v => v !== undefined && v !== null); resolve(args); }.bind(this)); //保證this仍是原來的this } catch (e) { reject(e) } }) } }
function callbackify(fn_promise) { if(typeof fn_promise !== 'function') throw new Error('The argument must be of type Function.'); return function (...args) { //返回一個函數 最後一個參數是回調 let callback = args.pop(); if(typeof callback !== 'function') throw new Error('The last argument must be of type Function.'); if(fn_promise() instanceof Promise){ fn_promise(args).then(data => { //回調執行 callback(null,data) }).catch(err => { //回調執行 callback(err,null) }) }else{ throw new Error('function must be return a Promise object'); } } }
我的而言,最好直接把代碼改爲promise
形式的,而不是對已有的callback
加上這個中間層,由於其實改動的成本差很少。但總有各類各樣的狀況,好比,你的回調函數已經有不少地方使用了,牽一髮而動全身,這時這個中間層仍是比較有用的。
Generator
函數是ES6
提供的一種異步編程解決方案,經過yield
標識位和next()
方法調用,實現函數的分段執行。
先從下面的例子看一下Generator
函數是怎麼定義和運行的。
function *gen() { yield "hello"; yield "generator"; return; } gen(); // 沒有輸出結果 var g = gen(); console.log(g.next()); // { value: 'hello', done: false } console.log(g.next()); // { value: 'generator', done: false } console.log(g.next()); // { value: 'undefined', done: true }
從上面能夠看到,Generator
函數定義時要帶*
,在直接執行gen()
時,沒有像普通的函數同樣,輸出結果,而是經過調用next()
方法獲得告終果。
這個例子中咱們引入了yield
關鍵字,分析下這個執行過程:
g
對象,指向gen
的句柄next()
,執行到yield hello
,暫緩執行,並返回了hello
next()
,繼續上一次的執行,執行到yield generator
,暫緩執行,並返回了generator
next()
,直接執行return
,並返回done:true
,代表結束。通過上面的分析,yield
實際就是暫緩執行的標示,每執行一次next()
,至關於指針移動到下一個yield
位置。next()
方法返回的結果是個對象,對象裏面的value
是運行結果,done
表示是否運行完成。
throw()
方法在函數體外拋出一個錯誤,而後在函數體內捕獲。
function *gen1() { try{ yield; } catch(e) { console.log('內部捕獲') } } let g1 = gen1(); g1.next(); g1.throw(new Error());
return()
方法返回給定值,並終結生成器,在return
後面的yield
不會再被執行。
function *gen2(){ yield 1; yield 2; yield 3; } let g2 = gen2(); g2.next(); // { value:1, done:false } g2.return(); // { value:undefined, done:true } g2.next(); // { value:undefined, done:true }
在ES2017
中,提供了async / await
兩個關鍵字來實現異步,是異步編程的最高境界,就是根本不用關心它是不是異步,不少人認爲它是異步編程的終極解決方案。async / await
寄生於Promise
,本質上仍是基於Generator
函數,能夠說是Generator
函數的語法糖,async
用於申明一個function
是異步的,而await
能夠認爲是async wait
的簡寫,等待一個異步方法執行完成。
async function demo() { let result = await Promise.resolve(123); console.log(result); } demo();
async
函數返回的是一個Promise
對象,在上述例子中,表示demo
是一個async
函數,await
只能用在async
函數裏面,表示等待Promise
返回結果後,再繼續執行,await
後面應該跟着Promise
對象(固然,跟着其餘返回值也不要緊,只是會當即執行,這樣就沒有意義了)。
Promise
雖然一方面解決了callback
的回調地獄,可是相對的把回調 「縱向發展」 了,造成了一個回調鏈:
function sleep(wait) { return new Promise((res,rej) => { setTimeout(() => { res(wait); },wait); }); } /* let p1 = sleep(100); let p2 = sleep(200); let p =*/ sleep(100).then(result => { return sleep(result + 100); }).then(result02 => { return sleep(result02 + 100); }).then(result03 => { console.log(result03); })
將上述代碼改爲async/await
寫法:
async function demo() { let result01 = await sleep(100); //上一個await執行以後纔會執行下一句 let result02 = await sleep(result01 + 100); let result03 = await sleep(result02 + 100); // console.log(result03); return result03; } demo().then(result => { console.log(result); });
由於async
返回的也是promise
對象,因此用then
接收就好了。
若是是reject
狀態,能夠用try-catch
捕捉:
let p = new Promise((resolve,reject) => { setTimeout(() => { reject('error'); },1000); }); async function demo(params) { try { let result = await p; } catch(e) { console.log(e); } } demo();
這是基本的錯誤處理,可是當內部出現一些錯誤時,和Promise
有點相似,demo()
函數不會報錯,仍是須要catch
回調捕捉,這就是內部的錯誤被 「靜默」 處理了。
let p = new Promise((resolve,reject) => { setTimeout(() => { reject('error'); },1000); }); async function demo(params) { // try { let result = name; // } catch(e) { // console.log(e); // } } demo().catch((err) => { console.log(err); })
最後,總結一下JavaScript
實現異步的5種方式的優缺點:
Promise
對象:經過then
方法來替代掉回調,解決了回調產生的參數不容易肯定的問題,可是相對的把回調 「縱向發展」 了,造成了一個回調鏈。Generator
函數:確實很好的解決了JavaScript
中異步的問題,可是得依賴執行器函數。async/await
:這多是javascript
中,解決異步的最好的方式了,讓異步代碼寫起來跟同步代碼同樣,可讀性和維護性都上來了。