前言:这里只是列出node.js异步编程一些基本概念,对深入的原理和机制并不了解,只是对自己的学习知识的巩固。若有错误请指出。
异步API和同步API
同步API:只有当前API执行完毕后,才会继续向下执行。
异步API:当前异步API执行不会阻塞后续代码执行。
在node.js中常见的异步API:
fs readFile('./deom/txt',(err,result)=>{ //读取文件
//operation
})
//服务器事件处理函数是异步API
const http = require("http");
const server = http.createServer();
server.on('request', (req, res) => {
//operation
})
异步API和同步API区别
1.返回结果不同
同步API可以直接从返回值中拿到相应结果,而异步API不可以。
var str = 'hello';
var str1 = str.concat(" node.js")
console.log(str1);
结果:
hello node.js
很显然这里的拼接字符串是同步API,可以直接获取返回值。
下面我们来看setTimeout()
。
function getMsg() {
setTimeout(function () {
return "hello node.js";
}, 2000)
}
let msg = getMsg();
console.log(msg);
结果:
PS D:\> node .\dome.js
undefined
此处返回的结果并不是我们所想的hello node.js
,这是因为setTimeout()
是异步API,当函数执行到异步API时,会把异步API放到异步代码执行区内,然后继续执行同步API,当同步API执行完会再去执行异步API,当同步API执行完时就会隐式的return undefined
,然后赋值给msg
且输出,当再去异步API再去执行返回结果时,getMsg
函数都已经返回undefined
且输出了。
那么如何解决这个问题呢?我们来看下面这个代码:
function getMsg(callback) {
setTimeout(function () {
callback({
msg: "hello node.js"
});
}, 2000);
}
getMsg(function (data) {
console.log(data);
})
结果:
PS D:\> node .\dome.js
{ msg: 'hello node.js' }
这里和上面的代码相比加入了回调函数就可以返回异步API的结果,我们来分析一下过程,首先在getMsg
函数中传入一个函数作为参数,即回调函数,当函数执行时会把回调函数传进setTimeout
中,当setTimeout
执行完后就会执行回调函数,在执行回调函数时传入一个对象作为参数。最后通过调用getMsg
函数并传入回调函数得到setTimeout
执行完返回结果。
所以异步API想要的到返回结果,可以通过回调函数,当异步API执行完毕后执行回调函数返回结果。
2.代码执行顺序不同
同步API:从上到下执行,上方代码会阻塞下方代码。
异步API:不用等待异步API执行完再去向下执行代码。可继续向下执行代码。
我们来看下面代码:
console.log("代码开始");
setTimeout(() => console.log("2s"), 2000);
setTimeout(() => console.log("0s"), 0);
console.log("代码结束了");
结果:
PS D:\> node .\dome.js
代码开始
代码结束
0s
2s
上面代码中,为什么在setTimeout
中0秒的定时器却在“代码结束”后输出呢?
这里分析下执行过程:
首先代码会从上到下依次执行,遇到“代码开始”放入同步代码执行区同时执行,然后遇到2s定时器放入异步代码执行区且不执行,同时会把异步API的回调函数放入回调函数队列且不执行。0s同上,最后把“代码结束”也放入同步代码执行区执行。现在同步全部执行,再去执行异步代码执行区,在当前异步代码执行完,就会把回调函数放入同步执行区执行。然后再去执行下一个异步API,重复。
所以可以看出,在代码执行时,异步API无论时间长短,即使0秒,都会等同步API执行完毕后再去执行异步API。
promise
现在这里有这样一个问题:
当代码运行时候,一个异步API后面的代码依赖异步API返回的结果时该怎么办???
通常我们可以把一个依赖异步API代码放入异步API的回调函数,这样就可以在异步API结束后有返回值再去执行依赖部分。但是这样有很大的毛病。
有这样一个需求:有三个文件1.txt,2.txt,3.txt,要求依次读取文件。
const fs = require("fs");
fs.readFile('./1.txt', 'utf8', (err, result1) => {
console.log(result1);
fs.readFile('./2.txt', 'utf8', (err, result2) => {
console.log(result2);
fs.readFile('./3.txt', 'utf8', (err, result3) => {
console.log(result3);
});
});
});
结果:
PS D:\新建文件夹> node .\dome.js
第一个文件
第二个文件
第三个文件
因为要求是依次读取,且fs.readFile()
它是异步API,受文件大小影响fs.readFile()
不能准确得到读取文件顺序。所以就要在读取完一个再去读取下一个,形成回调嵌套。
这样的代码晦涩难懂,难于维护,这样就形成的“回调地狱”。它是在异步API的回调函数中嵌套另外一个异步API,而另外一个异步API的回调函数再继续嵌套下一个异步API,往复循环。
解决方法:promise
Promise
是一个对象,它代表了一个异步操作的最终完成或者失败。Promise
是一个构造函数返回的对象。要通过new
创建。
如何使用promise呢?
Promise是一个构造函数,通过new调用,模板:
let promise = new Promise((resolve, reject) => {//构造函数,创建promise对象
setTimeout(() => {
if (true) resolve("成功")//将异步API结果输出函数体外部
else reject("失败信息")//若失败将失败信息输出函数体外部
}, 2000);
});
promise.then(result => console.log(result))
.catch(error => console.log(error))
在使用promise有以下约定:
- 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
- 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
- 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序执行。
通过promise就可以解决“回调地狱”
const fs = require("fs");
const {
resolve
} = require("path");
const {
rejects
} = require("assert");
function p1() {
return new Promise((resolve, rejects) => {
fs.readFile('./1.txt', 'utf8', (err, result1) => {
resolve(result1);
})
})
}
function p2() {
return new Promise((resolve, rejects) => {
fs.readFile('./2.txt', 'utf8', (err, result2) => {
resolve(result2);
})
})
}
function p3() {
return new Promise((resolve, rejects) => {
fs.readFile('./3.txt', 'utf8', (err, result3) => {
resolve(result3);
})
})
}
p1().then((result1) => {
console.log(result1);
return p2();
}).then((result2) => {
console.log(result2);
return p3();
}).then((result3) => {
console.log(result3);
})
结果:
PS D:\新建文件夹> node .\dome.js
第一个文件
第二个文件
第三个文件
这里把promise
放入新声明的函数中,是因为创建promise
对象会立即执行,同时把声明promise
对象返回方便后面链式调用。
这里明显比回调嵌套看起来干净整齐,同时更具有维护性。
异步函数
异步函数是ES2017的新特征。它可以将普通函数转化为异步函数,而且用return
返回结果。(结果包裹在promise
中,用return
代替resolve
)它的返回值是promise
。
const fs = require('fs');
const promisify = require('util').promisify;
const readFile = promisify(fs.readFile);
async function run() {
let p1 = await readFile('1.txt', 'utf8');
let p2 = await readFile('2.txt', 'utf8');
let p3 = await readFile('3.txt', 'utf8');
console.log(p1);
console.log(p2);
console.log(p3);
}
run();
结果:
PS D:\新建文件夹> node .\dome.js
第一个文件
第二个文件
第三个文件
这里首先获取util
模块的promisify
方法,这个方法会对异步API进行包装使其返回promise
对象。只有promise
对象才能使用异步操作。然后我们把fs.readFile
方法传递进去就会得到一个新方法,能返回promise
对象。
await
关键字则是暂停异步API向下执行,(类似于同步执行)直到promise
返回结果。注意:await
后面只能是promise
对象。
如有错误,请在底部留言。