8.异步处理

[回顾]事件循环

JS运行的环境称之为宿主环境。

执行栈:call stack,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。

JS引擎永远执行的是执行栈的最顶部。

异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。

浏览器宿主环境中包含5个线程:

  1. JS引擎:负责执行执行栈的最顶部代码
  2. GUI线程:负责渲染页面
  3. 事件监听线程:负责监听各种事件
  4. 计时线程:负责计时
  5. 网络线程:负责网络通信

当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。

JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
  • 微任务(队列):MutationObserver,Promise产生的回调进入微队列

MutationObserver用于监听某个DOM对象的变化

当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

Promise 基础

邓哥的烦恼

邓哥心中有很多女神,他今天下定决心,要向这些女神表白,他认为,只要女神够多,根据概率学原理,总有一个会接收他

稳妥起见,邓哥决定使用串行的方式进行表白:先给第1位女神发送短信,然后等待女神的回应,如果成功了,就结束,如果失败了,则再给第2位女神发送短信,依次类推

邓哥的女神一共有4位,名字分别是:李建国、王富贵、周聚财、刘人勇

发短信是一个重复性的劳动,邓哥是个程序员,因此决定用函数封装这个动作

js 复制代码
// 向某位女生发送一则表白短信
// name: 女神的姓名
// onFulffiled: 成功后的回调
// onRejected: 失败后的回调
function sendMessage(name, onFulffiled, onRejected) {
  // 模拟 发送表白短信
  console.log(
    `邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`
  );
  console.log(`等待${name}回复......`);
  // 模拟 女神回复需要一段时间
  setTimeout(() => {
    // 模拟 有10%的几率成功
    if (Math.random() <= 0.1) {
      // 成功,调用 onFuffiled,并传递女神的回复
      onFulffiled(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
    } else {
      // 失败,调用 onRejected,并传递女神的回复
      onRejected(`${name} -> 邓哥:你是个好人😜`);
    }
  }, 1000);
}

有了这个函数后,邓哥于是开始编写程序发送短信了

js 复制代码
// 首先向 李建国 发送消息
sendMessage(
  '李建国',
  (reply) => {
    // 如果成功了,输出回复的消息后,结束
    console.log(reply);
  },
  (reply) => {
    // 如果失败了,输出回复的消息后,向 王富贵 发送消息
    console.log(reply);
    sendMessage(
      '王富贵',
      (reply) => {
        // 如果成功了,输出回复的消息后,结束
        console.log(reply);
      },
      (reply) => {
        // 如果失败了,输出回复的消息后,向 周聚财 发送消息
        console.log(reply);
        sendMessage(
          '周聚财',
          (reply) => {
            // 如果成功了,输出回复的消息后,结束
            console.log(reply);
          },
          (reply) => {
            // 如果失败了,输出回复的消息后,向 刘人勇 发送消息
            console.log(reply);
            sendMessage(
              '刘人勇',
              (reply) => {
                // 如果成功了,输出回复的消息后,结束
                console.log(reply);
              },
              (reply) => {
                // 如果失败了,就彻底没戏了
                console.log(reply);
                console.log('邓哥命犯天煞孤星,注定孤独终老!!');
              }
            );
          }
        );
      }
    );
  }
);

该程序完成后,邓哥内心是崩溃的

这一层一层的回调嵌套,形成了传说中的「回调地狱 callback hell

邓哥是个完美主义者,怎么能忍受这样的代码呢?

要解决这样的问题,需要Promise出马

Promise规范

Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一

这套规范最早诞生于前端社区,规范名称为Promise A+

该规范出现后,立即得到了很多开发者的响应

Promise A+ 规定:

  1. 所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象 ,该对象称之为Promise对象,也叫做任务对象

  2. 每个任务对象,都应该有两个阶段、三个状态

    根据常理,它们之间存在以下逻辑:

    • 任务总是从未决阶段变到已决阶段,无法逆行
    • 任务总是从挂起状态变到完成或失败状态,无法逆行
    • 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
  3. 挂起->完成,称之为resolve挂起->失败称之为reject。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。

  4. 可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected

Promise API

ES6提供了一套API,实现了Promise A+规范

基本使用如下:

js 复制代码
// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});

pro.then(
  (data) => {
    // onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
  }
);

邓哥的解决方案

学习了ES6的Promise后,邓哥决定对sendMessage函数进行改造,改造结果如下:

js 复制代码
// 向某位女生发送一则表白短信
// name: 女神的姓名
// 该函数返回一个任务对象
function sendMessage(name) {
  return new Promise((resolve, reject) => {
    // 模拟 发送表白短信
    console.log(
      `邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`
    );
    console.log(`等待${name}回复......`);
    // 模拟 女神回复需要一段时间
    setTimeout(() => {
      // 模拟 有10%的几率成功
      if (Math.random() <= 0.1) {
        // 成功,调用 resolve,并传递女神的回复
        resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
      } else {
        // 失败,调用 reject,并传递女神的回复
        reject(`${name} -> 邓哥:你是个好人😜`);
      }
    }, 1000);
  });
}

之后,就可以使用该函数来发送消息了

js 复制代码
sendMessage('李建国').then(
  (reply) => {
    // 女神答应了,输出女神的回复
    console.log(reply);
  },
  (reason) => {
    // 女神拒绝了,输出女神的回复
    console.log(reason);
  }
);

至此,回调地狱的问题仍然没能解决

要解决回调地狱,还需要进一步学习Promise的知识

catch方法

.catch(onRejected) = .then(null, onRejected)

链式调用

  1. then方法必定会返回一个新的Promise

    可理解为后续处理也是一个任务

  2. 新任务的状态取决于后续处理:

    • 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据

    • 若有后续处理但还未执行,新任务挂起。

    • 若后续处理执行了,则根据后续处理的情况确定新任务的状态

      • 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
      • 后续处理执行有错,新任务的状态为失败,数据为异常对象
      • 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致

由于链式任务的存在,异步代码拥有了更强的表达力

js 复制代码
// 常见任务处理代码

/*
 * 任务成功后,执行处理1,失败则执行处理2
 */
pro.then(处理1).catch(处理2)

/*
 * 任务成功后,依次执行处理1、处理2
 */
pro.then(处理1).then(处理2)

/*
 * 任务成功后,依次执行处理1、处理2,若任务失败或前面的处理有错,执行处理3
 */
pro.then(处理1).then(处理2).catch(处理3)

邓哥的解决方案

js 复制代码
// 向某位女生发送一则表白短信
// name: 女神的姓名
// onFulffiled: 成功后的回调
// onRejected: 失败后的回调
function sendMessage(name) {
  return new Promise((resolve, reject) => {
    // 模拟 发送表白短信
    console.log(
      `邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`
    );
    console.log(`等待${name}回复......`);
    // 模拟 女神回复需要一段时间
    setTimeout(() => {
      // 模拟 有10%的几率成功
      if (Math.random() <= 0.1) {
        // 成功,调用 onFuffiled,并传递女神的回复
        resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
      } else {
        // 失败,调用 onRejected,并传递女神的回复
        reject(`${name} -> 邓哥:你是个好人😜`);
      }
    }, 1000);
  });
}

sendMessage('李建国')
  .catch((reply) => {
    // 失败,继续
    console.log(reply);
    return sendMessage('王富贵');
  })
  .catch((reply) => {
    // 失败,继续
    console.log(reply);
    return sendMessage('周聚财');
  })
  .catch((reply) => {
    // 失败,继续
    console.log(reply);
    return sendMessage('刘人勇');
  })
  .then(
    (reply) => {
      // 成功,结束
      console.log(reply);
      console.log('邓哥终于找到了自己的伴侣');
    },
    (reply) => {
      // 最后一个也失败了
      console.log(reply);
      console.log('邓哥命犯天煞孤星,无伴终老,孤独一生');
    }
  );

邓哥的新问题

邓嫂出门时,给邓哥交待了几个任务:

  1. 做饭

    可交给电饭煲完成

  2. 洗衣服

    可交给洗衣机完成

  3. 打扫卫生

    可交给扫地机器人完成

邓哥需要在所有任务结束后给邓嫂汇报工作,哪些成功了,哪些失败了

为了最大程度的节约时间,邓哥希望这些任务同时进行,最终汇总结果统一处理

每个任务可以看做是一个返回Promise的函数

js 复制代码
// 做饭
function cook() {
  return new Promise((resolve, reject) => {
    console.log('邓哥打开了电饭煲');
    setTimeout(() => {
      if (Math.random() < 0.5) {
        resolve('饭已ok');
      } else {
        reject('做饭却忘了加水,米饭变成了爆米花');
      }
    }, 2000);
  });
}

// 洗衣服
function wash() {
  return new Promise((resolve, reject) => {
    console.log('邓哥打开了洗衣机');
    setTimeout(() => {
      if (Math.random() < 0.5) {
        resolve('衣服已经洗好');
      } else {
        reject('洗衣服时停水了,洗了个寂寞');
      }
    }, 2500);
  });
}

// 打扫卫生
function sweep() {
  return new Promise((resolve, reject) => {
    console.log('邓哥打开了扫地机器人');
    setTimeout(() => {
      if (Math.random() < 0.5) {
        resolve('地板扫的非常干净');
      } else {
        reject('扫地机器人被哈士奇一爪掀翻了');
      }
    }, 3000);
  });
}

如何利用这三个函数实现邓哥的要求呢?

Promise的静态方法

方法名 含义
Promise.resolve(data) 直接返回一个完成状态的任务
Promise.reject(reason) 直接返回一个拒绝状态的任务
Promise.all(任务数组) 返回一个任务 任务数组全部成功则成功 任何一个失败则失败
Promise.any(任务数组) 返回一个任务 任务数组任一成功则成功 任务全部失败则失败
Promise.allSettled(任务数组) 返回一个任务 任务数组全部已决则成功 该任务不会失败
Promise.race(任务数组) 返回一个任务 任务数组任一已决则已决,状态和其一致

邓哥的解决方案

js 复制代码
Promise.allSettled([cook(), wash(), sweep()]).then((result) => {
  // 处理汇总结果
  const report = result
    .map((r) => (r.status === 'fulfilled' ? r.value : r.reason))
    .join(';');
  console.log(report);
});

消除回调

有了Promise,异步任务就有了一种统一的处理方式

有了统一的处理方式,ES官方就可以对其进一步优化

ES7推出了两个关键字asyncawait,用于更加优雅的表达Promise

async

async关键字用于修饰函数,被它修饰的函数,一定返回Promise

js 复制代码
async function method1(){
  return 1; // 该函数的返回值是Promise完成后的数据
}

method1(); // Promise { 1 }

async function method2(){
  return Promise.resolve(1); // 若返回的是Promise,则method得到的Promise状态和其一致
}

method2(); // Promise { 1 }

async function method3(){
  throw new Error(1); // 若执行过程报错,则任务是rejected
}

method3(); // Promise { <rejected> Error(1) }

await

await关键字表示等待某个Promise完成,它必须用于async函数中

js 复制代码
async function method(){
  const n = await Promise.resolve(1);
  console.log(n); // 1
}

// 上面的函数等同于
function method(){
  return new Promise((resolve, reject)=>{
    Promise.resolve(1).then(n=>{
      console.log(n);
      resolve(1)
    })
  })
}

await也可以等待其他数据

js 复制代码
async function method(){
  const n = await 1; // 等同于 await Promise.resolve(1)
}

如果需要针对失败的任务进行处理,可以使用try-catch语法

js 复制代码
async function method(){
  try{
    const n = await Promise.reject(123); // 这句代码将抛出异常
    console.log('成功', n)
  }
  catch(err){
    console.log('失败', err)
  }
}

method(); // 输出: 失败 123

邓哥表白的完美解决方案

邓哥的女神可不是只有4位,而是40位!

为了更加方便的编写表白代码,邓哥决定把这40位女神放到一个数组中,然后利用async和await轻松完成代码

js 复制代码
// 女神的名字数组
const beautyGirls = [
  '梁平',
  '邱杰',
  '王超',
  '冯秀兰',
  '赖军',
  '顾强',
  '戴敏',
  '吕涛',
  '冯静',
  '蔡明',
  '廖磊',
  '冯洋',
  '韩杰',
  '江涛',
  '文艳',
  '杜秀英',
  '丁艳',
  '邓静',
  '江刚',
  '乔刚',
  '史平',
  '康娜',
  '袁磊',
  '龙秀英',
  '姚静',
  '潘娜',
  '萧磊',
  '邵勇',
  '李芳',
  '谭芳',
  '夏秀英',
  '程娜',
  '武杰',
  '崔军',
  '廖勇',
  '崔强',
  '康秀英',
  '余磊',
  '邵勇',
  '贺涛',
];

// 向某位女生发送一则表白短信
// name: 女神的姓名
function sendMessage(name) {
  return new Promise((resolve, reject) => {
    // 模拟 发送表白短信
    console.log(
      `邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`
    );
    console.log(`等待${name}回复......`);
    // 模拟 女神回复需要一段时间
    setTimeout(() => {
      // 模拟 有10%的几率成功
      if (Math.random() <= 0.1) {
        // 成功,调用 onFuffiled,并传递女神的回复
        resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
      } else {
        // 失败,调用 onRejected,并传递女神的回复
        reject(`${name} -> 邓哥:你是个好人😜`);
      }
    }, 1000);
  });
}

// 批量表白的程序
async function proposal() {
  let isSuccess = false;
  for (const girl of beautyGirls) {
    try {
      const reply = await sendMessage(girl);
      console.log(reply);
      console.log('表白成功!');
      isSuccess = true;
      break;
    } catch (reply) {
      console.log(reply);
      console.log('表白失败');
    }
  }
  if (!isSuccess) {
    console.log('邓哥注定孤独一生');
  }
}
proposal();

面试题考点

Promise的基本概念

链式调用规则

  1. then方法必定会返回一个新的Promise

    可理解为后续处理也是一个任务

  2. 新任务的状态取决于后续处理:

    • 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据

    • 若有后续处理但还未执行,新任务挂起。

    • 若后续处理执行了,则根据后续处理的情况确定新任务的状态

      • 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
      • 后续处理执行有错,新任务的状态为失败,数据为异常对象
      • 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致

Promise的静态方法

方法名 含义
Promise.resolve(data) 直接返回一个完成状态的任务
Promise.reject(reason) 直接返回一个拒绝状态的任务
Promise.all(任务数组) 返回一个任务 任务数组全部成功则成功 任何一个失败则失败
Promise.any(任务数组) 返回一个任务 任务数组任一成功则成功 任务全部失败则失败
Promise.allSettled(任务数组) 返回一个任务 任务数组全部已决则成功 该任务不会失败
Promise.race(任务数组) 返回一个任务 任务数组任一已决则已决,状态和其一致

async和await

有了Promise,异步任务就有了一种统一的处理方式

有了统一的处理方式,ES官方就可以对其进一步优化

ES7推出了两个关键字asyncawait,用于更加优雅的表达Promise

async

async关键字用于修饰函数,被它修饰的函数,一定返回Promise

js 复制代码
async function method1(){
  return 1; // 该函数的返回值是Promise完成后的数据
}

method1(); // Promise { 1 }

async function method2(){
  return Promise.resolve(1); // 若返回的是Promise,则method得到的Promise状态和其一致
}

method2(); // Promise { 1 }

async function method3(){
  throw new Error(1); // 若执行过程报错,则任务是rejected
}

method3(); // Promise { <rejected> Error(1) }

await

await关键字表示等待某个Promise完成,它必须用于async函数中

js 复制代码
async function method(){
  const n = await Promise.resolve(1);
  console.log(n); // 1
}

// 上面的函数等同于
function method(){
  return new Promise((resolve, reject)=>{
    Promise.resolve(1).then(n=>{
      console.log(n);
      resolve(1)
    })
  })
}

await也可以等待其他数据

js 复制代码
async function method(){
  const n = await 1; // 等同于 await Promise.resolve(1)
}

如果需要针对失败的任务进行处理,可以使用try-catch语法

js 复制代码
async function method(){
  try{
    const n = await Promise.reject(123); // 这句代码将抛出异常
    console.log('成功', n)
  }
  catch(err){
    console.log('失败', err)
  }
}

method(); // 输出: 失败 123

事件循环

根据目前所学,进入事件队列的函数有以下几种:

  • setTimeout的回调,宏任务(macro task)
  • setInterval的回调,宏任务(macro task)
  • Promise的then函数回调,微任务(micro task)
  • requestAnimationFrame的回调,宏任务(macro task)
  • 事件处理函数,宏任务(macro task)

面试题

  1. 下面代码的输出结果是什么

    js 复制代码
    const promise = new Promise((resolve, reject) => {
        console.log(1); 
        resolve(); 
        console.log(2);
    })
    
    promise.then(() => {
        console.log(3);
    })
    
    console.log(4);
  2. 下面代码的输出结果是什么

    js 复制代码
    const promise = new Promise((resolve, reject) => {
        console.log(1); 
        setTimeout(()=>{
          console.log(2)
          resolve(); 
        	console.log(3);
        })
    })
    
    promise.then(() => {
        console.log(4);
    })
    
    console.log(5);
  3. 下面代码的输出结果是什么

    js 复制代码
    const promise1 = new Promise((resolve, reject) => {
    	setTimeout(() => {
        resolve()
      }, 1000)
    })
    const promise2 = promise1.catch(() => {
      return 2;
    })
    
    console.log('promise1', promise1) 
    console.log('promise2', promise2) 
    
    setTimeout(() => {
      console.log('promise1', promise1) 
      console.log('promise2', promise2) 
    }, 2000)
  4. 下面代码的输出结果是什么

    js 复制代码
    async function m(){
      const n = await 1;
      console.log(n);
    }
    
    m();
    console.log(2);
  5. 下面代码的输出结果是什么

    js 复制代码
    async function m(){
      const n = await 1;
      console.log(n);
    }
    
    (async ()=>{
      await m();
      console.log(2);
    })();
    
    console.log(3);
  6. 下面代码的输出结果是什么

    js 复制代码
    async function m1(){
      return 1;
    }
    
    async function m2(){
      const n = await m1();
      console.log(n)
      return 2;
    }
    
    async function m3(){
      const n = m2();
      console.log(n);
      return 3;
    }
    
    m3().then(n=>{
      console.log(n);
    });
    
    m3();
    
    console.log(4);
  7. 下面代码的输出结果是什么

    js 复制代码
    Promise.resolve(1)	
      .then(2)
      .then(Promise.resolve(3))
      .then(console.log)
  8. 下面代码的输出结果是什么

    js 复制代码
    var a;
    var b = new Promise((resolve, reject) => {
      console.log('promise1');
      setTimeout(()=>{
        resolve();
      }, 1000);
    }).then(() => {
      console.log('promise2');
    }).then(() => {
      console.log('promise3');
    }).then(() => {
      console.log('promise4');
    });
    
    a = new Promise(async (resolve, reject) => {
      console.log(a);
      await b;
      console.log(a);
      console.log('after1');
      await a
      resolve(true);
      console.log('after2');
    });
    
    console.log('end');
  9. 下面代码的输出结果是什么

    js 复制代码
    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
    	console.log('async2');
    }
    
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    
    async1();
    
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css