js執行機制

JavaScript執行機制,重點有兩點:

1.JavaScript是一門單線程語言。
2.Event Loop(事件循環)是JavaScript的執行機制。

既然說js是單線程,那就是在執行代碼的時候是從上往下執行的,先來看一段代碼:

        setTimeout(function(){
            console.log('定時器開始')
        });
        new Promise(function(resolve){
            console.log('Promise開始');
            resolve();
        }).then(function(){
            console.log('執行then函數')
        });
        console.log('代碼執行結束');

輸出結果:

image.png
關于javascript

javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker, 但javascript是單線程這一核心扔未改變。所以一切javascript版的“多線程”都是用單線程模擬出來的,一切javascript多線程都是紙老虎

JS為什么是單線程的

最初設計JS是用來在瀏覽器驗證表單操控DOM元素的是一門腳本語言,如果js是多線程的,那么兩個線程同時對一個DOM元素進行了相互沖突的操作,那么瀏覽器的解析器是無法執行的。

js為什么需要異步

如果js中不存在異步,只能自上而下執行,如果上一行解析時間很長,那么下面的代碼就會被阻塞。 對于用戶而言,阻塞就以為著“卡死”,這樣就導致了很差的用戶體驗。比如在進行ajax請求的時候如果沒有返回數據后面的代碼就沒辦法執行

js單線程又是如何實現異步的呢

js中的異步以及多線程都可以理解成為一種“假象”,就拿h5的WebWorker來說,子線程有諸多限制,不能控制DOM,不能修改全局對象等等,通常只用來做計算做數據處理。
這些限制并沒有違背我們之前的觀點,所以說是“假象”。JS異步的執行機制其實就是事件循環(eventloop),理解了eventloop機制,就理解了js異步的執行機制。

JS的事件循環(eventloop)是怎么運作的

事件循環、eventloop\運行機制 這三個術語其實說的是同一個東西,
“先執行同步操作異步操作排在事件隊列里”這樣的理解其實也沒有任何問題但如果深入的話會引出很多其他概念,比如event table和event queue, 我們來看運行過程:

  1. 首先判斷JS是同步還是異步,同步就進入主線程運行,異步就進入event table.
  2. 異步任務在event table中注冊事件,當滿足觸發條件后,(觸發條件可能是延時也可能是ajax回調),被推入event queue
  3. 同步任務進入主線程后一直執行,直到主線程空閑時,才會去event queue中查看是否有可執行的異步任務,如果有就推入主線程中。
image.png

那怎么知道主線程執行棧為空啊?js引擎存在monitoring process進程,會持續不斷的檢查 主線程 執行棧是否為空,一旦為空,就會去event queue那里檢查是否有等待被調用的函數。

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('發送成功!');
    }
})
console.log('代碼執行結束');
  • ajax進入event table,注冊回調函數success
  • 執行 console.log('代碼執行結束')
  • ajax事件完成,回調函數success進入event queue
  • 主線程從event queue讀取回調函數success并執行
setTimeout
setTimeout(() => {
  console.log('2秒到了')
}, 2000)

setTimeout是異步操作首先進入event table, 注冊的事件就是它的回調,觸發條件就是2秒之后,當滿足條件回調被推入event queue,當主線程空閑時會去event queue里查看是否有可執行的任務。

console.log(1) // 同步任務進入主線程
setTimeout(fun(),0)   // 異步任務,被放入event table, 0秒之后被推入event queue里
console.log(3) // 同步任務進入主線程

1、3是同步任務馬上會被執行,執行完成之后主線程空閑去event queue(事件隊列)里查看是否有任務在等待執行,這就是為什么setTimeout的延遲事件是0毫秒卻在最后執行的原因

但setTimeout延時的時間有時候并不是那么準確

setTimeout(() => {
  console.log('2秒到了')
}, 2000)
wait(9999999999)

分析運行過程:

  1. console進入Event Table并注冊,計時開始。
  2. 執行sleep函數,sleep方法雖然是同步任務但sleep方法進行了大量的邏輯運算,耗時超過了2秒
  3. 2秒到了,計時事件timeout完成,console進入Event queue, 但是sleep還沒執行完,主線程還被占用,只能等著。
  4. sleep終于執行完了, console終于從event queue進入了主線程執行,這個時候已經遠遠超過了2秒

