Analysis of callback and code design patterns in ES0en. js asynchronous programming

  • 2020-12-21 17:57:26
  • OfStack

NodeJS's biggest selling points - event mechanisms and asynchronous IO - are not transparent to developers. Developers need to write code asynchronously to make use of this selling point, which has been attacked by some opponents of NodeJS. However, asynchronous programming is the biggest feature of NodeJS. You can't really learn NodeJS without mastering asynchronous programming. This chapter introduces a variety of topics related to asynchronous programming.

In code, the direct embodiment of asynchronous programming is a callback. Asynchronous programming relies on callbacks, but you can't say that a program is asynchronous when a callback is used. Let's start by looking at the following code.


function heavyCompute(n, callback) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }

 callback(count);
}

heavyCompute(10000, function (count) {
 console.log(count);
});

console.log('hello');


100000000
hello

As you can see, the callback functions in the above code are still executed before the subsequent code. JS itself is single-threaded, and it is not possible to run another piece of code before it has finished running, so there is no concept of asynchronous execution.

However, if a function does something that creates an individual thread or process, does something in parallel with the JS main thread, and notises the JS main thread when it's done, that's not the case. Let's move on to the following code.


setTimeout(function () {
 console.log('world');
}, 1000);

console.log('hello');


hello
world


This time, you can see that the callback function is executed after the subsequent code. As above said, JS itself is single-threaded, cannot be executed asynchronously, so we can think setTimeout such JS specification outside of the special function provided by the operating environment to do is to create a parallel threads returns immediately after, let JS main process can then perform subsequent code, and after receiving notification of parallel process to invoke the callback function. In addition to common functions such as setTimeout and setInterval, such functions include asynchronous API such as fs.readFile provided by NodeJS.

In addition, we still go back to the fact that JS is single-threaded, which in fact determines that JS cannot execute other code, including the callback function, until it has executed 1 piece of code. That is, even if the parallel thread completes its work and notifies the JS main thread to execute the callback function, the callback function will not begin until the JS main thread is idle. Here is an example.


function heavyCompute(n) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }
}

var t = new Date();

setTimeout(function () {
 console.log(new Date() - t);
}, 1000);

heavyCompute(50000);


8520


As you can see, the callback function that should have been called 1 second later was significantly delayed because the JS main thread was busy running other code.

Code design pattern
Asynchronous programming has many unique code design patterns, and to achieve the same functionality, code written in a synchronous and asynchronous manner can be very different. Some of the common patterns are described below.

Function return value
It is a common requirement to use the output of one function as the input of another function. In synchronous mode, code is generally written as follows:


var output = fn1(fn2('input'));
// Do something.

However, in the asynchronous mode, because the result of function execution is not passed through the return value, but through the callback function, 1 generally write the code as follows:


fn2('input', function (output2) {
 fn1(output2, function (output1) {
  // Do something.
 });
});

As you can see, this way is a callback function sets of more than 1 DiaoHan back, too much easy to write > The code for the shape.

Through the array
It is also common to use a function to do 1 of the data members in turn while traversing through groups. If the function is executed synchronously, 1 would normally write the following code:


var len = arr.length,
 i = 0;

for (; i < len; ++i) {
 arr[i] = sync(arr[i]);
}

// All array items have processed.

If the function is executed asynchronously, there is no guarantee that all array members will be processed by the end of the loop. If array members must be serialized one by one, write asynchronous code as follows:


(function next(i, len, callback) {
 if (i < len) {
  async(arr[i], function (value) {
   arr[i] = value;
   next(i + 1, len, callback);
  });
 } else {
  callback();
 }
}(0, arr.length, function () {
 // All array items have processed.
}));

As you can see, the above code will pass in the next group member and start the next round of execution after the asynchronous function executes once and returns the execution result. Until all the array members are processed, the subsequent code execution will be triggered by means of callback.

If array members can be processed in parallel, but subsequent code still needs to execute after all array members have been processed, the asynchronous code is adjusted to the following form:


100000000
hello
0

As you can see, in contrast to the asynchronous serial traversal version, this code processes all the array members in parallel and uses the counter variable to determine when all the array members have been processed.

Exception handling
The exception catching and handling mechanism provided by JS itself -- try.. catch.. Can only be used for synchronized execution of code. Here is an example.


100000000
hello
1

100000000
hello
2

As you can see, the exception bubbles up along the code execution path 1 until it is caught when it encounters the first try statement. However, since asynchronous functions interrupt the code execution path, exceptions generated during and after the execution of asynchronous functions bubble up to the point where the execution path is interrupted, and if the 1 does not encounter the try statement, it is thrown as a global exception. Here is an example.


function async(fn, callback) {
 // Code execution path breaks here.
 setTimeout(function () {
  callback(fn());
 }, 0);
}

try {
 async(null, function (data) {
  // Do something.
 });
} catch (err) {
 console.log('Error: %s', err.message);
}


100000000
hello
4

Because the code execution path is interrupted, we need to catch the exception with the try statement before it bubbles to the breakpoint and pass the caught exception through the callback function. So we can modify the top example like this.


100000000
hello
5

100000000
hello
6

As you can see, the exception is caught again. In NodeJS, almost all asynchronous API are designed in this way, with the first argument in the callback function being err. So we can write our own asynchronous functions and handle exceptions in this way, keeping with NodeJS's design.

Now that we have exception handling, we can then think about how we wrote the code. Basically, our code is a loop of doing one thing, calling one function, doing another thing, calling another function, and so on. If we were writing synchronous code, we would only need to write 1 try statement at the code entry point to catch all bubbling exceptions, as shown below.


100000000
hello
7

However, if we are writing asynchronous code, we are left with nothing. Since each asynchronous function call interrupts the code execution path, exceptions can only be passed through the callback function, so we need to determine whether an exception has occurred in each callback function. Therefore, it only takes 3 asynchronous function calls to produce the following code.


function main(callback) {
 // Do something.
 asyncA(function (err, data) {
  if (err) {
   callback(err);
  } else {
   // Do something
   asyncB(function (err, data) {
    if (err) {
     callback(err);
    } else {
     // Do something
     asyncC(function (err, data) {
      if (err) {
       callback(err);
      } else {
       // Do something
       callback(null);
      }
     });
    }
   });
  }
 });
}

main(function (err) {
 if (err) {
  // Deal with exception.
 }
});

As you can see, the callbacks already complicate the code, and handling exceptions asynchronously adds complexity to the code.


Related articles: