摘要:事件多路復(fù)用器收集資源的事件并且把這些事件放入隊列中,直到事件被處理時都是阻塞狀態(tài)。最后,處理事件多路復(fù)用器返回的每個事件,此時,與系統(tǒng)資源相關(guān)聯(lián)的事件將被讀并且在整個操作中都是非阻塞的。
本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。
歡迎關(guān)注我的專欄,之后的博文將在專欄同步:
Encounter的掘金專欄
知乎專欄 Encounter的編程思考
segmentfault專欄 前端小站
Welcom to the Node.js Platform Node.js 的發(fā)展技術(shù)本身的發(fā)展
龐大的Node.js生態(tài)圈的發(fā)展
官方組織的維護
Node.js的特點 小模塊以package的形式盡可能多的復(fù)用模塊,原則上每個模塊的容量盡量小而精。
原則:
"Small is beautiful" ---小而精
"Make each program do one thing well" ---單一職責(zé)原則
因此,一個Node.js應(yīng)用由多個包搭建而成,包管理器(npm)的管理使得他們相互依賴而不起沖突。
如果設(shè)計一個Node.js的模塊,盡可能做到以下三點:
易于理解和使用
易于測試和維護
考慮到對客戶端(瀏覽器)的支持更友好
以及,Don"t Repeat Yourself(DRY)復(fù)用性原則。
以接口形式提供每個Node.js模塊都是一個函數(shù)(類也是以構(gòu)造函數(shù)的形式呈現(xiàn)),我們只需要調(diào)用相關(guān)API即可,而不需要知道其它模塊的實現(xiàn)。Node.js模塊是為了使用它們而創(chuàng)建,不僅僅是在拓展性上,更要考慮到維護性和可用性。
簡單且實用“簡單就是終極的復(fù)雜” ————達爾文
遵循KISS(Keep It Simple, Stupid)原則,即優(yōu)秀的簡潔的設(shè)計,能夠更有效地傳遞信息。
設(shè)計必須很簡單,無論在實現(xiàn)還是接口上,更重要的是實現(xiàn)比接口更簡單,簡單是重要的設(shè)計原則。
我們做一個設(shè)計簡單,功能完備,而不是完美的軟件:
實現(xiàn)起來需要更少的努力
允許用更少的速度進行更快的運輸資源
具有伸縮性,更易于維護和理解
促進社區(qū)貢獻,允許軟件本身的成長和改進
而對于Node.js而言,因為其支持JavaScript,簡單和函數(shù)、閉包、對象等特性,可取代復(fù)雜的面向?qū)ο蟮念愓Z法。如單例模式和裝飾者模式,它們在面向?qū)ο蟮恼Z言都需要很復(fù)雜的實現(xiàn),而對于JavaScript則較為簡單。
介紹Node.js 6 和 ES2015的新語法 let和const關(guān)鍵字ES5之前,只有函數(shù)和全局作用域。
if (false) { var x = "hello"; } console.log(x); // undefined
現(xiàn)在用let,創(chuàng)建詞法作用域,則會報出一個錯誤Uncaught ReferenceError: x is not defined
if (false) { let x = "hello"; } console.log(x);
在循環(huán)語句中使用let,也會報錯Uncaught ReferenceError: i is not defined:
for (let i = 0; i < 10; i++) { // do something here } console.log(i);
使用let和const關(guān)鍵字,可以讓代碼更安全,如果意外的訪問另一個作用域的變量,更容易發(fā)現(xiàn)錯誤。
使用const關(guān)鍵字聲明變量,變量不會被意外更改。
const x = "This will never change"; x = "...";
這里會報出一個錯誤Uncaught TypeError: Assignment to constant variable.
但是對于對象屬性的更改,const顯得毫無辦法:
const x = {}; x.name = "John";
上述代碼并不會報錯
但是如果直接更改對象,還是會拋出一個錯誤。
const x = {}; x = null;
實際運用中,我們使用const引入模塊,防止意外被更改:
const path = require("path"); let path = "./some/path";
上述代碼會報錯,提醒我們意外更改了模塊。
如果需要創(chuàng)建不可變對象,只是簡單的使用const是不夠的,需要使用Object.freeze()或deep-freeze
我看了一下源碼,其實很少,就是遞歸使用Object.freeze()
module.exports = function deepFreeze (o) { Object.freeze(o); Object.getOwnPropertyNames(o).forEach(function (prop) { if (o.hasOwnProperty(prop) && o[prop] !== null && (typeof o[prop] === "object" || typeof o[prop] === "function") && !Object.isFrozen(o[prop])) { deepFreeze(o[prop]); } }); return o; };箭頭函數(shù)
箭頭函數(shù)更易于理解,特別是在我們定義回調(diào)的時候:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(function(x) { return x % 2 === 0; });
使用箭頭函數(shù)語法,更簡潔:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(x => x % 2 === 0);
如果不止一個return語句則使用=> {}
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter((x) => { if (x % 2 === 0) { console.log(x + " is even"); return true; } });
最重要是,箭頭函數(shù)綁定了它的詞法作用域,其this與父級代碼塊的this相同。
function DelayedGreeter(name) { this.name = name; } DelayedGreeter.prototype.greet = function() { setTimeout(function cb() { console.log("Hello" + this.name); }, 500); } const greeter = new DelayedGreeter("World"); greeter.greet(); // "Hello"
要解決這個問題,使用箭頭函數(shù)或bind
function DelayedGreeter(name) { this.name = name; } DelayedGreeter.prototype.greet = function() { setTimeout(function cb() { console.log("Hello" + this.name); }.bind(this), 500); } const greeter = new DelayedGreeter("World"); greeter.greet(); // "HelloWorld"
或者箭頭函數(shù),與父級代碼塊作用域相同:
function DelayedGreeter(name) { this.name = name; } DelayedGreeter.prototype.greet = function() { setTimeout(() => console.log("Hello" + this.name), 500); } const greeter = new DelayedGreeter("World"); greeter.greet(); // "HelloWorld"類語法糖
class是原型繼承的語法糖,對于來自傳統(tǒng)的面向?qū)ο笳Z言的所有開發(fā)人員(如Java和C#)來說更熟悉,新語法并沒有改變JavaScript的運行特征,通過原型來完成更加方便和易讀。
傳統(tǒng)的通過構(gòu)造器 + 原型的寫法:
function Person(name, surname, age) { this.name = name; this.surname = surname; this.age = age; } Person.prototype.getFullName = function() { return this.name + "" + this.surname; } Person.older = function(person1, person2) { return (person1.age >= person2.age) ? person1 : person2; }
使用class語法顯得更加簡潔、方便、易懂:
class Person { constructor(name, surname, age) { this.name = name; this.surname = surname; this.age = age; } getFullName() { return this.name + "" + this.surname; } static older(person1, person2) { return (person1.age >= person2.age) ? person1 : person2; } }
但是上面的實現(xiàn)是可以互換的,但是,對于class語法來說,最有意義的是extends和super關(guān)鍵字。
class PersonWithMiddlename extends Person { constructor(name, middlename, surname, age) { super(name, surname, age); this.middlename = middlename; } getFullName() { return this.name + "" + this.middlename + "" + this.surname; } }
這個例子是真正的面向?qū)ο蟮姆绞?,我們聲明了一個希望被繼承的類,定義新的構(gòu)造器,并可以使用super關(guān)鍵字調(diào)用父構(gòu)造器,并重寫getFullName方法,使得其支持middlename。
對象字面量的新語法 允許缺省值:const x = 22; const y = 17; const obj = { x, y };允許省略方法名
module.exports = { square(x) { return x * x; }, cube(x) { return x * x * x; }, };key的計算屬性
const namespace = "-webkit-"; const style = { [namespace + "box-sizing"]: "border-box", [namespace + "box-shadow"]: "10px 10px 5px #888", };新的定義getter和setter方式
const person = { name: "George", surname: "Boole", get fullname() { return this.name + " " + this.surname; }, set fullname(fullname) { let parts = fullname.split(" "); this.name = parts[0]; this.surname = parts[1]; } }; console.log(person.fullname); // "George Boole" console.log(person.fullname = "Alan Turing"); // "Alan Turing" console.log(person.name); // "Alan"
這里,第二個console.log觸發(fā)了set方法。
模板字符串 其它ES2015語法函數(shù)默認(rèn)參數(shù)
剩余參數(shù)語法
拓展運算符
解構(gòu)賦值
new.target
代理
反射
Symbol
reactor模式reactor模式是Node.js異步編程的核心模塊,其核心概念是:單線程、非阻塞I/O,通過下列例子可以看到reactor模式在Node.js平臺的體現(xiàn)。
I/O是緩慢的在計算機的基本操作中,輸入輸出肯定是最慢的。訪問內(nèi)存的速度是納秒級(10e-9 s),同時訪問磁盤上的數(shù)據(jù)或訪問網(wǎng)絡(luò)上的數(shù)據(jù)則更慢,是毫秒級(10e-3 s)。內(nèi)存的傳輸速度一般認(rèn)為是GB/s來計算,然而磁盤或網(wǎng)絡(luò)的訪問速度則比較慢,一般是MB/s。雖然對于CPU而言,I/O操作的資源消耗并不算大,但是在發(fā)送I/O請求和操作完成之間總會存在時間延遲。除此之外,我們還必須考慮人為因素,通常情況下,應(yīng)用程序的輸入是人為產(chǎn)生的,例如:按鈕的點擊、即時聊天工具的信息發(fā)送。因此,輸入輸出的速度并不因網(wǎng)絡(luò)和磁盤訪問速率慢造成的,還有多方面的因素。
阻塞I/O在一個阻塞I/O模型的進程中,I/O請求會阻塞之后代碼塊的運行。在I/O請求操作完成之前,線程會有一段不定長的時間浪費。(它可能是毫秒級的,但甚至有可能是分鐘級的,如用戶按著一個按鍵不放的情況)。以下例子就是一個阻塞I/O模型。
// 直到請求完成,數(shù)據(jù)可用,線程都是阻塞的 data = socket.read(); // 請求完成,數(shù)據(jù)可用 print(data);
我們知道,阻塞I/O的服務(wù)器模型并不能在一個線程中處理多個連接,每次I/O都會阻塞其它連接的處理。出于這個原因,對于每個需要處理的并發(fā)連接,傳統(tǒng)的web服務(wù)器的處理方式是新開一個新的進程或線程(或者從線程池中重用一個進程)。這樣,當(dāng)一個線程因 I/O操作被阻塞時,它并不會影響另一個線程的可用性,因為他們是在彼此獨立的線程中處理的。
通過下面這張圖:
通過上面的圖片我們可以看到每個線程都有一段時間處于空閑等待狀態(tài),等待從關(guān)聯(lián)連接接收新數(shù)據(jù)。如果所有種類的I/O操作都會阻塞后續(xù)請求。例如,連接數(shù)據(jù)庫和訪問文件系統(tǒng),現(xiàn)在我們能很快知曉一個線程需要因等待I/O操作的結(jié)果等待許多時間。不幸的是,一個線程所持有的CPU資源并不廉價,它需要消耗內(nèi)存、造成CPU上下文切換,因此,長期占有CPU而大部分時間并沒有使用的線程,在資源利用率上考慮,并不是高效的選擇。
非阻塞I/O除阻塞I/O之外,大部分現(xiàn)代的操作系統(tǒng)支持另外一種訪問資源的機制,即非阻塞I/O。在這種機制下,后續(xù)代碼塊不會等到I/O請求數(shù)據(jù)的返回之后再執(zhí)行。如果當(dāng)前時刻所有數(shù)據(jù)都不可用,函數(shù)會先返回預(yù)先定義的常量值(如undefined),表明當(dāng)前時刻暫無數(shù)據(jù)可用。
例如,在Unix操作系統(tǒng)中,fcntl()函數(shù)操作一個已存在的文件描述符,改變其操作模式為非阻塞I/O(通過O_NONBLOCK狀態(tài)字)。一旦資源是非阻塞模式,如果讀取文件操作沒有可讀取的數(shù)據(jù),或者如果寫文件操作被阻塞,讀操作或?qū)懖僮鞣祷?b>-1和EAGAIN錯誤。
非阻塞I/O最基本的模式是通過輪詢獲取數(shù)據(jù),這也叫做忙-等模型??聪旅孢@個例子,通過非阻塞I/O和輪詢機制獲取I/O的結(jié)果。
resources = [socketA, socketB, pipeA]; while(!resources.isEmpty()) { for (i = 0; i < resources.length; i++) { resource = resources[i]; // 進行讀操作 let data = resource.read(); if (data === NO_DATA_AVAILABLE) { // 此時還沒有數(shù)據(jù) continue; } if (data === RESOURCE_CLOSED) { // 資源被釋放,從隊列中移除該鏈接 resources.remove(i); } else { consumeData(data); } } }
我們可以看到,通過這個簡單的技術(shù),已經(jīng)可以在一個線程中處理不同的資源了,但依然不是高效的。事實上,在前面的例子中,用于迭代資源的循環(huán)只會消耗寶貴的CPU,而這些資源的浪費比起阻塞I/O反而更不可接受,輪詢算法通常浪費大量CPU時間。
事件多路復(fù)用對于獲取非阻塞的資源而言,忙-等模型不是一個理想的技術(shù)。但是幸運的是,大多數(shù)現(xiàn)代的操作系統(tǒng)提供了一個原生的機制來處理并發(fā),非阻塞資源(同步事件多路復(fù)用器)是一個有效的方法。這種機制被稱作事件循環(huán)機制,這種事件收集和I/O隊列源于發(fā)布-訂閱模式。事件多路復(fù)用器收集資源的I/O事件并且把這些事件放入隊列中,直到事件被處理時都是阻塞狀態(tài)??聪旅孢@個偽代碼:
socketA, pipeB; wachedList.add(socketA, FOR_READ); wachedList.add(pipeB, FOR_READ); while(events = demultiplexer.watch(wachedList)) { // 事件循環(huán) foreach(event in events) { // 這里并不會阻塞,并且總會有返回值(不管是不是確切的值) data = event.resource.read(); if (data === RESOURCE_CLOSED) { // 資源已經(jīng)被釋放,從觀察者隊列移除 demultiplexer.unwatch(event.resource); } else { // 成功拿到資源,放入緩沖池 consumeData(data); } } }
事件多路復(fù)用的三個步驟:
資源被添加到一個數(shù)據(jù)結(jié)構(gòu)中,為每個資源關(guān)聯(lián)一個特定的操作,在這個例子中是read。
事件通知器由一組被觀察的資源組成,一旦事件即將觸發(fā),會調(diào)用同步的watch函數(shù),并返回這個可被處理的事件。
最后,處理事件多路復(fù)用器返回的每個事件,此時,與系統(tǒng)資源相關(guān)聯(lián)的事件將被讀并且在整個操作中都是非阻塞的。直到所有事件都被處理完時,事件多路復(fù)用器會再次阻塞,然后重復(fù)這個步驟,以上就是event loop。
上圖可以很好的幫助我們理解在一個單線程的應(yīng)用程序中使用同步的時間多路復(fù)用器和非阻塞I/O實現(xiàn)并發(fā)。我們能夠看到,只使用一個線程并不會影響我們處理多個I/O任務(wù)的性能。同時,我們看到任務(wù)是在單個線程中隨著時間的推移而展開的,而不是分散在多個線程中。我們看到,在單線程中傳播的任務(wù)相對于多線程中傳播的任務(wù)反而節(jié)約了線程的總體空閑時間,并且更利于程序員編寫代碼。在這本書中,你可以看到我們可以用更簡單的并發(fā)策略,因為不需要考慮多線程的互斥和同步問題。
在下一章中,我們有更多機會討論Node.js的并發(fā)模型。
介紹reactor模式現(xiàn)在來說reactor模式,它通過一種特殊的算法設(shè)計的處理程序(在Node.js中是使用一個回調(diào)函數(shù)表示),一旦事件產(chǎn)生并在事件循環(huán)中被處理,那么相關(guān)handler將會被調(diào)用。
它的結(jié)構(gòu)如圖所示:
reactor模式的步驟為:
應(yīng)用程序通過提交請求到時間多路復(fù)用器產(chǎn)生一個新的I/O操作。應(yīng)用程序指定handler,handler 在操作完成后被調(diào)用。提交請求到事件多路復(fù)用器是非阻塞的,其調(diào)用所以會立馬返回,將執(zhí)行權(quán)返回給應(yīng)用程序。
當(dāng)一組I/O操作完成,事件多路復(fù)用器會將這些新事件添加到事件循環(huán)隊列中。
此時,事件循環(huán)會迭代事件循環(huán)隊列中的每個事件。
對于每個事件,對應(yīng)的handler被處理。
handler,是應(yīng)用程序代碼的一部分,handler執(zhí)行結(jié)束后執(zhí)行權(quán)會交回事件循環(huán)。但是,在handler 執(zhí)行時可能請求新的異步操作,從而新的操作被添加到事件多路復(fù)用器。
當(dāng)事件循環(huán)隊列的全部事件被處理完后,循環(huán)會在事件多路復(fù)用器再次阻塞直到有一個新的事件可處理觸發(fā)下一次循環(huán)。
我們現(xiàn)在可以定義Node.js的核心模式:
模式(反應(yīng)器)阻塞處理I/O到在一組觀察的資源有新的事件可處理,然后以分派每個事件對應(yīng)handler的方式反應(yīng)。
OS的非阻塞I/O引擎每個操作系統(tǒng)對于事件多路復(fù)用器有其自身的接口,Linux是epoll,Mac OSX是kqueue,Windows的IOCP API。除外,即使在相同的操作系統(tǒng)中,每個I/O操作對于不同的資源表現(xiàn)不一樣。例如,在Unix下,普通文件系統(tǒng)不支持非阻塞操作,所以,為了模擬非阻塞行為,需要使用在事件循環(huán)外用一個獨立的線程。所有這些平臺內(nèi)和跨平臺的不一致性需要在事件多路復(fù)用器的上層做抽象。這就是為什么Node.js為了兼容所有主流平臺而
編寫C語言庫libuv,目的就是為了使得Node.js兼容所有主流平臺和規(guī)范化不同類型資源的非阻塞行為。libuv今天作為Node.js的I/O引擎的底層。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/89981.html
摘要:第篇簡單異構(gòu)系統(tǒng)之微服務(wù)一大致介紹因為在后面要利用集成異構(gòu)系統(tǒng),所以才有了本章節(jié)的微服務(wù)本章節(jié)使用了最簡單的請求截取的方式,截取不同的后綴做不同的響應(yīng)處理,簡直二實現(xiàn)步驟添加服務(wù)端文件引入模塊創(chuàng)建獲得請求的路徑訪問,將會返回歡迎 SpringCloud(第 026 篇)簡單異構(gòu)系統(tǒng)之 nodejs 微服務(wù) - 一、大致介紹 1、因為在后面要利用 SpringCloud 集成異構(gòu)系統(tǒng),所...
摘要:消息推送也是微信公眾號開發(fā)更為有趣的功能,涉及到文本消息圖片消息語音消息視頻消息音樂消息以及圖文消息。在文件中創(chuàng)建文件用于消息的管理。 一、寫在前面的話 ??當(dāng)用戶發(fā)送消息給公眾號時(或某些特定的用戶操作引發(fā)的事件推送時),會產(chǎn)生一個POST請求,開發(fā)者可以在響應(yīng)包(Get)中返回特定XML結(jié)構(gòu),來對該消息進行響應(yīng)。 ??消息推送也是微信公眾號開發(fā)更為有趣的功能,涉及到文本消息、圖片消...
摘要:隨著業(yè)務(wù)的爆發(fā),團隊人數(shù)迅速增長起來,團隊名也從前端開發(fā)部改名成體驗技術(shù)部,意在體現(xiàn)前端工程師的核心競爭力用技術(shù)解決產(chǎn)品體驗問題。前后端分離的研發(fā)模式在社區(qū)流行起來,體驗技術(shù)部最先實踐的是基于的應(yīng)用層方案。2008 年對中國人是復(fù)雜的一年,冰災(zāi),大地震,奧運會接踵而至。對玉伯來說也一樣,趕在奧運會排查臨時人口之前,玉伯從北京中科院軟件所離開,憑著自己幾年來在程序開發(fā)上的經(jīng)歷和對新興前端行業(yè)的...
摘要:異步最佳實踐避免回調(diào)地獄前端掘金本文涵蓋了處理異步操作的一些工具和技術(shù)和異步函數(shù)。 Nodejs 連接各種數(shù)據(jù)庫集合例子 - 后端 - 掘金Cassandra Module: cassandra-driver Installation ... 編寫 Node.js Rest API 的 10 個最佳實踐 - 前端 - 掘金全文共 6953 字,讀完需 8 分鐘,速讀需 2 分鐘。翻譯自...
摘要:一默認(rèn)使用的模塊化方案,默認(rèn)是的模塊化方案,兩者有本質(zhì)區(qū)別。的去尋找引入的依賴時,如果是自帶的模塊,比如文件模塊,只需要填寫即可。這是版本入口文件使用了兩個路由器路由,分別處理和請求邏輯。核心操作全部依賴模型對象來執(zhí)行。 一、Node.js默認(rèn)使用commonJs的模塊化方案,TypeScript默認(rèn)是ES6的模塊化方案,兩者有本質(zhì)區(qū)別。 1.Node.js的去尋找引入的依賴時,如果...
閱讀 2246·2019-08-30 15:53
閱讀 2477·2019-08-30 12:54
閱讀 1257·2019-08-29 16:09
閱讀 748·2019-08-29 12:14
閱讀 777·2019-08-26 10:33
閱讀 2513·2019-08-23 18:36
閱讀 2981·2019-08-23 18:30
閱讀 2142·2019-08-22 17:09