其實延遲2秒只是表示2秒后,setTimeout里的函數被推入event queue , 而event queue(事件隊列)里的任務,只有在主線程空閑時才會執行。
上述流程走完,我們知道setTimeout這個函數,是經過指定時間后,把要執行的任務(本例子中為console)加入到event queue中, 又因為單線程任務要一個一個執行,如果前面的任務需要的時間太久,那么只能等著,導致真正的延遲時間遠遠大于2秒。 我們還經常遇到setTimeout(fn,0)這樣的代碼,它的含義是,指定某個任務在主線最早的空閑時間執行,意思就是不用再等多少秒了, 只要主線程執行棧內的同步任務全部執行完成,棧為空就馬上執行。但是即便主線程為空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。

setIntval

以setIntval(fn,ms)為例,setIntval是循環執行的,setIntval會每隔指定的時間將注冊的函數置入event queue,不是每過ms會執行一次fn,而是每過ms秒,會有fn進入event queue。需要注意一點的是,一單setIntval的回調函數fn執行時間超過了延遲事件ms,那么就完成看不出來有時間間隔了。

除了廣義的同步任務和異步任務,我們對任務有更精細的定義:

  • 宏任務 包含整個script代碼塊,setTimeout, setIntval
  • 微任務 Promise , process.nextTick

在劃分宏任務、微任務的時候并沒有提到async/ await的本質就是Promise

那事件循環機制到底是怎么樣的?

不同類型的任務會進入對應的event queue, 比如setTime和setIntval會進入相同(宏任務)的event queue, 而Promise和process.nextTick會進入相同(微任務)的event queue.

Promise與事件循環
Promise在初始化時,傳入的函數是同步執行的,然后注冊then回調。注冊完之后,繼續往下執行同步代碼,在這之前,then的回調不會執行。同步代碼塊執行完畢后,才會在事件循環中檢測是否有可用的promise回調,如果有,那么執行,如果沒有,繼續下一個事件循環。

1. 宏任務,微任務都是隊列, 一段代碼執行時,會先執行宏任務中的同步代碼。
2. 進行第一輪事件循環的時候會把全部的js腳本當成一個宏任務來運行。
3. 如果執行中遇到setTimeout之類的宏任務,那么就把這個setTimeout內部的函數推入[宏任務的隊列]中,下一輪宏任務執行時調用。
4. 如果執行中遇到promise.then()之類的微任務,就會推入到[當前宏任務的微任務隊列]中, 在本輪宏任務的同步代碼都執行完成后,依次執行所有的微任務。
5. 第一輪事件循環中當執行完全部的同步腳步以及微任務隊列中的事件,這一輪事件循環就結束了, 開始第二輪事件循環。
6. 第二輪事件循環同理先執行同步腳本,遇到其他宏任務代碼塊繼續追加到[宏任務的隊列]中,遇到微任務,就會推入到[當前宏任務的微任務隊列]中,在本輪宏任務的同步代碼執行都完成后, 依次執行當前所有的微任務。
7. 開始第三輪循環往復..

下面用代碼來深入理解上面的機制:

setTimeout(function() {
    console.log('4')
})

new Promise(function(resolve) {
    console.log('1') // 同步任務
    resolve()
}).then(function() {
    console.log('3')
})
console.log('2')
  1. 這段代碼作為宏任務,進入主線程。
  2. 先遇到setTimeout,那么將其回調函數注冊后分發到宏任務event queue.
  3. 接下來遇到Promise, new Promise立即執行,then函數分發到微任務event queue
  4. 遇到console.log(), 立即執行
  5. 整體代碼script作為第一個宏任務執行結束, 查看當前有沒有可執行的微任務,執行then的回調。(第一輪事件循環結束了,我們開始第二輪循環)
  6. 從宏任務的event queue開始,我們發現了宏任務event queue中setTimeout對應的回調函數,立即執行。 執行結果: 1-2-3-4
console.log('1')
setTimeout(function() {
    console.log('2')
    process.nextTick(function() {
        console.log('3')
    })
    new Promise(function(resolve) {
        console.log('4')
        resolve()
    }).then(function() {
        console.log('5')
    })
})

