This series of articles is a translation and reading notes of Node.js Design Patterns Second Edition, updated on GitHub with a link to the translation.
Please pay attention to my column, the following blog will be synchronized in the column:
- The nuggets column of Encounter
- Programming Thoughts on Zhihu’s Encounter
- Segmentfault front end station
Welcom to the Node.js Platform
The development of the Node. Js
- The development of technology itself
- The huge
Node.js
Development of the ecosphere - Maintenance of official organization
The characteristics of the Node. Js
Small module
Reuse as many modules as possible in the form of package. In principle, the capacity of each module should be as small and precise as possible.
Principle:
- “Small is beautiful” — Small but fine
- “Make each program do one thing well” — Single responsibility principle
Thus, a Node.js application is built from multiple packages, managed by the package manager (NPM) so that they depend on each other without conflict.
If you’re designing a Node.js module, try to do the following:
- Easy to understand and use
- Easy to test and maintain
- Consider client (browser) support more friendly
And the Don’t Repeat Yourself(DRY) principle of reusability.
It is provided as an interface
Every Node.js module is a function (and the class is represented as a constructor), and we only need to call the relevant API without knowing the implementation of other modules. Node.js modules are created to use them, not only for extensibility, but also for maintainability and usability.
Simple and practical
“Simplicity is the ultimate complexity” ———— Darwin
Follow THE KISS(Keep It Simple, Stupid) principle of good, Simple design to communicate information more effectively.
Design must be simple, both in terms of implementation and interface. More importantly, implementation is simpler than interface. Simplicity is an important design principle.
We made a simple design, complete functionality, but not perfect software:
- It takes less effort to achieve
- Allows faster transportation of resources with less speed
- It is scalable and easier to maintain and understand
- Foster community contributions that allow the software itself to grow and improve
For Node.js, because of its JavaScript support, simplicity and features like functions, closures, and objects can replace the complex object-oriented class syntax. The singleton pattern and decorator pattern, for example, both require complex implementations in object-oriented languages and are simpler for JavaScript.
This section describes the new syntax of Node.js 6 and ES2015
Let and const keywords
Before ES5, there were only functions and global scopes.
if (false) {
var x = "hello";
}
console.log(x); // undefinedCopy the code
Uncaught ReferenceError: x is not defined
if (false) {
let x = "hello";
}
console.log(x);Copy the code
Uncaught ReferenceError: I is not defined:
for (let i = 0; i < 10; i++) {
// do something here
}
console.log(i);Copy the code
Using the let and const keywords makes your code safer and easier to find errors if you accidentally access a variable in another scope.
Use the const keyword to declare variables that cannot be accidentally changed.
const x = 'This will never change';
x = '... ';Copy the code
Uncaught TypeError: Assignment to constant variable
But const is helpless when an object property changes:
const x = {};
x.name = 'John';Copy the code
The above code does not report an error
However, if you change the object directly, an error is still thrown.
const x = {};
x = null;Copy the code
In practice, we use const to introduce modules to prevent accidental changes:
const path = require('path');
let path = './some/path';Copy the code
The above code will report an error alerting us to an unexpected module change.
If you want to create immutable objects, it is not enough to simply use const; use object.freeze () or deep-freeze instead
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;
};Copy the code
Arrow function
Arrow functions are easier to understand, especially when we define callbacks:
const numbers = [2.6.7.8.1];
const even = numbers.filter(function(x) {
return x % 2= = =0;
});Copy the code
Use the arrow function syntax for more conciseness:
const numbers = [2.6.7.8.1];
const even = numbers.filter(x= > x % 2= = =0);Copy the code
Use => {} if there is more than one return statement
const numbers = [2.6.7.8.1];
const even = numbers.filter((x) = > {
if (x % 2= = =0) {
console.log(x + ' is even');
return true; }});Copy the code
Most importantly, the arrow function is bound to its lexical scope, and its this is the same as the parent code block’s 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'Copy the code
To solve this problem, use the arrow function or 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'Copy the code
Or the arrow function, with the same scope as the parent code block:
function DelayedGreeter(name) {
this.name = name;
}
DelayedGreeter.prototype.greet = function() {
setTimeout((a)= > console.log('Hello' + this.name), 500);
}
const greeter = new DelayedGreeter('World');
greeter.greet(); // 'HelloWorld'Copy the code
Kind of syntactic sugar
Class is the syntactic sugar of prototype inheritance, and is more familiar to all developers from traditional object-oriented languages such as Java and C#. The new syntax does not change JavaScript’s runtime characteristics, and it is more convenient and readable to do it through prototypes.
The traditional way of writing by constructor + prototype:
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;
}Copy the code
Using class syntax is more concise, convenient, and easy to understand:
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; }}Copy the code
The above implementations are interchangeable, but the extends and super keywords make the most sense for class syntax.
class PersonWithMiddlename extends Person {
constructor(name, middlename, surname, age) {
super(name, surname, age);
this.middlename = middlename;
}
getFullName() {
return this.name + ' ' + this.middlename + ' ' + this.surname; }}Copy the code
This example is really object-oriented. We declare a class that we want to inherit, define a new constructor, and we can call the parent constructor using the super keyword, and rewrite the getFullName method so that it supports middlename.
New syntax for object literals
Default value:
const x = 22;
const y = 17;
const obj = { x, y };Copy the code
Method names are allowed to be omitted
module.exports = {
square(x) {
return x * x;
},
cube(x) {
returnx * x * x; }};Copy the code
The computed property of key
const namespace = '-webkit-';
const style = {
[namespace + 'box-sizing'] :'border-box',
[namespace + 'box-shadow'] :'10px 10px 5px #888'};Copy the code
New getters and setters defined
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"Copy the code
Here, the second console.log triggers the set method.
Template string
Other ES2015 grammars
- Function default arguments
- Residual parameter syntax
- Extended operator
- Deconstruction assignment
- new.target
- The agent
- reflection
- Symbol
Reactor model
Reactor model is the core module of Node.js asynchronous programming, and its core concept is: single-thread, non-blocking I/O. The following examples show the manifestation of REACTOR model in Node.js platform.
I/O is slow
In the basic operation of a computer, input and output must be the slowest. Access to memory is in the nanosecond range (10E-9 s), while simultaneous access to data on disk or data on the network is even slower in the millisecond range (10E-3 s). Memory transfer speed is generally considered as GB/s, while disk or network access speed is slower, generally MB/s. Although I/O operations are not costly to the CPU, there is always a time lag between sending an I/O request and the completion of the operation. In addition, we have to consider the human factor. Often, input to an application is generated by humans, such as button clicks and instant messaging. Therefore, the speed of input and output is not caused by slow network and disk access, but by many factors.
Blocking I/O
In a process that blocks the I/O model, I/O requests block the execution of subsequent code blocks. Threads have an indefinite amount of time to waste before the I/O request operation completes. (It could be millisecond, but it could even be minute, for example when the user holds down a key). The following example is a blocking I/O model.
Threads are blocked until the request is completed and data is available
data = socket.read();
// Request complete, data available
print(data);Copy the code
As we know, the server model that blocks I/O does not allow multiple connections to be processed in a single thread, and each I/O blocks the processing of other connections. For this reason, for each concurrent connection that needs to be processed, a traditional Web server’s approach is to start a new process or thread (or to reuse a process from the thread pool). This way, when one thread is blocked for AN I/O operation, it does not affect the availability of another thread because they are processed in separate threads.
Through this picture:
From the above picture we can see that each thread is idle for a period of time, waiting to receive new data from the associated connection. If all kinds of I/O operations block subsequent requests. For example, connecting to a database and accessing a file system, we can now quickly learn that a thread has to wait a lot of time for the result of an I/O operation. Unfortunately, CPU resources held by one thread are not cheap. It consumes memory and causes CPU context switching, so threads that occupy CPU for a long time but do not use it most of the time are not efficient choices in terms of resource utilization.
Non-blocking I/O
In addition to blocking I/O, most modern operating systems support another mechanism for accessing resources, namely non-blocking I/O. Under this mechanism, subsequent blocks of code do not wait for the return of I/O request data. If all data is not available at the current time, the function returns a predefined constant value (such as undefined), indicating that no data is available at the current time.
For example, on Unix operating systems, the FCNTL () function operates on an existing file descriptor, changing its mode of operation to non-blocking I/O(via the O_NONBLOCK status word). Once the resource is in non-blocking mode, the read or write operation returns -1 and EAGAIN errors if the read file operation has no data to read, or if the write file operation is blocked.
The most basic pattern of non-blocking I/O is to get data through polling, also known as the busy-equal model. Take a look at the following example to get the results of I/O through non-blocking I/O and polling mechanisms.
resources = [socketA, socketB, pipeA];
while(! resources.isEmpty()) {for (i = 0; i < resources.length; i++) {
resource = resources[i];
// Perform read operations
let data = resource.read();
if (data === NO_DATA_AVAILABLE) {
// No data is available at this time
continue;
}
if (data === RESOURCE_CLOSED) {
// The resource is freed and the link is removed from the queue
resources.remove(i);
} else{ consumeData(data); }}}Copy the code
As you can see, it is already possible to process different resources in one thread with this simple technique, but it is still not efficient. In fact, in the previous example, the loop used to iterate over resources only consumes precious CPU, and the waste of these resources is even more unacceptable than blocking I/O, and polling algorithms typically waste a lot of CPU time.
Event multiplexing
The busy-equal model is not an ideal technique for obtaining non-blocking resources. But fortunately, most modern operating systems provide a native mechanism to handle concurrency, and non-blocking resources (synchronous event multiplexers) are an efficient way to do it. This mechanism is known as the event loop mechanism, and the event collection and I/O queues are derived from the publish-subscribe model. The event multiplexer collects I/O events for the resource and queues them, blocking until the event is processed. Take a look at the pseudocode below:
socketA, pipeB;
wachedList.add(socketA, FOR_READ);
wachedList.add(pipeB, FOR_READ);
while(events = demultiplexer.watch(wachedList)) {
// Event loop
foreach(event in events) {
// There is no blocking and there is always a return value (whether it is an exact value or not)
data = event.resource.read();
if (data === RESOURCE_CLOSED) {
// The resource has been released and removed from the observer queue
demultiplexer.unwatch(event.resource);
} else {
// Get the resource successfully and add it to the buffer poolconsumeData(data); }}}Copy the code
Three steps for event multiplexing:
- Resources are added to a data structure that associates each resource with a specific operation, in this case
read
. - An event notifier consists of a set of observed resources that are called synchronously when an event is about to be triggered
watch
Function and returns the event that can be handled. - Finally, each event returned by the event multiplexer is processed, at which point the event associated with the system resource will be read and non-blocking throughout the operation. Until all events have been processed, the event multiplexer blocks again and repeats the process, so that’s it
event loop
.
The figure above provides a good understanding of concurrency in a single-threaded application using synchronous time multiplexers and non-blocking I/O. We can see that using only one thread does not affect our performance in handling multiple I/O tasks. At the same time, we see that tasks are spread out over time in a single thread rather than spread out across multiple threads. We see that tasks propagated in a single thread save the overall idle time of the thread compared to tasks propagated in multiple threads and are more convenient for programmers to write code. In this book, you can see that we can use a simpler concurrency strategy because we don’t have to worry about multi-threaded mutual exclusion and synchronization.
We’ll have more opportunities to discuss Node.js’s concurrency model in the next chapter.
Introducing the REACTOR Model
Now the REACTOR pattern, which uses a special algorithmically designed handler (represented in Node.js by a callback function) to be called once an event is generated and processed in an event loop.
Its structure is shown below:
The reactor model steps are:
- The application generates a new one by submitting the request to the time multiplexer
I/O
Operation. Application specificationhandler
.handler
Called after the operation is complete. Submitting a request to the event multiplexer is non-blocking, so its call returns immediately, returning execution rights to the application. - When a group of
I/O
When the operation is complete, the event multiplexer adds these new events to the event loop queue. - At this point, the event loop iterates over each event in the event loop queue.
- For each event, corresponding to
handler
Be processed. handler
Is part of the application code,handler
Execution is returned to the event loop after execution. However, inhandler
New asynchronous operations may be requested at execution time, so that new operations are added to the event multiplexer.- When all events in the event loop queue have been processed, the loop blocks again at the event multiplexer until a new event can be processed to trigger the next loop.
We can now define the core schema of Node.js:
The pattern (reactor) blocks processing I/O until a new event is available for processing on a set of observed resources, and then reacts by dispatching a handler for each event.
OS’s non-blocking I/O engine
Each operating system has its own interface to the event multiplexer, Linux is epoll, Mac OSX is KQueue, and Windows has an IOCP API. Except that, even on the same operating system, each I/O operation behaves differently for different resources. For example, under Unix, normal file systems do not support non-blocking operations, so to simulate non-blocking behavior, you need to use a separate thread outside the event loop. All of these inconsistencies within and across platforms need to be abstracted at the top of the event multiplexer. This is why Node.js wrote the C library Libuv to be compatible with all the major platforms, in order to make Node.js compatible with all the major platforms and standardize the non-blocking behavior of different types of resources. Libuv today serves as the underlying I/O engine for Node.js.