异步编程

  • 今天我们来聊聊异步编程,究竟什么是异步编程,什么由是同步编程呢?
  • 其实异步编程可以用一句话来概括,那就是:程序无须按照代码顺序自上而下的执行

CPU 与存储器

  • 我们要想了解异步编程就需要了解一下cpu和存储器,了解程序运行过程中CPU和存储器起到了什么作用或者说扮演了什么角色.

1. CPU

  • cpu是中央处理器,计算机核心部件,负责运算和指令调用。开发者编写的 JavaScript 代码在被编译为机器码以后就是通过 CPU 执行的。

2. 存储器

  • 内存:用于临时存储数据,断电后数据丢失。由于数据读写速度快,计算机中的应用都是在内存中运行的。

  • 磁盘:用于持久存储数据,断电后数据不丢失。内部有磁头依靠马达转动在盘片上读写数据, 速度比内存慢。

  • 计算机应用程序在没有运行时是存储在磁盘中的,当我们启动应用程序后,应用程序会被加载到内存中运行,应用程序中的指令会被中央处理器CPU来执行。

I/O 模型

  • 从数据库中查询数据(将磁盘中的文件内容读取到内存中),由于磁盘的读写速度比较慢,查询内容越多花费时间越多。无论 I/O 操作需要花费多少时间,在 I/O 操作执行完成后,CPU 都是需要获取到操作结果的,那么问题就来了,CPU 在发出 I/O 操作指令后是否要等待 I/O 操作执行完成呢 ? 这就涉及到 I/O 操作模型了,I/O 操作的模型有两种。
    • 第一种是 CPU 等待 I/O 操作执行完成获取到操作结果后再去执行其他指令,这是同步 I/O 操作 (阻塞 I/O)。
    • 第二种是 CPU 不等待I/O操作执行完成,CPU 在发出 I/O 指令后,内存和磁盘开始工作,CPU 继续执行其他指令。当 I/O 操作完成后再通知 CPU I/O 操作的结果是什么。这是异步 I/O 操作 (非阻塞 I/O) 。
    • 同步 I/O 在代码中的表现就是代码暂停执行等待 I/O 操作,I/O 操作执行完成后再执行后续代码。
    • 异步 I/O 在代码中的表现就是代码不暂停执行,I/O 操作后面的代码可以继续执行,当 I/O 操作执行完成后通过回调函数的方式通知 CPU,说 I/O 操作已经完成了,基于 I/O 操作结果的其他操作可以执行了 (通知 CPU 调用回调函数)。
    • 同步 I/O 异步 I/O区别就是是否等待 I/O 结果。

Nodejs 采用的就是异步非阻塞 I/O 模型。

  • 下面用一段代码展示:使用node内置模块先读取文档内容后,再输出Hello
// 调用node内置模块读取文件内容(fs模块)
const fs = require("fs")

// 先读取输出文件内容
fs.readFile("./x.txt", "utf-8", function (error, data) { console.log(data)})
// 在输出hello
console.log("Hello")
  • 结果展示:(证明node异步非阻塞的i/o模型)
    image

进程与线程

  • 每当我们运行应用程序时,操作系统都会创建该应用程序的实例对象,该实例对象就是应用程序的进程,操作系统会按照进程为单位为应用程序分配资源,比如内存,这样程序才能够在计算机的操作系统中运行起来。
    image

  • 线程被包裹在进程之中,是进程中的实际运作单位,一条线程指的就是进程中的一个单一顺序的控制流。也就是说,应用程序要做的事情都存储在线程之中。可以这样认为,一条线程就是一个待办列表,供 CPU 执行。
    image

Node中的JS运行是 单线程 还是 多线程 ?

  • Node.js 代码运行环境中,它为 JavaScript 代码的执行提供了一个主线程,通常我们所说的单线程指的就是这个主线程主线程用来执行所有的同步代码。但是 Node.js 代码运行环境本身是由 C++ 开发的,在 Node.js 内部它依赖了一个叫做libuvc++ 库,在这个库中它维护了一个线程池,默认情况下在这个线程池中存储了 4 个线程,JavaScript 中的异步代码就是在这些线程中执行的,所以说 JavaScript 代码的运行依靠了不止一个线程,所以 JavaScript 本质上还是多线程的。
    image