process.nextTick(function() {
    console.log('6')
})

new Promise(function(resolve) {
    console.log('7')
    resolve()
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9')
    process.nextTick(function() {
        console.log('10')
    })
    new Promise(function(resolve) {
        console.log('11')
        resolve()
    }).then(function() {
        console.log('12')
    })
})

1.整體script作為第一個宏任務進入主線程,遇到console.log(1)輸出1

  1. 遇到setTimeout, 其回調函數被分發到宏任務event queue中。我們暫且記為setTimeout1
    3.遇到process.nextTick(),其回調函數被分發到微任務event queue中,我們記為process1
    4.遇到Promise, new Promise直接執行,輸出7.then被分發到微任務event queue中,我們記為then1
  2. 又遇到setTimeout,其回調函數被分發到宏任務event queue中,我們記為setTimeout2.
  3. 現在開始執行微任務, 我們發現了process1和then1兩個微任務,執行process1,輸出6,執行then1,輸出8, 第一輪事件循環正式結束, 這一輪的結果輸出1,7,6,8.那么第二輪事件循環從setTimeout1宏任務開始
  4. 首先輸出2, 接下來遇到了process.nextTick(),統一被分發到微任務event queue,記為process2
    8.new Promise立即執行,輸出4,then也被分發到微任務event queue中,記為then2
  5. 現在開始執行微任務,我們發現有process2和then2兩個微任務可以執行輸出3,5. 第二輪事件循環結束,第二輪輸出2,4,3,5. 第三輪事件循環從setTimeout2哄任務開始
    10。 直接輸出9,跟第二輪事件循環類似,輸出9,11,10,12
  6. 完整輸出是1,7,6,8,2,4,3,5,9,11,10,12(請注意,node環境下的事件監聽依賴libuv與前端環境不完全相同,輸出順序可能會有誤差)

如果是setTimeout里面嵌套setTimeout, 那么嵌套的setTimeout的宏任務要在外面的宏任務排序的后面,往后排。看個例子

new Promise(function (resolve) { 
    console.log('1')// 宏任務一
    resolve()
}).then(function () {
    console.log('3') // 宏任務一的微任務
})
setTimeout(function () { // 宏任務二
    console.log('4')
    setTimeout(function () { // 宏任務五
        console.log('7')
        new Promise(function (resolve) {
            console.log('8')
            resolve()
        }).then(function () {
            console.log('10')
            setTimeout(function () {  // 宏任務七
                console.log('12')
            })
        })
        console.log('9')
    })
})
setTimeout(function () { // 宏任務三
    console.log('5')
})
setTimeout(function () {  // 宏任務四
    console.log('6')
    setTimeout(function () { // 宏任務六
        console.log('11')
    })
})
console.log('2') // 宏任務一

初步總結:宏任務是一個棧按先入先執行的原則,微任務也是一個棧也是先入先執行。但是每個宏任務都對應會有一個微任務棧,宏任務在執行過程中會先執行同步代碼再執行微任務棧。

async/await是什么

我們創建了promise但不能同步等待它執行完成。 我們只能通過then傳咦個回調函數這樣很容易再次陷入promise的回調地獄。 實際上, async/await在底層轉換成了promise和then回調函數,也就是說, 這是promise的語法糖。每次我們使用await, 解釋器都創建咦個promise對象,然后把剩下的async函數中的操作放到then回調函數中。 async/await的實現,離不開promise. 從字面意思來理解, async是“異步”的簡寫,而await是async wait的簡寫可以認為是等待異步方法執行完成。

async/await用來干什么

用來優化promise的回調問題,被稱為是異步的終極解決方案

async/await內部做了什么

async函數會返回一個Promise對象,如果在函數中return一個直接量(普通變量),async會把這個直接量通過Promise.resolve()封裝成Promise對象。如果你返回了promise那就以你返回的promise為準。await是在等待,等待運行的結果也就是返回值。await后面通常是一個異步操作(promise),但是這不代表await后面只能跟異步才做,await后面實際是可以接普通函數調用或者直接量。
async相當于 new Promise,await相當于then

await的等待機制

