摘要:事件多路復用器收集資源的事件并且把這些事件放入隊列中,直到事件被處理時都是阻塞狀態。最后,處理事件多路復用器返回的每個事件,此時,與系統資源相關聯的事件將被讀并且在整個操作中都是非阻塞的。
本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。
歡迎關注我的專欄,之后的博文將在專欄同步:
Encounter的掘金專欄
知乎專欄 Encounter的編程思考
segmentfault專欄 前端小站
Welcom to the Node.js Platform Node.js 的發展技術本身的發展
龐大的Node.js生態圈的發展
官方組織的維護
Node.js的特點 小模塊以package的形式盡可能多的復用模塊,原則上每個模塊的容量盡量小而精。
原則:
"Small is beautiful" ---小而精
"Make each program do one thing well" ---單一職責原則
因此,一個Node.js應用由多個包搭建而成,包管理器(npm)的管理使得他們相互依賴而不起沖突。
如果設計一個Node.js的模塊,盡可能做到以下三點:
易于理解和使用
易于測試和維護
考慮到對客戶端(瀏覽器)的支持更友好
以及,Don"t Repeat Yourself(DRY)復用性原則。
以接口形式提供每個Node.js模塊都是一個函數(類也是以構造函數的形式呈現),我們只需要調用相關API即可,而不需要知道其它模塊的實現。Node.js模塊是為了使用它們而創建,不僅僅是在拓展性上,更要考慮到維護性和可用性。
簡單且實用“簡單就是終極的復雜” ————達爾文
遵循KISS(Keep It Simple, Stupid)原則,即優秀的簡潔的設計,能夠更有效地傳遞信息。
設計必須很簡單,無論在實現還是接口上,更重要的是實現比接口更簡單,簡單是重要的設計原則。
我們做一個設計簡單,功能完備,而不是完美的軟件:
實現起來需要更少的努力
允許用更少的速度進行更快的運輸資源
具有伸縮性,更易于維護和理解
促進社區貢獻,允許軟件本身的成長和改進
而對于Node.js而言,因為其支持JavaScript,簡單和函數、閉包、對象等特性,可取代復雜的面向對象的類語法。如單例模式和裝飾者模式,它們在面向對象的語言都需要很復雜的實現,而對于JavaScript則較為簡單。
介紹Node.js 6 和 ES2015的新語法 let和const關鍵字ES5之前,只有函數和全局作用域。
if (false) { var x = "hello"; } console.log(x); // undefined
現在用let,創建詞法作用域,則會報出一個錯誤Uncaught ReferenceError: x is not defined
if (false) { let x = "hello"; } console.log(x);
在循環語句中使用let,也會報錯Uncaught ReferenceError: i is not defined:
for (let i = 0; i < 10; i++) { // do something here } console.log(i);
使用let和const關鍵字,可以讓代碼更安全,如果意外的訪問另一個作用域的變量,更容易發現錯誤。
使用const關鍵字聲明變量,變量不會被意外更改。
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";
上述代碼會報錯,提醒我們意外更改了模塊。
如果需要創建不可變對象,只是簡單的使用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; };箭頭函數
箭頭函數更易于理解,特別是在我們定義回調的時候:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(function(x) { return x % 2 === 0; });
使用箭頭函數語法,更簡潔:
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; } });
最重要是,箭頭函數綁定了它的詞法作用域,其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"
要解決這個問題,使用箭頭函數或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"
或者箭頭函數,與父級代碼塊作用域相同:
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是原型繼承的語法糖,對于來自傳統的面向對象語言的所有開發人員(如Java和C#)來說更熟悉,新語法并沒有改變JavaScript的運行特征,通過原型來完成更加方便和易讀。
傳統的通過構造器 + 原型的寫法:
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; } }
但是上面的實現是可以互換的,但是,對于class語法來說,最有意義的是extends和super關鍵字。
class PersonWithMiddlename extends Person { constructor(name, middlename, surname, age) { super(name, surname, age); this.middlename = middlename; } getFullName() { return this.name + "" + this.middlename + "" + this.surname; } }
這個例子是真正的面向對象的方式,我們聲明了一個希望被繼承的類,定義新的構造器,并可以使用super關鍵字調用父構造器,并重寫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觸發了set方法。
模板字符串 其它ES2015語法函數默認參數
剩余參數語法
拓展運算符
解構賦值
new.target
代理
反射
Symbol
reactor模式reactor模式是Node.js異步編程的核心模塊,其核心概念是:單線程、非阻塞I/O,通過下列例子可以看到reactor模式在Node.js平臺的體現。
I/O是緩慢的在計算機的基本操作中,輸入輸出肯定是最慢的。訪問內存的速度是納秒級(10e-9 s),同時訪問磁盤上的數據或訪問網絡上的數據則更慢,是毫秒級(10e-3 s)。內存的傳輸速度一般認為是GB/s來計算,然而磁盤或網絡的訪問速度則比較慢,一般是MB/s。雖然對于CPU而言,I/O操作的資源消耗并不算大,但是在發送I/O請求和操作完成之間總會存在時間延遲。除此之外,我們還必須考慮人為因素,通常情況下,應用程序的輸入是人為產生的,例如:按鈕的點擊、即時聊天工具的信息發送。因此,輸入輸出的速度并不因網絡和磁盤訪問速率慢造成的,還有多方面的因素。
阻塞I/O在一個阻塞I/O模型的進程中,I/O請求會阻塞之后代碼塊的運行。在I/O請求操作完成之前,線程會有一段不定長的時間浪費。(它可能是毫秒級的,但甚至有可能是分鐘級的,如用戶按著一個按鍵不放的情況)。以下例子就是一個阻塞I/O模型。
// 直到請求完成,數據可用,線程都是阻塞的 data = socket.read(); // 請求完成,數據可用 print(data);
我們知道,阻塞I/O的服務器模型并不能在一個線程中處理多個連接,每次I/O都會阻塞其它連接的處理。出于這個原因,對于每個需要處理的并發連接,傳統的web服務器的處理方式是新開一個新的進程或線程(或者從線程池中重用一個進程)。這樣,當一個線程因 I/O操作被阻塞時,它并不會影響另一個線程的可用性,因為他們是在彼此獨立的線程中處理的。
通過下面這張圖:
通過上面的圖片我們可以看到每個線程都有一段時間處于空閑等待狀態,等待從關聯連接接收新數據。如果所有種類的I/O操作都會阻塞后續請求。例如,連接數據庫和訪問文件系統,現在我們能很快知曉一個線程需要因等待I/O操作的結果等待許多時間。不幸的是,一個線程所持有的CPU資源并不廉價,它需要消耗內存、造成CPU上下文切換,因此,長期占有CPU而大部分時間并沒有使用的線程,在資源利用率上考慮,并不是高效的選擇。
非阻塞I/O除阻塞I/O之外,大部分現代的操作系統支持另外一種訪問資源的機制,即非阻塞I/O。在這種機制下,后續代碼塊不會等到I/O請求數據的返回之后再執行。如果當前時刻所有數據都不可用,函數會先返回預先定義的常量值(如undefined),表明當前時刻暫無數據可用。
例如,在Unix操作系統中,fcntl()函數操作一個已存在的文件描述符,改變其操作模式為非阻塞I/O(通過O_NONBLOCK狀態字)。一旦資源是非阻塞模式,如果讀取文件操作沒有可讀取的數據,或者如果寫文件操作被阻塞,讀操作或寫操作返回-1和EAGAIN錯誤。
非阻塞I/O最基本的模式是通過輪詢獲取數據,這也叫做忙-等模型。看下面這個例子,通過非阻塞I/O和輪詢機制獲取I/O的結果。
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) { // 此時還沒有數據 continue; } if (data === RESOURCE_CLOSED) { // 資源被釋放,從隊列中移除該鏈接 resources.remove(i); } else { consumeData(data); } } }
我們可以看到,通過這個簡單的技術,已經可以在一個線程中處理不同的資源了,但依然不是高效的。事實上,在前面的例子中,用于迭代資源的循環只會消耗寶貴的CPU,而這些資源的浪費比起阻塞I/O反而更不可接受,輪詢算法通常浪費大量CPU時間。
事件多路復用對于獲取非阻塞的資源而言,忙-等模型不是一個理想的技術。但是幸運的是,大多數現代的操作系統提供了一個原生的機制來處理并發,非阻塞資源(同步事件多路復用器)是一個有效的方法。這種機制被稱作事件循環機制,這種事件收集和I/O隊列源于發布-訂閱模式。事件多路復用器收集資源的I/O事件并且把這些事件放入隊列中,直到事件被處理時都是阻塞狀態??聪旅孢@個偽代碼:
socketA, pipeB; wachedList.add(socketA, FOR_READ); wachedList.add(pipeB, FOR_READ); while(events = demultiplexer.watch(wachedList)) { // 事件循環 foreach(event in events) { // 這里并不會阻塞,并且總會有返回值(不管是不是確切的值) data = event.resource.read(); if (data === RESOURCE_CLOSED) { // 資源已經被釋放,從觀察者隊列移除 demultiplexer.unwatch(event.resource); } else { // 成功拿到資源,放入緩沖池 consumeData(data); } } }
事件多路復用的三個步驟:
資源被添加到一個數據結構中,為每個資源關聯一個特定的操作,在這個例子中是read。
事件通知器由一組被觀察的資源組成,一旦事件即將觸發,會調用同步的watch函數,并返回這個可被處理的事件。
最后,處理事件多路復用器返回的每個事件,此時,與系統資源相關聯的事件將被讀并且在整個操作中都是非阻塞的。直到所有事件都被處理完時,事件多路復用器會再次阻塞,然后重復這個步驟,以上就是event loop。
上圖可以很好的幫助我們理解在一個單線程的應用程序中使用同步的時間多路復用器和非阻塞I/O實現并發。我們能夠看到,只使用一個線程并不會影響我們處理多個I/O任務的性能。同時,我們看到任務是在單個線程中隨著時間的推移而展開的,而不是分散在多個線程中。我們看到,在單線程中傳播的任務相對于多線程中傳播的任務反而節約了線程的總體空閑時間,并且更利于程序員編寫代碼。在這本書中,你可以看到我們可以用更簡單的并發策略,因為不需要考慮多線程的互斥和同步問題。
在下一章中,我們有更多機會討論Node.js的并發模型。
介紹reactor模式現在來說reactor模式,它通過一種特殊的算法設計的處理程序(在Node.js中是使用一個回調函數表示),一旦事件產生并在事件循環中被處理,那么相關handler將會被調用。
它的結構如圖所示:
reactor模式的步驟為:
應用程序通過提交請求到時間多路復用器產生一個新的I/O操作。應用程序指定handler,handler 在操作完成后被調用。提交請求到事件多路復用器是非阻塞的,其調用所以會立馬返回,將執行權返回給應用程序。
當一組I/O操作完成,事件多路復用器會將這些新事件添加到事件循環隊列中。
此時,事件循環會迭代事件循環隊列中的每個事件。
對于每個事件,對應的handler被處理。
handler,是應用程序代碼的一部分,handler執行結束后執行權會交回事件循環。但是,在handler 執行時可能請求新的異步操作,從而新的操作被添加到事件多路復用器。
當事件循環隊列的全部事件被處理完后,循環會在事件多路復用器再次阻塞直到有一個新的事件可處理觸發下一次循環。
我們現在可以定義Node.js的核心模式:
模式(反應器)阻塞處理I/O到在一組觀察的資源有新的事件可處理,然后以分派每個事件對應handler的方式反應。
OS的非阻塞I/O引擎每個操作系統對于事件多路復用器有其自身的接口,Linux是epoll,Mac OSX是kqueue,Windows的IOCP API。除外,即使在相同的操作系統中,每個I/O操作對于不同的資源表現不一樣。例如,在Unix下,普通文件系統不支持非阻塞操作,所以,為了模擬非阻塞行為,需要使用在事件循環外用一個獨立的線程。所有這些平臺內和跨平臺的不一致性需要在事件多路復用器的上層做抽象。這就是為什么Node.js為了兼容所有主流平臺而
編寫C語言庫libuv,目的就是為了使得Node.js兼容所有主流平臺和規范化不同類型資源的非阻塞行為。libuv今天作為Node.js的I/O引擎的底層。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89981.html
摘要:第篇簡單異構系統之微服務一大致介紹因為在后面要利用集成異構系統,所以才有了本章節的微服務本章節使用了最簡單的請求截取的方式,截取不同的后綴做不同的響應處理,簡直二實現步驟添加服務端文件引入模塊創建獲得請求的路徑訪問,將會返回歡迎 SpringCloud(第 026 篇)簡單異構系統之 nodejs 微服務 - 一、大致介紹 1、因為在后面要利用 SpringCloud 集成異構系統,所...
摘要:消息推送也是微信公眾號開發更為有趣的功能,涉及到文本消息圖片消息語音消息視頻消息音樂消息以及圖文消息。在文件中創建文件用于消息的管理。 一、寫在前面的話 ??當用戶發送消息給公眾號時(或某些特定的用戶操作引發的事件推送時),會產生一個POST請求,開發者可以在響應包(Get)中返回特定XML結構,來對該消息進行響應。 ??消息推送也是微信公眾號開發更為有趣的功能,涉及到文本消息、圖片消...
摘要:異步最佳實踐避免回調地獄前端掘金本文涵蓋了處理異步操作的一些工具和技術和異步函數。 Nodejs 連接各種數據庫集合例子 - 后端 - 掘金Cassandra Module: cassandra-driver Installation ... 編寫 Node.js Rest API 的 10 個最佳實踐 - 前端 - 掘金全文共 6953 字,讀完需 8 分鐘,速讀需 2 分鐘。翻譯自...
摘要:一默認使用的模塊化方案,默認是的模塊化方案,兩者有本質區別。的去尋找引入的依賴時,如果是自帶的模塊,比如文件模塊,只需要填寫即可。這是版本入口文件使用了兩個路由器路由,分別處理和請求邏輯。核心操作全部依賴模型對象來執行。 一、Node.js默認使用commonJs的模塊化方案,TypeScript默認是ES6的模塊化方案,兩者有本質區別。 1.Node.js的去尋找引入的依賴時,如果...
閱讀 2219·2019-08-30 15:53
閱讀 2444·2019-08-30 12:54
閱讀 1187·2019-08-29 16:09
閱讀 718·2019-08-29 12:14
閱讀 746·2019-08-26 10:33
閱讀 2461·2019-08-23 18:36
閱讀 2950·2019-08-23 18:30
閱讀 2111·2019-08-22 17:09