一,引出同步和异步

在之前的代码实现过程中,
在这里插入图片描述
前两个是通过返回值的方式取得结果,而第三个则是通过函数的方式得到读取的文件。
于是引出同步api和异步api

二,同步API和异步API

在这里插入图片描述
通俗地讲,就是代码一行行执行,只有本行语句执行结束了,才会去执行下一句。
在这里插入图片描述
这里的定时器代码就是异步API,程序并不会等待异步API执行完成之后再去执行其他代码。
然而学到这里,又学了几节课,我完全懵了,啥是同步啥是异步?又去查资料,看博客,又扯出阻塞,非阻塞的概念……
完全躺尸了!!!
异步的实现原理是啥?为啥程序不再一行行执行?

首先,先了解啥是同步,啥是异步。
同步(Synchronous),程序的执行顺序与任务的排列顺序是一致的、同步的,也就是按顺序执行代码。
异步模式则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。


<body>
       <button id="Button">展示异步操作</button>
       <script>
         var Button=document.getElementById('Button');
         Button.onclick=function(){
             alert('展示异步操作--a');
         }
         alert('展示异步操作--b');
       </script>
</body>

这就算异步,代码先弹出:展示异步操作–b,然后点击按钮后才执行事件函数弹出对话框。
接下来了解阻塞,非阻塞,同步,异步的区别。这两个例子来自于这个博客:
https://blog.csdn.net/u012308586/article/details/90606258

三,阻塞,非阻塞,同步,异步的区别。

网络中获取数据的读操作步骤:
等待数据准备。
数据从内核空间拷贝到用户空间。

同步与异步:
同步与异步是针对应用程序与内核的交互而言。从缓存中读取数据,如果缓存中数据还没有准备好,如果是同步操作,它会一直等待,直到操作完成。如果是异步操作,那么它会去做别的事情,等待数据准备好,内核通知它,它再去读取数据。

阻塞与非阻塞:
应用进程请求IO操作时,如果数据未准备好,如果请求立即返回就是非阻塞,不立即返回就是阻塞。简单来说,就是做一件事如果不能立即获得返回,需要等待,就是阻塞,否则可以理解为非阻塞。

故事:老王烧开水
出场人物:老王,两把水壶(水壶,响水壶)

同步阻塞: 效率是最低的
老王用水壶烧水,并且站在那里(阻塞),不管水开没开,每隔一定时间看看水开了没(同步->轮询)。
同步非阻塞: 实际上效率是低下的
老王用水壶烧水,不再傻傻的站在那里,跑去做别的事情(非阻塞),但是还是会每个一段时间过来看看水开了没,没开就继续去做的事情(同步->轮询)。
异步阻塞: 异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息时被阻塞。
老王用响水壶烧水,站在那里(阻塞),但是不再去看水开了没,而是等水开了,水壶会自动通知它(异步,内核通知进程)。
异步非阻塞: 效率更高,注册一个回调函数,就可以去做别的事情。
老王用响水壶烧水。跑去做别的事情(非阻塞),等待响水壶烧开水自动通知它(异步,内核通知进程)

同步/异步关注的是消息通知的机制(自己去看,还是自动通知),而阻塞/非阻塞关注的是程序(线程)等待消息通知时的状态(可以做其他事,还是停在那里)。

再来一个例子(还是那篇博文中的例子)
以小明下载文件打个比方,
1,同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。

同步体现在:等待下载完成通知;

阻塞体现在:等待下载完成通知过程中,不能做其他任务处理;

2,同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。

同步体现在:等待下载完成通知;

非阻塞体现在:等待下载完成通知过程中,去干别的任务了,只是时不时会瞄一眼进度条;【小明必须要在两个任务间切换,关注下载进度】

3,异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(看起来很傻,不是吗)。

异步体现在:下载完成“叮”一声通知;

阻塞体现在:等待下载完成“叮”一声通知过程中,不能做其他任务处理;

4,异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。

异步体现在:下载完成“叮”一声通知;

非阻塞体现在:等待下载完成“叮”一声通知过程中,去干别的任务了,只需要接收“叮”声通知即可;【软件处理下载任务,小明处理其他任务,不需关注进度,只需接收软件“叮”声通知,即可】

也就是说,同步/异步是“下载完成消息”通知的方式(机制),而阻塞/非阻塞则是在等待“下载完成消息”通知过程中的状态(能不能干其他任务)

四,异步API和同步API的区别-怎么拿到异步API的执行结果

node.js目前我学过的,好像同步阻塞,异步非阻塞这两种情况。
1,同步API可以从返回值中拿到API执行的结果,但是异步API是不可以的。
在这里插入图片描述
那异步API的执行结果怎么拿到呢?
实际上它是通过回调函数拿到的。所谓回调函数,就是自己定义好,让别人去调用的函数。

在这里插入图片描述
在这里插入图片描述
此时,这个匿名函数传递给形参,然后被调用了(函数执行了)
再修改代码。给匿名函数以形参:
在这里插入图片描述
如果,gerData函数内部有异步操作,那么,在异步操作完成之后,便可调用这个回调函数callback。
并且呢,把异步操作中执行的结果,通过参数的形式传递给callback。
那么,我们在getData里面的这个回调函数(就是那个匿名函数),就可以拿到这个异步操作执行的结果了。

