js的事件循環(Eventloop) 機制/js的宏任務微任務執行順序


這篇借助於同事准備的技術分享,其他技術文章,書本知識,自己的理解梳理而成

高級程序設計第三版:
js 是一門單線程的語言,運行於單線程的環境中,例如定時器等並不是線程,定時器僅僅只是計划代碼在未來的某個時間執行,瀏覽器負責排序,指派某段代碼在某個時間點運行
的優先級

1.為什么規定瀏覽器必須是單線程?
JS主要用途之一是操作DOM,如果JS同時有兩個線程,同時對同一個dom進行操作,一個需要刪除dom,一個需要添加dom,這時瀏覽器應該聽哪個線程的,如何判斷優先級,所以為了簡化操作,規定js是一門單線程的語言。

2.有關於js是單線程的理解
所謂的"JS是單線程的"是指解釋和執行JS代碼的線程,只有一個,一般稱之為“主線程”,而瀏覽器並不是單線程的,是多線程並且是多進程的,而對於前端最關心的還是渲染進程.

  1. GUI渲染線程
    ● 負責渲染瀏覽器界面,解析HTML、CSS,構建DOM樹和RenderObject樹,布局和繪制
    ● 當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行
    ● GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中,等到JS引擎空閑時立即被執行。
  2. JS引擎線程
    ● 也稱JS內核,負責處理JS腳本程序。例如V8引擎
    ● JS引擎一直等待着任務隊列中任務的到來,然后加以處理,一個Tab頁(Renderer進程)中無論什么時候都只有一個JS引擎線程在運行JS程序
    ● GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,頁面渲染就不連貫。
  3. 定時觸發器線程
    ● 傳說中的setInterval和setTimeout所在的線程
    ● 定時器線程其實只是一個計時的作用,他並不會真正執行時間到了的回調,真正執行這個回調的還是JS主線程。當時間到了,定時器線程就通知事件觸發線程,讓事件觸發線程將setTimeout的回調事件添加到待處理任務隊列的尾部,等待JS引擎的處理。
    ● W3C在HTML5標准中規定,要求setTimeout中低於4ms的時間間隔算4ms
  4. 事件觸發線程
    ● 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(可以理解為:JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
    ● 當JS引擎執行setTimeout時(或者是來自瀏覽器內核的其他線程,如鼠標點擊、ajax異步請求等),當這些事件滿足觸發條件被觸發時,該線程就會將對應回調事件添加到添加到待處理任務隊列的尾部,等待JS引擎的處理
    ● 由於JS是單線程關系,所以這些待處理任務隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行)
  5. 異步http請求線程
    ● 這個線程負責處理異步的ajax請求,當請求完成后如果設置有回調函數,他也會通知事件觸發線程,然后事件觸發線程將這個回調再放入任務隊列中尾部,等待JS引擎執行

3.單線程如何實現異步?
大家都知道JS是單線程的腳本語言,在同一時間,只能做同一件事,為了協調事件、用戶交互、腳本、UI渲染和網絡處理等行為,防止主線程阻塞,設計者給JS加了一個事件循環(Event Loop)的機制

3.1理解什么是執行上下文?
可以看這篇文章https://amberzqx.com/2020/02/04/JavaScript%E7%B3%BB%E5%88%97%E4%B9%8B%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E5%92%8C%E6%89%A7%E8%A1%8C%E6%A0%88/

當 JavaScript 代碼執行一段可執行代碼(executable code)時,會創建對應的執行上下文(execution context)。執行上下文(可執行代碼段)總共有三種類型:
全局執行上下文(全局代碼):不在任何函數中的代碼都位於全局執行上下文中,只有一個,瀏覽器中的全局對象就是 window 對象,this 指向這個全局對象。
函數執行上下文(函數體):只有調用函數時,才會為該函數創建一個新的執行上下文,可以存在無數個,每當一個新的執行上下文被創-建,它都會按照特定的順序執行一系列步驟。
Eval 函數執行上下文(eval 代碼): 指的是運行在 eval 函數中的代碼,很少用而且不建議使用

