Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 2022国产成人精彩在线视频,国产午夜精品免费一二区,嫩草成人国产精品

          整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript 異步編程指南 - 聊聊 Node.js 中的事件循環

          者: 五月君 來源:編程界|

          事件循環是一種控制應用程序的運行機制,在不同的運行時環境有不同的實現,上一節講了瀏覽器中的事件循環,它們有很多相似的地方,也有著各自的特點,本節討論下 Node.js 中的事件循環。

          了解 Node.js 中的事件循環

          Node.js 做為 JavaScript 的服務端運行時,主要與網絡、文件打交道,沒有了瀏覽器中事件循環的渲染階段。

          在瀏覽器中有 HTML 規范來定義事件循環的處理模型,之后由各瀏覽器廠商實現。Node.js 中事件循環的定義與實現均來自于 Libuv。

          Libuv 圍繞事件驅動的異步 I/O 模型而設計,最初是為 Node.js 編寫的,提供了一個跨平臺的支持庫。下圖展示了它的組成部分,Network I/O 是網絡處理相關的部分,右側還有文件操作、DNS,底部 epoll、kqueue、event ports、IOCP 這些是底層不同操作系統的實現。

          圖片來源:http://docs.libuv.org/en/v1.x/_images/architecture.png

          事件循環的六個階段

          當 Node.js 啟動時,它會初始化事件循環,處理提供的腳本,同步代碼入棧直接執行,異步任務(網絡請求、文件操作、定時器等)在調用 API 傳遞回調函數后會把操作轉移到后臺由系統內核處理。目前大多數內核都是多線程的,當其中一個操作完成時,內核通知 Node.js 將回調函數添加到輪詢隊列中等待時機執行。

          下圖左側是 Node.js 官網對事件循環過程的描述,右側是 Libuv 官網對 Node.js 的描述,都是對事件循環的介紹,不是所有人上來都能去看源碼的,這兩個文檔通常也是對事件循環更直接的學習參考文檔,在 Node.js 官網介紹的也還是挺詳細的,可以做為一個參考資料學習。

          左側 Node.js 官網展示的事件循環分為 6 個階段,每個階段都有一個 FIFO(先進先出)隊列執行回調函數,這幾個階段之間執行的優先級順序還是明確的。

          右側更詳細的描述了,在事件循環迭代前,先去判斷循環是否處于活動狀態(有等待的異步 I/O、定時器等),如果是活動狀態開始迭代,否則循環將立即退出。

          下面對每個階段分別討論。

          timers(定時器階段)

          首先事件循環進入定時器階段,該階段包含兩個 API setTimeout(cb, ms)、setInterval(cb, ms) 前一個是僅執行一次,后一個是重復執行。

          這個階段檢查是否有到期的定時器函數,如果有則執行到期的定時器回調函數,和瀏覽器中的一樣,定時器函數傳入的延遲時間總比我們預期的要晚,它會受到操作系統或其它正在運行的回調函數的影響。

          例如,下例我們設置了一個定時器函數,并預期在 1000 毫秒后執行。

          const now = Date.now(); 
          setTimeout(function timer1(){ 
            log(`delay ${Date.now() - now} ms`); 
          }, 1000); 
          setTimeout(function timer2(){ 
           log(`delay ${Date.now() - now} ms`); 
          }, 5000); 
          someOperation(); 
           
          function someOperation() { 
            // sync operation... 
            while (Date.now() - now < 3000) {} 
          } 

          當調用 setTimeout 異步函數后,程序緊接著執行了 someOperation() 函數,中間有些耗時操作大約消耗 3000ms,當完成這些同步操作后,進入一次事件循環,首先檢查定時器階段是否有到期的任務,定時器的腳本是按照 delay 時間升序存儲在堆內存中,首先取出超時時間最小的定時器函數做檢查,如果 **nowTime - timerTaskRegisterTime > delay** 取出回調函數執行,否則繼續檢查,當檢查到一個沒有到期的定時器函數或達到系統依賴的最大數量限制后,轉移到下一階段。

          在我們這個示例中,假設執行完 someOperation() 函數的當前時間為 T + 3000:

          • 檢查 timer1 函數,當前時間為 T + 3000 - T > 1000,已超過預期的延遲時間,取出回調函數執行,繼續檢查。
          • 檢查 timer2 函數,當前時間為 T + 3000 - T < 5000,還沒達到預期的延遲時間,此時退出定時器階段。

          pending callbacks

          定時器階段完成后,事件循環進入到 pending callbacks 階段,在這個階段執行上一輪事件循環遺留的 I/O 回調。根據 Libuv 文檔的描述:大多數情況下,在輪詢 I/O 后立即調用所有 I/O 回調,但是,某些情況下,調用此類回調會推遲到下一次循環迭代。聽完更像是上一個階段的遺留。

          idle, prepare

          idle, prepare 階段是給系統內部使用,idle 這個名字很迷惑,盡管叫空閑,但是在每次的事件循環中都會被調用,當它們處于活動狀態時。這一塊的資料介紹也不是很多。略...

          poll

          poll 是一個重要的階段,這里有一個概念觀察者,有文件 I/O 觀察者,網絡 I/O 觀察者等,它會觀察是否有新的請求進入,包含讀取文件等待響應,等待新的 socket 請求,這個階段在某些情況下是會阻塞的。

          阻塞 I/O 超時時間

          在阻塞 I/O 之前,要計算它應該阻塞多長時間,參考 Libuv 文檔上的一些描述,以下這些是它計算超時時間的規則:

          • 如果循環使用 UV_RUN_NOWAIT 標志運行、超時為 0。
          • 如果循環將要停止(uv_stop() 被調用),超時為 0。
          • 如果沒有活動的 handlers 或 request,超時為 0。
          • 如果有任何 idle handlers 處于活動狀態,超時為 0。
          • 如果有任何待關閉的 handlers,超時為 0。

          如果以上情況都沒有,則采用最近定時器的超時時間,或者如果沒有活動的定時器,則超時時間為無窮大,poll 階段會一直阻塞下去。

          示例一

          很簡單的一段代碼,我們啟動一個 Server,現在事件循環的其它階段沒有要處理的任務,它會在這里等待下去,直到有新的請求進來。

          const http = require('http'); 
          const server = http.createServer(); 
          server.on('request', req => { 
            console.log(req.url); 
          }) 
          server.listen(3000); 

          示例二

          結合階段一的定時器,在看個示例,首先啟動 app.js 做為服務端,模擬延遲 3000ms 響應,這個只是為了配合測試。再運行 client.js 看下事件循環的執行過程:

          • 首先程序調用了一個在 1000ms 后超時的定時器。
          • 之后調用異步函數 someAsyncOperation() 從網絡讀取數據,我們假設這個異步網路讀取需要 3000ms。
          • 當事件循環開始時先進入 timer 階段,發現沒有超時的定時器函數,繼續向下執行。
          • 期間經過 pending callbacks -> idle,prepare 當進入 poll 階段,此時的 http.get() 尚未完成,它的隊列為空,參考上面 poll 阻塞超時時間規則,事件循環機制會檢查最快到達閥值的計時器,而不是一直在這里等待下去。
          • 當大約過了 1000ms 后,進入下一次事件循環進入定時器,執行到期的定時器回調函數,我們會看到日志 setTimeout run after 1003 ms。
          • 在定時器階段結束之后,會再次進入 poll 階段,繼續等待。
          // client.js 
          const now = Date.now(); 
          setTimeout(() => log(`setTimeout run after ${Date.now() - now} ms`), 1000); 
          someAsyncOperation(); 
          function someAsyncOperation() { 
            http.get('http://localhost:3000/api/news', () => { 
              log(`fetch data success after ${Date.now() - now} ms`); 
            }); 
          } 
           
          // app.js 
          const http = require('http'); 
          http.createServer((req, res) => { 
            setTimeout(() => { res.end('OK!') }, 3000); 
          }).listen(3000); 

          當 poll 階段隊列為空時,并且腳本被 setImmediate() 調度過,此時,事件循環也會結束 poll 階段,進入下一個階段 check。

          check

          check 階段在 poll 階段之后運行,這個階段包含一個 API setImmediate(cb) 如果有被 setImmediate 觸發的回調函數,就取出執行,直到隊列為空或達到系統的最大限制。

          setTimeout VS setImmediate

          拿 setTimeout 和 setImmediate 對比,這是一個常見的例子,基于被調用的時機和定時器可能會受到計算機上其它正在運行的應用程序影響,它們的輸出順序,不總是固定的。

          setTimeout(() => log('setTimeout')); 
          setImmediate(() => log('setImmediate')); 
           
          // 第一次運行 
          setTimeout 
          setImmediate 
           
          // 第二次運行 
          setImmediate 
          setTimeout 

          setTimeout VS setImmediate VS fs.readFile

          但是一旦把這兩個函數放入一個 I/O 循環內調用,setImmediate 將總是會被優先調用。因為 setImmediate 屬于 check 階段,在事件循環中總是在 poll 階段結束后運行,這個順序是確定的。

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

          close callbacks

          在 Libuv 中,如果調用關閉句柄 uv_close(),它將調用關閉回調,也就是事件循環的最后一個階段 close callbacks。

          這個階段的工作更像是做一些清理工作,例如,當調用 socket.destroy(),'close' 事件將在這個階段發出,事件循環在執行完這個階段隊列里的回調函數后,檢查循環是否還 alive,如果為 no 退出,否則繼續下一次新的事件循環。

          包含 Microtask 的事件循環流程圖

          在瀏覽器的事件循環中,把任務劃分為 Task、Microtask,在 Node.js 中是按照階段劃分的,上面我們介紹了 Node.js 事件循環的 6 個階段,給用戶使用的主要是 timer、poll、check、close callback 四個階段,剩下兩個由系統內部調度。這些階段所產生的任務,我們可以看做 Task 任務源,也就是常說的 “Macrotask 宏任務”。

          通常我們在談論一個事件循環時還會包含 Microtask,Node.js 里的微任務有 Promise、還有一個也許很少關注的函數 queueMicrotask,它是在 Node.js v11.0.0 之后被實現的,參見 PR/22951。

          Node.js 中的事件循環在每一個階段執行后,都會檢查微任務隊列中是否有待執行的任務。

          Node.js 11.x 前后差異

          Node.js 在 v11.x 前后,每個階段如果即存在可執行的 Task 又存在 Microtask 時,會有一些差異,先看一段代碼:

          setImmediate(() => { 
            log('setImmediate1'); 
            Promise.resolve('Promise microtask 1') 
              .then(log); 
          }); 
          setImmediate(() => { 
            log('setImmediate2'); 
            Promise.resolve('Promise microtask 2') 
              .then(log); 
          }); 

          在 Node.js v11.x 之前,當前階段如果存在多個可執行的 Task,先執行完畢,再開始執行微任務。基于 v10.22.1 版本運行結果如下:

          setImmediate1 
          setImmediate2 
          Promise microtask 1 
          Promise microtask 2 

          在 Node.js v11.x 之后,當前階段如果存在多個可執行的 Task,先取出一個 Task 執行,并清空對應的微任務隊列,再次取出下一個可執行的任務,繼續執行。基于 v14.15.0 版本運行結果如下:

          setImmediate1 
          Promise microtask 1 
          setImmediate2 
          Promise microtask 2 

          在 Node.js v11.x 之前的這個執行順序問題,被認為是一個應該要修復的 Bug 在 v11.x 之后并修改了它的執行時機,和瀏覽器保持了一致,詳細參見 issues/22257 討論。

          特別的 process.nextTick()

          Node.js 中還有一個異步函數 process.nextTick(),從技術上講它不是事件循環的一部分,它在當前操作完成后處理。如果出現遞歸的 process.nextTick() 調用,這將會很糟糕,它會阻斷事件循環。

          如下例所示,展示了一個 process.nextTick() 遞歸調用示例,目前事件循環位于 I/O 循環內,當同步代碼執行完成后 process.nextTick() 會被立即執行,它會陷入無限循環中,與同步的遞歸不同的是,它不會觸碰 v8 最大調用堆棧限制。但是會破壞事件循環調度,setTimeout 將永遠得不到執行。

          fs.readFile(__filename, () => { 
            process.nextTick(() => { 
              log('nextTick'); 
              run(); 
              function run() { 
                process.nextTick(() => run()); 
              } 
            }); 
            log('sync run'); 
            setTimeout(() => log('setTimeout')); 
          }); 
           
          // 輸出 
          sync run 
          nextTick 

          將 process.nextTick 改為 setImmediate 雖然是遞歸的,但它不會影響事件循環調度,setTimeout 在下一次事件循環中被執行。

          fs.readFile(__filename, () => { 
            process.nextTick(() => { 
              log('nextTick'); 
              run(); 
              function run() { 
                setImmediate(() => run()); 
              } 
            }); 
            log('sync run'); 
            setTimeout(() => log('setTimeout')); 
          }); 
           
          // 輸出 
          sync run 
          nextTick 
          setTimeout 

          process.nextTick 是立即執行,setImmediate 是在下一次事件循環的 check 階段執行。但是,它們的名字著實讓人費解,也許會想這兩個名字交換下比較好,但它屬于遺留問題,也不太可能會改變,因為這會破壞 NPM 上大部分的軟件包。

          在 Node.js 的文檔中也建議開發者盡可能的使用 setImmediate(),也更容易理解。

          總結

          Node.js 事件循環分為 6 個階段,每個階段都有一個 FIFO(先進先出)隊列執行回調函數,這幾個階段之間執行的優先級順序還是明確的。

          事件循環的每一個階段,有時還會伴隨著一些微任務而運行,這里以 Node.js v11.x 版本為分界線會有一些差異,文中也都有詳細的介紹。

          在上一篇介紹了瀏覽器的事件循環機制,本篇又詳細的介紹了 Node.js 中的事件循環機制,留給大家一個思考問題,結合自己的理解,總結下瀏覽器與 Node.js 中事件循環的一些差異,這個也是常見的一個面試題,歡迎在留言區討論。

          在 Cnode 上看到的兩篇事件循環相關文章,推薦給大家,文章很精彩,評論也更加精彩。

          • https://cnodejs.org/topic/5a9108d78d6e16e56bb80882
          • https://cnodejs.org/topic/57d68794cb6f605d360105bf

          Reference

          http://docs.libuv.org/en/v1.x/design.html

          https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick

          avaScript 語言中的 for 循環用于多次執行代碼塊,它是 JavaScript 中最常用的一個循環工具,還可用于數組的遍歷循環等。

          我們為什么要使用 for 循環呢?打個比方,例如我們想要控制臺輸出1到1000之間的所有數字,如果單寫輸出語句,要寫1000句代碼,但是如果使用 for 循環,幾句代碼就能實現。總之,使用 for 循環能夠讓我們寫代碼更方便快捷(當然啦,否則要它干嘛)。

          for 循環語法

          語法如下所示:

          for(變量初始化; 條件表達式; 變量更新) {
              // 條件表達式為true時執行的語句塊
          }
          
          • 變量初始化,表示代碼塊開始前執行。
          • 條件表達式,定義運行循環代碼塊的條件。
          • 變量更新,在循環代碼塊每次被執行之后再執行。

          示例:

          例如我們在一個HTML文件中,編寫如下代碼,實現計算1到100的總和:

          <!DOCTYPE html>
          <html>
          <head>
          <meta charset="utf-8">
          <title>JS_俠課島(9xkd.com)</title>
          </head>
          <body>
          <script>
            var result = 0;
            for(var i = 1; i <= 100; i++) {
              result = result + i;
            }
            alert(result);
          </script>
          </body>   
          </html>
          

          在瀏覽器中打開這個文件,會彈出一個彈出層,彈出層中顯示的是1到100的總和:


          上述代碼中,我們聲明了一個變量 result 并給它賦值為 0,表示初始的總和為 0 。


          然后在 for 循環中三個語句:

          • 變量初始化 i = 1,表示從 1 開始計算。
          • 條件表達式 i <= 100,表示只要 i 小于等于 100 循環就會一直執行,當 i 大于 100 循環會停止。
          • 變量更新 i++,之前我們學運算符的時候學過,這是遞增運算符 ++,表示為其操作數增加 1。

          此時我們可以一點點來看這個 for 循環:

          第一次循環: result = 0 + 1   // 此時result值為0,  i的值為1
          第二次循環: result = 1 + 2   // 此時result值為0+1,i的值為2
          第三次循環: result = 3 + 3   // 此時result值為1+2,i的值為3
          第四次循環: result = 6 + 4   // 此時result值為3+3,i的值為4
          第五次循環: result = 10 + 5  // 此時result值為6+4,i的值為5
          ...
          

          我們只需要搞清楚 for 循環中的執行原理,不需要手動來計算求和,只要寫好代碼,執行代碼后計算機會很快會告訴我們1到 100 的總和。

          再補充一下,上述代碼中result = result + i,我們也可以寫成 result += i,這是我們之前學過的加賦值運算符,還記得嗎?

          示例:

          再來看一個例子,例如我們可以使用 for 循環來實現數組遍歷,首先定義一個數組 lst:

          var lst = ["a", "b", "c", "d", "e"];
          

          在寫 for 循環時,首先就是要搞清楚小括號里面的三個語句,因為我們可以通過數組中元素的下標索引來獲取元素的值,而數組的索引又是從 0 開始,所以變量初始化可以設置為i = 0。第二個條件表達式,因為數組中最后一個索引為 lst.length - 1,所以只要小于等于 lst.length - 1,循環就會一直執行。而i <= lst.length - 1 就相當于 i<lst.length。第三個變量更新,當循環每循環一次,索引值就加一,所以為 i++。

          所以循環可以像下面這樣寫:

          for(i = 0; i<lst.length; i++){
              console.log(lst[i]);  // 輸出數組中的元素值,從索引為0的值開始輸出,每次加1,一直到lst.length-1
          }
          

          輸出:

          a
          b
          c
          d
          e
          

          其實遍歷數組還有一種更好的方法,就是使用 for...in 循環語句來遍歷數組。

          for...in 循環

          for...in 循環主要用于遍歷數組或對象屬性,對數組或對象的屬性進行循環操作。for...in 循環中的代碼每執行一次,就會對數組的元素或者對象的屬性進行一次操作。

          語法如下:

          for (變量 in 對象) {
              // 代碼塊
          }
          

          for 循環括號內的變量是用來指定變量,指定的可以是數組對象或者是對象屬性。

          示例:

          使用 for...in 循環遍歷我們定義好的 lst 數組:

          var lst = ["a", "b", "c", "d", "e"];
          for(var l in lst){
              console.log(lst[l]);
          }
          

          輸出:

          a
          b
          c
          d
          e
          

          除了數組,for...in 循環還可以遍歷對象,例如我們遍歷 俠俠 的個人基本信息:

          var object = {
              姓名:'俠俠',
              年齡:'22',
              性別:'男',
              出生日期:'1997-08-05',
              職業:'程序員',
              特長:'跳舞'
          }
          
          for(var i in object) {
              console.log(i + ":" + object[i]);
          }
          

          輸出:

          姓名: 俠俠
          年齡: 22
          性別: 男
          出生日期: 1997-08-05
          職業:程序員
          特長:跳舞
          

          動手小練習

          1. 請自定義一個長度為7的數組,然后通過 for 循環將數組中的元素遍歷出來。
          2. 求和:1~100的奇數和。
          3. 求和:1~100的偶數和。
          4. 使用對象定義一個人的個人信息(包括姓名、性別、年齡、出生日期、興趣愛好、職業、特長等),然后使用 for...in 循環將這些信息遍歷輸出。

          avaScript 的 Event Loop(事件循環)是 JavaScript 運行時環境(如瀏覽器和 Node.js)的核心機制之一,它使得 JavaScript 能夠處理異步操作而不會阻塞程序的執行。了解 Event Loop 對于理解 JavaScript 的非阻塞行為和編寫高效的異步代碼至關重要。

          1. JavaScript 是單線程的

          首先,重要的是要理解 JavaScript 是一種單線程的語言。這意味著 JavaScript 在同一時間內只能執行一個任務。然而,JavaScript 需要能夠處理各種異步操作(如 AJAX 請求、文件讀取、用戶交互等),這些操作可能會花費很長時間完成。為了解決這個問題,JavaScript 采用了 Event Loop 和 Callback Queues(回調隊列)。

          2. 調用棧(Call Stack)

          調用棧是 JavaScript 代碼執行時的數據結構,用于存儲函數調用和返回地址。每當一個函數被調用時,它就會被推入調用棧,并在函數執行完畢后從棧中彈出。如果調用棧滿了(即達到了最大調用深度),則會發生棧溢出錯誤。

          3. 堆(Heap)

          堆是用于存儲對象、數組等引用類型的內存區域。與調用棧不同,堆是動態分配的,并且其大小不是固定的。

          4. Web APIs

          Web APIs 是瀏覽器提供的一組與瀏覽器功能交互的接口,如 DOM 操作、網絡請求等。這些 API 通常是異步的,并且它們有自己的線程或進程來處理請求。

          5. 任務隊列(Task Queue)和微任務隊列(Microtask Queue)

          當異步操作完成時(如 AJAX 請求、setTimeout、Promise 解決等),相應的回調函數會被放入任務隊列(或稱為宏任務隊列)或微任務隊列中。任務隊列中的任務在當前的執行棧清空后才會被執行,而微任務隊列中的任務會在當前執行棧清空后、但下一個宏任務執行前立即執行。

          6. Event Loop 的工作原理

          Event Loop 的工作流程可以概括為以下幾個步驟:

          1. 檢查調用棧:如果調用棧為空,則繼續;如果調用棧不為空,則等待直到調用棧為空。
          2. 執行微任務隊列:一旦調用棧為空,Event Loop 就會查看微任務隊列是否有任務。如果有,它會依次執行微任務隊列中的所有任務,然后再回到第一步。
          3. 執行宏任務隊列:在所有微任務都執行完畢后,Event Loop 會從宏任務隊列中取出一個任務放入調用棧執行。這個過程會不斷重復。

          7. 常見的宏任務和微任務

          • 宏任務(Macrotasks):包括 script(整體代碼)、setTimeout、setInterval、setImmediate(Node.js 環境)、I/O、UI rendering 等。
          • 微任務(Microtasks):包括 Promise.then、Promise.catch、Promise.finally、MutationObserver、process.nextTick(Node.js 環境)等。

          實例 1:setTimeout 和 Promise

          console.log('1');  
            
          setTimeout(() => {  
            console.log('setTimeout 宏任務隊列');  
          }, 0);  
            
          new Promise((resolve) => {  
            console.log('Promise 立即執行');  
            resolve();  
          }).then(() => {  
            console.log('then 微任務隊列');  
          });  
            
          console.log('2');
          
          //輸出順序
          1  
          Promise 立即執行 
          2  
          then  微任務隊列
          setTimeout 宏任務隊列

          解釋

          1. 首先,執行同步代碼,輸出 1。
          2. 然后,setTimeout 被調用,但因為它是一個宏任務,所以它的回調函數被放入宏任務隊列中等待。
          3. 接下來,new Promise 的構造函數被調用,立即執行并輸出 Promise。resolve() 被調用,但 .then() 中的回調函數是異步的,并且是一個微任務,所以它會被放入微任務隊列中。
          4. 同步代碼繼續執行,輸出 2。
          5. 當所有同步代碼執行完畢后,Event Loop 開始處理微任務隊列。它找到 .then() 的回調函數并執行,輸出 then。
          6. 最后,當微任務隊列為空時,Event Loop 轉到宏任務隊列,執行 setTimeout 的回調函數,輸出 setTimeout。

          實例 2:多個 Promise 和 setTimeout

          console.log('1');  
            
          setTimeout(() => {  
            console.log('setTimeout  宏任務隊列1');  
            new Promise((resolve) => {  
              console.log('Promise in setTimeout');  
              resolve();  
            }).then(() => {  
              console.log('then in setTimeout');  
            });  
            setTimeout(() => {  
              console.log('setTimeout 宏任務隊列2');  
            }, 0);  
          }, 0);  
            
          new Promise((resolve) => {  
            console.log('Promise 立即執行1');  
            resolve();  
          }).then(() => {  
            console.log('then 微任務隊列1');  
            new Promise((resolve) => {  
              console.log('Promise 立即執行2');  
              resolve();  
            }).then(() => {  
              console.log('then 微任務隊列2');  
            });  
          });  
            
          console.log('2');
          
          //輸出順序
          1 
          Promise 立即執行1  
          2  
          then 微任務隊列1 
          Promise 立即執行2  
          then 微任務隊列2
          setTimeout  宏任務隊列1  
          Promise in setTimeout  
          then in setTimeout  
          setTimeout  宏任務隊列2

          解釋

          1. 同步代碼首先執行,輸出 1、Promise 1 和 2。
          2. .then() 中的回調函數作為微任務被加入微任務隊列。
          3. 第一個 setTimeout 被調用,它的回調函數被加入宏任務隊列。
          4. 當所有同步代碼執行完畢后,開始執行微任務隊列中的任務。首先輸出then 微任務隊列1,然后執行 Promise 立即執行2then 微任務隊列2
          5. 微任務隊列為空后,執行宏任務隊列中的第一個任務(即第一個 setTimeout 的回調函數),輸出相關日志。
          6. 第二個 setTimeout 的回調函數也被加入宏任務隊列,并在當前宏任務執行完畢后執行。

          實例 3:async/await 與 Promise

          const async1= async () => {
             console.log('async1 1');  
             await async2();  
             console.log('async1 2');  
          }
             
          const async2= async () => {
             console.log('async2');  
          }
            
          console.log('1');  
          setTimeout(() => {  
            console.log('setTimeout 宏任務隊列');  
          }, 0);  
            
          async1();  
            
          new Promise((resolve) => {  
            console.log('promise 立即執行');  
            resolve();  
          }).then(() => {  
            console.log('then 微任務隊列');  
          });  
            
          console.log('2');
          
          //輸出順序
          1
          async1 1
          async2  
          promise 立即執行
          2
          async1 2
          then 微任務隊列
          setTimeout 宏任務隊列

          解釋

          1. 同步代碼首先執行,輸出1。
          2. async1() 被調用,輸出async1 1。
          3. await async2() 暫停 async1() 的執行,async2() 被調用并輸出 async2。因為 async2() 沒有返回 Promise 或沒有等待的異步操作,所以 await 后面的代碼在 async2() 執行完畢后繼續執行。
          4. 同步代碼首先執行,輸出promise 立即執行和2。
          5. 之后async2執行完畢后,同步代碼輸出async1 2,
          6. 當所有同步代碼執行完畢后,開始執行微任務隊列中的任務then 微任務隊列
          7. 最后執行宏任務隊列,輸出setTimeout 宏任務隊列

          結論

          Event Loop 是 JavaScript 異步編程的基石,它使得 JavaScript 能夠在不阻塞主線程的情況下處理各種異步操作。通過理解 Event Loop 的工作原理,我們可以更加高效地編寫異步代碼,避免潛在的錯誤和性能問題。


          主站蜘蛛池模板: 久久久国产一区二区三区| 亚洲国产一区视频| 国产精品男男视频一区二区三区| 日韩精品一区二区三区四区| 精品乱码一区二区三区四区| 中文字幕在线视频一区| 精品无码成人片一区二区| 国产在线一区二区在线视频| 久久中文字幕无码一区二区 | 日本一区二区不卡在线| 波多野结衣一区二区三区88| 国产Av一区二区精品久久| 99精品国产高清一区二区麻豆| 精品乱人伦一区二区三区| 亚洲中文字幕丝袜制服一区 | 国产中文字幕一区| 午夜DV内射一区区| 国产精品成人一区无码| 在线观看视频一区二区| 国产欧美色一区二区三区 | 日韩综合无码一区二区| 国产一区二区三区免费观在线 | 亚洲日韩一区二区三区| 精品一区高潮喷吹在线播放| 亚洲天堂一区二区| 亚洲欧洲一区二区| 亚洲天堂一区在线| 精品午夜福利无人区乱码一区| 中文字幕一区二区三区精彩视频| 一区二区三区观看免费中文视频在线播放 | 狠狠色婷婷久久一区二区三区| 亚洲av成人一区二区三区在线观看| 精品女同一区二区三区免费播放| 日本丰满少妇一区二区三区| 一区二区三区日本视频| 国产探花在线精品一区二区| 无码人妻精品一区二区三区东京热 | 成人精品一区久久久久| 一区二区三区中文字幕| 亚洲无线码一区二区三区| 亚洲福利电影一区二区?|