Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications.

Introduction of the Node

Node is a JavaScript server runtime based on the Chrome V8 engine.

The term “operating environment” has two meanings:

  • The JavaScript language runs on the server through Node, which is a bit like a JavaScript virtual machine in that sense.
  • Node provides a number of libraries that allow the JavaScript language to interact with the operating system (such as reading and writing files and creating child processes). Node is also a JavaScript library on a layer.

The characteristics of the Node

Asynchronous I/O

The concept of asynchronous I/O should be easy for anyone who has used Ajax to understand. By making an Ajax request:

$.post('/url', {title: 'test.js'}, function (data) { 
console.log('Response received'); 
}); 
console.log('End of sending Ajax request');
// End of sending Ajax request => Response received
Copy the code

After we call $.post(), the subsequent code is executed immediately, but the execution time of “received response” is unpredictable. The diagram below:

In Node, asynchronous I/O works similarly. Let’s take file reading as an example:

cosnt fs = require('fs')
fs.readFile('/path'.(err, file) = > {
  console.log('Reading file completed')})console.log('Initiate file reading')
// Initiate file reading => File reading is complete
Copy the code

I/O is time-consuming and expensive, and distributed I/O is even more expensive. Loading a resource with data from two different locations returns results that take M milliseconds for the first resource and N milliseconds for the second. If synchronous mode is used:

getData('from_db'); // The consumption time is M
getData('form_remote_api') // Consume time N
Copy the code

If we use an asynchronous approach, the acquisition of the first resource does not block the second resource, i.e. the request for the second resource does not depend on the end of the first resource, so we can enjoy the advantages of parallelism:

getData('form_db'.(req, res) = > {
   // The consumption time is M
});
getData('form_remote_api'.(req, res) = > {
   // Consume time N
})
Copy the code

The total time consumption of the former is M+N, and the latter is Max (M +N). As the complexity of applications increases, the advantages and disadvantages of synchronous versus asynchronous will gradually become apparent.

The asynchronous I/O flow of a Node is as follows:

Event-driven/event loops

  • Each Node process has only one main thread executing code, forming an execution context stack.
  • In addition to the main thread, an Event Queue is maintained. When network requests and other asynchronous operations come in, Node places them in the Event Queue but does not execute them immediately, so the code does not block and continues to run until the main thread completes.
  • Allocate one thread from the thread pool to execute, then a third, then a fourth. The main thread continuously checks for any unexecuted events in the event queue until all events in the event queue have been executed. After that, whenever a new event is added to the event queue, the main thread is notified to pull out the EventLoop in order. When an event completes, the main thread is notified, the main thread executes a callback, and the thread is returned to the thread pool.
  • The main thread repeats step three until the stack is consumed.

