JavaScript高级 async await 事件循环

126 阅读7分钟

1. async await

async关键字用于声明一个异步函数

async function foo() {
  console.log("foo function1")
  console.log("foo function2")
  console.log("foo function3")
}

异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行

异步函数有返回值时,和普通函数会有区别:

◼ 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中;

◼ 情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定;

◼ 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定

async function foo2() {
  // 1.返回一个普通的值
  // -> Promise.resolve(321)
  return ["abc", "cba", "nba"]

  // 2.返回一个Promise
  // return new Promise((resolve, reject) => {
  //   setTimeout(() => {
  //     resolve("aaa")
  //   }, 3000)
  // })

  // 3.返回一个thenable对象
  // return {
  //   then: function(resolve, reject) {
  //     resolve("bbb")
  //   }
  // }
}

如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递

async function foo() {
  console.log("---------1")

  return new Promise((resolve, reject) => {
    reject("err rejected")
  })
}

foo().then(res => {

}).catch(err => {
  console.log("继续执行其他的逻辑代码")
})

async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的,通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise,那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数

// 1.定义一些其他的异步函数
function requestData(url) {
  console.log("request data")
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(url)
    }, 3000)
  })
}

async function test() {
  console.log("test function")
  return "test"
}

async function bar() {
  console.log("bar function")

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("bar")
    }, 2000);
  })
}

async function demo() {
  console.log("demo function")
  return {
    then: function(resolve) {
      resolve("demo")
    }
  }
}


// 2.调用的入口async函数
async function foo() {
  console.log("foo function")

  const res1 = await requestData("why")
  console.log("res1:", res1)

  const res2 = await test()
  console.log("res2:", res2)

  const res3 = await bar()
  console.log("res3:", res3)

  const res4 = await demo()
  console.log("res4:", res4)
}

foo()

◼ 如果await后面是一个普通的值,那么会直接返回这个值;

◼ 如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;

◼ 如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值

2. 语法糖async await的原理

使用生成器实现异步请求

// 封装请求的方法: url -> promise(result)
function requestData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url)
        }, 2000)
    })
}

function* getData() {
  const res1 = yield requestData("why")
  const res2 = yield requestData(res1 + "kobe")
  const res3 = yield requestData(res2 + "james")
}

function execGenFn(genFn) {
  const generator = genFn()
  function exec(res) {
    const result = generator.next(res)
    if (result.done) return
    result.value.then(res => {
      exec(res)
    })
  }

  exec()
}

execGenFn(getData)

async await 实现

function requestData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url)
        }, 2000)
    })
}

async function getData() {
  const res1 = await requestData("why")
  const res2 = await requestData(res1 + "kobe")
  const res3 = await requestData(res2 + "james")
}

3. 进程 线程

进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式,我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程

线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中,每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

◼ 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;

◼ 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;

◼ 对于用户来说是感受不到这种快速的切换的

4. 浏览器中的JavaScript线程

我们经常会说JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node

目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出,每个进程中又有很多的线程,其中包括执行JavaScript代码的线程

JavaScript的代码执行是在一个单独的线程中执行的,这就意味着JavaScript的代码,在同一个时刻只能做一件事,如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

所以真正耗时的操作,实际上并不是由JavaScript线程在执行的,浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作,比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可

5. 宏任务 微任务队列

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

◼ 宏任务队列(macrotask queue):ajax setTimeout setInterval DOM监听 UIRendering等

◼ 微任务队列(microtask queue):Promise的then回调 Mutation Observer API queueMicrotask()等

那么事件循环对于两个队列的优先级是怎么样的呢?

1.main script中的代码优先执行(编写的顶层script代码)

2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行,也就是宏任务执行之前,必须保证微任务队列是空的,如果不为空,那么就优先执行微任务队列中的任务(回调)

6. throw try catch

1. throw

throw语句用于抛出一个用户自定义的异常

当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行)

function sum(num1, num2) {
  if (typeof num1 !== "number") {
    throw "type error: num1传入的类型有问题, 必须是number类型"
  }

  if (typeof num2 !== "number") {
    throw "type error: num2传入的类型有问题, 必须是number类型"
  }

  return num1 + num2
}

// 李四调用
const result = sum(123, 321)

JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象

Error包含三个属性:

  • messsage:创建Error对象时传入的message;

  • name:Error的名称,通常和类的名称一致;

  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;

function foo() {
  console.log("foo function1")
  throw new Error("我是错误信息")
}

Error有一些自己的子类:

  • RangeError:下标值越界时使用的错误类型;

  • SyntaxError:解析语法错误时使用的错误类型;

  • TypeError:出现类型错误时,使用的错误类型;

2. try catch

如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中,而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行

但是很多情况下当出现异常时,我们并不希望程序直接推出,而是希望可以正确的处理异常,这个时候我们就可以使用try catch

在ES10(ES2019)中,catch后面绑定的error可以省略。

当然,如果有一些必须要执行的代码,我们可以使用finally来执行

7. 面试题

1. 面试题一

console.log("script start")

setTimeout(function () {
  console.log("setTimeout1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("setTimeout2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

console.log("script end")

2. 面试题二

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')