執行上下文又包括三個生命周期階段:創建階段 → 執行階段 → 回收階段
JS引擎創建了執行上下文棧(執行棧)來管理執行上下文。可以把執行上下文棧認為是一個存儲函數調用的棧結構,遵循先進后出,后進先出的原則,就像下面的漢諾塔,第一個最大的先進去,當拿出來的時候肯定是最后一個出來的,最小的那個后進去,拿出來的時候是最先拿出來的~因為JS執行中最先進入全局環境,所以處於"棧底的永遠是全局執行上下文"。而處於"棧頂的是當前正在執行函數的執行上下文"

舉個例子:

 const firstFunction = () => {
            console.log('1');
            secondFunction();
            console.log('2');
        }
        const secondFunction = () => {
            console.log('3');
        }
        firstFunction();
        console.log(4)

        // 1324
        //從上到下的執行
        //圖一:從上到下執行,先是全局作用域,那就是棧底第一個
        //圖二: firstFunction的調用,打印出1,現在棧頂是secondFunction,因為函數里面還沒有執行完,所以還沒有被銷毀
        //圖三: secondFunction的調用,打印3,secondFunction,因為函數里面執行完,所以要被銷毀到圖四
        //再執行棧頂firstFunction里面的2到圖5


看上面的圖是不是對應漢諾塔放進去,拿出來的一個過程

3.2理解同步任務,異步任務,任務隊列
JavaScript 是一個單線程序的解釋器,因此一定時間內只能執行一段代碼。為了控制要執行的代碼,就有一個 JavaScript 任務隊列。這些任務會按照將它們添加到隊列的順序執行如果隊列是空的,那么添加的代碼會立即執行;如果隊列不是空的,那么它就要等前面的代碼執行完了以后再執行.同步任務指的是,在主線程上,排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程,而進入“任務隊列”(task queue)的任務,只有“任務隊列”通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。

3.3 js的事件循環機制
具體來說,異步運行機制如下:
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)述過程會不斷重復,也就是常說的Event Loop(事件循環)

3.4宏任務微任務
js的任務又分為宏任務和微任務(btw,不要歸類為同步任務異步任務和宏任務微任務扯上聯系,拋開同步異步的概念去理解宏任務微任務)
前端常見的宏任務,微任務分類:
macro-task(宏任務):包括整體代碼script,setTimeout,setInterval,setImmediate(node或者小眾瀏覽器支持)
micro-task(微任務):Promise,process.nextTick(node 環境支持)
注意:
Promise 新建后就會立即執行,也就是說new Promise構造函數是同步任務,但Promise的注冊的then回調和catch回調才是微任務
宏任務:可以理解是每次執行棧執行的代碼就是一個宏任務,所有宏任務都是添加到任務隊列,所以”任務隊列又叫宏任務隊列”,這個任務隊列由事件觸發線程來單獨維護的

微任務
可以理解是在當前宏任務執行結束后立即執行的任務

每一次事件循環,是先執行宏任務,再執行宏任務里面的微任務,看到里面兩個字了嗎?????每一次事件循環只執行一個宏任務

注意:
1.微任務隊列里邊的優先級process.nextTick()>Promise.then()
2.setInterval,setImmediate的執行順序后續補充,目前前端幾乎用不到setImmediate,不要慌

總結:
每一次循環稱為 tick, 每一次tick的任務如下:
1、執行一個宏任務(執行棧中沒有就從任務隊列中獲取)
2、宏任務執行過程中如果遇到微任務,就將它添加到微任務隊列中
3、宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
4、重復1到3步驟
宏任務 > 所有微任務 > 宏任務>微任務,也就是每一次事件循環,是先執行宏任務,再執行宏任務里面的微任務,下一輪還是先執行宏任務,再執行下一輪的微任務

練習題目:
example1

      console.log('1')
        setTimeout(() => {
          console.log('2')
        }, 0)
        Promise.resolve().then(() => {
          console.log('3')
        }).then(() => {
          console.log('4')
        })
        console.log('5')
      // 1 5 3 4 2
      //第一輪循環,執行宏任務script里面的同步任務,遇到微任務掛起,先執行15
      // 執行完宏任務,執行微任務 3, 4 微任務執行完畢了就是下一輪事件循環的開始
      //  第二輪循環 執行 settimout里面的2