如果await后面跟的不是一個promise,那await后面表達式的運算結果就是它等到的東西,如果await后面跟的是一個promise對象,await它會'阻塞'后面的diamante,等著promise對象resolve, 然后得到resolve的值作為await表達式的運算結果。但是此"阻塞"非彼“阻塞”,這就是await必須用在async函數中的原因。 async函數調用不會造成"阻塞",它內部所有的“阻塞”都被封裝在一個promise對象中異步執行(這里的阻塞理解成異步等待更合理)

async/await在使用過程中有什么規定

每個async方法都返回一個promise, await只能出現在async函數中

async/await在什么場景使用

單一的promise鏈并不能發現async/await的有事,但是如果需要處理由多個promise組成的then鏈的時候,優勢就能體現出來了(Promise通過then鏈來解決多層回調的問題,現在又用async/awai來進一步優化它)

async/await如何使用

假設一個業務,分多個步驟完成,每個步驟都是異步的且依賴于上一個步驟的結果

function myPromise(n) {
    return new Promise(resolve => {
        console.log(n)
        setTimeout(() => resolve(n+1), n)
    })
}
function step1(n) {
    return myPromise(n)
}
function step2(n) {
    return myPromise(n)
}
function step3(n) {
    return myPromise(n)
}

//如果用 Promise 實現
step1(1000)
.then(a => step2(a))
.then(b => step3(b))
.then(result => {
    console.log(result)
})

//如果用 async/await 來實現呢
async function myResult() {
    const a = await step1(1000)
    const b = await step2(a)
    const result = await step3(b)
    return result
}
myResult().then(result => {
    console.log(result)
}).catch(err => {
    // 如果myResult內部有語法錯誤會觸發catch方法
})

看的出來async/await的寫法更多優雅一些要比promise的鏈接調用更多直觀也易于維護

我們來看在任務隊列中async/await的運行機制,先給出大概方向再通過案例來證明:

  1. async定義的是一個promise函數和普通函數一樣只要不調用就不會進入事件隊列。
  2. async內部如果沒有主動return promise, 那么async會把函數的返回值用promise包裝
  3. await關鍵字必須出現在async函數中,await后面不是必須要跟一個異步操作,也可以是咦個普通表達式
  4. 遇到await關鍵字,await右邊的語句會被立即執行然后await下面的代碼進入等待狀態,等待await得到結果。 await后面如果不是promise對象,await會阻塞后面的代碼,先執行async外面的同步代碼,同步代碼執行完,再回到async內部,把這個非promise的東西,作為await表達式的結果。await后面如果是promise對象,await也會暫停async后面的代碼,先執行async外面的同步代碼,等著promise對象fulfilled,然后把resolve的參數作為await表達式的運算結果
setTimeout(function () {
  console.log('6')
}, 0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('5')
}
async function async2() {
  console.log('3')
}
async1()
console.log('4')
  1. 6是宏任務在下一輪事件循環執行
  2. 先同步輸出1,然后調用async1(),輸出2
  3. await async2()會先運行async2(), 5進入等待狀態
  4. 輸出3, 這個時候先執行async函數外的同步代碼輸出4.
  5. 最后await拿到等待的結果繼續往下執行輸出5.
    6, 進入第二輪事件循環輸出6.


    image.png

測試代碼的輸出結果,看到async1函數輸出2, 立馬執行await 的async2函數,輸出3, 但是沒有立即返回,而是先執行async2外面的同步代碼,最后得到返回值111111給await async2函數,


image.png
console.log('1')
async function async1() {
  console.log('2')
  await 'await的結果'
  console.log('5')
}

async1()
console.log('3')

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
  1. 首先輸出1, 然后進入async1函數,輸出2
  2. await后面雖然是一個直接量,但是還是先執行async函數外的同步代碼
  3. 輸出3, 進入promise輸出4,then回調進入微任務隊列
  4. 現在同步代碼執行完了, 回到async函數繼續執行輸出5
    5, 最后運行微任務輸出6
async function async1() {
  console.log('2')
  await async2()
  console.log('7')
}

async function async2() {
  console.log('3')
}

setTimeout(function () {
  console.log('8')
}, 0)

