面试题 - 五种异步处理的实现方案

一、异步:现在与将来

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 - 使用注意点

  1. 因为await命令后面的函数可能会被reject,所以最好把await放到try-catch中
try{
    file1 = await readFile("1.txt");
  }catch (err){
    console.log(err)
  }
  1. 多个await命令后面的异步操作,如果不存在关联关系,建议同时触发
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
  1. await 命令只能应用在async函数中,如果用在普通函数中,就会报错。
  2. 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]([object Object]&name=异步处理.png&originHeight=942&originWidth=686&originalType=binary&ratio=1&size=33538&status=done&style=none&taskId=uc2e995d1-494b-4afa-aa85-db605b86745)