JS异步编程的几种解决方案

579 阅读3分钟

JavaScript环境存在了许多异步函数,如网络请求、读取文件等。还有一些函数如setTimeout、setInterval等允许我们执行异步行为。换句话说,我们执行代码并不一定会按照代码的顺序来执行,如下面是一个异步计算两个数相加的函数

let count;
function asyncSum(num1, num2) {
  setTimeout(() => {
    count = num1 + num2;
  }, 1000);
}
asyncSum(1,2);
console.log(count);

猜猜上面的执行结果是什么? 问题来了,既然我们执行代码并不一定会按照代码的顺序来执行,我们如何拿到asyncSum的计算结果呢?

callback

我们可以将其写在回调函数中,回调函数不是js中独有的概念,回调函数是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。

将上面的代码我们改造一下:

function asyncSum(num1, num2, callback) {
  setTimeout(() => {
    let count = num1 + num2;
    callback(count);
  }, 1000);
}

现在我们就可以通过下面这种方式使用count了

asyncSum(1,2,(val) => console.log(`我拿到count了:${val}`))

在上述示例中,我们并没有考虑出现 error 的情况,如果asyncSum执行失败怎么办?我们的回调应该能够对此作出反应。

function asyncSum(num1, num2, callback) {
  setTimeout(() => {
    let count;
    try {
      count = num1 + num2;
    } catch (error) {
      callback(error);
      return;
    }
    callback(null, count);
  }, 1000);
}

现在我们可以这样子使用了

asyncSum(1,2,(error, val) => console.log(`我拿到count了:${val}`))

上面所使用的方法其实很普遍。它被称为“Error优先回调(error-first callback)”风格。 约定是: callback 的第一个参数是为 error 而保留的。一旦出现 error,callback(err) 就会被调用。 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 callback(null, result1, result2…) 就会被调用。 因此,单一的 callback 函数可以同时具有报告 error 和传递返回结果的作用 node中的大量api均是采用的Error优先回调如下面是官网的一段事例

fs.open('myfile', 'r', (err, fd) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.error('myfile does not exist');
      return;
    }
    throw err;
  }
  readMyData(fd);
});

乍一看,这是一种可行的异步编程方式。的确如此,对于一个或两个嵌套的调用看起来还不错。 但对于一个接一个的多个异步行为,代码将会变成这样:

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...加载完所有脚本后继续 (*)
          }
        });

      }
    })
  }
});

如果调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,有时这些被称为“回调地狱”,这也是前期js被广为诟病的一个地方。

promise

关于promise的详细介绍见developer.mozilla.org/zh-CN/docs/… Promises是一种模式,它能够让异步操作变得看起来更像是同步的,本质上Promise是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。 不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:

  • 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序执行。

改造一下我们的代码:

function asyncSum(num1, num2) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      let count;
      try {
        count = num1 + num2;
        res(count);
      } catch (error) {
        rej(error)
      }
    }, 1000);
  });
}

现在我们可以去掉回调函数数了,Promise 很棒的一点就是链式调用(chaining)

asyncSum(1,3).then((val) => {
  console.log(val)
  return asyncSum(val, 4);
}).then(val => {
  console.log(`第二层val${val}`);
  //也可以返回同步代码
  return val;
}).then(val => {
  console.log(`第三层val${val}`);
}).catch(error => console.log(error));

promise + async/await

在 ECMAScript 2017 标准的 async/await 语法糖中,这种与同步形式代码的对称性得到了极致的体现:

(async() => {
  let res1 = await asyncSum(1,3);
  console.log(res1);
  let res2 = await asyncSum(res1,4)
  console.log(`第二层val${val}`);
})()

待完善。。。。。