因爲javascript是單線程
的,只能在JS引擎的主線程上運行的,因此js代碼只能一行一行的執行,不能在同一時間執行多個js代碼任務,這就致使若是有一段耗時較長的計算,或者是一個ajax請求等IO操做,若是沒有異步的存在,就會出現用戶長時間等待,而且因爲當前任務還未完成,因此這時候全部的其餘操做都會無響應。javascript
js最開始只是爲了處理一些表單驗證和DOM操做而被創造出來的,因此主要爲了語言的輕量和簡單採用了單線程
的模式。多線程模型
相比單線程
要複雜不少,好比多線程須要處理線程間資源的共享問題,還要解決狀態同步等問題。html
JavaScript的事件執行機制:java
當JS解析執行時,會被引擎分爲兩類任務,同步任務(synchronous)
和 異步任務(asynchronous)。
對於同步任務來講,會被推到執行棧按順序去執行這些任務。對於異步任務來講,當其能夠被執行時,會被放到一個 異步任務隊列(task queue)
裏等待JS引擎去執行。當執行棧中的全部同步任務完成後,JS引擎纔會去異步任務隊列裏查看是否有任務存在,並將找到的任務放到執行棧中去執行,執行完了又會去異步任務隊列裏查看是否有已經能夠執行的任務。這種循環檢查的機制,就叫作事件循環(Event Loop)
。異步任務隊列也
被分爲 微任務(microtask)隊列
& 宏任務(macrotask)隊列。
git
Event Loop的完整執行順序是:github
首先執行執行棧裏的任務。ajax
執行棧清空後,檢查微任務(microtask)隊列,將可執行的微任務所有執行。編程
取宏任務(macrotask)隊列中的第一項執行。json
回到第二步。segmentfault
注意: 微任務隊列每次全執行,宏任務隊列每次只取一項執行。數組
setTimeout(() => { console.log('我是第一個宏任務'); Promise.resolve().then(() => { console.log('我是第一個宏任務裏的第一個微任務'); }); Promise.resolve().then(() => { console.log('我是第一個宏任務裏的第二個微任務'); }); }, 0); setTimeout(() => { console.log('我是第二個宏任務'); }, 0); Promise.resolve().then(() => { console.log('我是第一個微任務'); }); console.log('執行同步任務');
最後的執行結果是:
// 執行同步任務 // 我是第一個微任務 // 我是第一個宏任務 // 我是第一個宏任務裏的第一個微任務 // 我是第一個宏任務裏的第二個微任務 // 我是第二個宏任務
常見的異步模式:回調函數;事件監聽;發佈/訂閱模式(又稱觀察者模式);promise;Generator
函數;ES7中async/await。
回調函數:回調函數是異步操做最基本的方法,好比有一個異步操做(asyncFn),和一個同步操做(normalFn)。若是按照正常的JS處理機制來講,同步操做必定發生在異步以前。以下:
function asyncFn() { setTimeout(() => { console.log('asyncFn'); }, 0) } function normalFn() { console.log('normalFn'); } asyncFn(); normalFn(); // normalFn // asyncFn
若是我想要將順序改變,可使用回調的方式處理:
function asyncFn(callback) { setTimeout(() => { console.log('asyncFn'); callback(); }, 0) } function normalFn() { console.log('normalFn'); } asyncFn(normalFn); // asyncFn // normalFn
事件監聽:這是一種事件驅動模式,異步任務的執行不取決於代碼的順序,而取決於某個事件是否發生。好比經過點擊按鈕或者trigger的方式觸發這個事件。
發佈/訂閱模式(又稱觀察者模式):其實它像是事件監聽模式的升級版。在發佈/訂閱模式中,能夠想象存在一個消息中心的地方,首先能夠在裏邊「註冊一條消息」,以後被註冊的這條消息能夠被感興趣的若干人「訂閱」,一旦將來這條「消息被髮布」,則全部訂閱了這條消息的人都會獲得提醒。這個就是發佈/訂閱模式的設計思路,接下來咱們來實現一個簡單的發佈/訂閱模式:
首先咱們先實現一個消息中心的雛形:
// 先實現一個消息中心的構造函數,用來建立一個消息中心 function MessageCenter(){ var _messages = {}; // 全部註冊的消息都存在這裏 this.regist = function(){}; // 用來註冊消息的方法 this.subscribe = function(){}; // 用來訂閱消息的方法 this.fire = function(){}; // 用來發布消息的方法 }
接下來完善下regist,subscribe和fire這三個方法:
function MessageCenter(){ var _messages = {}; // 對於regist方法,它只負責註冊消息,就只接收一個註冊消息的類型(標識)參數就行了。 this.regist = function(msgType){ // 判斷是否重複註冊 if(typeof _messages[msgType] === 'undefined'){ _messages[msgType] = []; // 數組中會存放訂閱者 }else{ console.log('這個消息已經註冊過了'); } } // 對於subscribe方法,須要訂閱者和已經註冊了的消息進行綁定,msgType是要被綁定的消息類型,subFn是訂閱者獲得消息後的處理函數 this.subscribe = function(msgType, subFn){ // 判斷是否有這個消息 if(typeof _messages[msgType] !== 'undefined'){ _messages[msgType].push(subFn); }else{ console.log('這個消息還沒註冊過,沒法訂閱') } } // 最後咱們實現下fire這個方法,就是去發佈某條消息,並通知訂閱這條消息的全部訂閱者函數 this.fire = function(msgType, args){ // msgType是消息類型或者說是消息標識,而args能夠設置這條消息的附加信息 // 仍是發佈消息時,判斷下有沒有這條消息 if(typeof _messages[msgType] === 'undefined') { console.log('沒有這條消息,沒法發佈'); return false; } var events = { type: msgType, args: args || {} }; _messages[msgType].forEach(function(sub){ sub(events); }) } }
這樣,一個簡單的發佈/訂閱模式就完成了,此時咱們就能夠用他來處理一些異步操做了:
var msgCenter = new MessageCenter(); msgCenter.regist('A'); msgCenter.subscribe('A', subscribeFn); function subscribeFn(events) { console.log(events.type, events.args); // A, fire msg } // ----- setTimeout(function(){ msgCenter.fire('A', 'fire msg'); }, 1000);
接下來幾個函數用來解決,異步中 回調函數嵌套問題 (callback hell) 回調地獄。
Promise:ES6推出的一種異步編程的解決方案。其實在ES6以前,不少異步的工具庫就已經實現了各類相似的解決方案,而ES6將其寫進了語言標準,統一了用法。Promise解決了回調等解決方案嵌套的問題而且使代碼更加易讀,有種在寫同步方法的既視感:
function asyncFn1() { console.log('asyncFn1 run'); return new Promise(function(resolve, reject) { setTimeout(function(){ resolve(); }, 1000) }) } function asyncFn2() { console.log('asyncFn2 run'); return new Promise(function(resolve, reject) { setTimeout(function(){ resolve(); }, 1000) }) } function normalFn3() { console.log('normalFn3 run'); } asyncFn1().then(asyncFn2).then(normalFn3); // f1返回一個Promise對象,通過一秒後resolve,到then(asyncFn2)裏執行asyncFn2函數 ,通過一秒後resolve,到then(normalFn3)裏執行normalFn3函數。
Generator函數:是一種特殊的函數,他有這麼幾個特色:
聲明時須要在function
後面加上*
,而且配合函數裏面yield
關鍵字來使用;
在執行Generator函數的時候,其會返回一個Iterator遍歷器對象,經過其next方法,將Generator函數體內的代碼以yield爲界分步執行;
具體來講當執行Generator函數時,函數並不會執行,而是須要調用Iterator遍歷器對象的next方法,這時程序纔會執行從頭或者上一個yield以後
到 到下一個yield或者return或者函數體尾部
之間的代碼,而且將yield後面的值,包裝成json對象返回。就像上面的例子中的{value: xxx, done: xxx};
value取的yield或者return後面的值,不然就是undefined,done的值若是碰到return或者執行完成則返回true,不然返回false。事實上Generator函數不像Promise同樣是專門用來解決異步處理而產生的,人們只是使用其特性來產出了一套異步的解決方案,因此使用Generator並不像使用Promise同樣有一種開箱即用的感受。其更像是在Promise或者回調這類的解決方案之上又封裝了一層。
接下來如何使用Generator函數進行異步編程:
var g; function asyncFn() { setTimeout(function(){ g.next(); }, 1000) } function normalFn() { console.log('normalFn run'); } function* oneGenerator() { yield asyncFn(); return normalFn(); } g = oneGenerator(); g.next(); // 這裏在我調用next方法的時候執行了asyncFn函數 // 而後咱們的但願是在異步完成時自動去再調用g.next()來進行下面的操做,因此咱們必須在上面asyncFn函數體內的寫上g.next(); 這樣才能正常運行。
Async/Await
async-await 是創建在 promise機制之上的,它是promise和generator的語法糖。Generator 函數的執行必須靠執行器(next),而async函數自帶執行器。
async函數使用方法:須要async放置在函數的前面。async函數老是返回一個promise,async函數是沒有return返回值的。若是代碼中有return,JavaScript會自動把返回的這個value值包裝成promise的resolved值。以下,返回爲resolve爲1的promise對象:
async function f() { return 'hello world'; } f().then(v => console.log(v)) // "hello world"
async函數內部拋出錯誤,會致使返回的Promise對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到:
async function f() { throw new Error('出錯了'); } f().then( v => console.log(v), e => console.log(e) ) // Error: 出錯了
async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。
async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)<\/title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log) // 函數getTitle內部有三個操做:抓取網頁、取出文本、匹配頁面標題。只有這三個操做所有完成,纔會執行then方法裏面的console.log。
await命令:正常狀況下,await命令後面是一個 Promise 對象。若是不是,會被轉成一個當即resolve的 Promise 對象:
async function f() { return await 123; } f().then(v => console.log(v)) // 123
await命令後面的 Promise 對象若是變爲reject狀態,則reject的參數會被catch方法的回調函數接收到:
async function f() { await Promise.reject('出錯了'); } f() .then(v => console.log(v)) .catch(e => console.log(e)) // 出錯了
只要一個await語句後面的 Promise 變爲reject,那麼整個async函數都會中斷執行:
async function f() { await Promise.reject('出錯了'); await Promise.resolve('hello world'); // 不會執行 }
有時,咱們但願即便前一個異步操做失敗,也不要中斷後面的異步操做。這時能夠將第一個await放在try...catch結構裏面,這樣無論這個異步操做是否成功,第二個await都會執行:
async function f() { try { await Promise.reject('出錯了'); } catch(e) { } return await Promise.resolve('hello world'); } f().then(v => console.log(v)) // hello world
另外一種方法是await後面的 Promise 對象再跟一個catch方法,處理前面可能出現的錯誤:
async function f() { await Promise.reject('出錯了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); } f() .then(v => console.log(v)) // 出錯了 // hello world
關於Async/Await的執行順序
這裏的執行順序挺複雜的,接下來看幾個在網上例子吧!
function consoleA(){ console.log("A") } async function consoleB(){ await consoleA() // await 至關與於執行了一個 Promise.then(....) console.log("B") // 而console.log("B") 在 then 裏 } (function consoleC(){ consoleB().then(_ => { console.log("D") }) console.log("C") })() // 依次輸出 A C B D
以上例子中,首先consoleC函數執行,以後調用consoleB函數,await consoleA()以後執行consoleA函數, 打印 "A" ,consoleA函數直接返回,await consoleA是Promise對象至關於await consoleA().then(() => {console.log("B") }),因此把console.log("B")加入執行隊列(task queue ),consoleB函數返回,同理consoleB().then() 把 console.log("D") 加入了執行隊列(task queue) ,以後執行同步任務console.log("C") 打印 "C",當前 task 結束。task queue 還有兩個任務,一個是 log("B") ,一個是 log("D") ,相繼執行。
接下來看一個複雜的例子:
function testSometing() { console.log("執行testSometing"); return "testSometing"; } async function testAsync() { console.log("執行testAsync"); return Promise.resolve("hello async"); } async function test() { console.log("test start..."); const v1 = await testSometing();//關鍵點1 console.log(v1); const v2 = await testAsync(); console.log(v2); console.log(v1, v2); } test(); var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//關鍵點2 promise.then((val)=> console.log(val)); // test start... // 執行testSometing // promise start.. // test end... // testSometing // 執行testAsync // promise // hello async // testSometing hello async
當test
函數執行到const v1 = await testSometing()
的時候,會先執行testSometing
這個函數打印出「執行testSometing」的字符串,而後由於await
至關與執行了一個 Promise.then(....),在這裏至關於await testSometing().then(() => {console.log(v1);}),因此console.log(v1)不會當即執行,因爲是異步的會放到異步任務隊列裏,代碼會跳出函數接着向下執行,而後打印出「promise start..」,接下來會把返回的promise
放入異步任務隊列,繼續執行打印「test end…」,等本輪事件循環執行結束後,又會跳回到test函數中(async函數),等待以前await
後面表達式testSometing()的返回值,因此返回的是一個字符串「testSometing」,test
函數繼續執行,執行到const v2 = await testAsync();
和以前同樣又會跳出test函數,執行後續代碼,此時事件循環就到了異步任務隊列裏,執行promise.then((val)=> console.log(val))中
then後面的語句,以後和前面同樣又跳回到test函數繼續執行。
下邊的例子在上邊的例子基礎上作了改變,在testSometing函數前加了async,因此testSometing返回的是一個promise對象了,因此把它推到了異步任務隊列裏,沒有當即執行,執行了以前推到異步任務隊列裏的promise變量,以後在回頭執行的testSometing的resolve(即他的return ‘testSometing’)
async function testSometing() { console.log("執行testSometing"); return "testSometing"; } async function testAsync() { console.log("執行testAsync"); return Promise.resolve("hello async"); } async function test() { console.log("test start..."); const v1 = await testSometing(); console.log(v1); const v2 = await testAsync(); console.log(v2); console.log(v1, v2); } test(); var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//3 promise.then((val)=> console.log(val)); console.log("test end...") // test start... // 執行testSometing // promise start.. // test end... // promise // testSometing // 執行testAsync // hello async // testSometing hello async