拾遺記3: Async/Await執行時機

前置知識

想要了解Async/Await關鍵字內部是怎麼運行的?在什麼時機運行?須要提早了解js異步編程,宏任務、微任務概念。javascript

js異步編程

瀏覽器是多線程的,其包含以下多個線程:java

  • js主引擎
  • ajax請求線程。
  • DOM處理線程。
  • 定時任務線程。
  • 其餘線程。

咱們一般所說javascript是單線程的指的是js主引擎,其負責js代碼的解釋,預處理和執行。js主引擎維護一個執行棧,會按照順序依次執行放入執行棧中的代碼。若是某段代碼執行時間過長,那麼剩餘代碼會等待。ajax

console.log('excute start')
const date = new Date()
while (new Date() - date < 3000) {
    // 模擬阻塞執行棧3秒
    console.log('waiting')
}
// 3秒後纔會執行
console.log('excute end')

可是,當咱們在js代碼中調用瀏覽器其餘的api(如定時任務)時,js主引擎就會執行異步操做,執行棧中的其餘任務會繼續執行,不用等待異步操做完成。編程

console.log('excute start')
setTimeout(() => {
    // 3秒後當即執行
    console.log('async excute')
}, 3000);
// 當即執行,不用等待3秒
console.log('excute end')

那麼js主引擎怎麼知道異步操做執行完畢呢?除了執行棧,還存在一個消息隊列。其餘api執行完異步操做後,會將回調事件放入消息隊列中。當執行棧中的任務所有執行完畢後,js主引擎會去查看消息隊列中是否有任務,若是有任務,就會取出最開始的任務將其放入執行棧中,執行棧會當即執行該任務。segmentfault

js代碼執行過程就是執行棧依次執行代碼,執行完成後去消息隊列中取出任務放入執行棧繼續執行,這個過程會不斷的重複,也就構成Event Loop(事件循環)。api

咱們一般使用回調函數來做爲異步任務執行完成後執行的事件,可是當回調函數嵌套過深就會造成回調地獄,嚴重影響代碼的可讀性。promise

ajax('ajax1', function () {
    ajax('ajax2', function () {
        ajax('ajax3', function () {
            ajax('ajax4', function () {
                // ....
            })
        })
    })
})

爲了解決回調函數嵌套致使的回調地獄問題,ES2015提出了Promise方案,Promise經過鏈式調用讓回調函數扁平化,減小了回調嵌套。瀏覽器

new Promise(resolve => {
    ajax('ajax1', function (data) {
        resolve(data)
    })
}).then(value => {
    return new Promise(resolve => {
        ajax('ajax2', function (data) {
            resolve(data)
        })
    })
}).then(value => {
    return new Promise(resolve => {
        ajax('ajax3', function (data) {
            resolve(data)
        })
    })
})

同時,ES中還包含Generator生成器,其配合執行器co函數也能夠解決回調函數嵌套問題。多線程

function* main() {
    const v1 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('promise 1')
        }, 1000);
    })
    console.log(v1)
    const v2 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('promise 2')
        }, 1000);
    })
    console.log(v2)
}
// 執行器
function co(gen) {
    let generator = gen()
    function handleResult(result) {
        if (result.done) return
        else {
            result.value.then((value) => {
                handleResult(generator.next(value))
            }, err => {
                console.log(err)
            })
        }
    }
    handleResult(generator.next())
}

// 執行
co(main)

爲了更進一步簡化異步編程語法,ES2017提出Async/Await語法糖,其本質就是包裝了Generator生成器。異步

async function main() {
    let v1 = await new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 1')
        }, 1000);
    })
    console.log(v1)
    let v2 = await new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 2')
        }, 1000);
    })
    console.log(v2)
}
// 執行
main()

若是揭開語法糖,其至關於

function* main() {
    let v1 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 1')
        }, 1000);
    })
    console.log(v1)
    let v2 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 2')
        }, 1000);
    })
    console.log(v2)
}

宏任務vs微任務

宏任務和微任務能夠理解爲異步任務的進一步細分,只不過執行時機不一樣,當某個宏任務執行完畢以後,會查找微任務消息隊列中是否存在微任務,若是有,那麼就執行全部微任務,而後再執行下一個宏任務。
下面是盜用的一張執行邏輯圖:
1053223-20180831162350437-143973108.png

常見的生成宏任務的有:setTimeout(同等延遲時間下,setTimeOut的優先級高於setImmediate),setInterval, setImmediate, I/O, UI rendering。
常見的生成微任務的有:Promise,queueMicrotask。

setTimeout(() => {
    console.log('timeout')
}, 0)
new Promise(resolve => {
    resolve('promise')
}).then(v => {
    console.log(v)
})

上述代碼中,因爲Promise生成的是微任務,全部其早於setTimeout生成的宏任務,所以先輸出promise,再輸出timeout。

宏任務和微任務的執行過程能夠用下面的代碼簡要說明:

// 事件循環, 主線程
while (macroQueue.waitForMessage()) {
    // 1. 執行完調用棧上當前的宏任務(同步任務)
    // call stack
    // 2. 遍歷微任務隊列,把微任務隊裏上的全部任務都執行完畢(清空微任務隊列)
    // 微任務又能夠往微任務隊列中添加微任務
    for (let i = 0; i < microQueue.length; i++) {
        // 獲取並執行下一個微任務(先進先出)
        microQueue[i].processNextMessage()
    }
    
    // 3. 渲染(渲染線程)

    // 4. 從宏任務隊列中取 一個 任務,進入下一個消息循環
    macroQueue.processNextMessage();
}

Async/Await

上面講過,Async/Await是爲解決異步回調提出的方案,其本質是對Generator生成器進行包裝的語法糖。

  • 若是async方法中沒有await關鍵字,那麼其就能夠認爲是一個普通的方法。
async function main() {
    console.log('await')
}
console.log('next')
main()
// 輸出
// await
// next
  • 若是await關鍵字後面是一個Promise對象,那麼至關於在Promise對象中注入了then處理方法接受異步操做返回值,開啓一個微任務,所以會先輸出next,再輸出await。
async function main() {
    const v =await new Promise(resolve => {
        resolve('await')
    })
    console.log(v)
}
main()
console.log('next')
// 輸出
// next
// await
  • 若是await關鍵字後面的Promise對象中沒有執行resolve方法,就會致使Promise一直處在pending狀態,沒法執行then方法,所以await後面的代碼不會執行,所以下例中的console.log(v)不會執行。
async function main() {
    const v = await new Promise(resolve => { })
    console.log(v)
}
main()
console.log('next')
// 輸出
// next
  • 若是await關鍵字後面是一個非Promise的普通數據,那麼其至關於執行Promise.resolve()。
async function main() {
    const v = await 'await'
    console.log(v)
}
main()
console.log('next')
// 輸出
// next
// await

// 至關於
async function main() {
    const v = await Promise.resolve('await')
    console.log(v)
}
  • 若是async函數返回非Promise對象,那麼返回值會被封裝成Promise對象。
async function fn() {
    return 'foo'
    // 若是沒有return,至關於return undefined
}
fn().then(data => {
    console.log(data)
})

// 至關於
function fn1() {
    return new Promise(resolve => {
        resolve('foo')
    })
}
相關文章
相關標籤/搜索