Each event cycle consists of six phases.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code
  • Timers phase: This phase executes the timer (setTimeout,setIntervalThe callback
  • I/O Callbacks phase: Perform some system call errors, such as error callbacks for network communication
  • Idle, prepare: Used only internally
  • Poll phase: Fetch new I/O events, where node will block under appropriate conditions
  • The check phase: performsetImmediate()The callback
  • The close callbacks phase: performsocketthecloseEvent callback.

The callback function

In addition to asynchrony and events, the callback function is also one of the best ways to accept the data returned by an asynchronous call. But this kind of programming method is especially unaccustomed to many people who are used to synchronous programming. When our business logic is complex, callbacks are nested too much, the code becomes more complex, less readable, more complex to maintain, and more complex to debug. This is callback hell.

function doByCallback(id, next) {
  startJobA(id, (err, resA) = > {
    startJobB(id, (err, resB) = > {
      startJobC(id, (err, resC) = > {
      	startJobD(id, (err, resD) = > {
          if (err) return next(err);
          return next(null, resD); })})})})}Copy the code

Of course, there are many ways to solve this problem, such as async,then.js,generator, etc.

const methodAsync = async (id, next) => {
  try {
    const resA = await startJobA(id);
 	  const resB = awaitstartJobB(id); . }catch (err) {
    returnnext(err); }}function *methodGenerator(id, next) {
  try {
    const resA = yield startJobA(id);
 	  const resB = yieldstartJobB(id); . }catch (err) {
    returnnext(err); }}Copy the code

Single thread

JavaScript is single-threaded, which is maintained in Node. And in Node, JavaScript cannot share any state with other threads. The biggest advantage of single-threading is that you don’t have to worry about state synchronization as much as multithreading does, no deadlocks, and no performance overhead of context exchange. Node implements asynchronous non-blocking I/O by handing over all blocking operations to an internal thread pool and scheduling round trips with no real I/O operations.

But single threading has its own weaknesses:

  • Unable to utilize multi-core CPUS
  • An error can cause an entire application to quit, causing health problems
  • A large number of calculations occupy the CPU, causing asynchronous I/O calls to fail

But then HTML5 customized the standard for Web Workers. Web Workers can create worker threads to perform computations to solve the problem of massive JavaScript computations blocking UI rendering.

Node interaction with the operating system

From the design architecture of the Node library, Node is roughly divided into three layers:

  • The Node Standard Library layer, which is written in JavaScript, is an API that we can call directly, which can be found in the lib directory of the source code.
  • No Bindings, this layer is the key of communication between JavaScript and the lower layer C/C++, the upper layer through the Bindings call the lower layer, exchange data with each other.
  • This last infrastructure layer, which is the key to running Node.js, is also written in C/C++.
    • V8: The Google Javascript VM is also the key to why Node.js uses Javascript. It provides an environment for Javascript to run in non-browser side, and its efficiency is one of the reasons why Node.js is so efficient.
    • Libuv: Providing Node.js with cross-platform, thread pooling, event pooling, and asynchronous I/O capabilities is what makes Node.js so powerful.
    • C-ares: Provides asynchronous dnS-related processing capabilities.
    • .

Let’s take a quick look at an example of Node’s interaction logic with the operating system:

const fs = require('fs');
fs.open('./test.json'.'w'.(err, fd) = > {
  // do something
});
Copy the code

Here is a simple example of reading a local test.json file. The entire call flow looks like this:

Lib /fs.js → SRC /node_file.cc → uv_fs

async function open(path, flags, mode) {
    mode = modeNum(mode, 0o666);  
    path = getPathFromURL(path); 
    validatePath(path);  
    validateUint32(mode, 'mode'); 
    return new FileHandle(
        await binding.openFileHandle(pathModule.toNamespacedPath(path),          
        stringToFlags(flags),             
        mode, 
        kUsePromises
    ));
}
Copy the code

node_file.cc

static void Open(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  const int argc = args.Length(a);if(req_wrap_async ! =nullptr) {  // open(path, flags, mode, req)
    AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
              uv_fs_open, *path, flags, mode);
  } else {  // open(path, flags, mode, undefined, ctx)
    CHECK_EQ(argc, 5);
    FSReqWrapSync req_wrap_sync;
    FS_SYNC_TRACE_BEGIN(open);
    int result = SyncCall(env, args[4], &req_wrap_sync, "open",
                          uv_fs_open, *path, flags, mode);
    FS_SYNC_TRACE_END(open);
    args.GetReturnValue().Set(result); }}Copy the code

uv_fs

dstfd = uv_fs_open(NULL,
                     &fs_req,
                     req->new_path,
                     dst_flags,
                     statsbuf.st_mode,
                     NULL);
  uv_fs_req_cleanup(&fs_req);
Copy the code

Package with NMP

Node organizes its core modules and allows third-party file modules to be written and used in an orderly manner. However, because in the third-party module, the distribution between modules is still hash, can not be directly applied to each other. Outside of modules, packages and NPMS are mechanisms that tie modules together.

Package structure

├─ Package.json # ├─ bin # ├ for JavaScript ├─ lib # ├ for JavaScript ├─ SRC # ├ for JavaScript ├─ ├ ─ ├ ─ test.txt doc # A directory for storing documents ├ ─ test.txt # A directory for storing unit test casesCopy the code

Package description file with NPM

Package description files are mainly used to express non-code related information. It is a JSON-formatted file, usually located at the root of the project, that contains only the necessary parts of the package. The package.json file in CommonJS defines the following required fields.

{	
    "main": "index.ts".// Define the entry file for the NPM package, which can be used by both browser and Node environments. The module import method require() only checks this field when importing a package and uses it as an entry for the rest of the package
    "module": "index.ts".// An entry file that defines the ESM specification for NPM packages. Browser and Node environments can use this file
    "scripts": {}, // Script object description
    "dependencies": {}, // Use the list of packages that the current package depends on.
    "devDependencies": {}, // Dependencies are required only at development time.
}
Copy the code

NPM package installation command

npm install xxxx --save 
npm install xxxx --save-dev
npm install xxxx -g
Copy the code