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 hugeNode.jsDevelopment 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 caseread.
  • An event notifier consists of a set of observed resources that are called synchronously when an event is about to be triggeredwatchFunction 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 itevent 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 multiplexerI/OOperation. Application specificationhandler.handlerCalled 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 ofI/OWhen 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 tohandlerBe processed.
  • handlerIs part of the application code,handlerExecution is returned to the event loop after execution. However, inhandlerNew 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.