摘要:編寫異步代碼可能是一種不同的體驗,尤其是對異步控制流而言?;卣{(diào)函數(shù)的準(zhǔn)則在編寫異步代碼時,要記住的第一個規(guī)則是在定義回調(diào)時不要濫用閉包。為回調(diào)創(chuàng)建命名函數(shù),避免使用閉包,并將中間結(jié)果作為參數(shù)傳遞。
本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。
歡迎關(guān)注我的專欄,之后的博文將在專欄同步:
Encounter的掘金專欄
知乎專欄 Encounter的編程思考
segmentfault專欄 前端小站
Asynchronous Control Flow Patterns with CallbacksNode.js這類語言習(xí)慣于同步的編程風(fēng)格,其CPS風(fēng)格和異步特性的API是其標(biāo)準(zhǔn),對于新手來說可能難以理解。編寫異步代碼可能是一種不同的體驗,尤其是對異步控制流而言。異步代碼可能讓我們難以預(yù)測在Node.js中執(zhí)行語句的順序。例如讀取一組文件,執(zhí)行一串任務(wù),或者等待一組操作完成,都需要開發(fā)人員采用新的方法和技術(shù),以避免最終編寫出效率低下和不可維護(hù)的代碼。一個常見的錯誤是回調(diào)地獄,代碼量急劇上升又不可讀,使得簡單的程序也難以閱讀和維護(hù)。在本章中,我們將看到如何通過使用一些規(guī)則和一些模式來避免回調(diào),并編寫干凈、可管理的異步代碼。我們將看到控制流庫,如async,可以極大地簡化我們的問題,提升我們的代碼可讀性,更易于維護(hù)。
異步編程的困難JavaScript中異步代碼的順序錯亂無疑是很容易的。閉包和對匿名函數(shù)的定義可以使開發(fā)人員有更好的編程體驗,而并不需要開發(fā)人員手動對異步操作進(jìn)行管理和跳轉(zhuǎn)。這是符合KISS原則的。簡單且能保持異步代碼控制流,讓它在更短的時間內(nèi)工作。但不幸的是,回調(diào)嵌套是以犧牲諸如模塊性、可重用性和可維護(hù)性,增大整個函數(shù)的大小,導(dǎo)致糟糕的代碼結(jié)構(gòu)為代價的。大多數(shù)情況下,創(chuàng)建閉包在功能上是不需要的,但這更多是一種約束,而不是與異步編程相關(guān)的問題。認(rèn)識到回調(diào)嵌套會使得我們的代碼變得笨拙,然后根據(jù)最適合的解決方案采取相應(yīng)的方法解決回調(diào)地獄,這是新手與專家的區(qū)別。
創(chuàng)建一個簡單的Web爬蟲為了解釋上述問題,我們創(chuàng)建了一個簡單的Web爬蟲,一個命令行應(yīng)用,其接受一個URL為輸入,然后可以把其內(nèi)容下載到一個文件中。在下列代碼中,我們會依賴以下兩個npm庫。
此外,我們還將引用一個叫做./utilities的本地模塊。
我們的應(yīng)用程序的核心功能包含在一個名為spider.js的模塊中。如下所示,首先加載我們所需要的依賴包:
const request = require("request"); const fs = require("fs"); const mkdirp = require("mkdirp"); const path = require("path"); const utilities = require("./utilities");
接下來,我們將創(chuàng)建一個名為spider()的新函數(shù),該函數(shù)接受URL為參數(shù),并在下載過程完成時調(diào)用一個回調(diào)函數(shù)。
function spider(url, callback) { const filename = utilities.urlToFilename(url); fs.exists(filename, exists => { if (!exists) { console.log(`Downloading ${url}`); request(url, (err, response, body) => { if (err) { callback(err); } else { mkdirp(path.dirname(filename), err => { if (err) { callback(err); } else { fs.writeFile(filename, body, err => { if (err) { callback(err); } else { callback(null, filename, true); } }); } }); } }); } else { callback(null, filename, false); } }); }
上述函數(shù)執(zhí)行以下任務(wù):
檢查該URL的文件是否已經(jīng)下載過,即驗證相應(yīng)文件是否已經(jīng)被創(chuàng)建:
fs.exists(filename, exists => ...
如果文件還沒有被下載,則執(zhí)行下列代碼進(jìn)行下載操作:
request(url, (err, response, body) => ...
然后,我們需要確定目錄下是否已經(jīng)包含了該文件:
mkdirp(path.dirname(filename), err => ...
最后,我們把HTTP請求返回的報文主體寫入文件系統(tǒng):
mkdirp(path.dirname(filename), err => ...
要完成我們的Web爬蟲應(yīng)用程序,只需提供一個URL作為輸入(在我們的例子中,我們從命令行參數(shù)中讀取它),我們只需調(diào)用spider()函數(shù)即可。
spider(process.argv[2], (err, filename, downloaded) => { if (err) { console.log(err); } else if (downloaded) { console.log(`Completed the download of "${filename}"`); } else { console.log(`"${filename}" was already downloaded`); } });
現(xiàn)在,我們開始嘗試運(yùn)行Web爬蟲應(yīng)用程序,但是首先,確保已有utilities.js模塊和package.json中的所有依賴包已經(jīng)安裝到你的項目中:
npm install
之后,我們執(zhí)行我們這個爬蟲模塊來下載一個網(wǎng)頁,使用以下命令:
node spider http://www.example.com
我們的Web爬蟲應(yīng)用程序要求在我們提供的URL中總是包含協(xié)議類型(例如,http://)。另外,不要期望HTML鏈接被重新編寫,也不要期望下載像圖片這樣的資源,因為這只是一個簡單的例子來演示異步編程是如何工作的。
回調(diào)地獄看看我們的spider()函數(shù),我們可以發(fā)現(xiàn),盡管我們實現(xiàn)的算法非常簡單,但是生成的代碼有幾個級別的縮進(jìn),而且很難讀懂。使用阻塞式的同步API實現(xiàn)類似的功能是很簡單的,而且很少有機(jī)會讓它看起來如此錯誤。然而,使用異步CPS是另一回事,使用閉包可能會導(dǎo)致出現(xiàn)難以閱讀的代碼。
大量閉包和回調(diào)將代碼轉(zhuǎn)換成不可讀的、難以管理的情況稱為回調(diào)地獄。它是Node.js中最受認(rèn)可和最嚴(yán)重的反模式之一。一般來說,對于JavaScript而言。受此問題影響的代碼的典型結(jié)構(gòu)如下:
asyncFoo(err => { asyncBar(err => { asyncFooBar(err => { //... }); }); });
我們可以看到,用這種方式編寫的代碼是如何形成金字塔形狀的,由于深嵌的原因?qū)е碌碾y以閱讀,稱為“末日金字塔”。
像前面的代碼片段這樣的代碼最明顯的問題是可讀性差。由于嵌套太深,幾乎不可能跟蹤回調(diào)函數(shù)的結(jié)束位置和另一個回調(diào)函數(shù)開始的位置。
另一個問題是由每個作用域中使用的變量名的重疊引起的。通常,我們必須使用類似甚至相同的名稱來描述變量的內(nèi)容。最好的例子是每個回調(diào)接收到的錯誤參數(shù)。有些人經(jīng)常嘗試使用相同名稱的變體來區(qū)分每個范圍內(nèi)的對象,例如,error、err、err1、err2等等。另一些人則傾向于隱藏在范圍中定義的變量,總是使用相同的名稱。例如,err。這兩種選擇都遠(yuǎn)非完美,而且會造成混淆,并增加導(dǎo)致bug的可能性。
此外,我們必須記住,雖然閉包在性能和內(nèi)存消耗方面的代價很小。此外,它們還可以創(chuàng)建不易識別的內(nèi)存泄漏,因為我們不應(yīng)該忘記,由閉包引用的任何上下文變量都不會被垃圾收集所保留。
關(guān)于對于V8的閉包工作原理,可以參考Vyacheslav Egorov的博客文章。
如果我們看一下我們的spider()函數(shù),我們會清楚地注意到它便是一個典型的回調(diào)地獄的場景,并且在這個函數(shù)中有我們剛才描述的所有問題。這正是我們將在本章中學(xué)習(xí)的模式和技巧所要解決的問題。
使用簡單的JavaScript既然我們已經(jīng)遇到了第一個回調(diào)地獄的例子,我們知道我們應(yīng)該避免什么。然而,在編寫異步代碼時,這并不是惟一的關(guān)注點。事實上,有幾種情況下,控制一組異步任務(wù)的流需要使用特定的模式和技術(shù),特別是如果我們只使用普通的JavaScript而沒有任何外部庫的幫助的情況下。例如,通過按順序應(yīng)用異步操作來遍歷集合并不像在數(shù)組中調(diào)用forEach()那樣簡單,但實際上它需要一種類似于遞歸的技術(shù)。
在本節(jié)中,我們將學(xué)習(xí)如何避免回調(diào)地獄,以及如何使用簡單的JavaScript實現(xiàn)一些最常見的控制流模式。
回調(diào)函數(shù)的準(zhǔn)則在編寫異步代碼時,要記住的第一個規(guī)則是在定義回調(diào)時不要濫用閉包。濫用閉包一時很爽,因為它不需要對諸如模塊化和可重用性這樣的問題進(jìn)行額外的思考。但是,我們已經(jīng)看到,這種做法弊大于利。大多數(shù)情況下,修復(fù)回調(diào)地獄問題并不需要任何庫、花哨的技術(shù)或范式的改變,只是一些常識。
以下是一些基本原則,可以幫助我們更少的嵌套,并改進(jìn)我們的代碼的組織:
盡可能退出外層函數(shù)。根據(jù)上下文,使用return、continue或break,以便立即退出當(dāng)前代碼塊,而不是使用if...else代碼塊。其他語句。這將有助于優(yōu)化我們的代碼結(jié)構(gòu)。
為回調(diào)創(chuàng)建命名函數(shù),避免使用閉包,并將中間結(jié)果作為參數(shù)傳遞。命名函數(shù)也會使它們在堆棧跟蹤中更優(yōu)雅。
代碼盡可能模塊化。并盡可能將代碼分成更小的、可重用的函數(shù)。
回調(diào)調(diào)用的準(zhǔn)則為了展示上述原則,我們通過重構(gòu)Web爬蟲應(yīng)用程序來說明。
對于第一步,我們可以通過刪除else語句來重構(gòu)我們的錯誤檢查方式。這是在我們收到錯誤后立即從函數(shù)中返回。因此,看以下代碼:
if (err) { callback(err); } else { // 如果沒有錯誤,執(zhí)行該代碼塊 }
我們可以通過編寫下面的代碼來改進(jìn)我們的代碼結(jié)構(gòu):
if (err) { return callback(err); } // 如果沒有錯誤,執(zhí)行該代碼塊
有了這個簡單的技巧,我們立即減少了函數(shù)的嵌套級別,它很簡單,不需要任何復(fù)雜的重構(gòu)。
在執(zhí)行我們剛才描述的優(yōu)化時,一個常見的錯誤是在調(diào)用回調(diào)函數(shù)之后忘記終止函數(shù),即return。對于錯誤處理場景,以下代碼是bug的典型來源:
if (err) { callback(err); } // 如果沒有錯誤,執(zhí)行該代碼塊
在這個例子中,即使在調(diào)用回調(diào)之后,函數(shù)的執(zhí)行也會繼續(xù)。那么避免這種情況的出現(xiàn),return語句是十分必要的。還要注意,函數(shù)返回的輸出是什么并不重要,實際結(jié)果(或錯誤)是異步生成的,并傳遞給回調(diào)。異步函數(shù)的返回值通常被忽略。該屬性允許我們編寫如下的代碼:
return callback(...);
否則我們必須拆成兩條語句來寫:
callback(...); return;
接下來我們繼續(xù)重構(gòu)我們的spider()函數(shù),我們可以嘗試識別可復(fù)用的代碼片段。例如,將給定字符串寫入文件的功能可以很容易地分解為一個多帶帶的函數(shù):
function saveFile(filename, contents, callback) { mkdirp(path.dirname(filename), err => { if (err) { return callback(err); } fs.writeFile(filename, contents, callback); }); }
遵循同樣的原則,我們可以創(chuàng)建一個名為download()的通用函數(shù),它將URL和文件名作為輸入,并將URL的內(nèi)容下載到給定的文件中。在內(nèi)部,我們可以使用前面創(chuàng)建的saveFile()函數(shù)。
function download(url, filename, callback) { console.log(`Downloading ${url}`); request(url, (err, response, body) => { if (err) { return callback(err); } saveFile(filename, body, err => { if (err) { return callback(err); } console.log(`Downloaded and saved: ${url}`); callback(null, body); }); }); }
最后,修改我們的spider()函數(shù):
function spider(url, callback) { const filename = utilities.urlToFilename(url); fs.exists(filename, exists => { if (exists) { return callback(null, filename, false); } download(url, filename, err => { if (err) { return callback(err); } callback(null, filename, true); }) }); }
spider()函數(shù)的功能和接口仍然是完全相同的,改變的僅僅是代碼的組織方式。通過應(yīng)用上述基本原則,我們能夠極大地減少代碼的嵌套,同時增加了它的可重用性和可測試性。實際上,我們可以考慮導(dǎo)出saveFile()和download(),這樣我們就可以在其他模塊中重用它們。這也使我們能夠更容易地測試他們的功能。
我們在這一節(jié)中進(jìn)行的重構(gòu)清楚地表明,大多數(shù)時候,我們所需要的只是一些規(guī)則,并確保我們不濫用閉包和匿名函數(shù)。它的工作非常出色,只需最少的工作量,并且只使用原始的JavaScript。
順序執(zhí)行現(xiàn)在開始探尋異步控制流的執(zhí)行順序,我們會通過開始分析一串異步代碼來探尋其控制流。
按順序執(zhí)行一組任務(wù)意味著一次一個接一個地運(yùn)行它們。執(zhí)行順序很重要,必須保證其正確性,因為列表中一個任務(wù)的結(jié)果可能會影響下一個任務(wù)的執(zhí)行。下圖說明了這個概念:
上述異步控制流有一些不同的變化:
按順序執(zhí)行一組已知任務(wù),無需鏈接或傳遞執(zhí)行結(jié)果
使用任務(wù)的輸出作為下一個輸入(也稱為chain,pipeline,或者waterfall)
在每個元素上運(yùn)行異步任務(wù)時迭代一個集合,一個元素接一個元素
對于順序執(zhí)行而言,盡管在使用直接樣式阻塞API實現(xiàn)很簡單,但通常情況下使用異步CPS時會導(dǎo)致回調(diào)地獄問題。
按順序執(zhí)行一組已知的任務(wù)在上一節(jié)中實現(xiàn)spider()函數(shù)時,我們已經(jīng)遇到了順序執(zhí)行的問題。通過研究如下方式,我們可以更好地控制異步代碼。以該代碼為準(zhǔn)則,我們可以用以下模式來解決上述問題:
function task1(callback) { asyncOperation(() => { task2(callback); }); } function task2(callback) { asyncOperation(result() => { task3(callback); }); } function task3(callback) { asyncOperation(() => { callback(); //finally executes the callback }); } task1(() => { //executed when task1, task2 and task3 are completed console.log("tasks 1, 2 and 3 executed"); });
上述模式顯示了在完成一個異步操作后,再調(diào)用下一個異步操作。該模式強(qiáng)調(diào)任務(wù)的模塊化,并且避免在處理異步代碼使用閉包。
順序迭代我們前面描述的模式如果我們預(yù)先知道要執(zhí)行什么和有多少個任務(wù),這些模式是完美的。這使我們能夠?qū)π蛄兄邢乱粋€任務(wù)的調(diào)用進(jìn)行硬編碼,但是如果要對集合中的每個項目執(zhí)行異步操作,會發(fā)生什么?在這種情況下,我們不能對任務(wù)序列進(jìn)行硬編碼。相反的是,我們必須動態(tài)構(gòu)建它。
為了顯示順序迭代的例子,讓我們?yōu)?b>Web爬蟲應(yīng)用程序引入一個新功能。我們現(xiàn)在想要遞歸地下載網(wǎng)頁中的所有鏈接。要做到這一點,我們將從頁面中提取所有鏈接,然后按順序逐個地觸發(fā)我們的Web爬蟲應(yīng)用程序。
第一步是修改我們的spider()函數(shù),以便通過調(diào)用一個名為spiderLinks()的函數(shù)觸發(fā)頁面所有鏈接的遞歸下載。
此外,我們現(xiàn)在嘗試讀取文件,而不是檢查文件是否已經(jīng)存在,并開始爬取其鏈接。這樣,我們就可以恢復(fù)中斷的下載。最后還有一個變化是,我們確保我們傳遞的參數(shù)是最新的,還要限制遞歸深度。結(jié)果代碼如下:
function spider(url, nesting, callback) { const filename = utilities.urlToFilename(url); fs.readFile(filename, "utf8", (err, body) => { if (err) { if (err.code! == "ENOENT") { return callback(err); } return download(url, filename, (err, body) => { if (err) { return callback(err); } spiderLinks(url, body, nesting, callback); }); } spiderLinks(url, body, nesting, callback); }); }
現(xiàn)在我們可以創(chuàng)建這個新版本的Web爬蟲應(yīng)用程序的核心,即spiderLinks()函數(shù),它使用順序異步迭代算法下載HTML頁面的所有鏈接。注意我們在下面的代碼塊中定義的方式:
function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); } let links = utilities.getPageLinks(currentUrl, body); //[1] function iterate(index) { //[2] if(index === links.length) { return callback(); } spider(links[index], nesting - 1, function(err) { //[3] if(err) { return callback(err); } iterate(index + 1); }); } iterate(0); //[4] }
從這個新功能中的重要步驟如下:
我們使用utilities.getPageLinks()函數(shù)獲取頁面中包含的所有鏈接的列表。此函數(shù)僅返回指向相同主機(jī)名的鏈接。
我們使用一個稱為iterate()的本地函數(shù)來遍歷鏈接,該函數(shù)需要下一個鏈接的索引進(jìn)行分析。在這個函數(shù)中,我們首先要檢查索引是否等于鏈接數(shù)組的長度,如果等于則是迭代完成,在這種情況下我們立即調(diào)用callback()函數(shù),因為這意味著我們處理了所有的項目。
這時,處理鏈接已準(zhǔn)備就緒。我們通過遞歸調(diào)用spider()函數(shù)。
作為spiderLinks()函數(shù)的最后一步也是最重要的一步,我們通過調(diào)用iterate(0)來開始迭代。
我們剛剛提出的算法允許我們通過順序執(zhí)行異步操作來迭代數(shù)組,在我們的例子中是spider()函數(shù)。
我們現(xiàn)在可以嘗試這個新版本的Web爬蟲應(yīng)用程序,并觀看它一個接一個地遞歸地下載網(wǎng)頁的所有鏈接。要中斷這個過程,如果有很多鏈接可能需要一段時間,請記住我們可以隨時使用Ctrl + C。如果我們決定恢復(fù)它,我們可以通過啟動Web爬蟲應(yīng)用程序并提供與上次結(jié)束時相同的URL來恢復(fù)執(zhí)行。
現(xiàn)在我們的網(wǎng)絡(luò)Web爬蟲應(yīng)用程序可能會觸發(fā)整個網(wǎng)站的下載,請仔細(xì)考慮使用它。例如,不要設(shè)置高嵌套級別或離開爬蟲運(yùn)行超過幾秒鐘。用數(shù)千個請求重載服務(wù)器是不道德的。在某些情況下,這也被認(rèn)為是非法的。需要考慮后果!
我們之前展示的spiderLinks()函數(shù)的代碼是一個清楚的例子,說明了如何在應(yīng)用異步操作時迭代集合。我們還可以注意到,這是一種可以適應(yīng)任何其他情況的模式,我們需要在集合的元素或通常的任務(wù)列表上按順序異步迭代。該模式可以推廣如下:
function iterate(index) { if (index === tasks.length) { return finish(); } const task = tasks[index]; task(function() { iterate(index + 1); }); } function finish() { // 迭代完成的操作 } iterate(0);
注意到,如果task()是同步操作,這些類型的算法變得真正遞歸。在這種情況下,可能造成調(diào)用棧的溢出。
我們剛剛提出的模式是非常強(qiáng)大的,因為它可以適應(yīng)幾種情況。例如,我們可以映射數(shù)組的值,或者我們可以將迭代的結(jié)果傳遞給迭代中的下一個,以實現(xiàn)一個reduce算法,如果滿足特定的條件,我們可以提前退出循環(huán),或者甚至可以迭代無限數(shù)量的元素。
我們還可以選擇將解決方案進(jìn)一步推廣:
iterateSeries(collection, iteratorCallback, finalCallback);
通過創(chuàng)建一個名為iterator的函數(shù)來執(zhí)行任務(wù)列表,該函數(shù)調(diào)用集合中的下一個可執(zhí)行的任務(wù),并確保在當(dāng)前任務(wù)完成時調(diào)用迭代器結(jié)束的回調(diào)函數(shù)。
并行在某些情況下,一組異步任務(wù)的執(zhí)行順序并不重要,我們只需要在所有這些運(yùn)行的任務(wù)完成時通知我們。使用并行執(zhí)行流更好地處理這種情況,如下圖所示:
如果我們認(rèn)為Node.js是單線程的話,這可能聽起來很奇怪,但是如果我們記住我們在第一章中討論過的內(nèi)容,我們意識到即使我們只有一個線程,我們?nèi)匀豢梢詫崿F(xiàn)并發(fā),由于Node.js的非阻塞性質(zhì)。實際上,在這種情況下,并行字不正確地使用,因為這并不意味著任務(wù)同時運(yùn)行,而是它們的執(zhí)行由底層的非阻塞API執(zhí)行,并由事件循環(huán)進(jìn)行交織。
我們知道,當(dāng)一個任務(wù)允許事件循環(huán)執(zhí)行另一個任務(wù)時,或者是說一個任務(wù)允許控制回到事件循環(huán)。這種工作流的名稱為并發(fā),但為了簡單起見,我們?nèi)匀粫褂貌⑿小?/p>
下圖顯示了兩個異步任務(wù)可以在Node.js程序中并行運(yùn)行:
通過上圖,我們有一個Main函數(shù)執(zhí)行兩個異步任務(wù):
Main函數(shù)觸發(fā)Task 1和Task 2的執(zhí)行。由于這些觸發(fā)異步操作,這兩個函數(shù)會立即返回,并將控制權(quán)返還給主函數(shù),之后等到事件循環(huán)完成再通知主線程。
當(dāng)Task 1的異步操作完成時,事件循環(huán)給與其線程控制權(quán)。當(dāng)Task 1同步操作完成時,它通知Main函數(shù)。
當(dāng)Task 2的異步操作完成時,事件循環(huán)給與其線程控制權(quán)。當(dāng)Task 2同步操作完成時,它再次通知Main函數(shù)。在這一點上,Main函數(shù)知曉Task 1和Task 2都已經(jīng)執(zhí)行完畢,所以它可以繼續(xù)執(zhí)行其后操作或?qū)⒉僮鞯慕Y(jié)果返回給另一個回調(diào)函數(shù)。
簡而言之,這意味著在Node.js中,我們只能執(zhí)行并行異步操作,因為它們的并發(fā)性由非阻塞API在內(nèi)部處理。在Node.js中,同步阻塞操作不能同時運(yùn)行,除非它們的執(zhí)行與異步操作交錯,或者通過setTimeout()或setImmediate()延遲。我們將在第九章中更詳細(xì)地看到這一點。
Web爬蟲版本3上邊的Web爬蟲在并行異步操作上似乎也算表現(xiàn)得很完美。到目前為止,應(yīng)用程序正在遞歸地執(zhí)行鏈接頁面的下載。但性能不是最佳的,想要提升這個應(yīng)用的性能很容易。
要做到這一點,我們只需要修改spiderLinks()函數(shù),確保spider()任務(wù)只執(zhí)行一次,當(dāng)所有任務(wù)都執(zhí)行完畢后,調(diào)用最后的回調(diào),所以我們對spiderLinks()做如下修改:
function spiderLinks(currentUrl, body, nesting, callback) { if (nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } let completed = 0, hasErrors = false; function done(err) { if (err) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { return callback(); } } links.forEach(link => { spider(link, nesting - 1, done); }); }
上述代碼有何變化?,現(xiàn)在spider()函數(shù)的任務(wù)全部同步啟動??梢酝ㄟ^簡單地遍歷鏈接數(shù)組和啟動每個任務(wù),我們不必等待前一個任務(wù)完成再進(jìn)行下一個任務(wù):
links.forEach(link => { spider(link, nesting - 1, done); });
然后,使我們的應(yīng)用程序知曉所有任務(wù)完成的方法是為spider()函數(shù)提供一個特殊的回調(diào)函數(shù),我們稱之為done()。當(dāng)爬蟲任務(wù)完成時,done()函數(shù)設(shè)定一個計數(shù)器。當(dāng)完成的下載次數(shù)達(dá)到鏈接數(shù)組的大小時,調(diào)用最終回調(diào):
function done(err) { if (err) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { callback(); } }
通過上述變化,如果我們現(xiàn)在試圖對網(wǎng)頁運(yùn)行我們的爬蟲,我們將注意到整個過程的速度有很大的改進(jìn),因為每次下載都是并行執(zhí)行的,而不必等待之前的鏈接被處理。
模式此外,對于并行執(zhí)行流程,我們可以提取我們方案,以便適應(yīng)于不同的情況提高代碼的可復(fù)用性。我們可以使用以下代碼來表示模式的通用版本:
const tasks = [ /* ... */ ]; let completed = 0; tasks.forEach(task => { task(() => { if (++completed === tasks.length) { finish(); } }); }); function finish() { // 所有任務(wù)執(zhí)行完成后調(diào)用 }
通過小的修改,我們可以調(diào)整模式,將每個任務(wù)的結(jié)果累積到一個list中,以便過濾或映射數(shù)組的元素,或者一旦完成了一個或一定數(shù)量的任務(wù)即可調(diào)用finish()回調(diào)。
用并發(fā)任務(wù)修復(fù)競爭條件注意:如果是沒有限制的情況下,并行執(zhí)行的一組異步任務(wù),然后等待所有異步任務(wù)完成后執(zhí)行回調(diào)這種方式,其方法是計算它們的執(zhí)行完成的數(shù)目。
當(dāng)使用阻塞I/O與多線程組合的方式時,并行運(yùn)行一組任務(wù)可能會導(dǎo)致一些問題。但是,我們剛剛看到,在Node.js中卻不一樣,并行運(yùn)行多個異步任務(wù)實際上在資源方面消耗較低。這是Node.js最重要的優(yōu)點之一,因此在Node.js中并行化成為一種常見的做法,而且這并是多么復(fù)雜的技術(shù)。
Node.js的并發(fā)模型的另一個重要特征是我們處理任務(wù)同步和競爭條件的方式。在多線程編程中,這通常使用諸如鎖,互斥條件,信號量和觀察器之類的構(gòu)造來實現(xiàn),這些是多線程語言并行化的最復(fù)雜的方面之一,對性能也有很大的影響。在Node.js中,我們通常不需要一個花哨的同步機(jī)制,因為所有運(yùn)行在單個線程上!但是,這并不意味著我們沒有競爭條件。相反,他們可以相當(dāng)普遍。問題的根源在于異步操作的調(diào)用與其結(jié)果通知之間的延遲。舉一個具體的例子,我們可以再次參考我們的Web爬蟲應(yīng)用程序,特別是我們創(chuàng)建的最后一個版本,其實際上包含一個競爭條件。
問題在于在開始下載相應(yīng)的URL的文檔之前,檢查文件是否已經(jīng)存在的spider()函數(shù):
function spider(url, nesting, callback) { if(spidering.has(url)) { return process.nextTick(callback); } spidering.set(url, true); const filename = utilities.urlToFilename(url); fs.readFile(filename, "utf8", function(err, body) { if(err) { if(err.code !== "ENOENT") { return callback(err); } return download(url, filename, function(err, body) { if(err) { return callback(err); } spiderLinks(url, body, nesting, callback); }); } spiderLinks(url, body, nesting, callback); }); }
現(xiàn)在的問題是,在同一個URL上操作的兩個爬蟲任務(wù)可能會在兩個任務(wù)之一完成下載并創(chuàng)建一個文件,導(dǎo)致第二個任務(wù)開始下載之前,在同一個文件上調(diào)用fs.readFile()的結(jié)果不對,致使下載兩次。這種情況如下圖所示:
上圖顯示了Task 1和Task 2如何在Node.js的單個線程中交錯執(zhí)行,以及異步操作如何實際引入競爭條件。在我們的情況下,兩個爬蟲任務(wù)最終會下載相同的文件。
我們?nèi)绾谓鉀Q這個問題?答案比我們想象的要簡單得多。實際上,我們所需要的只是一個變量(互斥變量),可以相互排除運(yùn)行在同一個URL上的多個spider()任務(wù)。這可以通過以下代碼來實現(xiàn):
const spidering = new Map(); function spider(url, nesting, callback) { if (spidering.has(url)) { return process.nextTick(callback); } spidering.set(url, true); // ... }并行執(zhí)行頻率限制
通常,如果不控制并行任務(wù)頻率,并行任務(wù)就會導(dǎo)致過載。想象一下,有數(shù)千個文件要讀取,訪問的URL或數(shù)據(jù)庫查詢并行運(yùn)行。在這種情況下,常見的問題是系統(tǒng)資源不足,例如,當(dāng)嘗試一次打開太多文件時,利用可用于應(yīng)用程序的所有文件描述符。在Web應(yīng)用程序中,它還可能會創(chuàng)建一個利用拒絕服務(wù)(DoS)攻擊的漏洞。在所有這種情況下,最好限制同時運(yùn)行的任務(wù)數(shù)量。這樣,我們可以為服務(wù)器的負(fù)載增加一些可預(yù)測性,并確保我們的應(yīng)用程序不會耗盡資源。下圖描述了一個情況,我們將五個任務(wù)并行運(yùn)行并發(fā)限制為兩段:
從上圖可以清楚我們的算法如何工作:
我們可以執(zhí)行盡可能多的任務(wù),而不超過并發(fā)限制。
每當(dāng)任務(wù)完成時,我們再執(zhí)行一個或多個任務(wù),同時確保任務(wù)數(shù)量達(dá)不到限制。
并發(fā)限制我們現(xiàn)在提出一種模式,以有限的并發(fā)性并行執(zhí)行一組給定的任務(wù):
const tasks = ... let concurrency = 2, running = 0, completed = 0, index = 0; function next() { while (running < concurrency && index < tasks.length) { task = tasks[index++]; task(() => { if (completed === tasks.length) { return finish(); } completed++, running--; next(); }); running++; } } next(); function finish() { // 所有任務(wù)執(zhí)行完成 }
該算法可以被認(rèn)為是順序執(zhí)行和并行執(zhí)行之間的混合。事實上,我們可能會注意到我們之前介紹的兩種模式的相似之處:
我們有一個迭代器函數(shù),我們稱之為next(),有一個內(nèi)部循環(huán),并行執(zhí)行盡可能多的任務(wù),同時保持并發(fā)限制。
我們傳遞給每個任務(wù)的回調(diào)檢查是否完成了列表中的所有任務(wù)。如果還有任務(wù)要運(yùn)行,它會調(diào)用next()來執(zhí)行下一個任務(wù)。
全局并發(fā)限制我們的Web爬蟲應(yīng)用程序非常適合應(yīng)用我們所學(xué)到的限制一組任務(wù)的并發(fā)性。事實上,為了避免同時爬上數(shù)千個鏈接的情況,我們可以通過在并發(fā)下載數(shù)量上增加一些措施來限制并發(fā)量。
0.11之前的Node.js版本已經(jīng)將每個主機(jī)的并發(fā)HTTP連接數(shù)限制為5.然而,這可以改變以適應(yīng)我們的需要。請查看官方文檔http://nodejs.org/docs/v0.10.... axsockets中的更多內(nèi)容。從Node.js 0.11開始,并發(fā)連接數(shù)沒有默認(rèn)限制。
我們可以將我們剛剛學(xué)到的模式應(yīng)用到我們的spiderLinks()函數(shù),但是我們將獲得的只是限制一個頁面中的一組鏈接的并發(fā)性。如果我們選擇了并發(fā)量為2,我們最多可以為每個頁面并行下載兩個鏈接。然而,由于我們可以一次下載多個鏈接,因此每個頁面都會產(chǎn)生另外兩個下載,這樣遞歸下去,其實也沒有完全做到并發(fā)量的限制。
使用隊列我們真正想要的是限制我們可以并行運(yùn)行的全局下載操作數(shù)量。我們可以略微修改之前展示的模式,但是我們寧愿把它作為一個練習(xí),因為我們想借此機(jī)會引入另一個機(jī)制,它利用隊列來限制多個任務(wù)的并發(fā)性。讓我們看看這是如何工作的。
我們現(xiàn)在要實現(xiàn)一個名為TaskQueue類,它將隊列與我們之前提到的算法相結(jié)合。我們創(chuàng)建一個名為taskQueue.js的新模塊:
class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.queue = []; } pushTask(task) { this.queue.push(task); this.next(); } next() { while (this.running < this.concurrency && this.queue.length) { const task = this.queue.shift(); task(() => { this.running--; this.next(); }); this.running++; } } };
上述類的構(gòu)造函數(shù)只作為輸入的并發(fā)限制,但除此之外,它初始化運(yùn)行和隊列的變量。前一個變量是用于跟蹤所有正在運(yùn)行的任務(wù)的計數(shù)器,而后者是將用作隊列以存儲待處理任務(wù)的數(shù)組。
pushTask()方法簡單地將新任務(wù)添加到隊列中,然后通過調(diào)用this.next()來引導(dǎo)任務(wù)的執(zhí)行。
next()方法從隊列中生成一組任務(wù),確保它不超過并發(fā)限制。
我們可能會注意到,這種方法與限制我們前面提到的并發(fā)性的模式有一些相似之處。它基本上從隊列開始盡可能多的任務(wù),而不超過并發(fā)限制。當(dāng)每個任務(wù)完成時,它會更新運(yùn)行任務(wù)的計數(shù),然后再次調(diào)用next()來啟動另一輪任務(wù)。 TaskQueue類的有趣屬性是它允許我們動態(tài)地將新的項目添加到隊列中。另一個優(yōu)點是,現(xiàn)在我們有一個中央實體負(fù)責(zé)限制我們?nèi)蝿?wù)的并發(fā)性,這可以在函數(shù)執(zhí)行的所有實例中共享。在我們的例子中,它是spider()函數(shù),我們將在稍后看到。
Web爬蟲版本4現(xiàn)在我們有一個通用的隊列來執(zhí)行有限的并行流程中的任務(wù),我們可以在我們的Web爬蟲應(yīng)用程序中直接使用它。我們首先加載新的依賴關(guān)系并通過將并發(fā)限制設(shè)置為2來創(chuàng)建TaskQueue類的新實例:
const TaskQueue = require("./taskQueue"); const downloadQueue = new TaskQueue(2);
接下來,我們使用新創(chuàng)建的downloadQueue更新spiderLinks()函數(shù):
function spiderLinks(currentUrl, body, nesting, callback) { if (nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } let completed = 0, hasErrors = false; links.forEach(link => { downloadQueue.pushTask(done => { spider(link, nesting - 1, err => { if (err) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { callback(); } done(); }); }); }); }
這個函數(shù)的這種新的實現(xiàn)是非常容易的,它與這本章前面提到的無限并行執(zhí)行的算法非常相似。這是因為我們將并發(fā)控制委托給TaskQueue對象,我們唯一要做的就是檢查所有任務(wù)是否完成??瓷鲜龃a中如何定義我們的任務(wù):
我們通過提供自定義回調(diào)來運(yùn)行spider()函數(shù)。
在回調(diào)中,我們檢查與spiderLinks()函數(shù)執(zhí)行相關(guān)的所有任務(wù)是否完成。當(dāng)這個條件為真時,我們調(diào)用spiderLinks()函數(shù)的最后回調(diào)。
在我們的任務(wù)結(jié)束時,我們調(diào)用了done()回調(diào),以便隊列可以繼續(xù)執(zhí)行。
在我們進(jìn)行這些小的變化之后,我們現(xiàn)在可以嘗試再次運(yùn)行Web爬蟲應(yīng)用程序。這一次,我們應(yīng)該注意到,同時不會有兩個以上的下載。
async庫如果我們到目前為止我們分析的每一個控制流程模式看一下,我們可以看到它們可以用作構(gòu)建可重用和更通用的解決方案的基礎(chǔ)。例如,我們可以將無限制的并行執(zhí)行算法包裝到一個接受任務(wù)列表的函數(shù)中,并行運(yùn)行它們,并且當(dāng)它們都完成時調(diào)用給定的回調(diào)函數(shù)。將控制流算法轉(zhuǎn)化為可重用功能的這種方式可以導(dǎo)致更具聲明性和表達(dá)性的方式來定義異步控制流,這正是async所做的。async庫是一個非常流行的解決方案,在Node.js和JavaScript中來說,用于處理異步代碼。它提供了一組功能,可以大大簡化不同配置中一組任務(wù)的執(zhí)行,并為異步處理集合提供了有用的幫助。即使有其他幾個具有相似目標(biāo)的庫,由于它的受歡迎程度,因此async是Node.js中的一個事實上的標(biāo)準(zhǔn)。
順序執(zhí)行async庫可以在實現(xiàn)復(fù)雜的異步控制流程時大大幫助我們,但是一個難題就是選擇正確的庫來解決問題。例如,對于順序執(zhí)行,有大約20個不同的函數(shù)可供選擇,包括eachSeries(), mapSeries(), filterSeries(), rejectSeries(), reduce(), reduceRight(), detectSeries(), concatSeries(), series(), whilst(), doWhilst(), until(), doUntil(), forever(), waterfall(), compose(), seq(), applyEachSeries(), iterator(), 和timesSeries()
。
選擇正確的函數(shù)是編寫更穩(wěn)固和可讀的代碼的重要一步,但這也需要一些經(jīng)驗和實踐。在我們的例子中,我們將僅介紹其中的一些情況,但它們?nèi)詫槔斫夂陀行У厥褂脦斓钠溆嗖糠痔峁﹫詫嵉幕A(chǔ)。
下面,通過例子說明async庫如何工作,我們將用于我們的Web爬蟲應(yīng)用程序。我們直接從版本2開始,按順序遞歸地下載所有的鏈接。
但是,首先我們確保將async庫安裝到我們當(dāng)前的項目中:
npm install async
然后我們需要從spider.js模塊加載新的依賴項:
const async = require("async");已知一組任務(wù)的順序執(zhí)行
我們先修改download()函數(shù)。如下所示,它依次做了以下三件事:
下載URL的內(nèi)容。
創(chuàng)建一個新目錄(如果尚不存在)。
將URL的內(nèi)容保存到文件中。
async.series()可以實現(xiàn)順序執(zhí)行一組任務(wù):
async.series(tasks, [callback])
async.series()接受一個任務(wù)列表和一個在所有任務(wù)完成后調(diào)用的回調(diào)函數(shù)作為參數(shù)。每個任務(wù)只是一個接受回調(diào)函數(shù)的函數(shù),當(dāng)任務(wù)完成執(zhí)行時,這個回調(diào)函數(shù)被調(diào)用:
function task(callback) {}
async的優(yōu)勢是它使用與Node.js相同的回調(diào)約定,它會自動處理錯誤傳播。所以,如果任何一個任務(wù)調(diào)用它的回調(diào)并且產(chǎn)生了一個錯誤,async將跳過列表中剩余的任務(wù),直接跳轉(zhuǎn)到最后的回調(diào)。
考慮到這一點,讓我們看看如何通過使用async來修改上述的download()函數(shù):
function download(url, filename, callback) { console.log(`Downloading ${url}`); let body; async.series([ callback => { request(url, (err, response, resBody) => { if (err) { return callback(err); } body = resBody; callback(); }); }, mkdirp.bind(null, path.dirname(filename)), callback => { fs.writeFile(filename, body, callback); } ], err => { if (err) { return callback(err); } console.log(`Downloaded and saved: ${url}`); callback(null, body); }); }
對比起這段代碼的回調(diào)地獄版本,使用async方式使我們能夠更好地組織我們的異步任務(wù)。并且不會嵌套回調(diào),因為我們只需要提供一個的任務(wù)列表,通常對于用于每個異步操作,然后異步任務(wù)將依次執(zhí)行:
首先是下載URL的內(nèi)容。我們將響應(yīng)體保存到一個閉包變量(body)中,以便它可以與其他任務(wù)共享。
創(chuàng)建并保存下載的頁面的目錄。我們通過執(zhí)行mkdirp()函數(shù)實現(xiàn),并和創(chuàng)建的目錄路徑綁定。這樣,我們可以節(jié)省幾行代碼并增加其可讀性。
最后,我們將下載的URL的內(nèi)容寫入文件。在這種情況下,我們無法執(zhí)行部分應(yīng)用程序(就像我們在第二個任務(wù)中所做的那樣),因為變量body只在系列中的下載任務(wù)完成后才可用。但是,通過將任務(wù)的回調(diào)直接傳遞到fs.writeFile()函數(shù),我們?nèi)匀豢梢酝ㄟ^利用異步的自動錯誤管理來保存一些代碼行。
4.完成所有任務(wù)后,將調(diào)用async.series()的最后回調(diào)。在我們的例子中,我們只是做一些錯誤管理,然后返回body變量來回調(diào)download()函數(shù)。
對于上述情況,async.series()的一個可替代的方法是async.waterfall(),它仍然按順序執(zhí)行任務(wù),但另外還提供每個任務(wù)的輸出作為下一個輸入。在我們的情況下,我們可以使用這個特征來傳播body變量直到序列結(jié)束。
順序迭代在前面講了如何按順序執(zhí)行一組任務(wù)。上面的例子async.series()來做到這一點。可以使用相同的功能來實現(xiàn)Web爬蟲版本2的spiderLinks()函數(shù)。然而,async為特定的情況提供了一個更合適的API,遍歷一個集合,這個API是async.eachSeries()。我們來使用它來重新實現(xiàn)我們的spiderLinks()函數(shù)(版本2,串行下載),如下所示:
function spiderLinks(currentUrl, body, nesting, callback) { if (nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } async.eachSeries(links, (link, callback) => { spider(link, nesting - 1, callback); }, callback); }
如果我們將使用async的上述代碼與使用純JavaScript模式實現(xiàn)的相同功能的代碼進(jìn)行比較,我們將注意到async在代碼組織和可讀性方面給我們帶來的巨大優(yōu)勢。
并行執(zhí)行async不具有處理并行流的功能,其中可以找到each(),map(),filter(),reject(),detect(),some(),every(),concat(),parallel(),applyEach()和times()。它們遵循與我們已經(jīng)看到的用于順序執(zhí)行的功能相同的邏輯,區(qū)別在于所提供的任務(wù)是并行執(zhí)行的。
為了證明這一點,我們可以嘗試應(yīng)用上述功能之一來實現(xiàn)我們的Web爬蟲應(yīng)用程序的第三版,即使用無限制的并行流程來執(zhí)行下載。
如果我們記住我們之前使用的代碼來實現(xiàn)spiderLinks()函數(shù)的順序版本,那么調(diào)整它使其并行工作就比較簡單:
function spiderLinks(currentUrl, body, nesting, callback) { // ... async.each(links, (link, callback) => { spider(link, nesting - 1, callback); }, callback); }
這個函數(shù)與我們用于順序下載的功能完全相同,但是使用的是async.each()而非async.eachSeries()。這清楚地表明了使用庫(例如async)抽象異步流的功能。代碼不再綁定到特定的執(zhí)行流程了,沒有專門為此寫的代碼。大多數(shù)只是應(yīng)用邏輯。
限制并行執(zhí)行如果你想知道async還可以用來限制并行任務(wù)的并發(fā)性,答案是肯定的。我們有一些我們可以使用的函數(shù),即eachLimit(),mapLimit(),parallelLimit(),queue()和cargo()。
我們試圖利用其中的一個來實現(xiàn)Web爬蟲應(yīng)用程序的第4版,以有限的并發(fā)性并行執(zhí)行鏈接的下載。幸運(yùn)的是,async有async.queue(),它的工作方式與本章前面創(chuàng)建的TaskQueue類似。 async.queue()函數(shù)創(chuàng)建一個新的隊列,它使用一個worker()函數(shù)來執(zhí)行一組具有指定并發(fā)限制的任務(wù):
const q = async.queue(worker, concurrency);
worker()函數(shù)作為輸入接收要運(yùn)行的任務(wù)和一個回調(diào)函數(shù)作為參數(shù),當(dāng)任務(wù)完成時執(zhí)行回調(diào):
function worker(task, callback);
我們應(yīng)該注意到在這個例子中 task 可以是任何類型,而不僅僅只能是函數(shù)。實際上, worker有責(zé)任以最適當(dāng)?shù)姆绞教幚砣蝿?wù)。新建任務(wù),可以通過q.push(task, callback)將任務(wù)添加到隊列中。一個任務(wù)處理完后,關(guān)聯(lián)一個任務(wù)的回調(diào)函數(shù)必須被worker調(diào)用。
現(xiàn)在,我們再次修改我們的代碼實現(xiàn)一個全面并行的有并發(fā)限制的執(zhí)行流,利用async.queue(),首先,我們需要創(chuàng)建一個隊列:
const downloadQueue = async.queue((taskData, callback) => { spider(taskData.link, taskData.nesting - 1, callback); }, 2);
代碼很簡單。我們正在創(chuàng)建一個并發(fā)限制為2的新隊列,讓一個工作人員只需使用與任務(wù)關(guān)聯(lián)的數(shù)據(jù)調(diào)用我們的spider()函數(shù)。接下來,我們實現(xiàn)spiderLinks()函數(shù):
function spiderLinks(currentUrl, body, nesting, callback) { if (nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } const completed = 0, hasErrors = false; links.forEach(function(link) { const taskData = { link: link, nesting: nesting }; downloadQueue.push(taskData, err => { if (err) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { callback(); } }); }); }
前面的代碼應(yīng)該看起來非常熟悉,因為它幾乎和使用TaskQueue對象來實現(xiàn)相同流程的代碼相同。此外,在這種情況下,要分析的重要部分是將新任務(wù)推入隊列的位置。在這一點上,我們確保我們傳遞一個回調(diào),使我們能夠檢查當(dāng)前頁面的所有下載任務(wù)是否完成,并最終調(diào)用最終回調(diào)。
辛虧有async.queue(),我們可以輕松地復(fù)制我們的TaskQueue對象的功能,再次證明了通過async,我們可以避免從頭開始編寫異步控制流模式,減少我們的工作量,代碼量更加簡潔。
總結(jié)在本章開始的時候,我們說Node.js的編程可能很難因為它的異步性,特別是對于以前在其他平臺上開發(fā)的人而言。然而,在本章中,我們展示了異步API如何可以從簡單原生JavaScript開始,從而為我們分析更復(fù)雜的技術(shù)奠定了基礎(chǔ)。然后我們看到,除了為每一種口味提供編程風(fēng)格,我們所掌握的工具確實是多樣化的,并為我們大部分的問題提供了很好的解決方案。例如,我們可以選擇async庫來簡化最常見的流程。
還有更為先進(jìn)的技術(shù),如Promise和Generator函數(shù),這將是下一章的重點。當(dāng)了解所有這些技術(shù)時,能夠根據(jù)需求選擇最佳解決方案,或者在同一個項目中使用多種技術(shù)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/89979.html
摘要:以下展示它是如何工作的函數(shù)使用構(gòu)造函數(shù)創(chuàng)建一個新的對象,并立即將其返回給調(diào)用者。在傳遞給構(gòu)造函數(shù)的函數(shù)中,我們確保傳遞給,這是一個特殊的回調(diào)函數(shù)。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專欄,之后的博文將在專欄同步: Encounter的掘金專欄 知乎專欄...
摘要:中的流十分強(qiáng)大,它對處理潛在的大文件提供了支持,也抽象了一些場景下的數(shù)據(jù)處理和傳遞。本文將會提供兩個在編寫基于流的工具時,私以為有些用的兩個。 Node.js中的流十分強(qiáng)大,它對處理潛在的大文件提供了支持,也抽象了一些場景下的數(shù)據(jù)處理和傳遞。正因為它如此好用,所以在實戰(zhàn)中我們常?;谒鼇砭帉懸恍┕ぞ?函數(shù)/庫 ,但往往又由于自己對流的某些特性的疏忽,導(dǎo)致寫出的 函數(shù)/庫 在一些情況會達(dá)...
摘要:階段是事件循環(huán)的第一階段習(xí)慣上往往都會設(shè)置數(shù)將回調(diào)函數(shù)添加到事件循環(huán)的階段的隊列中等待執(zhí)行。 后端知識點總結(jié)——NODE.JS(高級) 1.Node入門: 什么是: 針對網(wǎng)絡(luò)應(yīng)用開發(fā)的平臺主要特征: 基于Google的JavaScript運(yùn)行時引擎V8 擴(kuò)展了Node標(biāo)準(zhǔn)類庫: TCP,同步或異步文件管理,HTTP 為什么使用Node: 可以在服務(wù)器端運(yùn)行js: 現(xiàn)有前端團(tuán)隊可直...
摘要:在這樣的程序中,異步編程通常是有幫助的。最初是為了使異步編程簡單方便而設(shè)計的。在年設(shè)計時,人們已經(jīng)在瀏覽器中進(jìn)行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風(fēng)格。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Node.js 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)...
摘要:基礎(chǔ)的端到端的基準(zhǔn)測試顯示大約比快八倍。所謂單線程,就是指一次只能完成一件任務(wù)。在服務(wù)器端,異步模式甚至是唯一的模式,因為執(zhí)行環(huán)境是單線程的,如果允許同步執(zhí)行所有請求,服務(wù)器性能會急劇下降,很快就會失去響應(yīng)。 模塊 Node.js 提供了exports 和 require 兩個對象,其中 exports 是模塊公開的接口,require 用于從外部獲取一個模塊的接口,即所獲取模塊的 e...
閱讀 2957·2023-04-26 01:32
閱讀 1552·2021-09-13 10:37
閱讀 2288·2019-08-30 15:56
閱讀 1681·2019-08-30 14:00
閱讀 3057·2019-08-30 12:44
閱讀 1972·2019-08-26 12:20
閱讀 1070·2019-08-23 16:29
閱讀 3236·2019-08-23 14:44