在这里插入图片描述
用这种方式修改之前的代码,先是调用getMsg函数,此时实参是匿名函数,然后执行异步操作,等两秒钟之后,执行callback函数(也就是匿名函数),此时这个匿名函数的实参是异步操作的结果。于是就取得了异步操作的执行结果。

五,同步API和异步API的区别-代码执行顺序

同步api从上到下依次执行,前面的代码会阻塞后面代码的执行。
在这里插入图片描述
如上图,会等待for循环结束后才开始执行后面的代码。
而异步api不会等待API执行完成后在再向下执行。
在这里插入图片描述
如这个代码,就不是按顺序执行的。
node.js是把所有的同步API执行完之后,再执行异步API,当异步API执行完毕之后,系统会把回调函数队列中对应的回调函数放到同步代码执行区域执行。
在这里插入图片描述

六,Node.js中的异步API

在这里插入图片描述
这就是比较典型的异步编程,node.js中的fs模块中的readFile方法就是一个函数,而(err,result)=>{}则是回调函数。当readFile里面的代码执行完毕之后,系统会来调用这个回调函数。

// 在模块中,readFile函数可能就是这样定义的:
function readFile(filepath,callback){
	……
	异步代码块
	……
	// 异步代码执行的结果正是error和result,作为回调函数callback的参数!!
	//以此来调用执行回调函数。
	//也就是说,这个回调函数就是起一个响水壶的提示作用(水烧开了就会响)
	//即异步代码操作结束后就会调用callback
	callback(error,result)
}
// 使用时:
readFile(filepath,(error,result)=>{
	res.end(result)
	//也就是把异步操作的结果搞出来执行其他操作
})

所以,我们的readFile代码是这样运行的:这个方法的第二个参数是个函数!而且在方法内部传入实参调用了!!!而这个实参其实就是方法内部的运行结果!!

fs.readFile(realPath,(error,result)=>{
		if(error==null){
			res.writeHead(200,{
				'content-type':type
			})
			//指明文件类型,配之以对应的解析格式
			res.end(result)
			
		}else{
			res.writeHead(404,{
				'content-type':'text/html;charset=utf8'
			})
			//注意解析格式写在这里,如果写前面,会导致以html格式解析读取到的result
			//也即是只会显示html,而不会有样式了。
			res.end('读取文件失败')
			return
		}
	})

这种做法好机智啊!
于是,可以知道啥是回调函数
当程序跑起来的时候,应用程序会通过API调用库里面预先准备好的函数。但是有些库函数却需要先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入、后来又被调用的函数被称为回调函数。
也就是说,**先定义好函数体(回调函数),作为一个参数传入,然后要使用时调用。**这又和闭包不同,闭包的函数体在另一个函数内部,而回调函数则常常作为函数的一个参数传入,然后在函数内部合适位置调用。

七,回调地狱

