<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                &emsp;&emsp;雖然 Node.js 是單線程的,但是在融合了[libuv](https://github.com/libuv/libuv)后,使其有能力非常簡單地就構建出高性能和可擴展的網絡應用程序。 &emsp;&emsp;下圖是 Node.js 的簡單架構圖,基于 V8 和 libuv,其中 Node Bindings 為 JavaScript 和 C++ 搭建了一座溝通的橋梁,使得 JavaScript 可以訪問 V8 和 libuv 向上層提供的 API。 :-: ![](https://img.kancloud.cn/52/6d/526d4497ed2e7c4d8fa7cb56b27adfd7_714x298.png =600x) &emsp;&emsp;本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。 ## 一、術語解析 &emsp;&emsp;接下來會對幾個與 Node.js 相關的術語做單獨的解析,其中事件循環會單獨細講。 **1)libuv** &emsp;&emsp;libuv 是一個事件驅動、非阻塞異步的 I/O 庫,并且具備跨平臺的能力,提供了一套事件循環(Event Loop)機制和一些核心工具,例如定時器、文件訪問、線程池等。 **2)非阻塞異步的I/O** &emsp;&emsp;非阻塞是指線程不會被操作系統掛起,可以處理其他事情。 &emsp;&emsp;異步是指調用者發起一個調用后,可以立即返回去做別的事。 &emsp;&emsp;I/O(Input/Output)即輸入/輸出,通常指數據在存儲器或其他周邊設備之間的輸入和輸出。 &emsp;&emsp;它是信息處理系統(例如計算機)與外部世界(可能是人類或另一信息處理系統)之間的通信。 &emsp;&emsp;將這些關鍵字組合在一起就能理解 Node.js 的高性能有一部分是通過避免等待 I/O(讀寫數據庫、文件訪問、網絡調用等)響應來實現的。 **3)事件驅動** &emsp;&emsp;事件驅動是一種異步化的程序設計模型,通過用戶動作、操作系統或應用程序產生的事件,來驅動程序完成某個操作。 &emsp;&emsp;在 Node.js 中,事件主要來源于網絡請求、文件讀寫等,它們會被事件循環所處理。 &emsp;&emsp;在瀏覽器的 DOM 系統中使用的也非常廣泛,例如為按鈕綁定 click 事件,在用點擊按鈕時,彈出提示或提交表單等。 **4)單線程** &emsp;&emsp;Node.js 的單線程是指運行 JavaScript 代碼的主線程,網絡請求或異步任務等都交給了底層的線程池中的線程來處理,其處理結果再通過事件循環向主線程告知。 &emsp;&emsp;單線程意味著所有任務需要排隊有序執行,如果出現一個計算時間很長的任務,那么就會占據主線程,其他任務只能等待,所以說 Node.js 不適合 CPU 密集型的場景。 &emsp;&emsp; 經過以上術語的分析可知,Node.js 的高性能和高并發離不開異步,所以有必要深入了解一下 Node.js 的異步原理。 # 二、事件循環 &emsp;&emsp;當 Node.js 啟動時會初始化事件循環,這是一個無限循環。 &emsp;&emsp;下圖是事件循環的一張運行機制圖,新任務或完成 I/O 任務的回調,都會添加到事件循環中。 :-: ![](https://img.kancloud.cn/52/a4/52a4eca858c57f37c1eeea0f825c2fbd_1131x634.png =600x) &emsp;&emsp;下面是按照運行優先級簡化后的[六個循環階段](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)。 ~~~ ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘ ~~~ &emsp;&emsp;每個階段都有一個 FIFO 回調隊列,當隊列耗盡或達到回調上限時,事件循環將進入下一階段,如此往復。 1. timers:執行由 setTimeout() 和 setInterval() 安排的回調。在此階段內部,會維護一個定時器的小頂堆,按到期時間排序,先到期的先運行。 2. pending callbacks:處理上一輪循環未執行的 I/O 回調,例如網絡、I/O 等異常時的回調。 3. idle,prepare:僅 Node 內部使用。 4. poll:執行與 I/O 相關的回調,除了關閉回調、定時器調度的回調和 setImmediate() , 適當的條件下 Node 將阻塞在這里。 5. check:調用 setImmediate() 回調。 6. close callbacks:關閉回調,例如 socket.on("close", callback)。 &emsp;&emsp;在[deps/uv/src/unix/core.c](https://github.com/nodejs/node/blob/master/deps/uv/src/unix/core.c)文件中聲明了事件循環的核心代碼,旁邊還有個 win 目錄,應該就是指 Windows 系統中 libuv 相關的處理。 &emsp;&emsp;其實事件循環就是一個大的 while 循環?,具體如下所示。 &emsp;&emsp;代碼中的 UV\_RUN\_ONCE 就是上文 poll 階段中的適當的條件,在每次循環結束前,執行完 close callbacks 階段后,會再執行一次已到期的定時器。 ~~~ static int uv__loop_alive(const uv_loop_t* loop) { return uv__has_active_handles(loop) || uv__has_active_reqs(loop) || loop->closing_handles != NULL; } int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; // 檢查事件循環中是否還有待處理的handle、request、closing_handles是否為NULL r = uv__loop_alive(loop); // 更新事件循環時間戳 if (!r) uv__update_time(loop); // 啟動事件循環 while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); uv__run_timers(loop); // timers階段,執行已到期的定時器 ran_pending = uv__run_pending(loop); // pending階段 uv__run_idle(loop); // idle階段 uv__run_prepare(loop);// prepare階段 timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); uv__io_poll(loop, timeout); // poll階段 /* Run one final update on the provider_idle_time in case uv__io_poll * returned because the timeout expired, but no events were received. This * call will be ignored if the provider_entry_time was either never set (if * the timeout == 0) or was already updated b/c an event was received. */ uv__metrics_update_idle_time(loop); uv__run_check(loop); // check階段 uv__run_closing_handles(loop); // close階段 if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */ uv__update_time(loop); uv__run_timers(loop); // 執行已到期的定時器 } r = uv__loop_alive(loop); // 在 UV_RUN_ONCE 和 UV_RUN_NOWAIT 模式中,跳出當前循環 if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } /* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */ if (loop->stop_flag != 0) loop->stop_flag = 0; // 標記當前的 stop_flag 為 0,表示跑完這輪,事件循環就結束了 return r; } ~~~ **1)setTimeout 和 setImmediate** &emsp;&emsp;setTimeout 會在最前面的 timers 階段被執行,而 setImmediate 會在 check 階段被執行。 &emsp;&emsp;但在下面的示例中,timeout 和 immediate 的打印順序是不確定的。 &emsp;&emsp;在 setTimeout()[官方文檔](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#settimeoutcallback-delay-args)中曾提到,當延遲時間大于 2147483647(24.8天) 或小于 1 時,將默認被設為 1。 &emsp;&emsp;所以下面的 setTimeout(callback, 0) 相當于 setTimeout(callback, 1)。 &emsp;&emsp;雖然在源碼中會先運行 uv\_\_run\_timers(),但是由于上一次的循環耗時可能超過 1ms,也可能小于 1ms,所以定時器有可能還未到期。 &emsp;&emsp;如此的話,就會造成打印順序的不確定性,上述分析過程[參考了此處](https://cnodejs.org/topic/57d68794cb6f605d360105bf#57d7b1f53f3cb94e6b326746)。 ~~~ setTimeout(() => { console.log('timeout') }, 0); setImmediate(() => { console.log('immediate') }); ~~~ &emsp;&emsp;如果將 setTimeout() 和 setImmediate() 注冊到 I/O 回調中運行,那么順序就是確定的,先 immediate 再 timeout。 ~~~ const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }); ~~~ &emsp;&emsp;這是因為 readFile() 的回調會在 poll 階段運行,而在 uv\_\_io\_poll() 之后,就會立即執行 uv\_\_run\_check(),從而就能保證先打印 immediate 。 &emsp;&emsp;在自己的日常工作中,曾使用過一個基于 setTimeout() 的定時任務庫:[node-schedule](https://github.com/node-schedule/node-schedule)。 &emsp;&emsp;由于延遲時間最長為 24.8 天,所以該庫巧妙的運用了一個遞歸來彌補時間的上限。 ~~~ Timeout.prototype.start = function() { if (this.after <= TIMEOUT_MAX) { this.timeout = setTimeout(this.listener, this.after) } else { var self = this this.timeout = setTimeout(function() { self.after -= TIMEOUT_MAX self.start() }, TIMEOUT_MAX) } if (this.unreffed) { this.timeout.unref() } } ~~~ **2)與瀏覽器中的事件循環的差異** &emsp;&emsp;在瀏覽器的事件循環中,沒有那么細的循環階段,不過有兩個非常重要的概念,那就是宏任務和微任務。 &emsp;&emsp;宏任務包括 setTimeout()、setInterval()、requestAnimationFrame、Ajax、fetch()、腳本標簽代碼等。 &emsp;&emsp;微任務包括 Promise.then()、MutationObserver。 &emsp;&emsp;在 Node.js 中,[process.nextTick()](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processnexttickcallback-args)是微任務的一種,setTimeout()、setInterval()、setImmediate() 等都屬于宏任務。 &emsp;&emsp;在 Node版本 < 11 時,執行完一個階段的所有任務后,再執行process.nextTick(),最后是其他微任務。 &emsp;&emsp;可以這樣理解,process.nextTick() 維護了一個獨立的隊列,不存在于事件循環的任何階段,而是在各個階段切換的間隙執行。 &emsp;&emsp;即從一個階段切換到下個階段前執行,執行時機如下所示。 ~~~ ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ nextTickQueue nextTickQueue │ ┌─────────────┴─────────────┐ │ │ poll │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ │ │ check │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘ ~~~ &emsp;&emsp;但是在 Node 版本 >= 11 之后,會處理的和瀏覽器一樣,也是每執行完一個宏任務,就將其微任務也一并完成。 &emsp;&emsp;在下面這個示例中, setTimeout() 內先聲明 then(),再聲明 process.nextTick(),最后執行一條打印語句。 &emsp;&emsp;接著在 setTimeout() 之后再次聲明了 process.nextTick()。? ~~~ // setTimeout setTimeout(() => { Promise.resolve().then(function() { console.log('promise'); }); process.nextTick(() => { console.log('setTimeout nextTick'); }); console.log('setTimeout'); }, 0); // nextTick process.nextTick(() => { console.log('nextTick'); }); ~~~ &emsp;&emsp;我本地運行的 Node 版本是 16,所以最終的打印順序如下所示。 ~~~ nextTick setTimeout setTimeout nextTick promise ~~~ &emsp;&emsp;外面的 process.nextTick() 要比 setTimeout() 先運行,里面的打印語句最先執行,然后是 process.nextTick(),最后是 then()。 **3)sleep()** &emsp;&emsp;有一道比較經典的題目是編寫一個 sleep() 函數,實現線程睡眠,在日常開發中很容易就會遇到。 &emsp;&emsp;搜集了多種實現函數,有些是同步,有些是異步。 &emsp;&emsp;第一種是同步函數,創建一個循環,占用主線程,直至循環完畢,這種方式也叫循環空轉,比較浪費CPU性能,不推薦。 ~~~ function sleep(ms) { var start = Date.now(), expire = start + ms; while (Date.now() < expire); } ~~~ &emsp;&emsp;第二至第四種都是異步函數,本質上線程并沒有睡眠,事件循環仍在運行,下面是 Promise + setTimeout() 組合實現的 sleep() 函數。 ~~~ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ~~~ &emsp;&emsp;第三種是利用 util 庫的[promisify()](https://nodejs.org/dist/latest-v18.x/docs/api/util.html#utilpromisifyoriginal)函數,返回一個 Promise 版本的定時器。 ~~~ function sleep(ms) { const { promisify } = require('util'); return promisify(setTimeout)(ms); } ~~~ &emsp;&emsp;第四種是當 Node 版本 >= 15 時可以使用,在[timers庫](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#timerspromisessettimeoutdelay-value-options)中直接得到一個 Promise 版本的定時器。 ~~~ function sleep(ms) { const { setTimeout } = require('timers/promises'); return setTimeout(ms); } ~~~ &emsp;&emsp;第五種是同步函數,可利用[Atomics.wait](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait)阻塞事件循環,直至線程超時,實現細節在此不做說明了。 ~~~ function sleep(ms) { const sharedBuf = new SharedArrayBuffer(4); const sharedArr = new Int32Array(sharedBuf); return Atomics.wait(sharedArr, 0, 0, ms); } ~~~ &emsp;&emsp;還可以編寫 C/C++ 插件,直接調用操作系統的 sleep() 函數,此處不做展開。 參考資料: [Event Loop](https://mp.weixin.qq.com/s/RNYYNR7A01V-Y2aC1wNsGw)?[事件循環源碼](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter5/chapter5-1.html)?[Node.js技術棧](https://www.nodejs.red/#/nodejs/translate/everything-you-need-to-know-about-node-js-lnc?id=the-event-loop%ef%bc%88%e4%ba%8b%e4%bb%b6%e5%be%aa%e7%8e%af%ef%bc%89) [nodejs真的是單線程嗎?](https://segmentfault.com/a/1190000014926921) [Nodejs探秘:深入理解單線程實現高并發原理](https://imweb.io/topic/5b6cf97093759a0e51c917c8) [什么是CPU密集型、IO密集型?](https://zhuanlan.zhihu.com/p/62766037)?[libuv](https://luohaha.github.io/Chinese-uvbook/source/introduction.html)?[I/O](https://zh.m.wikipedia.org/zh-cn/I/O) [JavaScript 運行機制詳解:再談Event Loop](https://www.ruanyifeng.com/blog/2014/10/event-loop.html) [Node.js Event Loop 的理解 Timers,process.nextTick()](https://cnodejs.org/topic/57d68794cb6f605d360105bf) [瀏覽器與Node的事件循環(Event Loop)有何區別?](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26) [Why is the EventLoop for Browsers and Node.js Designed This Way?](https://blog.bitsrc.io/why-is-the-eventloop-for-browsers-and-node-js-designed-this-way-f7f794696c?gi=29723793aa09) [Node.js 事件循環](https://learnku.com/articles/38802)?[Phases of the Node JS Event Loop](https://medium.com/@kunaltandon.kt/process-nexttick-vs-setimmediate-vs-settimeout-explained-wrt-different-event-loop-phases-c0506b12921d) [如何實現線程睡眠?](https://www.nodejs.red/#/nodejs/tips/sleep?id=%e4%ba%8c%ef%bc%9a%e5%ae%9a%e6%97%b6%e5%99%a8-promise-%e5%ae%9e%e7%8e%b0-sleep) [nodejs中的并發編程](https://segmentfault.com/a/1190000022113106) ***** > 原文出處: [博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html) [知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120) 已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看