事件循环与Promise

93 阅读5分钟

内容引用自Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript | DigitalOcean

EventLoop

有这么一段代码

function first(){
  console.log(1)
}

function second(){
  setTimeout(()=>{
    console.log(2)
  }, 0)
}

function third(){
  console.log(3)
}

first()
second()
third()
// 依次输出:1,3,2

原理很简单:异步代码总会在同步代码执行完成后再执行。 发生这种情况的原因就在于名为Event Loop的机制去处理并发(或叫做并行)事件。
因为JS一次只能运行一个任务(单线程),所以就需要event loop机制去告知何时执行特定的指令。event loop的实现是基于栈(称之为调用栈)和队列(称之为任务队列)。 上段代码的运行过程如下:

  1. first()推入调用栈,执行该函数,然后出栈
  2. 再把second()推入调用栈,执行该函数。
  3. setTimeout推入调用栈,执行该函数,该函数启动了一个计时器然后把匿名函数推入到任务队列,执行完毕,setTimeout出栈。
  4. third()入栈,执行,出栈
  5. event loop会检查任务队列里是否有待执行的任务,发现有一个打印出2的匿名函数,则把该匿名函数从任务队列推出,任务队列清空。再推入到调用栈,再执行,然后出栈,调用栈清空。

小结

  • event loop是基于(调用)栈和(任务)队列实现的
  • 任务队列可以看作是函数的等待区,用于存储待执行的任务。
  • 当调用栈清空后,会检查任务队列里是否有待执行的任务,根据队列FIFO的原则,清空任务队列,依次推入调用栈。最终实现调用栈和任务队列的双清空。
  • setTimeout的第二个参数0代表的含义并不是0秒后执行,而是指在0秒内推入到任务队列当中。
  • 调用栈和任务队列不是简单的上下层级结构,可以是多层。可以看如下代码:
function first() {
  console.log(1);
}

function second() {
  setTimeout(() => {
    console.log(4);
    setTimeout(() => {
      console.log(2);
      setTimeout(() => {
        console.log(7);
      }, 0);
    }, 0);
  }, 2000);
}

function third() {
  console.log(3);
  setTimeout(() => {
    console.log(5);
    setTimeout(() => {
      console.log(6);
    }, 0);
  }, 0);
}

first();
second();
third();
// 输出:1,3,5,6,4,2,7

上面的代码别因为看着很烧脑而灰心。因为这多少有点回调地狱,正常人都会绕进去,但根据上述小总结,稍稍花点时间一定能想明白。

Promise

这不是面向新手的教程,主要是自己学习的总结,所以跳过回调和回调地狱的概念,直接了解下Promise
Promise代表一个异步函数的完成。It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume.
我们创建一个Promise对象

const promise = new Promise((resolve, reject)=>{
  // resolve('We did it!')
})
console.log(promise)

// 输出
// Promise{<pending>}
// [[Prototype]]: Promise
// [[PromiseState]]: "pending"
// [[PromiseResult]]: undefined

Promise对象有三个状态值:

  • pending: Initial state before being resolved or rejected
  • fulfilled: Successful operation, promise has resolved
  • rejected: Failed operation, promise has rejected

当promise被resolve了之后,就可以使用then方法,该方法会返回PromiseResult值作为参数。

模拟异步请求

我们使用PromisesetTimeout模拟一个异步请求,请求数据

const promise = new Promise((resolve, reject)=>{
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

promise.then((response)=>{
  console.log(response)
})

通过使用then方法,保证了这个response只会在2秒后打印出来,不需要使用内嵌的回调函数。
如果then方法return了一个值,那么就可以在末尾再使用then方法,实现链式调用。

// Chain a promise
promise
  .then((firstResponse) => {
    // Return a new value for the next then
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

Promise和容错处理

一个异步请求,通常可能会因为服务端的错误而返回与预期相悖的结果,这时候就需要容错处理。看代码:

function getUsers(onSuccess){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      if(onSuccess){
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data')
      }
    }, 1000)
  })
}

getUsers(false)
  .then((response)=>{
    console.log(response)
  })
  .catch((error)=>{
    console.error(error)
  })
  .finally(()=>{
    console.log('request finished')
  })

getUsers函数模拟了一个异步请求用户列表的功能,请求成功则返回用户数据,请求失败则返回抓取失败的响应。
如果我们传入一个false值,则会调用catch方法,把响应信息打印出来,如果传入true,则会把用户数据打印出来。但不管传入truefalse,最终都会执行finally方法。

Promise代码案例

下面的fetch方法是个两步过程,需要做链式调用(当时面试问Promise就是因为这个东西搞混了,犯了一个很低级的错误...)

// Fetch a user from the GitHub API
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

async和await

这两个关键字是ES7引入的,其实就是写异步代码的语法糖,可以用写同步任务的逻辑去处理异步任务。上面的代码使用语法糖就可以这么写:

// Handle fetch with async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Execute async function
getUser()

then方法的链式调用就不再需要了,直接两个await解决,有了语法糖的存在,上述异步代码就看着非常直观了。
容错处理的话也不需要用catch方法了,加上finally的话直接使用try-catch-finally结构:

// Handling success and errors with async/await
async function getUser() {
  try {
    // Handle success in try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Handle error in catch
    console.error(error)
  } finally {
    console.log('finished')
  }
}

总结

现在的异步js代码通常直接使用async/await语法糖,但还是要了解Promise的原理,Promise有语法糖无法取代的额外特征,详情还是翻阅mdn文档。