JavaScript异步发展史

一道面试题引发的血战。
问:怎么理解异步的发展过程,例如axios、ajax、promise、await、async、generator等?

  1. 为什么需要异步?

    • JavaScript是单线程语言,同一时间只能做一件事情;

    • 同步任务进入主线程,按顺序执行,意味着阻塞,前一段代码必须执行完成了才能执行后边代码;

    • 异步是不会造成阻塞的。异步任务进入任务队列,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程。比如ajax、setTimeout/setInterval等。上述过程不断循环,称为事件循环(EventLoop);

    • 事件循环是js实现异步的一种方式,也是js的执行机制 。

      题外话:
      1. 除了同步、异步任务,对任务有更精细的定义:宏任务[整体代码的script/setTimeout/setInterval]、微任务[Promise/process.nextTick];
      2. js的执行和运行有很大区别,js在不同环境下,比如node、浏览器等,执行方式(即执行机制,事件循环eventLoop)是不同的。而运行是指js解析引擎大多是统一的。
      3. 关于事件循环机制,详细请参考

  2. 发展过程

    • 异步最早的解决方案是回调函数,比如:
      1.setTimeout/setInterval
      2 ajax
      3 nodeAPI
      4 事件回调
      例如这样一个需求:先读取文件A,再根据文件A的内容去读取文件B,然后根据文件B的内容读取文件C...

      let fs = require('fs');
      fs.readFile('1.txt', 'utf-8', function(err, data) {
          if (err) {
              console.log(err);
          } else {
              console.log(data);
          }
      });
      
      // 回调地狱
      fs.readFile(A, function (err, data) {
          fs.readFile(B, function (err, data) {
              fs.readFile(C, function (err, data) {
                  // ...
              });
          });            
      });
      

      优点是简单;缺点是回调嵌套难以维护,不方便统一处理错误,不能try catch错误以及回调地狱。

    • Promise一定程度上解决了“回调地狱”和统一处理错误问题。
      好像Promise.all()方法也能实现,我们来看一看:

      Promise.all([
          readfilePromise('A'),
          readfilePromise('B'),
          readfilePromise('C'),
      ])
          .then((dataA, dataB, dataC) => {
              // ...
          })
          .catch(err => console.log(err));
      

      实际上这个方法的含义是等A、B、C三个文件全部读取完成后,进行操作,三个文件是独立的,不是上述栗子中的依赖关系。详细请参考MDN

      // Promise解决“回调地狱”以及统一处理错误问题
      let readfilePromise = function (filepath) {
          return new Promise(function (resolve, reject) {
              // console.log(a);
              fs.readFile(filepath, function(err, data) {
                  if (err) {
                      reject(err);
                  } else {
                      resolve(data);
                  }
              });
          });
      }
      readfilePromise('A')
          .then(data => {
              return readfilePromise('B');
          })
          .then(data => {
              return readfilePromise('C');
          })
          .catch(err => console.log(err)); // 该catch可以统一处理其前边所有错误,包括Promise内部的语法错误、异步reject、then里的错误等,且不会影响到Promise外部的代码
      
      

      好像解决了一些问题,但一定程度上又存在新问题:
      1.错误不能被try catch,只有通过catch回调捕获;
      2.使用promise的链式回调没从根本上解决回调地狱问题,它是一种新的写法,而不是新的语法,比如后一个请求依赖于前一个的结果,仍然需要嵌套多个promise实例或者then(...);

    • Generator函数是es6提供的一种新的异步编程解决方案,遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续向后执行。缺点是需要结合co函数,否则不能自动执行;

    • async/await是generator的语法糖,内置执行器,使得异步代码看起来像同步代码,可以try catch错误;

      async function readdependantFile () {
          try {
              let dataA = await readfilePromise('A');
              let dataB = await readfilePromise('B');
              let dataC = await readfilePromise('C');
          } catch (err) {
              console.log(err);
          }
      }
      readdependantFile();
      
  3. 总结:异步编程的目标是让异步逻辑的代码看起来像同步一样。

    回调函数 --> Promise --> Generator --> async/await

参考《ECMAScript6 入门》 阮一峰、《细说JS异步发展历程》 刘小夕@segmentfault

你可能感兴趣的:(JavaScript异步发展史)