Home  >  Article  >  Web Front-end  >  Analyzing callbacks and code design patterns in Node.js asynchronous programming_node.js

Analyzing callbacks and code design patterns in Node.js asynchronous programming_node.js

WBOY
WBOYOriginal
2016-05-16 15:15:131186browse

The biggest selling point of NodeJS - event mechanism and asynchronous IO, are not transparent to developers. Developers need to write code asynchronously to take advantage of this selling point, which has been criticized by some NodeJS opponents. But no matter what, asynchronous programming is indeed the biggest feature of NodeJS. Without mastering asynchronous programming, you cannot say that you have truly learned NodeJS. This chapter will introduce various knowledge related to asynchronous programming.

In code, the direct manifestation of asynchronous programming is callbacks. Asynchronous programming relies on callbacks, but it cannot be said that the program becomes asynchronous after using callbacks. We can first look 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 function in the above code is still executed before subsequent code. JS itself runs in a single thread, and it is impossible to run other code before a piece of code has finished running, so there is no concept of asynchronous execution.

However, if what a function does is to create another thread or process, do something in parallel with the JS main thread, and notify the JS main thread when the thing is done, then the situation is different. Let's take a look at 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 mentioned above, JS itself is single-threaded and cannot be executed asynchronously. Therefore, we can think that what special functions provided by the running environment outside of JS specifications such as setTimeout do is to create a parallel thread and return immediately, allowing the JS master to The process can then execute subsequent code and execute the callback function after receiving notification from the parallel process. In addition to the common ones such as setTimeout and setInterval, such functions also include asynchronous APIs provided by NodeJS such as fs.readFile.

In addition, we still return to the fact that JS runs in a single thread, which determines that JS cannot execute other code, including callback functions, before executing a piece of code. In other words, even if the parallel thread completes its work and notifies the JS main thread to execute the callback function, the callback function will not start execution until the JS main thread is idle. The following is such 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 actual execution time of the callback function that was supposed to be called after 1 second was greatly delayed because the JS main thread was busy running other code.

Code Design Patterns
Asynchronous programming has many unique code design patterns. In order to achieve the same function, the code written in synchronous mode and asynchronous mode will be very different. Some common patterns are introduced below.

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

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

In asynchronous mode, since the function execution result is not passed through the return value, but through the callback function, the code is generally written in the following way:

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

As you can see, this method is one callback function nested within one callback function. If there are too many, it is easy to write >-shaped code.

Traverse the array
When traversing an array, it is also a common requirement to use a function to perform some processing on the data members in sequence. If the function is executed synchronously, the following code will generally be written:

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, the above code cannot guarantee that all array members have been processed after the loop ends. If array members must be processed serially one after another, asynchronous code is generally written 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 only passes in the next array member and starts the next round of execution after the asynchronous function is executed once and returns the execution result. Until all array members are processed, the execution of subsequent code is triggered through callbacks. .

If array members can be processed in parallel, but subsequent code still requires all array members to be processed before they can be executed, the asynchronous code will be adjusted to the following form:

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

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

Exception handling
The exception catching and handling mechanism provided by JS itself - try..catch.., can only be used for synchronously executed code. Below is an example.

function sync(fn) {
 return fn();
}

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

Error: object is not a function

As you can see, the exception will bubble along the code execution path until it is caught when it encounters the first try statement. However, since asynchronous functions interrupt the code execution path, when exceptions generated during and after the execution of the asynchronous function bubble up to the location where the execution path is interrupted, if no try statement is encountered, they will be thrown as a global exception. Below 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);
}

/home/user/test.js:4
  callback(fn());
     ^
TypeError: object is not a function
 at null._onTimeout (/home/user/test.js:4:13)
 at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因为代码执行路径被打断了,我们就需要在异常冒泡到断点之前用 try 语句把异常捕获住,并通过回调函数传递被捕获的异常。于是我们可以像下边这样改造上边的例子。

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

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

Error: object is not a function

可以看到,异常再次被捕获住了。在 NodeJS 中,几乎所有异步 API 都按照以上方式设计,回调函数中第一个参数都是 err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与 NodeJS 的设计风格保持一致。

有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个 try 语句就能捕获所有冒泡上来的异常,示例如下。

function main() {
 // Do something.
 syncA();
 // Do something.
 syncB();
 // Do something.
 syncC();
}

try {
 main();
} catch (err) {
 // Deal with exception.
}

但是,如果我们写的是异步代码,就只有呵呵了。由于每次异步函数调用都会打断代码执行路径,只能通过回调函数来传递异常,于是我们就需要在每个回调函数里判断是否有异常发生,于是只用三次异步函数调用,就会产生下边这种代码。

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.
 }
});

可以看到,回调函数已经让代码变得复杂了,而异步方式下对异常的处理更加剧了代码的复杂度。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn