<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何跟面試官解釋事件循環(huán)

          共 9632字,需瀏覽 20分鐘

           ·

          2021-04-06 21:13

          關(guān)于事件循環(huán)的問題面試官都尤其的偏愛,所以說準(zhǔn)備面試如果不搞懂事件循環(huán)是非常危險的。

          當(dāng)面試官問你了解瀏覽器事件循環(huán)嗎?這只是一個開始,接下來:

          • 為什么js在瀏覽器中有事件循環(huán)機制
          • 事件循環(huán)有哪些任務(wù)
          • 為什么要用微任務(wù),只有宏任務(wù)不行嗎
          • 瀏覽器中事件循環(huán)機制怎么執(zhí)行的?與Node中有何區(qū)別
          • setTimeout為什么沒有按寫好的延遲時間執(zhí)行?
          • ...

          這一系列圍繞事件循環(huán)的問題都有可能會一步一步的讓你回答

          本文圍繞以下幾個內(nèi)容來展開,讓你輕松的回答面試官關(guān)于事件循環(huán)系列問題。

          圖片.png

          為什么會有事件循環(huán)機制

          JavaScript的一大特點就是單線程,也就是說,同一時間只能做一件事。那為什么要設(shè)計成單線程呢,多線程效率不是更高嗎?

          有這樣一個場景:假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)?

          所以,JavaScript從誕生就是單線程。但是單線程就導(dǎo)致有很多任務(wù)需要排隊,只有一個任務(wù)執(zhí)行完才能執(zhí)行后一個任務(wù)。如果某個執(zhí)行時間太長,就容易造成阻塞;為了解決這一問題,JavaScript引入了事件循環(huán)機制

          事件循環(huán)是什么

          Javascript單線程任務(wù)被分為同步任務(wù)異步任務(wù)

          • 同步任務(wù):立即執(zhí)行的任務(wù),在主線程上排隊執(zhí)行,前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù);
          • 異步任務(wù):異步執(zhí)行的任務(wù),不進(jìn)入主線程, 而是在異步任務(wù)有了結(jié)果后,將注冊的回調(diào)函數(shù)放入任務(wù)隊列中等待主線程空閑的時候讀取執(zhí)行。

          注意:異步函數(shù)在相應(yīng)輔助線程中處理完成后,即異步函數(shù)達(dá)到觸發(fā)條件了,就把回調(diào)函數(shù)推入任務(wù)隊列中,而不是說注冊一個異步任務(wù)就會被放在這個任務(wù)隊列中

          同步任務(wù)與異步任務(wù)流程圖:

          從上面流程圖中可以看到,主線程不斷從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,這種運行機制就叫做Event Loop(事件循環(huán))!

          事件循環(huán)中的兩種任務(wù)

          在JavaScript中,除了廣義的同步任務(wù)和異步任務(wù),還可以細(xì)分,一種是宏任務(wù)(MacroTask)也叫Task,一種叫微任務(wù)(MicroTask)。

          二者執(zhí)行順序流程圖如下:

          每次單個宏任務(wù)執(zhí)行完畢后, 檢查微任務(wù)隊列是否為空, 如果不為空,會按照先入先出的規(guī)則全部執(zhí)行完微任務(wù)后, 清空微任務(wù)隊列, 然后再執(zhí)行下一個宏任務(wù),如此循環(huán)

          如何區(qū)分宏任務(wù)與微任務(wù)呢?

          • 宏任務(wù):macrotask,又稱為task,  可以理解為每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(wù)(包括每次從事件隊列中獲取一個事件回調(diào)并放到執(zhí)行棧中執(zhí)行)。

          一般包括:script(可以理解為外層同步代碼)、setTimeout、setInterval 、setImmediate、I/O操作

          • 微任務(wù):microtask, 又稱為job, 可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。包括:Promise.then/cath /finally回調(diào)(平時常見的)、 MutationObserver回調(diào)(html5新特性)

          為什么要有微任務(wù)呢?

          既然我們知道了微任務(wù)與宏任務(wù),但異步任務(wù)為什么要區(qū)分宏任務(wù)與微任務(wù)呢,只有宏任務(wù)不可以嗎?

          因為事件隊列其實是一個“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu),排在前面的事件會優(yōu)先被主線程讀取, 那如果突然來了一個優(yōu)先級更高的任務(wù),還讓去人家排隊,就很不理性化, 所以需要引入微任務(wù)。

          舉一個現(xiàn)實生活中的例子:

          就是我們?nèi)ャy行辦理業(yè)務(wù)時, 并不是到了就能辦理, 而是需要先取號排隊, 等到柜臺業(yè)務(wù)員辦理完當(dāng)前客戶業(yè)務(wù)才能繼續(xù)叫號進(jìn)行下一個。

          這時每一個來辦理業(yè)務(wù)的人就可以認(rèn)為是銀行柜員的一個宏任務(wù)來存在, 當(dāng)辦理到你的業(yè)務(wù)時, 你本來只是要重新綁定一下手機號, 但是突然想到明天要參加婚禮,需要隨份子錢, 此時你和柜員說你要取money, 這時候柜員不能告訴你,讓你重新取號排隊(不合理的要求)。

          其實這時候就相當(dāng)于你突然提出了一個新的任務(wù),這個任務(wù)就相當(dāng)于是一個微任務(wù) ,它要在下一個宏任務(wù)之前完成。

          在當(dāng)前的微任務(wù)沒有執(zhí)行完成時,是不會執(zhí)行下一個宏任務(wù)的。

          面試中如果問到這里,基本已經(jīng)了解事件循環(huán)的理論掌握情況, 接下來可能就會說,來做一下下面幾道題吧, 考察你的實際理解到什么程度。

          事件循環(huán)典型題目分析

          案例1:代碼執(zhí)行結(jié)果是什么

          async function async1() {
              console.log("async1 start")
              await async2()
              console.log("async1 end")
          }

          async function async2(){
              console.log("async2")
          }

          console.log("script start")

          setTimeout(function(){
              console.log("setTimeout")
          }, 0)

          async1()

          new Promise(function(resolve){
              console.log("promise1")
              resolve()
          }).then(function(){
              console.log("promise2")
          })

          console.log("script end")

          這里我們討論瀏覽器中的執(zhí)行結(jié)果,分析:

          1. 建立執(zhí)行上下文,先執(zhí)行同步任務(wù),輸出『script start』

          2. 往下執(zhí)行,遇到setTimeout,將其回調(diào)函數(shù)放入宏任務(wù)隊列,等待執(zhí)行

          3. 繼續(xù)往下執(zhí)行,調(diào)用async1:

            1. 是同步任務(wù),輸出『async1 start』
            2. 接下來await async2(), 這里的代碼相當(dāng)于new Promise(()=>{async2()}),而將 await 后面的全部代碼放到.then()中去;所以輸出『async2』,把async2()后面的代碼放到微任務(wù)中
          4. 繼續(xù)執(zhí)行,有個new Promise 輸出『promise1』,當(dāng)resolve后,將.then()的回調(diào)函數(shù)放到微任務(wù)隊列中(記住Promise本身是同步的立即執(zhí)行函數(shù),then是異步執(zhí)行函數(shù))。

          5. 繼續(xù)往下執(zhí)行, 輸出『script end』,此時調(diào)用棧被清空,可以執(zhí)行異步任務(wù)

          6. 開始第一次事件循環(huán):7.1 由于整個script 算一個宏任務(wù),因此該宏任務(wù)已經(jīng)執(zhí)行完畢 7.2 檢查微任務(wù)隊列, 發(fā)現(xiàn)其中放入了2個微任務(wù)(分別在3.2步,4步放入), 執(zhí)行輸出『async1 end』,『promise2』,第一次循環(huán)結(jié)束

          7. 開始第二次循環(huán):

            1. 從宏任務(wù)開始, 檢查宏任務(wù)隊列中有setTimeout回調(diào), 輸出『setTimeout』
            2. 檢查微任務(wù)隊列,無可執(zhí)行的微任務(wù), 第二次循環(huán)結(jié)束

          注意:async/await底層是基于Promise封裝的,所以await前面的代碼相當(dāng)于new Promise,是同步進(jìn)行的,await后面的代碼相當(dāng)于.then回調(diào),才是異步進(jìn)行的。

          最后執(zhí)行結(jié)果如下:

          script start
          async1 start
          async2
          promise1
          script end
          async1 end
          promise2
          setTimeout

          關(guān)于第3步代碼執(zhí)行分析:

          async function async1() {
              console.log("async1 start")
              await async2()
              console.log("async1 end")
          }

          改為Promise寫法就是:

          async function async1() {
          new Promise((resolve, reject) =>{
              console.log("async1 start")
              resolve(async2())
          }).then(()=>{
              // 執(zhí)行 async1 函數(shù)await之后的語句
              console.log("async1 end")
          })
          }

          再看下面一道題

          案例2:代碼執(zhí)行結(jié)果是什么

          console.log("start");
          setTimeout(() => {
              console.log("children2")
              Promise.resolve().then(() =>{
                  console.log("children3")
              })
          }, 0)

          new Promise(function(resolve, reject){
              console.log("children4")
              setTimeout(function(){
                  console.log("children5")
                  resolve("children6")
              }, 0)
          }).then(res =>{
              console.log("children7")
              setTimeout(() =>{
                  console.log(res)
              }, 0)
          })

          分析執(zhí)行順序:

          1. 首先將整體代碼作為一個宏任務(wù)執(zhí)行,輸出『start』

          2. 接著遇到setTimeout,0ms后將其回調(diào)函數(shù)放入宏任務(wù)隊列

          3. 接下來遇到Promise, 由于Promise本身是立即執(zhí)行函數(shù), 所以先輸出『children4』

            3-1. 在Promise中遇到setTimeout, 將其回調(diào)放入宏任務(wù)隊列中;整體代碼執(zhí)行完畢

          4. 然后檢查并執(zhí)行所有微任務(wù), 因為沒有微任務(wù), 所以第一次事件循環(huán)結(jié)束,開始第二輪

          5. 執(zhí)行第2步放入的宏任務(wù),輸出『children2』 5-1. 遇到Promise,并直接調(diào)用了resolve,將.then回調(diào)加入都微任務(wù)隊列中

          6. 檢查并執(zhí)行所有微任務(wù), 輸出『children3』, 沒有多余的微任務(wù), 所以第二輪事件循環(huán)結(jié)束,開始第三輪事件循環(huán)

          7. 執(zhí)行3-1中放入的宏任務(wù), 輸出『children5』, 并且調(diào)用了resolve, 所以將對應(yīng)的.then回調(diào)放入到微任務(wù)隊列中

          8. 檢查并執(zhí)行所以微任務(wù), 輸出『children7』,遇到setTimeout,將其加入到宏任務(wù)隊列中,開始第四輪事件循環(huán)

          9. 執(zhí)行第8步加入的宏任務(wù), 輸出『children6』, 沒有任何微任務(wù), 第四輪事件循環(huán)結(jié)束。

          最后執(zhí)行結(jié)果:

          start
          children4
          children2
          children3
          children5
          children7
          children6

          注意:有的小伙伴在第3步中容易錯誤的將.then的回調(diào)放入微任務(wù)隊列;因為沒有調(diào)用resolve或者reject之前是不算異步任務(wù)完成的, 所以不能將回調(diào)放入事件隊列

          Node和瀏覽器的事件循環(huán)的區(qū)別?

          Node的事件循環(huán)是libuv實現(xiàn)的,引用一張官網(wǎng)的圖:

          Node的事件循環(huán)

          圖中表示的是事件循環(huán)包含的6個階段,大體的task(宏任務(wù))執(zhí)行順序是這樣的:

          • timers定時器:本階段執(zhí)行已經(jīng)安排的 setTimeout() 和 setInterval() 的回調(diào)函數(shù)。
          • pending callbacks待定回調(diào):執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調(diào)。
          • idle, prepare:僅系統(tǒng)內(nèi)部使用,可以不必理會。
          • poll 輪詢:檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù),它們由計時器和 setImmediate() 排定的之外),其余情況 node 將在此處阻塞。
          • check 檢測:setImmediate() 回調(diào)函數(shù)在這里執(zhí)行。
          • close callbacks 關(guān)閉的回調(diào)函數(shù):一些準(zhǔn)備關(guān)閉的回調(diào)函數(shù),如:socket.on('close', ...)。

          首先需要知道的是Node版本不同,執(zhí)行順序有所差異。因為Node v11之后, 事件循環(huán)的原理發(fā)生了變化,和瀏覽器執(zhí)行順序趨于一致,都是每執(zhí)行一個宏任務(wù)就執(zhí)行完微任務(wù)隊列。

          Node v10及以前,微任務(wù)和宏任務(wù)在Node的執(zhí)行順序:

          1. 執(zhí)行完一個階段的所有任務(wù)
          2. 執(zhí)行完nextTick隊列里面的內(nèi)容
          3. 然后執(zhí)行完微任務(wù)隊列的內(nèi)容

          Node v10及以前的版本,微任務(wù)會在事件循環(huán)的各個階段之間執(zhí)行,也就是一個階段執(zhí)行完畢,就會去執(zhí)行微任務(wù)隊列的任務(wù):

          圖片.png

          瀏覽器與Node執(zhí)行順序分別是什么

          setTimeout(()=>{
              console.log('timer1')

              Promise.resolve().then(function() {
                  console.log('promise1')
              })
          }, 0)

          setTimeout(()=>{
              console.log('timer2')

              Promise.resolve().then(function() {
                  console.log('promise2')
              })
          }, 0)

          // 瀏覽器中:
          timer1
          promise1
          timer2
          promise2

          // 在Node中:
          timer1
          timer2
          promise1
          promise2

          在這個例子中,Node的邏輯如下(再強調(diào)一下Node v10及以下):

          最初timer1和timer2就在timers階段中。開始時首先進(jìn)入timers階段,執(zhí)行timer1的回調(diào)函數(shù),打印timer1,并將promise1.then回調(diào)放入微任務(wù)隊列,同樣的步驟執(zhí)行timer2,打印timer2;至此,timer階段執(zhí)行結(jié)束,event loop進(jìn)入下一個階段之前,執(zhí)行微任務(wù)隊列的所有任務(wù),依次打印promise1、promise2。

          setImmediate 的setTimeout的區(qū)別

          setImmediate大部分瀏覽器暫時不支持,只有IE10、11支持,具體可見MDN。setImmediatesetTimeout是相似的,但根據(jù)它們被調(diào)用的時間以不同的方式表現(xiàn)。

          • setImmediate設(shè)計用于在當(dāng)前poll階段完成后check階段執(zhí)行腳本 。
          • setTimeout 安排在經(jīng)過最?。╩s)后運行的腳本,在timers階段執(zhí)行。

          舉個例子:

          setTimeout(() => {
            console.log('setTimeout');
          }, 0);

          setImmediate(() => {
            console.log('setImmediate');
          });

          其執(zhí)行順序為:

          遇到setTimeout,雖然設(shè)置的是0毫秒觸發(fā),但是被node.js強制改為1毫秒,塞入times階段 遇到setImmediate塞入check階段 同步代碼執(zhí)行完畢,進(jìn)入Event Loop 先進(jìn)入times階段,檢查當(dāng)前時間過去了1毫秒沒有,如果過了1毫秒,滿足setTimeout條件,執(zhí)行回調(diào),如果沒過1毫秒,跳過 跳過空的階段,進(jìn)入check階段,執(zhí)行setImmediate回調(diào) 可見,1毫秒是個關(guān)鍵點,所以在上面的例子中,setImmediate不一定在setTimeout之前執(zhí)行了。

          process.nextTick()與 Promise回調(diào)誰先執(zhí)行?

          process.nextTick()是Node環(huán)境下的方法, 所以我們基于Node談?wù)摗?/p>

          process.nextTick()是一個特殊的異步API,其不屬于任何的Event Loop階段。事實上Node在遇到這個API時,Event Loop根本就不會繼續(xù)進(jìn)行,會馬上停下來執(zhí)行process.nextTick(),這個執(zhí)行完后才會繼續(xù)Event Loop??梢钥匆幌聜€例子:

          var fs = require('fs')

          fs.readFile(__filename, () => {
              setTimeout(() => {
                  console.log('setTimeout');
              }, 0);

              setImmediate(() => {
                  console.log('setImmediate');
                  
                  process.nextTick(() => {
                    console.log('nextTick 2');
                  });
              });

              process.nextTick(() => {
                console.log('nextTick 1');
              });
          });

          // 執(zhí)行結(jié)果
          nextTick 1
          setImmediate
          nextTick 2
          setTimeout

          執(zhí)行流程梳理:

          1. 代碼都在readFile回調(diào)中,回調(diào)執(zhí)行時處于poll階段
          2. 遇到setTimeout,雖然延時設(shè)置的是0, 但是相當(dāng)于setTimeout(fn,1),將其回調(diào)函數(shù)放入后面的timers階段
          3. 接下來遇到setImmediate,將其回調(diào)函數(shù)放入到后面的check階段
          4. 遇到process.nextTick, 立即執(zhí)行, 輸出 『nextTick 1』
          5. 執(zhí)行到下一個階段check,輸出『setImmediate』, 又遇到nextTick,執(zhí)行輸出『nextTick 2』
          6. 執(zhí)行到下一個timers階段, 輸出『setTimeout』

          這種機制其實類似于我們前面講的微任務(wù),但是并不完全一樣,比如同時有nextTickPromise的時候,肯定是nextTick先執(zhí)行,原因是nextTick的隊列比Promise隊列優(yōu)先級更高。來看個例子:

          setImmediate(() => {
            console.log('setImmediate');
          });
          Promise.resolve().then(()=>{
              console.log('promise')
          })
          process.nextTick(()=>{
              console.log('nextTick')
          })

          // 運行結(jié)果
          nextTick
          promise
          setImmediate

          總結(jié)

          文章包含了為什么會有事件循環(huán), 事件循環(huán)是什么,事件循環(huán)的運行機制以及Node和瀏覽器中事件循環(huán)的異同點,通過文章的學(xué)習(xí), 面對開篇提出的面試題,相信你都可以輕松的搞定。

          關(guān)注公眾號【前端飯圈】獲取文章中的源碼, 另外還準(zhǔn)備了10道關(guān)于事件循環(huán)的面試題, 如果你想檢測以下自己是否完全掌握,可以領(lǐng)取題目, 回復(fù)【事件循環(huán)】即可領(lǐng)取。



          參考文章:

          https://www.ruanyifeng.com/blog/2014/10/event-loop.html

          https://my.oschina.net/u/4390738/blog/3199580

          https://www.jianshu.com/p/23fad3814398

          https://juejin.cn/post/6844904004653154317

          - EOF -


          推薦閱讀  點擊標(biāo)題可跳轉(zhuǎn)

          1、面試官:談?wù)刅ue和React的區(qū)別?

          2、面試官:Vue中的 computed 和 watch的區(qū)別

          3、從輸入 URL 到頁面展示,這中間發(fā)生了什么?

          瀏覽 90
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天免费看黄片 | 日韩一级片大全 | 色黄视频在线 | 无码中文字幕在线视频 | 男女啪啪啪啪啪啪网站 |