一、基本概念

1. 异步

所谓 " 异步 “,简单说就是一个任务分成两段, 先执行第一段, 然后转而执行其他任务去, 等做好了准备, 再回过头执行第二段。

比如, 有一个任务是读取文件进行处理, 任务的第一段是向操作系统发出请求, 要求读取文件。 然后, 程序执行其他任务, 等到操作系统返回文件,再接着执行任务的第二段( 处理文件)。 这种不连续的执行, 就叫做异步。

相应地, 连续的执行就叫做同步。 由于是连续执行, 不能插入其他任务, 所以操作系统从硬盘读取文件的这段时间, 程序只能干等着什么也做不了。

2. 回调函数

javascript 语言对异步编程的实现, 就是回调函数。 所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到准备好了,继续执行的时候, 就直接调用这个函数。 它的英语名字 callback, 直译过来就是 " 回调 “。

读取文件进行处理, 是这样的,readfile 是异步的,readfileSync 是同步的。

fs.readfile('/etc/passwd', function(err, data) {
  if(err) throw err;
  console.log(data);
});

上面代码中, readfile 函数的第二个参数, 就是回调函数, 也就是任务的第二段。 等到操作系统返回了 / etc / passwd这个文件以后, 回调函数才会执行。

==一个有趣的问题是, 为什么 node.js 约定, 回调函数的第一个参数, 必须是错误对象 err( 如果没有错误, 该参数就是 null)? 原因是执行分成两段, 在这两段之间抛出的错误, 程序无法捕捉, 只能当作参数, 传入第二段。==

3. promise

“想像一下你是个小孩,你妈妈 promise 承诺你下星期给你一部新手机”

只有下周来临的时候,你才会知道你真的得到一部手机,或者是骗你玩的。

这就是 promise. 一个 promise 有三种状态:

  1. Pending: 待定,你不知道你是否能得到一部手机
  2. Resolved: 妈妈很高兴,你得到一部手机
  3. Rejected: 妈妈不高兴,你没有得到一部手机

状态图如下:

image-20220527222312367

语法很简单:

// promise syntax look like this
new Promise(function (resolve, reject) { ... } );

再看一下上面问题的解决方法:

首先确定 Mom 的状态是 unhappy,然后建立一个 Promise 来确定是否得到手机,最后用一个函数 askMom 来调用这个 Promise

resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then。但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作。

reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。

但是请注意以下两点:

  • resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
  • resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。
var isMomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // resolved
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // rejected
        }

    }
);

var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // yay, you got a new phone
            console.log(fulfilled);
             // output: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // oops, mom didn't buy it
            console.log(error.message);
             // output: 'mom is not happy'
        });
};

askMom();

很有点意思吧。抽象了,那就实际点。

javascripts 中的 fetch 函数就是基于Promise范式的,promise 的 resolves 绑在了 Response 上。Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数,.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。 .then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:

    fetch("http://192.168.1.6/graph_image_hubot.php?action=view&local_graph_id=13619&rra_id=5")
    .then(response =>  {
      if (response.ok) {
         return response.buffer();
      }
      throw new Error('Network response was not ok.');
    })
    .then(buffer => {
      const formData = new FormData();
      formData.append( 'media', buffer, {filename: 'bandwidth.png', contentType: 'image/png' } );
      robot.wwork.sendImageMessage(owner, formData);
    });

上面的程序实际上是从 cacti 服务器拿到一张流量图片,然后 fetch 的结果 anyway,response总是有东西的,都会是成功的,所以我们必须再用 response.ok 来判断,成功就继续得到 buffer 流,最终送到 hubot 中去。三个过程用 then 串了起来,每一步都成功才会进行下一步。

这样写避免了回调地狱,看起来也比较舒服。

那什么时候用 promise 呢,它是异步的,有大IO读写硬盘、读写网络的时候用,上面是读写网络。下面再来一个读写磁盘的:

const {promise: {readFile, writeFile}} = require('fs');
(async () => {
    let content = await readFile('./data.txt', 'utf8');
    await writeFile('2.txt', content, 'utf8');
    console.log('ok');
})();

上面的语法是 ES7 的,它更尽了一步,首先声明一个匿名的函数是 async 异步的,然后用await来进行等待,将读和写两个大操作都放到 await 的一步操作中去,这样程序看起来就变成同步的一步步等待了。跟 ES6 的 promise 比起来,更进了一步。