一、基本概念
1. 异步
所谓 " 异步 “,简单说就是一个任务分成两段, 先执行第一段, 然后转而执行其他任务去, 等做好了准备, 再回过头执行第二段。
比如, 有一个任务是读取文件进行处理, 任务的第一段是向操作系统发出请求, 要求读取文件。 然后, 程序执行其他任务, 等到操作系统返回文件,再接着执行任务的第二段( 处理文件)。 这种不连续的执行, 就叫做异步。
相应地, 连续的执行就叫做同步。 由于是连续执行, 不能插入其他任务, 所以操作系统从硬盘读取文件的这段时间, 程序只能干等着什么也做不了。
2. 回调函数
javascript 语言对异步编程的实现, 就是回调函数。 所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到准备好了,继续执行的时候, 就直接调用这个函数。 它的英语名字 callback, 直译过来就是 " 回调 “。
读取文件进行处理, 是这样的,readfile 是异步的,readfileSync 是同步的。
1fs.readfile('/etc/passwd', function(err, data) {
2 if(err) throw err;
3 console.log(data);
4});
上面代码中, readfile 函数的第二个参数, 就是回调函数, 也就是任务的第二段。 等到操作系统返回了 / etc / passwd这个文件以后, 回调函数才会执行。
==一个有趣的问题是, 为什么 node.js 约定, 回调函数的第一个参数, 必须是错误对象 err( 如果没有错误, 该参数就是 null)? 原因是执行分成两段, 在这两段之间抛出的错误, 程序无法捕捉, 只能当作参数, 传入第二段。==
3. promise
“想像一下你是个小孩,你妈妈 promise 承诺你下星期给你一部新手机”
只有下周来临的时候,你才会知道你真的得到一部手机,或者是骗你玩的。
这就是 promise. 一个 promise 有三种状态:
- Pending: 待定,你不知道你是否能得到一部手机
- Resolved: 妈妈很高兴,你得到一部手机
- Rejected: 妈妈不高兴,你没有得到一部手机
状态图如下:
语法很简单:
1// promise syntax look like this
2new 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。
1var isMomHappy = false;
2
3// Promise
4var willIGetNewPhone = new Promise(
5 function (resolve, reject) {
6 if (isMomHappy) {
7 var phone = {
8 brand: 'Samsung',
9 color: 'black'
10 };
11 resolve(phone); // resolved
12 } else {
13 var reason = new Error('mom is not happy');
14 reject(reason); // rejected
15 }
16
17 }
18);
19
20var askMom = function () {
21 willIGetNewPhone
22 .then(function (fulfilled) {
23 // yay, you got a new phone
24 console.log(fulfilled);
25 // output: { brand: 'Samsung', color: 'black' }
26 })
27 .catch(function (error) {
28 // oops, mom didn't buy it
29 console.log(error.message);
30 // output: 'mom is not happy'
31 });
32};
33
34askMom();
很有点意思吧。抽象了,那就实际点。
javascripts 中的 fetch 函数就是基于Promise范式的,promise 的 resolves 绑在了 Response 上。Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数,.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。 .then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:
1 fetch("http://192.168.1.6/graph_image_hubot.php?action=view&local_graph_id=13619&rra_id=5")
2 .then(response => {
3 if (response.ok) {
4 return response.buffer();
5 }
6 throw new Error('Network response was not ok.');
7 })
8 .then(buffer => {
9 const formData = new FormData();
10 formData.append( 'media', buffer, {filename: 'bandwidth.png', contentType: 'image/png' } );
11 robot.wwork.sendImageMessage(owner, formData);
12 });
上面的程序实际上是从 cacti 服务器拿到一张流量图片,然后 fetch 的结果 anyway,response总是有东西的,都会是成功的,所以我们必须再用 response.ok 来判断,成功就继续得到 buffer 流,最终送到 hubot 中去。三个过程用 then 串了起来,每一步都成功才会进行下一步。
这样写避免了回调地狱,看起来也比较舒服。
那什么时候用 promise 呢,它是异步的,有大IO读写硬盘、读写网络的时候用,上面是读写网络。下面再来一个读写磁盘的:
1const {promise: {readFile, writeFile}} = require('fs');
2(async () => {
3 let content = await readFile('./data.txt', 'utf8');
4 await writeFile('2.txt', content, 'utf8');
5 console.log('ok');
6})();
上面的语法是 ES7 的,它更尽了一步,首先声明一个匿名的函数是 async 异步的,然后用await来进行等待,将读和写两个大操作都放到 await 的一步操作中去,这样程序看起来就变成同步的一步步等待了。跟 ES6 的 promise 比起来,更进了一步。