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
者: 五月君 來源:編程界|
事件循環是一種控制應用程序的運行機制,在不同的運行時環境有不同的實現,上一節講了瀏覽器中的事件循環,它們有很多相似的地方,也有著各自的特點,本節討論下 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、定時器等),如果是活動狀態開始迭代,否則循環將立即退出。
下面對每個階段分別討論。
首先事件循環進入定時器階段,該階段包含兩個 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:
定時器階段完成后,事件循環進入到 pending callbacks 階段,在這個階段執行上一輪事件循環遺留的 I/O 回調。根據 Libuv 文檔的描述:大多數情況下,在輪詢 I/O 后立即調用所有 I/O 回調,但是,某些情況下,調用此類回調會推遲到下一次循環迭代。聽完更像是上一個階段的遺留。
idle, prepare 階段是給系統內部使用,idle 這個名字很迷惑,盡管叫空閑,但是在每次的事件循環中都會被調用,當它們處于活動狀態時。這一塊的資料介紹也不是很多。略...
poll 是一個重要的階段,這里有一個概念觀察者,有文件 I/O 觀察者,網絡 I/O 觀察者等,它會觀察是否有新的請求進入,包含讀取文件等待響應,等待新的 socket 請求,這個階段在某些情況下是會阻塞的。
在阻塞 I/O 之前,要計算它應該阻塞多長時間,參考 Libuv 文檔上的一些描述,以下這些是它計算超時時間的規則:
如果以上情況都沒有,則采用最近定時器的超時時間,或者如果沒有活動的定時器,則超時時間為無窮大,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 看下事件循環的執行過程:
// 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 階段在 poll 階段之后運行,這個階段包含一個 API setImmediate(cb) 如果有被 setImmediate 觸發的回調函數,就取出執行,直到隊列為空或達到系統的最大限制。
拿 setTimeout 和 setImmediate 對比,這是一個常見的例子,基于被調用的時機和定時器可能會受到計算機上其它正在運行的應用程序影響,它們的輸出順序,不總是固定的。
setTimeout(() => log('setTimeout'));
setImmediate(() => log('setImmediate'));
// 第一次運行
setTimeout
setImmediate
// 第二次運行
setImmediate
setTimeout
但是一旦把這兩個函數放入一個 I/O 循環內調用,setImmediate 將總是會被優先調用。因為 setImmediate 屬于 check 階段,在事件循環中總是在 poll 階段結束后運行,這個順序是確定的。
fs.readFile(__filename, () => {
setTimeout(() => log('setTimeout'));
setImmediate(() => log('setImmediate'));
})
在 Libuv 中,如果調用關閉句柄 uv_close(),它將調用關閉回調,也就是事件循環的最后一個階段 close callbacks。
這個階段的工作更像是做一些清理工作,例如,當調用 socket.destroy(),'close' 事件將在這個階段發出,事件循環在執行完這個階段隊列里的回調函數后,檢查循環是否還 alive,如果為 no 退出,否則繼續下一次新的事件循環。
在瀏覽器的事件循環中,把任務劃分為 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 在 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 討論。
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 上看到的兩篇事件循環相關文章,推薦給大家,文章很精彩,評論也更加精彩。
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(變量初始化; 條件表達式; 變量更新) {
// 條件表達式為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 循環中三個語句:
此時我們可以一點點來看這個 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 循環括號內的變量是用來指定變量,指定的可以是數組對象或者是對象屬性。
示例:
使用 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
職業:程序員
特長:跳舞
avaScript 的 Event Loop(事件循環)是 JavaScript 運行時環境(如瀏覽器和 Node.js)的核心機制之一,它使得 JavaScript 能夠處理異步操作而不會阻塞程序的執行。了解 Event Loop 對于理解 JavaScript 的非阻塞行為和編寫高效的異步代碼至關重要。
首先,重要的是要理解 JavaScript 是一種單線程的語言。這意味著 JavaScript 在同一時間內只能執行一個任務。然而,JavaScript 需要能夠處理各種異步操作(如 AJAX 請求、文件讀取、用戶交互等),這些操作可能會花費很長時間完成。為了解決這個問題,JavaScript 采用了 Event Loop 和 Callback Queues(回調隊列)。
調用棧是 JavaScript 代碼執行時的數據結構,用于存儲函數調用和返回地址。每當一個函數被調用時,它就會被推入調用棧,并在函數執行完畢后從棧中彈出。如果調用棧滿了(即達到了最大調用深度),則會發生棧溢出錯誤。
堆是用于存儲對象、數組等引用類型的內存區域。與調用棧不同,堆是動態分配的,并且其大小不是固定的。
Web APIs 是瀏覽器提供的一組與瀏覽器功能交互的接口,如 DOM 操作、網絡請求等。這些 API 通常是異步的,并且它們有自己的線程或進程來處理請求。
當異步操作完成時(如 AJAX 請求、setTimeout、Promise 解決等),相應的回調函數會被放入任務隊列(或稱為宏任務隊列)或微任務隊列中。任務隊列中的任務在當前的執行棧清空后才會被執行,而微任務隊列中的任務會在當前執行棧清空后、但下一個宏任務執行前立即執行。
Event Loop 的工作流程可以概括為以下幾個步驟:
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 宏任務隊列
解釋:
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
解釋:
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 宏任務隊列
解釋:
Event Loop 是 JavaScript 異步編程的基石,它使得 JavaScript 能夠在不阻塞主線程的情況下處理各種異步操作。通過理解 Event Loop 的工作原理,我們可以更加高效地編寫異步代碼,避免潛在的錯誤和性能問題。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。