前言
本文的前置文章
不知道看了上一篇之后,是否对node形成了初步的印象。是的,异步编程的实现给cpu处理IO的效率带来了极大的提高。但是,异步编程在发展的过程可远没有我说的那么简单,关于异步还有很多你需要知道的地方,这一篇我将继续和你一起探究。
本文涉及到的知识点
Promise 一个可以将回调转换成async的语法
async 和 await 上次提到的两个关键字
node中的异步处理逻辑
我会从两个例子来介绍node的异步,希望你看完能对他有一个直观的感受。这是两个很有意思也很典型的例子,如果你觉得有点疑惑,那也是非常正常的,可以试着搜索一下其他的材料。
1、setTimeout。
先从setTimeout这个函数说起,setTimeout(handle,timeout)是javaScript中一个典型的异步函数,其中handle是一个回调函数,这个函数的作用是经过了timeout毫秒之后回来调用这个handle函数。上一篇中说过,异步函数都是用这些回调函数来实现的。so,node的底层实际上就是这么一些回调函数,学习异步,先从这些回调函数开始。
我们先对这样一段代码进行实验
console.log('1'); //1
setTimeout(()=>{ //2
console.log('2');
},1000);
console.log('3'); //3
setTimeout(()=>{ //4
console.log('4');
},0);
setTimeout(()=>{ //5
console.log('5');
},0);
console.log('6'); //6
大家可以先想一下可能会输出什么。
直觉来看,函数往下走,首先输出1,第二个函数那里会让console.log('2')延后一秒,然后输出3,第四个函数那让console.log('4')延后0秒,那应该就是直接输出了,然后再输出5,再输出6,隔了1秒之后最后输出2。
可以实验一下这段代码,他的最终输出是:1 3 6 4 5 2
嗯?6要比4和5早输出?为什么会这样?
实际上,在node中执行顺序是这样的,在执行到第四个函数的时候(前面已经输出了1和3),这时候由于setTimeout是一个异步函数,他会把里面的console.log('4')放进一个事件树中,等所有代码都执行完了、时间到了、再来执行他,函数5同理。所以,这里即使他的timeout参数是0,也不会马上就执行。
所以学习node的异步,你首先要尽量让自己免受以前同步写法的影响。要知道,他并不是像java一样,从一个入口进去,一直运行到结束。在node这里,他会额外维护着一个事件队列。
2、async与await
再来看一下这段代码
async function func(){
console.log(1);
await func1();
console.log(3);
await func2();
console.log(5);
}
async function func1(){
console.log(2);
}
async function func2(){
console.log(4);
}
func();
输出的结果是 1 2 3 4 5 。
可见在在单个async函数内部中,只要调用了await进行阻塞,内部就是严格的顺序执行的。
但是多个async函数之间可就不是这样。
//这个函数的意思是暂停ms毫秒,然后返回num。
async function sleep(num,ms){//下面会解释为什么这么写
return new Promise((resolve,reject)=>{
setTimeout(()=>{resolve(num)}, ms);
});
}
async function print(num,ms){
while(true){
let res = await sleep(num,ms);
console.log(res);
}
}
async function application(){
print(0,2000); //1
print(100,5000); //2
}
application();
可以跑一下这个例子。他的输出结果是
11
12
22
13
14
23
15
16
17
24
首先,application调用了两个async函数,没有用await来控制他们的顺序执行,所以第一个print不会影响到第二个print。
第二,在单个async里,由于有await阻塞sleep,所以一个sleep执行完了才接着执行下一个sleep,不会出现一瞬间执行很多个setTimeout的效果。
有了这种语法,即使只有一个单线程,但实现上似乎就变成了多线程。这就是异步编程的奇异之处。
promise与async
在上面那个例子中,应该有人会好奇这里为什么要这么写
async function sleep(num,ms){
return new Promise((resolve,reject)=>{
setTimeout(()=>{resolve(num)}, ms);
});
}
这里是为了将setTimeout这个回调函数转变为async这种同步写法。
同上面所说的,如果你在一个函数中多次调用异步函数,那他在直观上是跟多线程一样并行执行的。
function application(){
setTimeout(()=>{console.log('hello')},5000);
setTimeout(()=>{console.log('hello')},5000);
setTimeout(()=>{console.log('hello')},5000);
}
application();
这个程序执行后,会在过了5秒之后,连续输出3个helle。那如果我想每隔5秒输出一个呢?不好意思,你只能用promise将他转化为async可以识别的内容。转化完了之后再用await来阻塞执行。
async function printAfter5s(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{console.log('hello')}, 5000);
});
}
async function application(){
await printAfter5s();
await printAfter5s();
await printAfter5s();
}
application();
promise就是回调函数转化为async的一个桥梁。
当然他也有其他的写法如thePromise.then().then().reject()这种,但我一般都不用,感觉最好的还是让他和async配合使用,比较直观。
如何封装一个回调函数
一般我是这么封装的
function myFunction(){ //这里和async function myFunction没有区别
return new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve('hello world');
} else {
reject(new Error('a error'));
}
});
}
首先,一个函数如果返回的是一个Promise对象,那他默认就是一个async函数。
其次,resolve函数和reject函数里面的参数,会分别被这样接收
async function application(){
try{
let res = await myFunction();
console.log(res);
} catch(err) {
console.log(err.message);
}
}
如果异步操作成功,那就会输出hello world,如果失败了就输出一个 a error。
一般来说,一个规范的回调函数(setTimeout除外),都会提供一个默认的回调函数给你,比如mysql的官方示例中查询是这样的。
connection.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
if (error) throw error;
console.log('The solution is: ', results[0].solution);
});
数据库首先会进行查询,如果查询成功,results中就会有数据;如果失败了(比如你的查询语句有问题/没有这个表),那error就会有数据(值是一个Error对象)
这是我们就可以对他进行简单的封装
function query(sql){
return new Promise((resolve,reject)=>{
connection.query(sql, function (error, results, fields) {
if (error) reject(error);
resolve(results);
});
});
}
这样,我们就可以用await query('SELECT 1 + 1 AS solution');来调用他了。
规范来讲,大多数的异步函数提供的回调函数,其参数都是error放在第一位,结果放在error后面,有这些规范的函数,我们都可以使用utils.promisify来将他转变为async
比如最常见的文件读取函数
const util = require('util');
const fs = require('fs');
const readFileAsync = util.promisify(fs.readFile);
async function application(){
try{
let res = await readFileAsync('e://a.txt');
} catch(err) {
console.log(err);
}
}
application();
需要注意的是,await 需要放在async函数里面,所以每次我都得套一层函数在上面,写起来稍显复杂,在实际编写的时候其实不会这么复杂。
这一篇就先讲到这里,希望对你有些帮助。
如果觉得有用的话可以留下你的点赞和评论,蟹蟹