JavaScript的事件執行機制及異步

因爲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

 

 

原文:http://www.javashuo.com/article/p-aauembml-z.html

https://segmentfault.com/a/1190000011883659

相關文章
相關標籤/搜索