摘要:定時(shí)器階段這個(gè)是事件循環(huán)開(kāi)始的階段,綁定到這個(gè)階段的隊(duì)列,保留著定時(shí)器的回調(diào),盡管它并沒(méi)有將回調(diào)推入隊(duì)列中,但是以最小的堆來(lái)維持計(jì)時(shí)器并且在到達(dá)規(guī)定的事件后執(zhí)行回調(diào)。
本文,將會(huì)詳細(xì)的講解 node.js 事件循環(huán)工作流程和生命周期
一些常見(jiàn)的誤解最常見(jiàn)的誤解之一,事件循環(huán)是 Javascript 引擎(V8,spiderMonkey等)的一部分。事實(shí)上事件循環(huán)主要利用 Javascript 引擎來(lái)執(zhí)行代碼。
首先沒(méi)有棧,其次這個(gè)過(guò)程是復(fù)雜的,有多個(gè)隊(duì)列(像數(shù)據(jù)結(jié)構(gòu)中的隊(duì)列)參與。但是大多數(shù)開(kāi)發(fā)者知道多少有的回調(diào)函數(shù)被推進(jìn)一個(gè)單一的隊(duì)列里面,是完全錯(cuò)誤的。
由于錯(cuò)誤的 node.js 事件循環(huán)圖,我們中有一部分人認(rèn)為u有兩個(gè)線(xiàn)程。一個(gè)執(zhí)行 Javascript,另一個(gè)執(zhí)行事件循環(huán)。事實(shí)上都在一個(gè)線(xiàn)程里面運(yùn)行。
另一個(gè)非常大的誤解是 setTimeout 的回調(diào)函數(shù)在給定的延遲完成之后被(可能是 OS 或者 內(nèi)核)推進(jìn)一個(gè)隊(duì)列。
作為常見(jiàn)的事件循環(huán)描述只有一個(gè)隊(duì)列;所以一些開(kāi)發(fā)者認(rèn)為 setImmediate 將回調(diào)放在工作隊(duì)列的前面。這是完全錯(cuò)誤的,在 Javascript 的工作隊(duì)列都是先進(jìn)先出的。
事件循環(huán)的架構(gòu)在我們開(kāi)始描述事件循環(huán)的工作流程時(shí),知道它的架構(gòu)非常重要。下圖為事件循環(huán)真正的工作流程:
圖中不同的盒子代表不同的階段,每個(gè)階段執(zhí)行特定的工作。每個(gè)階段都有一個(gè)隊(duì)列(這里說(shuō)成隊(duì)列主要是為了更好理解;真實(shí)的數(shù)據(jù)結(jié)構(gòu)可能不是隊(duì)列),Javascript 可以在任何一個(gè)階段執(zhí)行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是循環(huán)的一部分,它們之中的回調(diào)可以在任意階段執(zhí)行。它們有更高的優(yōu)先級(jí)去執(zhí)行。
現(xiàn)在你知道了事件循環(huán)是不同階段和不同隊(duì)列的結(jié)合;下面是每個(gè)階段的描述。
這個(gè)是事件循環(huán)開(kāi)始的階段,綁定到這個(gè)階段的隊(duì)列,保留著定時(shí)器(setTimeout, setInterval)的回調(diào),盡管它并沒(méi)有將回調(diào)推入隊(duì)列中,但是以最小的堆來(lái)維持計(jì)時(shí)器并且在到達(dá)規(guī)定的事件后執(zhí)行回調(diào)。
這個(gè)階段執(zhí)行在事件循環(huán)中 pending_queue 里的回調(diào),這些回調(diào)時(shí)被之前的操作推入的。例如當(dāng)你嘗試往 tcp 中寫(xiě)入一些東西,這個(gè)工作完成了,然后回調(diào)被推入到隊(duì)列中。錯(cuò)誤處理的回調(diào)也在這里。
盡管名字是空閑(idle),但是每個(gè) tick 都運(yùn)行。Prepare 也在輪詢(xún)階段開(kāi)始之前運(yùn)行。不管怎樣,這兩個(gè)階段是 node 主要做一些內(nèi)部操作的階段。
可能整個(gè)事件循環(huán)最重要的一個(gè)階段就是 poll phase。這個(gè)階段接受新傳入的連接(新的 Socket 建立等)和數(shù)據(jù)(文件讀取等)。我們可以將輪詢(xún)階段分成幾個(gè)不同的部分。
如果在 watch_queue (這個(gè)隊(duì)列被綁定到輪詢(xún)階段)有東西,它們將會(huì)被一個(gè)接著一個(gè)的執(zhí)行知道隊(duì)列為空或者系統(tǒng)達(dá)到最大的限制。
一旦隊(duì)列為空,node 就會(huì)等待新的連接。等待或者睡眠的事件取決于多種因素。
輪詢(xún)的下一個(gè)階段是 check pahse,這個(gè)專(zhuān)用于 setImmediate 的階段。為什么需要一個(gè)專(zhuān)門(mén)的隊(duì)列來(lái)處理 setImmediate 回調(diào)?這是因?yàn)檩喸?xún)階段的行為,待會(huì)兒將在流程部分討論?,F(xiàn)在只需要記住檢查(check)階段主要處理 setImmediate() 的回調(diào)。
回調(diào)的關(guān)閉(stocket.on("close", () => {}))都在這里處理的,更像一個(gè)清理階段。
nextTickQueue 中的任務(wù)保留在被 process.nextTick() 觸發(fā)的回調(diào)。 microTaskQueue 保留著被 Promise 觸發(fā)的回調(diào)。它們都不是事件循環(huán)地一部分(不是在 libUV 中開(kāi)發(fā)地),而是在 node 中。在 C/C++ 和 Javascript 有交叉的時(shí)候,它們都是盡可能快地被調(diào)用。因此它們應(yīng)該在當(dāng)前操作運(yùn)行后(不一定是當(dāng)前 js 回調(diào)執(zhí)行完)。
事件循環(huán)地工作流程當(dāng)在你的控制臺(tái)運(yùn)行 node my-script.js ,node 設(shè)置事件循環(huán)然后運(yùn)行你主要的模塊(my-script.js)事件循環(huán)的外部。一旦主要模塊執(zhí)行完,node 將會(huì)檢查循環(huán)是否還活著(事件循環(huán)中是否還有事情要做)?如果沒(méi)有,將會(huì)在執(zhí)行退出回調(diào)后退出。process, on("exit", foo) 回調(diào)(退出回調(diào))。但是如果循環(huán)還活著,node 將會(huì)從計(jì)時(shí)器階段進(jìn)入循環(huán)。
事件循環(huán)進(jìn)入計(jì)時(shí)器階段并且檢查在計(jì)時(shí)器隊(duì)列中是否有需要執(zhí)行的。好吧,這句話(huà)聽(tīng)起來(lái)非常簡(jiǎn)單,但是事件循環(huán)實(shí)際上要執(zhí)行一些步驟發(fā)現(xiàn)合適的回調(diào)。實(shí)際上計(jì)時(shí)器腳本以升序儲(chǔ)存在堆內(nèi)存中。它首先獲取到一個(gè)執(zhí)行計(jì)時(shí)器,計(jì)算下是否 now-registeredTime == delta?如果是,他會(huì)執(zhí)行這個(gè)計(jì)時(shí)器的回調(diào)并且檢查下一個(gè)計(jì)時(shí)器。直到找到一個(gè)還沒(méi)有約定時(shí)間的計(jì)時(shí)器,它會(huì)停止檢查其他的定時(shí)器(因?yàn)槎〞r(shí)器都以升序排好了)并且移到下一個(gè)階段了。
假設(shè)你調(diào)用了 setTimeout 4次創(chuàng)建了4個(gè)定時(shí)器,分別相對(duì)于時(shí)間 t 來(lái)說(shuō) 100,200,300,400 的差值。
假設(shè)事件循環(huán)在 t+250 進(jìn)入到了計(jì)時(shí)器階段。它會(huì)首先看下計(jì)時(shí)器 A,A 的過(guò)期時(shí)間是 t+100。但是現(xiàn)在時(shí)間是 t+250。因此它將執(zhí)行綁定在計(jì)時(shí)器 A 上的回調(diào)。然后去檢查計(jì)時(shí)器 B,發(fā)現(xiàn)它的過(guò)期時(shí)間是 t+200,因此也會(huì)執(zhí)行 B 的回調(diào)?,F(xiàn)在它會(huì)檢查 C,發(fā)現(xiàn)它的過(guò)期時(shí)間是 t+300,因此將會(huì)離開(kāi)它。時(shí)間循環(huán)不會(huì)去檢查 D,因?yàn)橛?jì)時(shí)器是按升序拍好的;因此 D 的閾值比 C 大。然而這個(gè)階段有一個(gè)系統(tǒng)相關(guān)的硬限制,如果達(dá)到系統(tǒng)依賴(lài)最大限制數(shù)量,即使有未執(zhí)行的計(jì)時(shí)器,它也會(huì)移到下一個(gè)階段。
計(jì)時(shí)器階段后,事件循環(huán)將會(huì)進(jìn)入到了懸而未決的 I/O 階段,然后檢查一下 pengding_queue 中是否有來(lái)自于之前的懸而未決的任務(wù)的回調(diào)。如果有,一個(gè)接一個(gè)的執(zhí)行,直到隊(duì)列為空,或者達(dá)到系統(tǒng)的最大限制。之后,事件循環(huán)將會(huì)移到 idle handler 階段,其次是準(zhǔn)備階段做一些內(nèi)部的操作。然后最終可能進(jìn)入到最重要的階段 poll phase。
像名字說(shuō)的那樣,這是一個(gè)觀察的階段。觀察是否有新的請(qǐng)求或者連接傳入。當(dāng)事件循環(huán)進(jìn)入輪詢(xún)階段,它會(huì)在 watcher_queue 中執(zhí)行腳本,包含文件讀響應(yīng),新的 socket 或者 http 連接請(qǐng)求,直到事件耗盡或者像其他階段那樣達(dá)到系統(tǒng)依賴(lài)上限。假設(shè)沒(méi)有要執(zhí)行的回調(diào),輪詢(xún)?cè)谀承┨囟ǖ臈l件下將會(huì)等待一會(huì)兒。如果在檢查隊(duì)列(check queue),懸而未決隊(duì)列(pending queue),或者關(guān)閉隊(duì)列(closing callbacks queue 或者 idle handler queue)里面有任何任務(wù)等待,它將等待 0 毫秒。然后它會(huì)根據(jù)定時(shí)器堆來(lái)決定等待時(shí)間執(zhí)行第一個(gè)定時(shí)器(如果可獲?。?。如果第一個(gè)定時(shí)器閾值經(jīng)過(guò)了,毫無(wú)疑問(wèn)它不需要等待(就會(huì)執(zhí)行第一個(gè)定時(shí)器)。
輪詢(xún)階段結(jié)束之后,立即來(lái)到檢查階段。這個(gè)階段的隊(duì)列中有被 api setImmediate 觸發(fā)的回調(diào)。它將會(huì)像其他階段那樣一個(gè)接著一個(gè)的執(zhí)行,直到隊(duì)列為空或者達(dá)到依賴(lài)系統(tǒng)的最大限制。
完成在檢查階段的任務(wù)之后,事件循環(huán)的下一個(gè)目的地是處理關(guān)閉或者銷(xiāo)毀類(lèi)型的回調(diào) close callback。事件循環(huán)執(zhí)行完這個(gè)階段的隊(duì)列中的回調(diào)后,它會(huì)檢查循環(huán)(loop)是否還活著,如果沒(méi)有,退出。但是如果還有工作要做,它會(huì)進(jìn)入下一個(gè)循環(huán);因此在計(jì)時(shí)器階段。如果你認(rèn)為之前例子中的定時(shí)器(A & B)過(guò)期,那么現(xiàn)在定時(shí)器階段將會(huì)從定時(shí)器 C 開(kāi)始檢查是否過(guò)期。
因此,這兩個(gè)隊(duì)列的回調(diào)函數(shù)什么時(shí)候運(yùn)行?它們當(dāng)然在從當(dāng)前階段到下一個(gè)階段之前盡可能快的運(yùn)行。不像其他階段,它們兩個(gè)沒(méi)有系統(tǒng)依賴(lài)的醉倒限制,node 運(yùn)行它們直到兩個(gè)隊(duì)列是空的。然而,nextTickQueue 會(huì)比 microTaskQueue 有著更高的任務(wù)優(yōu)先級(jí)。
我從 Javascript 開(kāi)發(fā)者哪里聽(tīng)到普遍的一個(gè)詞就是 ThreadPool。一個(gè)普遍的誤解是,nodejs 有一個(gè)處理所有異步操作的進(jìn)程池。但是實(shí)際上進(jìn)程池是 libUV (nodejs用來(lái)處理異步的第三方庫(kù))庫(kù)中的。之所以沒(méi)有在圖中畫(huà)出來(lái),是因?yàn)樗皇茄h(huán)機(jī)制的一部分。目前,并不是每個(gè)異步任務(wù)都會(huì)被進(jìn)程池處理的。libUV 能夠靈活使用操作系統(tǒng)的異步 api 來(lái)保持環(huán)境為事件驅(qū)動(dòng)。然而操作系統(tǒng)的 api 不能做文件讀取,dns 查詢(xún)等,這些由進(jìn)程池來(lái)處理,默認(rèn)只有 4 個(gè)進(jìn)程。你可以通過(guò)設(shè)置 uv_threadpool_size 的環(huán)境變量增加進(jìn)程數(shù)直到 128.
帶有示例的工作流程希望你能理解事件循環(huán)是如何工作的。C 語(yǔ)言 中同步的 while 幫助 Javascript 成為異步的。每次只處理一件事但是很吶阻塞。當(dāng)然,無(wú)論我們?nèi)绻枋隼碚摚詈玫睦斫膺€是示例,因此,讓我們通過(guò)一些代碼片段來(lái)理解這個(gè)腳本。
setTimeout(() => {console.log("setTimeout"); }, 0); setImmediate(() => {console.log("setImmediate"); });
你能夠猜到上面的輸出嗎?好吧,你可能認(rèn)為 setTimeout 會(huì)先被打印出來(lái),但是不能保證,為什么呢?執(zhí)行完主模塊之后進(jìn)入計(jì)時(shí)器階段,他可能不會(huì)或者會(huì)發(fā)現(xiàn)你的計(jì)時(shí)器耗盡了。為什么呢?一個(gè)計(jì)時(shí)器腳本是根據(jù)系統(tǒng)時(shí)間和你提供的增量時(shí)間注冊(cè)的。setTimeout 調(diào)用的同時(shí),計(jì)時(shí)器腳本被寫(xiě)入到了內(nèi)存中,根據(jù)你的機(jī)器性能和其他運(yùn)行在它上面的操作(不是node)的不同,可能會(huì)有一個(gè)很小的延遲。另一點(diǎn)時(shí),node僅僅在進(jìn)入計(jì)時(shí)器階段(每一輪遍歷)之前設(shè)置一個(gè)變量 now,將 now 作為當(dāng)前時(shí)間。因此你可以說(shuō)相當(dāng)于精確的時(shí)間有點(diǎn)問(wèn)題。這就是不確定性的原因。如果你在一個(gè)計(jì)時(shí)器代碼的回調(diào)里面指向相同的代碼會(huì)得到相同的結(jié)果。
然而,如果你移動(dòng)這段代碼到 i/o 周期里,保證 setImmediate 回調(diào)會(huì)先于 setTimeout 運(yùn)行。
fs.readFile("my-file-path.txt", () => { setTimeout(() => {console.log("setTimeout");}, 0); setImmediate(() => {console.log("setImmediate");}); });
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setImmediate(foo); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
上面的例子非常簡(jiǎn)單。調(diào)用函數(shù) foo 函數(shù)內(nèi)部再通過(guò) setImmediate 遞歸調(diào)用 foo 直到 1000。在我的電腦上面,大概花費(fèi)了 6 到 8 毫秒。仙子啊修改下上面的代碼,把 setImmedaite(foo) 換成 setTimeout(foo, o)。
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setTimeout(foo, 0); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
現(xiàn)在在我的電腦上面運(yùn)行這段代碼花費(fèi)了 1400+ms。為什么會(huì)這樣?它們都沒(méi)有 i/o 事件,應(yīng)該一樣才對(duì)。上面兩個(gè)例子等待事件是 0.為什么花費(fèi)這么長(zhǎng)時(shí)間?通過(guò)事件比較找到了偏差,CPU 密集型任務(wù),花費(fèi)更多的時(shí)間。注冊(cè)計(jì)時(shí)器腳本也花費(fèi)事件。定時(shí)器的每個(gè)階段都需要做一些操作來(lái)決定一個(gè)定時(shí)器是否應(yīng)該執(zhí)行。長(zhǎng)時(shí)間的執(zhí)行也會(huì)導(dǎo)致更多的 ticks。然而,在 setImmediate 中,只有檢查這一個(gè)階段,就好像在一個(gè)隊(duì)列里面然后執(zhí)行就行了。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000);
你認(rèn)為上面輸出是什么?是的,它會(huì)輸出 foo 然后輸出 setTimeout。2秒后被 nextTickQueue 遞歸調(diào)用 foo() 打印出第一個(gè) foo。當(dāng)所有的 nextTickQueue 執(zhí)行完了,開(kāi)始執(zhí)行其他(比如 setTimeout 回調(diào))的。
所以是每個(gè)回調(diào)執(zhí)行完之后,開(kāi)始檢查 nextTickQueue 的嗎? 我們改下代碼看下。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000); setTimeout(()=>{console.log("Other setTimeout"); }, 2000);
在 setTimeout 之后,我僅僅用一樣的延遲時(shí)間添加了另一個(gè)輸出 Other setTimeout 的 setTimeout。盡管不能保證,但是有可能會(huì)在輸出第一個(gè) foo 之后輸出 Other setTimeout 。相同的定時(shí)器分為一個(gè)組,nextTickQueue 會(huì)在正在進(jìn)行中的回調(diào)組執(zhí)行完之后執(zhí)行。
一些普遍的問(wèn)題就像我們大多數(shù)人都認(rèn)為事件循環(huán)是在一個(gè)多帶帶的線(xiàn)程里面,將回調(diào)推入一個(gè)隊(duì)列,然后一個(gè)接著一個(gè)執(zhí)行。第一次讀到這篇文章的讀者可能會(huì)感到疑惑,Javascript 在哪里執(zhí)行的?正如我早些時(shí)候說(shuō)的,只有一個(gè)線(xiàn)程,來(lái)自于本身使用 V8 或者其他引擎的事件循環(huán)的 Javascript 代碼也是在這里運(yùn)行的。執(zhí)行是同步的,如果當(dāng)前的 Javascript 執(zhí)行還沒(méi)有完成,事件循環(huán)不會(huì)傳播。
首先不是0,而是1.當(dāng)你設(shè)置一個(gè)計(jì)時(shí)器,時(shí)間為小于 1,或者大于 2147483647ms 的時(shí)候,它會(huì)自動(dòng)設(shè)置為 1.因此你如果設(shè)置 setTimeout 的延遲時(shí)間為 0,它會(huì)自動(dòng)設(shè)置為1.
此外,setImmediate 會(huì)減少額外的檢查。因此 setImmediate 會(huì)執(zhí)行的更快一些。它也放置在輪詢(xún)階段之后,因此來(lái)自于任何一個(gè)到來(lái)的請(qǐng)求 setImmediate 回調(diào)將會(huì)立即被執(zhí)行。
setImmediate 和 process.nextTick() 都命名錯(cuò)了。所以功能上,setImmediate 在下一個(gè) tick 執(zhí)行,nextTick 是馬上執(zhí)行的。
由于 nextTickQueue 沒(méi)有回調(diào)執(zhí)行的限制。因此如果你遞歸地執(zhí)行 process.nextTick(),你地程序可能永遠(yuǎn)在事件循環(huán)中出不來(lái),無(wú)論你在其他階段有什么。
它可能會(huì)初始化計(jì)時(shí)器,但回調(diào)可能永遠(yuǎn)不會(huì)被調(diào)用。因?yàn)槿绻?node 在 exit callback 階段,它已經(jīng)跳出事件循環(huán)了。因此沒(méi)有回去執(zhí)行。
一些短地結(jié)論事件循環(huán)沒(méi)有工作棧
事件循環(huán)不在一個(gè)多帶帶地線(xiàn)程里面,Javascript 的執(zhí)行也不是像從隊(duì)列中彈出一個(gè)回調(diào)執(zhí)行那么簡(jiǎn)單。
setImmediate 沒(méi)有將回調(diào)推入到工作隊(duì)列地頭部,有一個(gè)專(zhuān)門(mén)的階段和隊(duì)列。
setImmediate 在下一個(gè)循環(huán)執(zhí)行,nextTick 實(shí)際上是馬上執(zhí)行。
當(dāng)心,如果遞歸調(diào)用的話(huà),nextTickQueue 可能會(huì)阻塞你的 node 代碼。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/108190.html
摘要:輪詢(xún)投票處理下一次處理的新事件立即設(shè)置運(yùn)行通過(guò)注冊(cè)的所有回調(diào)關(guān)閉執(zhí)行所有的回調(diào)工作處理延遲此度量標(biāo)準(zhǔn)測(cè)量線(xiàn)程池處理異步任務(wù)需要多長(zhǎng)時(shí)間。高工作時(shí)間處理延遲表明繁忙耗盡的線(xiàn)程池。 原文=> What you should know to really understand the Node.js Event Loop Node.js 是一個(gè)基于事件的平臺(tái)。這就意味著在Node中發(fā)生的所...
摘要:變量的說(shuō)法來(lái)自于,這是在多線(xiàn)程模型下出現(xiàn)并發(fā)問(wèn)題的一種解決方案。目前已經(jīng)有庫(kù)實(shí)現(xiàn)了應(yīng)用層棧幀的可控編碼,同時(shí)可以在該棧幀存活階段綁定相關(guān)數(shù)據(jù),我們便可以利用這種特性實(shí)現(xiàn)類(lèi)似多線(xiàn)程下的變量。 ThreadLocal變量的說(shuō)法來(lái)自于Java,這是在多線(xiàn)程模型下出現(xiàn)并發(fā)問(wèn)題的一種解決方案。ThreadLocal變量作為線(xiàn)程內(nèi)的局部變量,在多線(xiàn)程下可以保持獨(dú)立,它存在于線(xiàn)程的生命周期內(nèi),可以在...
摘要:前端每周清單第期現(xiàn)狀分析與優(yōu)化策略單元測(cè)試爬蟲(chóng)作者王下邀月熊編輯徐川前端每周清單專(zhuān)注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開(kāi)發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開(kāi)發(fā)教程工程實(shí)踐深度閱讀開(kāi)源項(xiàng)目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化策略...
摘要:需要校驗(yàn)字節(jié)信息是否符合規(guī)范,避免惡意信息和不規(guī)范數(shù)據(jù)危害運(yùn)行安全。具有相同哈希值的鍵值對(duì)會(huì)組成鏈表。通過(guò)在協(xié)議下添加了一層協(xié)議對(duì)數(shù)據(jù)進(jìn)行加密從而保證了安全。常見(jiàn)的非對(duì)稱(chēng)加密包括等。 類(lèi)加載過(guò)程 Java 中類(lèi)加載分為 3 個(gè)步驟:加載、鏈接、初始化。 加載。 加載是將字節(jié)碼數(shù)據(jù)從不同的數(shù)據(jù)源讀取到JVM內(nèi)存,并映射為 JVM 認(rèn)可的數(shù)據(jù)結(jié)構(gòu),也就是 Class 對(duì)象的過(guò)程。數(shù)據(jù)源可...
閱讀 3858·2023-04-25 17:45
閱讀 3503·2021-09-04 16:40
閱讀 1058·2019-08-30 13:54
閱讀 2197·2019-08-29 12:59
閱讀 1466·2019-08-26 12:11
閱讀 3348·2019-08-23 15:17
閱讀 1572·2019-08-23 12:07
閱讀 3944·2019-08-22 18:00