整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          JavaScript 異步編程指南 - 聊聊 Node.js 中的事件循環(huán)

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

          事件循環(huán)是一種控制應(yīng)用程序的運(yùn)行機(jī)制,在不同的運(yùn)行時(shí)環(huán)境有不同的實(shí)現(xiàn),上一節(jié)講了瀏覽器中的事件循環(huán),它們有很多相似的地方,也有著各自的特點(diǎn),本節(jié)討論下 Node.js 中的事件循環(huán)。

          了解 Node.js 中的事件循環(huán)

          Node.js 做為 JavaScript 的服務(wù)端運(yùn)行時(shí),主要與網(wǎng)絡(luò)、文件打交道,沒有了瀏覽器中事件循環(huán)的渲染階段。

          在瀏覽器中有 HTML 規(guī)范來定義事件循環(huán)的處理模型,之后由各瀏覽器廠商實(shí)現(xiàn)。Node.js 中事件循環(huán)的定義與實(shí)現(xiàn)均來自于 Libuv。

          Libuv 圍繞事件驅(qū)動(dòng)的異步 I/O 模型而設(shè)計(jì),最初是為 Node.js 編寫的,提供了一個(gè)跨平臺(tái)的支持庫。下圖展示了它的組成部分,Network I/O 是網(wǎng)絡(luò)處理相關(guān)的部分,右側(cè)還有文件操作、DNS,底部 epoll、kqueue、event ports、IOCP 這些是底層不同操作系統(tǒng)的實(shí)現(xiàn)。

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

          事件循環(huán)的六個(gè)階段

          當(dāng) Node.js 啟動(dòng)時(shí),它會(huì)初始化事件循環(huán),處理提供的腳本,同步代碼入棧直接執(zhí)行,異步任務(wù)(網(wǎng)絡(luò)請求、文件操作、定時(shí)器等)在調(diào)用 API 傳遞回調(diào)函數(shù)后會(huì)把操作轉(zhuǎn)移到后臺(tái)由系統(tǒng)內(nèi)核處理。目前大多數(shù)內(nèi)核都是多線程的,當(dāng)其中一個(gè)操作完成時(shí),內(nèi)核通知 Node.js 將回調(diào)函數(shù)添加到輪詢隊(duì)列中等待時(shí)機(jī)執(zhí)行。

          下圖左側(cè)是 Node.js 官網(wǎng)對(duì)事件循環(huán)過程的描述,右側(cè)是 Libuv 官網(wǎng)對(duì) Node.js 的描述,都是對(duì)事件循環(huán)的介紹,不是所有人上來都能去看源碼的,這兩個(gè)文檔通常也是對(duì)事件循環(huán)更直接的學(xué)習(xí)參考文檔,在 Node.js 官網(wǎng)介紹的也還是挺詳細(xì)的,可以做為一個(gè)參考資料學(xué)習(xí)。

          左側(cè) Node.js 官網(wǎng)展示的事件循環(huán)分為 6 個(gè)階段,每個(gè)階段都有一個(gè) FIFO(先進(jìn)先出)隊(duì)列執(zhí)行回調(diào)函數(shù),這幾個(gè)階段之間執(zhí)行的優(yōu)先級(jí)順序還是明確的。

          右側(cè)更詳細(xì)的描述了,在事件循環(huán)迭代前,先去判斷循環(huán)是否處于活動(dòng)狀態(tài)(有等待的異步 I/O、定時(shí)器等),如果是活動(dòng)狀態(tài)開始迭代,否則循環(huán)將立即退出。

          下面對(duì)每個(gè)階段分別討論。

          timers(定時(shí)器階段)

          首先事件循環(huán)進(jìn)入定時(shí)器階段,該階段包含兩個(gè) API setTimeout(cb, ms)、setInterval(cb, ms) 前一個(gè)是僅執(zhí)行一次,后一個(gè)是重復(fù)執(zhí)行。

          這個(gè)階段檢查是否有到期的定時(shí)器函數(shù),如果有則執(zhí)行到期的定時(shí)器回調(diào)函數(shù),和瀏覽器中的一樣,定時(shí)器函數(shù)傳入的延遲時(shí)間總比我們預(yù)期的要晚,它會(huì)受到操作系統(tǒng)或其它正在運(yùn)行的回調(diào)函數(shù)的影響。

          例如,下例我們設(shè)置了一個(gè)定時(shí)器函數(shù),并預(yù)期在 1000 毫秒后執(zhí)行。

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

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

          在我們這個(gè)示例中,假設(shè)執(zhí)行完 someOperation() 函數(shù)的當(dāng)前時(shí)間為 T + 3000:

          • 檢查 timer1 函數(shù),當(dāng)前時(shí)間為 T + 3000 - T > 1000,已超過預(yù)期的延遲時(shí)間,取出回調(diào)函數(shù)執(zhí)行,繼續(xù)檢查。
          • 檢查 timer2 函數(shù),當(dāng)前時(shí)間為 T + 3000 - T < 5000,還沒達(dá)到預(yù)期的延遲時(shí)間,此時(shí)退出定時(shí)器階段。

          pending callbacks

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

          idle, prepare

          idle, prepare 階段是給系統(tǒng)內(nèi)部使用,idle 這個(gè)名字很迷惑,盡管叫空閑,但是在每次的事件循環(huán)中都會(huì)被調(diào)用,當(dāng)它們處于活動(dòng)狀態(tài)時(shí)。這一塊的資料介紹也不是很多。略...

          poll

          poll 是一個(gè)重要的階段,這里有一個(gè)概念觀察者,有文件 I/O 觀察者,網(wǎng)絡(luò) I/O 觀察者等,它會(huì)觀察是否有新的請求進(jìn)入,包含讀取文件等待響應(yīng),等待新的 socket 請求,這個(gè)階段在某些情況下是會(huì)阻塞的。

          阻塞 I/O 超時(shí)時(shí)間

          在阻塞 I/O 之前,要計(jì)算它應(yīng)該阻塞多長時(shí)間,參考 Libuv 文檔上的一些描述,以下這些是它計(jì)算超時(shí)時(shí)間的規(guī)則:

          • 如果循環(huán)使用 UV_RUN_NOWAIT 標(biāo)志運(yùn)行、超時(shí)為 0。
          • 如果循環(huán)將要停止(uv_stop() 被調(diào)用),超時(shí)為 0。
          • 如果沒有活動(dòng)的 handlers 或 request,超時(shí)為 0。
          • 如果有任何 idle handlers 處于活動(dòng)狀態(tài),超時(shí)為 0。
          • 如果有任何待關(guān)閉的 handlers,超時(shí)為 0。

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

          示例一

          很簡單的一段代碼,我們啟動(dòng)一個(gè) Server,現(xiàn)在事件循環(huán)的其它階段沒有要處理的任務(wù),它會(huì)在這里等待下去,直到有新的請求進(jìn)來。

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

          示例二

          結(jié)合階段一的定時(shí)器,在看個(gè)示例,首先啟動(dòng) app.js 做為服務(wù)端,模擬延遲 3000ms 響應(yīng),這個(gè)只是為了配合測試。再運(yùn)行 client.js 看下事件循環(huán)的執(zhí)行過程:

          • 首先程序調(diào)用了一個(gè)在 1000ms 后超時(shí)的定時(shí)器。
          • 之后調(diào)用異步函數(shù) someAsyncOperation() 從網(wǎng)絡(luò)讀取數(shù)據(jù),我們假設(shè)這個(gè)異步網(wǎng)路讀取需要 3000ms。
          • 當(dāng)事件循環(huán)開始時(shí)先進(jìn)入 timer 階段,發(fā)現(xiàn)沒有超時(shí)的定時(shí)器函數(shù),繼續(xù)向下執(zhí)行。
          • 期間經(jīng)過 pending callbacks -> idle,prepare 當(dāng)進(jìn)入 poll 階段,此時(shí)的 http.get() 尚未完成,它的隊(duì)列為空,參考上面 poll 阻塞超時(shí)時(shí)間規(guī)則,事件循環(huán)機(jī)制會(huì)檢查最快到達(dá)閥值的計(jì)時(shí)器,而不是一直在這里等待下去。
          • 當(dāng)大約過了 1000ms 后,進(jìn)入下一次事件循環(huán)進(jìn)入定時(shí)器,執(zhí)行到期的定時(shí)器回調(diào)函數(shù),我們會(huì)看到日志 setTimeout run after 1003 ms。
          • 在定時(shí)器階段結(jié)束之后,會(huì)再次進(jìn)入 poll 階段,繼續(xù)等待。
          // 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); 

          當(dāng) poll 階段隊(duì)列為空時(shí),并且腳本被 setImmediate() 調(diào)度過,此時(shí),事件循環(huán)也會(huì)結(jié)束 poll 階段,進(jìn)入下一個(gè)階段 check。

          check

          check 階段在 poll 階段之后運(yùn)行,這個(gè)階段包含一個(gè) API setImmediate(cb) 如果有被 setImmediate 觸發(fā)的回調(diào)函數(shù),就取出執(zhí)行,直到隊(duì)列為空或達(dá)到系統(tǒng)的最大限制。

          setTimeout VS setImmediate

          拿 setTimeout 和 setImmediate 對(duì)比,這是一個(gè)常見的例子,基于被調(diào)用的時(shí)機(jī)和定時(shí)器可能會(huì)受到計(jì)算機(jī)上其它正在運(yùn)行的應(yīng)用程序影響,它們的輸出順序,不總是固定的。

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

          setTimeout VS setImmediate VS fs.readFile

          但是一旦把這兩個(gè)函數(shù)放入一個(gè) I/O 循環(huán)內(nèi)調(diào)用,setImmediate 將總是會(huì)被優(yōu)先調(diào)用。因?yàn)?setImmediate 屬于 check 階段,在事件循環(huán)中總是在 poll 階段結(jié)束后運(yùn)行,這個(gè)順序是確定的。

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

          close callbacks

          在 Libuv 中,如果調(diào)用關(guān)閉句柄 uv_close(),它將調(diào)用關(guān)閉回調(diào),也就是事件循環(huán)的最后一個(gè)階段 close callbacks。

          這個(gè)階段的工作更像是做一些清理工作,例如,當(dāng)調(diào)用 socket.destroy(),'close' 事件將在這個(gè)階段發(fā)出,事件循環(huán)在執(zhí)行完這個(gè)階段隊(duì)列里的回調(diào)函數(shù)后,檢查循環(huán)是否還 alive,如果為 no 退出,否則繼續(xù)下一次新的事件循環(huán)。

          包含 Microtask 的事件循環(huán)流程圖

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

          通常我們在談?wù)撘粋€(gè)事件循環(huán)時(shí)還會(huì)包含 Microtask,Node.js 里的微任務(wù)有 Promise、還有一個(gè)也許很少關(guān)注的函數(shù) queueMicrotask,它是在 Node.js v11.0.0 之后被實(shí)現(xiàn)的,參見 PR/22951。

          Node.js 中的事件循環(huán)在每一個(gè)階段執(zhí)行后,都會(huì)檢查微任務(wù)隊(duì)列中是否有待執(zhí)行的任務(wù)。

          Node.js 11.x 前后差異

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

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

          在 Node.js v11.x 之前,當(dāng)前階段如果存在多個(gè)可執(zhí)行的 Task,先執(zhí)行完畢,再開始執(zhí)行微任務(wù)。基于 v10.22.1 版本運(yùn)行結(jié)果如下:

          setImmediate1 
          setImmediate2 
          Promise microtask 1 
          Promise microtask 2 

          在 Node.js v11.x 之后,當(dāng)前階段如果存在多個(gè)可執(zhí)行的 Task,先取出一個(gè) Task 執(zhí)行,并清空對(duì)應(yīng)的微任務(wù)隊(duì)列,再次取出下一個(gè)可執(zhí)行的任務(wù),繼續(xù)執(zhí)行。基于 v14.15.0 版本運(yùn)行結(jié)果如下:

          setImmediate1 
          Promise microtask 1 
          setImmediate2 
          Promise microtask 2 

          在 Node.js v11.x 之前的這個(gè)執(zhí)行順序問題,被認(rèn)為是一個(gè)應(yīng)該要修復(fù)的 Bug 在 v11.x 之后并修改了它的執(zhí)行時(shí)機(jī),和瀏覽器保持了一致,詳細(xì)參見 issues/22257 討論。

          特別的 process.nextTick()

          Node.js 中還有一個(gè)異步函數(shù) process.nextTick(),從技術(shù)上講它不是事件循環(huán)的一部分,它在當(dāng)前操作完成后處理。如果出現(xiàn)遞歸的 process.nextTick() 調(diào)用,這將會(huì)很糟糕,它會(huì)阻斷事件循環(huán)。

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

          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 雖然是遞歸的,但它不會(huì)影響事件循環(huán)調(diào)度,setTimeout 在下一次事件循環(huán)中被執(zhí)行。

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

          process.nextTick 是立即執(zhí)行,setImmediate 是在下一次事件循環(huán)的 check 階段執(zhí)行。但是,它們的名字著實(shí)讓人費(fèi)解,也許會(huì)想這兩個(gè)名字交換下比較好,但它屬于遺留問題,也不太可能會(huì)改變,因?yàn)檫@會(huì)破壞 NPM 上大部分的軟件包。

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

          總結(jié)

          Node.js 事件循環(huán)分為 6 個(gè)階段,每個(gè)階段都有一個(gè) FIFO(先進(jìn)先出)隊(duì)列執(zhí)行回調(diào)函數(shù),這幾個(gè)階段之間執(zhí)行的優(yōu)先級(jí)順序還是明確的。

          事件循環(huán)的每一個(gè)階段,有時(shí)還會(huì)伴隨著一些微任務(wù)而運(yùn)行,這里以 Node.js v11.x 版本為分界線會(huì)有一些差異,文中也都有詳細(xì)的介紹。

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

          在 Cnode 上看到的兩篇事件循環(huán)相關(guān)文章,推薦給大家,文章很精彩,評(píng)論也更加精彩。

          • 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 循環(huán)用于多次執(zhí)行代碼塊,它是 JavaScript 中最常用的一個(gè)循環(huán)工具,還可用于數(shù)組的遍歷循環(huán)等。

          我們?yōu)槭裁匆褂?for 循環(huán)呢?打個(gè)比方,例如我們想要控制臺(tái)輸出1到1000之間的所有數(shù)字,如果單寫輸出語句,要寫1000句代碼,但是如果使用 for 循環(huán),幾句代碼就能實(shí)現(xiàn)。總之,使用 for 循環(huán)能夠讓我們寫代碼更方便快捷(當(dāng)然啦,否則要它干嘛)。

          for 循環(huán)語法

          語法如下所示:

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

          示例:

          例如我們在一個(gè)HTML文件中,編寫如下代碼,實(shí)現(xiàn)計(jì)算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>
          

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


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


          然后在 for 循環(huán)中三個(gè)語句:

          • 變量初始化 i = 1,表示從 1 開始計(jì)算。
          • 條件表達(dá)式 i <= 100,表示只要 i 小于等于 100 循環(huán)就會(huì)一直執(zhí)行,當(dāng) i 大于 100 循環(huán)會(huì)停止。
          • 變量更新 i++,之前我們學(xué)運(yùn)算符的時(shí)候?qū)W過,這是遞增運(yùn)算符 ++,表示為其操作數(shù)增加 1。

          此時(shí)我們可以一點(diǎn)點(diǎn)來看這個(gè) for 循環(huán):

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

          我們只需要搞清楚 for 循環(huán)中的執(zhí)行原理,不需要手動(dòng)來計(jì)算求和,只要寫好代碼,執(zhí)行代碼后計(jì)算機(jī)會(huì)很快會(huì)告訴我們1到 100 的總和。

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

          示例:

          再來看一個(gè)例子,例如我們可以使用 for 循環(huán)來實(shí)現(xiàn)數(shù)組遍歷,首先定義一個(gè)數(shù)組 lst:

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

          在寫 for 循環(huán)時(shí),首先就是要搞清楚小括號(hào)里面的三個(gè)語句,因?yàn)槲覀兛梢酝ㄟ^數(shù)組中元素的下標(biāo)索引來獲取元素的值,而數(shù)組的索引又是從 0 開始,所以變量初始化可以設(shè)置為i = 0。第二個(gè)條件表達(dá)式,因?yàn)閿?shù)組中最后一個(gè)索引為 lst.length - 1,所以只要小于等于 lst.length - 1,循環(huán)就會(huì)一直執(zhí)行。而i <= lst.length - 1 就相當(dāng)于 i<lst.length。第三個(gè)變量更新,當(dāng)循環(huán)每循環(huán)一次,索引值就加一,所以為 i++。

          所以循環(huán)可以像下面這樣寫:

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

          輸出:

          a
          b
          c
          d
          e
          

          其實(shí)遍歷數(shù)組還有一種更好的方法,就是使用 for...in 循環(huán)語句來遍歷數(shù)組。

          for...in 循環(huán)

          for...in 循環(huán)主要用于遍歷數(shù)組或?qū)ο髮傩裕瑢?duì)數(shù)組或?qū)ο蟮膶傩赃M(jìn)行循環(huán)操作。for...in 循環(huán)中的代碼每執(zhí)行一次,就會(huì)對(duì)數(shù)組的元素或者對(duì)象的屬性進(jìn)行一次操作。

          語法如下:

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

          for 循環(huán)括號(hào)內(nèi)的變量是用來指定變量,指定的可以是數(shù)組對(duì)象或者是對(duì)象屬性。

          示例:

          使用 for...in 循環(huán)遍歷我們定義好的 lst 數(shù)組:

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

          輸出:

          a
          b
          c
          d
          e
          

          除了數(shù)組,for...in 循環(huán)還可以遍歷對(duì)象,例如我們遍歷 俠俠 的個(gè)人基本信息:

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

          輸出:

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

          動(dòng)手小練習(xí)

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

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

          1. JavaScript 是單線程的

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

          2. 調(diào)用棧(Call Stack)

          調(diào)用棧是 JavaScript 代碼執(zhí)行時(shí)的數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)函數(shù)調(diào)用和返回地址。每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),它就會(huì)被推入調(diào)用棧,并在函數(shù)執(zhí)行完畢后從棧中彈出。如果調(diào)用棧滿了(即達(dá)到了最大調(diào)用深度),則會(huì)發(fā)生棧溢出錯(cuò)誤。

          3. 堆(Heap)

          堆是用于存儲(chǔ)對(duì)象、數(shù)組等引用類型的內(nèi)存區(qū)域。與調(diào)用棧不同,堆是動(dòng)態(tài)分配的,并且其大小不是固定的。

          4. Web APIs

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

          5. 任務(wù)隊(duì)列(Task Queue)和微任務(wù)隊(duì)列(Microtask Queue)

          當(dāng)異步操作完成時(shí)(如 AJAX 請求、setTimeout、Promise 解決等),相應(yīng)的回調(diào)函數(shù)會(huì)被放入任務(wù)隊(duì)列(或稱為宏任務(wù)隊(duì)列)或微任務(wù)隊(duì)列中。任務(wù)隊(duì)列中的任務(wù)在當(dāng)前的執(zhí)行棧清空后才會(huì)被執(zhí)行,而微任務(wù)隊(duì)列中的任務(wù)會(huì)在當(dāng)前執(zhí)行棧清空后、但下一個(gè)宏任務(wù)執(zhí)行前立即執(zhí)行。

          6. Event Loop 的工作原理

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

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

          7. 常見的宏任務(wù)和微任務(wù)

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

          實(shí)例 1:setTimeout 和 Promise

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

          解釋

          1. 首先,執(zhí)行同步代碼,輸出 1。
          2. 然后,setTimeout 被調(diào)用,但因?yàn)樗且粋€(gè)宏任務(wù),所以它的回調(diào)函數(shù)被放入宏任務(wù)隊(duì)列中等待。
          3. 接下來,new Promise 的構(gòu)造函數(shù)被調(diào)用,立即執(zhí)行并輸出 Promise。resolve() 被調(diào)用,但 .then() 中的回調(diào)函數(shù)是異步的,并且是一個(gè)微任務(wù),所以它會(huì)被放入微任務(wù)隊(duì)列中。
          4. 同步代碼繼續(xù)執(zhí)行,輸出 2。
          5. 當(dāng)所有同步代碼執(zhí)行完畢后,Event Loop 開始處理微任務(wù)隊(duì)列。它找到 .then() 的回調(diào)函數(shù)并執(zhí)行,輸出 then。
          6. 最后,當(dāng)微任務(wù)隊(duì)列為空時(shí),Event Loop 轉(zhuǎn)到宏任務(wù)隊(duì)列,執(zhí)行 setTimeout 的回調(diào)函數(shù),輸出 setTimeout。

          實(shí)例 2:多個(gè) Promise 和 setTimeout

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

          解釋

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

          實(shí)例 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 宏任務(wù)隊(duì)列');  
          }, 0);  
            
          async1();  
            
          new Promise((resolve) => {  
            console.log('promise 立即執(zhí)行');  
            resolve();  
          }).then(() => {  
            console.log('then 微任務(wù)隊(duì)列');  
          });  
            
          console.log('2');
          
          //輸出順序
          1
          async1 1
          async2  
          promise 立即執(zhí)行
          2
          async1 2
          then 微任務(wù)隊(duì)列
          setTimeout 宏任務(wù)隊(duì)列

          解釋

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

          結(jié)論

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


          主站蜘蛛池模板: 日本高清无卡码一区二区久久| 91精品一区二区| 国产成人精品一区二区三区免费| 久久国产午夜一区二区福利| 色噜噜狠狠一区二区三区果冻 | 秋霞鲁丝片一区二区三区| 国产一区二区三区免费观在线| 久久精品道一区二区三区| 无码人妻av一区二区三区蜜臀| 日本在线视频一区| 精品性影院一区二区三区内射 | 全国精品一区二区在线观看| 国产无线乱码一区二三区| 亚洲av无码一区二区三区人妖 | 国产一区二区三区夜色| 国产精品视频一区二区三区经| 国产亚洲福利一区二区免费看| 亚洲一区二区三区在线观看精品中文 | 八戒久久精品一区二区三区| 精品一区二区三区免费| 国产高清视频一区二区| 射精专区一区二区朝鲜| 日本一区二区三区中文字幕| 天堂资源中文最新版在线一区| 国产乱子伦一区二区三区| 在线中文字幕一区| 成人区人妻精品一区二区不卡网站| 亚洲一区二区三区精品视频| 国产成人精品久久一区二区三区av| 久久精品无码一区二区WWW| 一区二区手机视频| 久久一区二区三区精华液使用方法| 色噜噜AV亚洲色一区二区| 日本一区二区三区在线观看视频| 亚洲国产精品第一区二区| 一区二区三区四区精品| 在线观看中文字幕一区| 中文字幕在线一区二区在线| 久久一区二区三区99| 波多野结衣中文一区二区免费| 精品人妻AV一区二区三区|