下面我们再聊聊js中天生的异步语法

1. 基于回调函数的异步编程

  • 回调函数是指通过函数参数的方式将一个函数传递到另一个函数中,参数函数就是回调函数。

  • 我们经常将回调函数写成callback,实际上它是 call then back 的简写,含义是调用后返回,就是在主函数中调用参数函数,参数函数调用完成后返回主函数继续执行主函数中的代码。

  • 异步编程中,异步 API 执行的结果就是通过回调函数传递参数的方式传递到上层代码中的。但是这种方法存在一个弊端,那就是回调地狱

// (1). 回调函数(callBack)
// 我们设置一个方法来模拟数据请求(url:请求地址,callback:请求成功后的回调函数)
const cbAjax = (url,callback)=>{//设置一个定时来模拟数据请求的耗时
setTimeout(()=>{
// console.log('数据请求回来了!',{name:'lam'});
callback({url,name:'lam'})
},1000)
}

// 调用方法: A -> B -> c(先获取A数据,再通过A数据中的Url获取B数据,最后同上获取C数据)
cbAjax('CallBack请求url',(dataA)=>{
cbAjax(dataA.url,(dataB)=>{
cbAjax(dataB.url,(dataC)=>{
console.log(dataC);
})
})
})
/* 循环一层一层的嵌套,这就是回调地狱 */

2. 基于Promise的异步编程

  • Promise JavaScript异步编程解决方案,可以解决回调函数方案中的回调地狱问题。

  • 可以将 Promise 理解为容器,用于包裹异步API的容器,当容器中的异步API执行完成后,Promise 允许我们在容器的外面获取异步 API 的执行结果,从而避免回调函数嵌套。

  • Promise 翻译为承若,表示它承若帮我们做一些事情,既然它承若了它就要去做,做就会有一个过程,就会有一个结果,结果要么是成功要么是失败。

  • 所以在 Promise 中有三种状态, 分别为等待(pending)成功(fulfilled)失败(rejected)

  • 默认状态为等待,等待可以变为成功,等待可以变为失败。

  • 状态一旦更改不可改变,成功不能变回等待,失败不能变回等待,成功不能变成失败,失败不能变成成功。

// (2). promise方法
// 我们设置一个方法来模拟数据请求(url:请求地址)
const pAjax = (url)=>{//设置一个定时来模拟数据请求的耗时
return new Promise((res,rej)=>{
setTimeout(()=>{
res({url,name:'lam'})
},1000)
})
}

pAjax('Promise请求数据的Url')
.then((dataA)=>{
return pAjax(dataA.url)
})
.then((dataB)=>{
return pAjax(dataB.url)
})
.then((dataC)=>{
console.log(dataC);
})
.catch((err)=>{//捕获错误
console.log(err);//输出错误
})

/* 这种链式调用就很好的解决了回调地狱问题,是代码看起来更加高逼格 */

async/await异步函数解决异步编程

  • Promise 虽然解决了回调地狱的问题,但是代码看起来仍然不够简洁。

  • 使用async/await可以更加简化异步编程代码的编写

  • async 声明异步函数的关键字,异步函数的返回值会被自动填充到 Promise 对象中。

  • await 关键字后面只能放置返回 Promise 对象的 API

  • await 关键字可以暂停函数执行,等待 Promise 执行完后返回执行结果。

  • await 关键字只能出现在异步函数中(只能出现在async的里面,不能单独存在)。

// (3). async/await方法
// 我们设置一个方法来模拟数据请求(url:请求地址)
const pAjax = (url)=>{//设置一个定时来模拟数据请求的耗时
return new Promise((res,rej)=>{
setTimeout(()=>{
res({url,name:'lam'})
},1000)
})
}

async function AsyncAjax(url){
const dataA = await pAjax(url)
const dataB = await pAjax(dataA.url)
const dataC = await pAjax(dataB.url)
console.log(dataC);
}
AsyncAjax('Async的请求url')