在这里插入图片描述
将console.log('文件读取结果‘)这句代码放置在回调函数中执行是可以,但如果需要依次读取a,b,c,三个文件呢?
这样写的代码就是:
在这里插入图片描述
这种多层回调的写法,就称为回调地狱,很难维护。
为了解决回调地狱的问题,又引出了es6中的一个叫做Promise的东西。

八,Promise出现是为了解决node.js中异步编程产生的回调地狱问题

它其实是一种异步编程语法上的改进,可以让我们异步API的执行和结果的处理,进行分离。

const fs=require('fs')
let promise  =new Promise((resolve,reject)=>{
	fs.readFile('./1.txt','utf8',(err,result)=>{
		if(err!=null){
			reject(err)
		}else{
			resolve(result)
		}
	})
})
promise.then((result)=>{
	console.log(result)
})
//执行promise里面的then方法,这里面要求你传递一个匿名函数,也就是说
// 当异步API执行成功的时候,你调用resolve,
// 那么你调用resolve实际上就是在调用then里面的匿名参数
//相当于再用一次回调函数,把结果拿到全局来。
.catch((err)=>{
	console.log(err)
})

这时候已经把异步过程和执行结果做到了分离,呃,我还不是很懂。。。。
那就先来看看它是如何做到分离的吧。
现在将之前的依次读取三个文件的代码修改如下:

let p1 =new Promise((resolve,reject)=>{
	fs.readFile('./1.txt','utf8',(err,result)=>{
		resolve(result)
		//如果读取成功,利用resolve传递到外面
	})
})
let p2 =new Promise((resolve,reject)=>{
	fs.readFile('./2.txt','utf8',(err,result)=>{
		resolve(result)
		//如果读取成功,利用resolve传递到外面
	})
})
let p3 =new Promise((resolve,reject)=>{
	fs.readFile('./3.txt','utf8',(err,result)=>{
		resolve(result)
		//如果读取成功,利用resolve传递到外面
	})
})

但是现在依旧无法保证读取文件的操作是依次进行的,因为一旦new一个promise对象,就会执行其内部的代码,返回result。为了让文件读取操作依次进行,将代码修改如下:
各自放到一个函数里面去,想要读取哪个文件就调用哪个函数:

function p1(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./1.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
function p2(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./2.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
function p3(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./3.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
//p1返回值是一个Promise对象,执行then方法,
//异步API,readFile执行成功的时候,你调用resolve,实际上就是在调用then里面的匿名函数
//也就是resolve是个函数参数,而then里面的匿名函数就是它的实参!
p1().then((r1)=>{
	console.log(r1)
})

想要链式编程,依次读取文件:

function p1(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./1.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
function p2(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./2.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
function p3(){
	return new Promise((resolve,reject)=>{
		fs.readFile('./3.txt','utf8',(err,result)=>{
			resolve(result)
			//如果读取成功,利用resolve传递到外面
		})
	})
}
//p1返回值是一个Promise对象,执行then方法,
//异步API,readFile执行成功的时候,你调用resolve,实际上就是在调用then里面的匿名函数
//也就是resolve是个函数参数,而then里面的匿名函数就是它的实参!
p1().then((r1)=>{
	console.log(r1)
	return p2()   //这里又会返回一个Promise对象了!
				  //于是就可以继续使用promise的then方法了
})
.then((r1)=>{
	console.log(r1)
	return p3()  //这里又会返回一个Promise对象了!
})
.then((r1)=>{
	console.log(r1)
})

也就是说,Promise的使用,把异步操作的结果提取到外面来了,以后想访问哪个结果,就访问哪个promise的then的匿名函数的对应参数即可。
这就解决了回调地狱的问题。

九,node.js的异步编程

现在我们已经通过本promise解决了回调地狱的问题,但是现在的写法还是很繁琐的。
为了让代码看起来更加清晰明了,es7中又产生了异步函数的办法。
它实际上是对promise对象的封装,只开放了简洁好用的关键字给我们使用。
在这里插入图片描述
在这里插入图片描述
在普通函数前面增加async关键字,普通函数就变成了异步函数

//1,在普通函数前面加上async关键字,就变成异步函数
//2,异步函数的默认返回值是一个promise对象
async function fn(){
	
}
console.log(fn())

在这里插入图片描述
实际上,异步函数是把你函数的返回值放在一个promise对象里面。
在这里插入图片描述
也就是说,这时候,返回值放置在promise里面,想要获取这个返回值。就需要使用promise.then()

//1,在普通函数前面加上async关键字,就变成异步函数
//2,异步函数的默认返回值是一个promise对象
async function fn(){
	return 123
}
// console.log(fn())
fn().then(function(data){
	console.log(data)
})
//也就是异步函数中的return关键字替代了resolve方法!
//执行return 123,实际上就是调用function(data){
//	console.log(data)
// }函数,且参数是return的值。

那promise中de的reject对应的关键字又是啥?是throw!
需要注意的是**,throw的代码一旦执行,后续的代码就不执行了,因为发生了错误**
1,在普通函数前面加上async关键字,就变成异步函数
2,异步函数的默认返回值是一个promise对象
3,在异步函数内部使用throw关键字进行错误的抛出,使用return关键字传递结果

//1,在普通函数前面加上async关键字,就变成异步函数
//2,异步函数的默认返回值是一个promise对象
async function fn(){
	// return 123
	throw '发生了一些错误'
}
fn().then(function(data){
	console.log(data)
})
.catch((error)=>{
	console.log(error)
})

在这里插入图片描述

十,异步函数await关键字

1,它只能出现在异步函数中。
2,await promise 它可以暂停异步函数的执行,等待promise对象返回结果后再向下执行代码
我们再以依次读取三个文件为例子:

async function p1(){
	return 'p1'
}
async function p2(){
	return 'p2'
}
async function p3(){
	return 'p3'
}
async function run(){
	let r1 = await p1()
	//也就是说,await关键字不让程序往下执行了,直到返回结果
	//也就是可以直接返回结果,传递给r1了。
	//这样一来,异步就变成了同步!
	let r2 = await p2()
	let r3 = await p3()
	console.log(r1)
	console.log(r2)
	console.log(r3)
}
run()

也就是说,await关键字不让程序往下执行了,直到返回结果,也就是可以直接返回结果,传递给r1了。这样一来,异步就变成了同步!
在这里插入图片描述

十一,使用异步函数改写依次读取三个文件的代码

const fs=require('fs')
const promisify=require('util').promisify
//拿到这个方法,它的作用是让node.js的库函数变成返回值是promise对象
const readFile=promisify(fs.readFile)
//让promisify处理readFile这个库函数,然后使它变成返回值是一个promise对象。
async function run(){
	let r1=await readFile('./1.txt','utf8')
	let r2=await readFile('./2.txt','utf8')
	let r3=await readFile('./3.txt','utf8')
	//把异步操作变成个同步操作,直接获取值了!
	console.log(r1)
	console.log(r2)
	console.log(r3)
}
run()
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