面试题 - 五种异步处理的实现方案
一、异步:现在与将来
1 - 异步机制
- 什么是异步机制
a. 对于一段js代码,主要分为两块,一块是现在执行,一块是将来执行。
b. 一旦把一部分代码包装成一个函数,并指定它在响应某个事件时执行,那就形成了一个将来时代码块,同时也引入了异步机制。
2 - 事件循环机制
- 什么是事件循环机制
a. js 引擎本身做的事情是:在需要的时候,在给定的任意时间段执行单个代码块。即 js 引擎本身并没有时间概念,只是按需执行js的代码片段。
b. js 引擎运行的宿主环境都提供了一种机制:能够处理程序中多个块的执行,并且在每个块执行时都调用js 引擎。这种机制被称为 事件循环机制。
- 关于setTimeout的异步说明
a. 在执行到 setTimeout() 时,并没有立刻把回调函数挂在事件循环队列中,它只是设定了一个定时器。当定时器结束后,宿主环境会把回调函数放在事件循环队列中。
b. 如果计时器结束后,事件循环队列中有20个项目在等待,那回调函数就被放在20个项目之后,当前面20个项目执行结束后,才会触发回调函数。即:定时器只能保证事件不会在设定的时间之前触发,但是在之后的什么时候触发取决于事件队列。
c. 一般来说,通常没有抢占式的方式直接把事件放在队列之首。
- 模拟事件循环机制的执行
let eventLoop = [],event ; // 先进,先出
while (true){
if (eventLoop.length > 1){
event = eventLoop.shift(); // 拿到事件队列中的第一个事件,每一个事件相当于一个 tick
try {
event();
}catch (err){
new Error(err);
}
}
}
3 - 任务循环
- 什么是任务循环
任务循环是在ES6中提出的新概念,可以理解为 当每一个 tick 执行时,任务会挂在每一个tick之后执行。在事件循环的每个tick中,可能触发的异步操作会挂在每一个tick列表最后,即当前tick执行结束后,执行任务。
4 - 并行和并发
- 并行线程:指能够同时发生的事情,多个线程能够共享单个进程的数据。但是在js中,事件循环把自身的工作分成一个个任务顺序执行,并不允许对共享内存进行并行访问和修改。
- 并发“进程”:两个或多个事件链随时间发展交替执行。但是同一时刻,一次只能从事件队列中处理一个事件。
- 非交互:互补影响
- 交互:产生竞态
- 协作:取得一个长期的进程,把他分隔成多个步骤或多批任务进行。
// 模拟处理大数据协作
let res = [];
function responseData(data){
let chunk = data.splice(0,1000);
res = res.concat(
chunk.map(item => ++item)
);
if (data.length > 0){
setTimeout(function (){
responseData(data)
},0);
}
}
二、常见的实现异步编程的方案
- 回调
- 事件监听
- Promise
- Generator
- async/await
三、回调
1 - 回调地狱
- 回调地狱伪代码
// 嵌套回调 - 伪代码
addEventListener('click',function handler(){
setTimeout(function request(){
ajax(url , function response(data){
if (data === 'hello'){
request()
}else {
handler()
}
})
},5000)
})
// 链式回调 - 伪代码
addEventListener('click', handler);
function handler(){
setTimeout(request,5000)
}
function request(){
ajax(url , response)
}
function response(data){
console.log(data);
}
- 回调地狱的本质
- 我们需要在大量的逻辑代码中跳来跳去查看代码执行顺序
- 需要硬编码处理回调函数在被调用的过程中会遇到的各种问题,而这种问题大多数情况下是不可复用的,所以使得代码更加难以理解和维护。
- 回调地狱 - 回调函数的执行信任问题
// 回调地狱的信任问题 - 假设有一个第三方库 Ajax2,
Ajax2(param , function ({a,b}){
// 根据返回结果执行扣费函数
})
// 01 - 过早调用:没有a/b,扣费为0
// 02 - 过晚调用/不调用:sum值已修改
// 03 - 调用多次:sum被累加多次,扣费多次
// 04 - 没有a或者b属性,扣费失败
// 05 - 吞掉会出现的异常,扣费失败
四、Promise
1 - Promise机制
- 什么是promise
promise 是 ES6中提出的一种解决异步处理方案的方法,是一种封装和组合未来值的易于复用的机制。Promise一旦决议,它就变成了一个不可改变的值。
- 如何判断一个值是不是Promise
识别Promise(或者行为类似于Promise的东西),就是定义某种称之为thenable的东西。将其定义为任何具有then方法的对象和函数。任何这样的值就是和Promise行为一致的值。
// Promise 的链式调用
let P = new Promise(((resolve, reject) => {
// 处理一些事情
resolve(2)
}));
P.then(v => {
console.log(v); // 2
return v * 2
}).then(vv => {
console.log(vv) // 4
})
2 - Promise 的状态
Promise 本身的三种状态
- pending:初始状态,没有完成也没有被拒绝
- fulfilled:已成功完成的状态
- rejected:失败完成的状态
说明:Promise 的状态一旦发生改变,就不能再次修改,即内部状态的修改时不可逆的。
3 - Promise 的静态方法
//1.获取轮播数据列表
function getBannerList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('轮播数据')
},300)
})
}
//2.获取店铺列表
function getStoreList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('店铺数据')
},500)
})
}
//3.获取分类列表
function getCategoryList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('分类数据')
},700)
})
}
let promiseArray = [getBannerList() , getStoreList() , getCategoryList()];
- Promise.all
参数:可迭代的对象,例如Array
作用:当所有结果成功返回时按照请求顺序返回成功。当其中有一个失败方法时,则进入失败方法。
// 所有的Promise 都执行成功了才会走then方法,有一个失败了,就会走reject方法,参数是reject的原因
let data1 = Promise.all(promiseArray).then(v=>{
console.log(v) // [ '轮播数据', '店铺数据', '分类数据' ]
}).catch(err => {
console.log(err)
});
- Promise.allSetted
参数:可迭代的对象
作用:不管执行成功与失败,都返回每一个参数的状态和执行结果
let data2 = Promise.allSettled(promiseArray).then(v => {
console.log(v)
}).catch(err=>{
console.log(err)
})
- Promise.any
参数:可迭代对象
作用: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态
let data3 = Promise.any(promiseArray).then(v=>{
console.log(v);
}).catch((err) => {
console.log(err)
})
- Promise.race
参数:可迭代对象
作用: race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。
用途:设置超时时间,判断一些资源是否在规定时间内加载成功。
// 哪个Promise先获取到结果,则整个Promise的状态都被决议无法修改
let getImage = function (){
return new Promise((resolve, reject) => {
setTimeout(function (){
resolve('图片请求成功')
},1000)
})
}
let timeOut = function (){
return new Promise(((resolve, reject) => {
setTimeout(function (){
resolve('图片加载失败');
},500)
}))
}
let ImageResult = Promise.race([getImage(),timeOut()]).then(v => {
console.log(v) //图片加载失败
}).catch((err) => {
console.log(err)
})
4 - Promise 链式流
- 能够形成链式流的原因
- 每次执行.then() 函数都会自动创建一个新的Promise并返回
- 在处理函数内部,如果返回一个值或者抛出一个异常,新连接的Promise会立即被决议
- 如果内部处理函数返回的是一个Promise 或者 thenable,那它将会展开代替自动创建的Promise。
- 如何在链式中引入异步操作
let p1 = new Promise((resolve, reject) => {
setTimeout(function () {
console.log("500ms 后执行");
resolve(500)
},500)
})
p1.then(v => {
console.log("我等待了500ms才执行");
console.log(v)
return v *2
})
// 500ms 后执行
// 我等待了500ms才执行
// 500
- 解决回调地狱问题
链式流通过把嵌套的回调函数形式修改成了链式调用,解决了回调函数产生的回调地狱的问题。
五、Generator
1 - Generator 简介
- 什么是Generator
Generator 是一种带*的函数,但是又不是一种函数。他需要配合yield关键字使用,控制函数的执行顺序。
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen(); // 执行被阻塞,不会执行任何语句
t.next(1); // 程序继续执行,遇到yield关键字后停止。
t.next(2); // next 使得函数继续执行,a输出next参数2,遇到yield又暂停
t.next(3); // b输出3;
t.next(4); // c输出4;
t.next(5); // d输出5;
- Generoter的执行关键
- 第一步:调用* 函数后,函数不会立即执行,程序被阻塞住
- 第二步:调用next方法,程序向下继续执行,遇到yield被暂停
- 第三步:直到返回的done为true后,执行结束。
- yield关键字
next执行后返回的结果为一个对象,格式为{value:yield关键字后的值,done:是否结束};执行next()函数时传进入的参数作为yield执行后的结果。
function* gen() {
let a = yield function (){
return 1
};
console.log(a); // undefined
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
console.log(t.next()); // { value: [Function], done: false }
console.log(t.next()) // { value: 222, done: false }
2 - Generator + thunk 函数
- 什么是thunk函数
对一个具有公共功能的函数进行的封装,接受一个参数,返回一个可接受参数的含有定制功能的函数。
- Generator + thunk 函数实现异步功能
// Generator + thunk
function thunk(fileName){
return (callBack) => {
fs.readFile(fileName,callBack)
}
}
function * fileGen(){
const a = yield thunk("1.txt");
console.log(a); // <Buffer e6 96 87 e6 a1 a3 31>
const b = yield thunk("2.txt");
console.log(b) // <Buffer e6 96 87 e6 a1 a3 e4 ba 8c>
}
let fileG = fileGen();
fileG.next().value((err, data1) => {
fileG.next(data1).value((err,data2) => {
fileG.next(data2)
})
})
- 定义递归自执行的函数
function run(gen){
const cb = function (err,data){
let res = gen.next(data);
if (res.done) return ;
res.value(cb)
}
cb()
}
run(fileG);
3 - Generator + Promise
// Generator + Promise
function promiseThunk(fileName){
return new Promise((resolve, reject) => {
fs.readFile(fileName,(err,data) => {
if (err){
reject(err);
}else {
resolve(data)
}
})
})
}
function * gen2(){
const data1 = yield promiseThunk("1.txt")
console.log(data1);
const data2 = yield promiseThunk('2.txt');
console.log(data2)
}
function run2(g){
let gen = g();
let cb = function (err,data){
let res = gen.next(data);
if (res.done) return res.value;
res.value.then(function (data){
cb(err,data)
})
}
cb();
}
run2(gen2);
六、Async / Await
1 - 基础用法
- async/await 中内置了执行器,不用手动指定next
- 相比于 *,yield,语义更加明确
- async函数执行后返回一个Promise
- 适用性更广:yield后面只能是thunk函数或Promise对象,而await关键字后可以是Promise或者其他基础类型。
// 读取文件
function readFile(filename){
return new Promise((resolve, reject) =>{
if (filename){
fs.readFile(filename,(err,data) => {
if (err) reject(err);
resolve(data)
})
}
})
}
async function readFiles(){
let file1 = await readFile("1.txt");
let file2 = await readFile("2.txt");
console.log(file1.toString());
console.log(file2.toString());
return "读取成功"
}
readFiles().then(res => console.log(res));
2 - 错误处理
- 如果await后面的函数报错,则等同于async函数返回的promise被reject
- 为了防止await函数报错,建议await函数放在try-catch中
- 多个await 可以统一放在try-catch中
3 - 使用注意点
- 因为await命令后面的函数可能会被reject,所以最好把await放到try-catch中
try{
file1 = await readFile("1.txt");
}catch (err){
console.log(err)
}
- 多个await命令后面的异步操作,如果不存在关联关系,建议同时触发
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
- await 命令只能应用在async函数中,如果用在普通函数中,就会报错。
- async函数会保留上下文执行栈。
4 - async 实现原理
async 实现的基础时内置了执行器,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(gen){
const g = gen();
return new Promise((resolve, reject) => {
const step = function (data){
let result;
try {
result = g.next(data)
}catch (e) {
return reject(e)
}
if (result.done){
return resolve(result.value)
}
Promise.resolve(result.value).then(res => {
step(res)
},function (err){
return gen.throw(err)
})
}
step();
})
}
七、异步总结
![异步处理.png](https://img-blog.csdnimg.cn/img_convert/bc5b65886abede68704f2dbc784bb83a.png#clientId=uc2c94173-7b0e-4&from=ui&id=u14d1a94d&margin=[object Object]&name=异步处理.png&originHeight=942&originWidth=686&originalType=binary&ratio=1&size=33538&status=done&style=none&taskId=uc2e995d1-494b-4afa-aa85-db605b86745)