console.log('1')
async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
console.log('5')
  1. 首先輸出同步代碼1, 然后進入async1方法輸出2
  2. 因為遇到await所以先進群async2方法, 后面的7處于等待狀態
  3. 在async2中輸出3, 現在跳出async函數先執行外面的同步代碼
  4. 輸出4,5.then回調進入微任務棧
  5. 現在宏任務執行完了,然后回到async1函數接著往下執行輸出7
  6. 執行微任務輸出6
  7. 進入下一輪事件循環輸出8
    完整輸出:1-2-3-4-5-7-6-8
async function async1() {
  console.log('2')
  const data = await async2()
  console.log(data)
  console.log('8')
}

async function async2() {
  return new Promise(function (resolve) {
    console.log('3')
    resolve('await的結果')
  }).then(function (data) {
    console.log('6')
    return data
  })
}
console.log('1')

setTimeout(function () {
  console.log('9')
}, 0)

async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('7')
})
console.log('5')
  1. 函數async1和async2只是定義先不管它, 首先輸出1
  2. setTimeout作為宏任務進入宏任務隊列等待下一輪事件循環
  3. 進入async1函數輸出2,await下面的代碼進入等待狀態。
  4. 進入async2函數輸出3,then回調進入微任務隊列
  5. 現在執行外面的同步代碼, 輸出4,5,then回調進入微任務隊列
    6, 按序執行微任務,輸出6,7. 現在回到async1函數,
    7, 輸出data, 也就是await關鍵字等到的內容, 接著輸出8
    8, 進行下一輪事件循環輸出9
    執行結果: 1-2-3-4-5-6-7-await的結果-8-9
setTimeout(function () {
  console.log('8')
}, 0)

async function async1() {
  console.log('1')
  const data = await async2()
  console.log('6')
  return data
}

async function async2() {
  return new Promise(resolve => {
    console.log('2')
    resolve('async2的結果')
  }).then(data => {
    console.log('4')
    return data
  })
}

async1().then(data => {
  console.log('7')
  console.log(data)
})

new Promise(function (resolve) {
  console.log('3')
  resolve()
}).then(function () {
  console.log('5')
})

  1. setTimeout作為宏任務進入宏任務隊列等待下一輪事件循環
  2. 先執行async1函數, 輸出1,然后6進入等待狀態,現在執行async2
  3. 輸出2, then回調進入微任務隊列
    4, 接下來執行外面的同步代碼3, then回調進入微任務隊列
    5, 按序列執行微任務,輸出4,5. 下面會帶async1函數
    6,輸出了4之后執行return data, await拿到了內容
    7, 繼續執行輸出6, 執行了后面的return data才出發async1的then回調輸出7以及data
    8, 進行第二輪事件循環輸出8,
    完整輸出結果:1-2-3-4-5-6-7-async2的結果-8

https://juejin.im/post/59e85eebf265da430d571f89
https://juejin.im/post/5c148ec8e51d4576e83fd836

面試題
題目一:

setTimeout(function() {
    console.log('4')
})

new Promise(function(resolve) {
    console.log('1') // 同步任務
    resolve()
}).then(function() {
    console.log('3')
})
console.log('2')

輸出結果: 1-2-3-4

console.log('1')
setTimeout(function() {
    console.log('2')
    process.nextTick(function() {
        console.log('3')
    })
    new Promise(function(resolve) {
        console.log('4')
        resolve()
    }).then(function() {
        console.log('5')
    })
})

process.nextTick(function() {
    console.log('6')
})

new Promise(function(resolve) {
    console.log('7')
    resolve()
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9')
    process.nextTick(function() {
        console.log('10')
    })
    new Promise(function(resolve) {
        console.log('11')
        resolve()
    }).then(function() {
        console.log('12')
    })

輸出結果:
1-7-6-8-2-4-3-5-9-11-10-12

setTimeout(function () {
  console.log('6')
}, 0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('5')
}
async function async2() {
  console.log('3')
}
async1()
console.log('4')

輸出結果:
1-2-3-4-5-6

console.log('1')
async function async1() {
  console.log('2')
  await 'await的結果'
  console.log('5')
}

async1()
console.log('3')

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
輸出結果:
1-2-3-4-5-6
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容