example2
es6那本書---Promise 新建后就會立即執行

  console.log('1')
    new Promise((resolve) => {
      console.log('2')
      setTimeout(()=>{
        console.log('4')
        resolve()
        }, 0)
    }).then(() => {
      console.log('3')
    })

    // 1,2,4,3
    // 第一輪: 宏 script 打印出:12 
    // 第二輪: 宏 setTimeout 打印出: 4,3
    // 這里要注意Promise的注冊的then回調和catch回調才是微任務,所以resolve()是在setTimeout里面調的
    // 屬於第二輪的微任務

example3

  new Promise((resolve) => {
      console.log('1')
      setTimeout(() => {
        console.log('2')
      }, 1000)
      resolve()
    }).then(() => {
      console.log('3')
    })
    // 132
    // 基於example2,可以明白,resolve()在setTimeout之外調用,還是屬於第一輪宏任務里面的微任務

example4

console.log('1')
   new Promise((resolve) => {
     console.log('2')
     resolve()
     console.log('3')
   }).then(() => {
     console.log('4')
   })
   console.log('5')

   // 第一輪  宏: script 打印: 1235 微:4
   // 12354
   // 要注意的是3不用等resolve回調完再執行哦,因為並沒有還可以繼續執行,await可以阻塞下面的執行

example5

 console.log('1')
       setTimeout(() => {
           console.log('2')
           new Promise((resolve) => {
               console.log('3')
               resolve()
           }).then(() => {
               console.log('4')
           })
       })
       new Promise((resolve) => {
           console.log('5')
           resolve()
       }).then(() => {
           console.log('6')
       })
       console.log('7')
       setTimeout(() => {
           console.log('8')
           new Promise((resolve) => {
               console.log('9')
               resolve()
           }).then(() => {
               console.log('10')
           })
       })

   // 第一輪循環 宏:script  打印: 157  微:resolve() 打印:6
   // 第二輪循環 宏:第一個setTimeout 打印:23  微:resolve() 打印:4
   // 第三輪: 宏:第二個setTimeout 打印:89 微:resolve() 打印:10
   // 15762348910

example6 在node下面執行,這個例子一般,不值得一看

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')
        })
    })

    // 第一輪 宏:script 打印:17 微: process.nextTick以及Promise.then 打印: 6 8
    // 第二輪:宏: 第一個settimeout  打印: 24 微:process.nextTick以及Promise.then 打印:35
    // 第三輪: 宏: 第二個settimeout 打印: 9 11 微: process.nextTick以及Promise.then 打印: 10 12
    // 1768 2435    9 11 10 12

example6

 async function async1(){
            console.log('1')
            await async2()
            console.log('2')
        }
        async function async2(){
            console.log('3')
        }
        console.log('4')
        setTimeout(function(){
            console.log('5') 
        },0)  
        async1();
        new Promise(function(resolve){
            console.log('6')
            resolve();
        }).then(function(){
            console.log('7')
        })
        console.log('8')

        // 第一輪事件循環 宏:script 打印:4 1 3 6 8 微: await之后的結果 以及resolve() 打印: 2 7
        // 第二輪事件循環 宏:setTimeout 打印: 5 
        // 4 1 3 6 8  2 7 5
        //注意 await xx的時候,相當於xx這里直接創建了一個new promise,所以async2函數是new promise,會立即執行, await的結果是promise.then的結果,並且沒有成功finish會阻塞下面的執行,所以2會在微任務拿到結果之后執行

還有一個例子,前端目前用不到哈,后續更新:

console.log('1')
setTimeout(() => {
  console.log('2')
  process.nextTick(() => {
    console.log('3')
  })
  new Promise((resolve) => {
    console.log('4')
    resolve()
  }).then(() => {
    console.log('5')
  })
})
new Promise((resolve) => {
  console.log('7')
  resolve()
}).then(() => {
  console.log('8')
})
console.log('9')
process.nextTick(() => {
  console.log('10')
})
setImmediate(() => {
  console.log('15')
  process.nextTick(() => {
    console.log('16')
  })
  new Promise((resolve) => {
    console.log('17')
    resolve()
  }).then(() => {
    console.log('18')
  })
})
setTimeout(() => {
  console.log('11')
  new Promise((resolve) => {
    console.log('12')
    resolve()
  }).then(() => {
    console.log('13')
  })
  process.nextTick(() => {
    console.log('14')
  })
})
1,7,9,10,8,
2,4,3,5
 11  12  14  13  
 15  17  16  18 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM