logan70 / Blog

写博客的地方,觉得有用的给个Star支持一下~
81 stars 9 forks source link

执行机制 - JavaScript异步编程及Event Loop #33

Open logan70 opened 4 years ago

logan70 commented 4 years ago

JavaScript异步编程及Event Loop

异步编程

为何需要异步编程

JavaScript是单线程语言,也就是说同一时间只能运行一个任务。一般来说这没什么问题,但是如果运行耗时过长的任务,将会阻塞后续任务的执行,包括UI渲染。

所以一些非密集计算的任务,比如文件I/O,HTTP Request,定时器等任务,完全没必要在主线程等待其完成,而是应该在创建任务后交出主线程控制权,去执行其他任务,待其完成后再处理。这就引出了异步编程

需要注意的是,ECMAScript(JavsScript的语言规范)并没有定义这些异步特性,所以异步特性的实现都需要依赖于JavaScript运行环境,例如浏览器、Node等。

如何实现异步编程

浏览器提供了JS引擎不具备的特性,我们称之为Web API,例如我们常见的DOM事件监听、Ajax、定时器等。通过这些特性可以实现异步、非阻塞的行为。其执行机制会在下个部分 Event Loop 中详细讲解。

异步编程语法

JavaScript发展至今,异步编程语法主要有以下几种:

/* ------------------- Callback ---------------- */
function asyncFn(callback) {
  setTimeout(() => {
    callback()
  }, 1000)
}

const callbackFn = () => console.log('Callback has been invoked')

asyncFn(callbackFn)

/* ------------------- Promise ---------------- */
function asyncFn() {
  return new Promise(resolve => {
    setTimeout(() => resolve(), 1000)
  })
}

asyncFn().then(name => console.log('Promise fulfilled')

/* ------------------- Generator ---------------- */
// Generator生成器函数执行时会返回一个Generator迭代器
// 也就是说Generator本身只是一个状态机,需要由调用者来改变它的状态
function *fetchUser () {
  const user = yield ajax()
  console.log(user)
}

const f = fetchUser()

// 加入的控制代码
const result = f.next()
result.value.then((d) => {
  f.next(d)
})

/* ------------------- Async/Await ---------------- */
// Async/Await 可以理解为是 Generator + Promise 的语法糖
// async 对应 *,await 对应 yield,然后自动实现Generator的流程控制
async function getUser() {
  const user = await ajax()
  return user
}

// getUser用Generator + Promise表示并执行
function *getUser() {
  const user = yeild ajax()
  return user
}

const g = getUser()
const result = g.next()
result.value.then(res => {
  g.next(res)
})

Event Loop

首先推荐一个Event Loop可视化执行的网站。

基本概念

执行机制

Event Loop

  1. 执行同步代码,碰到异步代码交由Web API处理(异步任务完成后Web API将回调函数加入相应任务队列/微任务队列);
  2. 同步代码执行完成后,检查微任务队列中是否存在待执行微任务,存在则取出第一个微任务执行,执行完成后再次检查执行,直至微任务队列为空;

    需要注意的是,若微任务执行过程中往微任务队列加入了新的微任务,也会在本步骤内被执行。

  3. 此时调用栈为空,检查任务队列中是否存在待执行宏任务,存在则取出第一个宏任务执行,也就是回调了第1步循环进行。

巩固练习

// 题目出自https://juejin.im/post/5a6155126fb9a01cb64edb45#heading-2
console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(3)
        resolve()
    }).then(() => {
        console.log(4)
    })
})

new Promise(resolve => {
    console.log(5)
    resolve()
}).then(() => {
    console.log(6)
})
  1. 执行同步代码,打印1
  2. 执行setTimeout,交由Web API处理,Web API将其加入任务队列;
  3. 执行new Promise,其参数函数为同步执行,打印5,然后该Promise变为fulfilled状态,将then内回调加入微任务队列;
  4. 当前任务执行完毕,查看微任务队列,有一个待执行微任务,取出执行,打印6
  5. 微任务队列为空,查看任务队列,有一个待执行宏任务,及setTimeout的回调,取出执行;
  6. 打印2,同步执行new Promise中函数,打印3,Promise置为fulfilled,将then内回调加入为任务队列;
  7. 当前任务执行完毕,查看微任务队列,有一个待执行微任务,取出执行,打印4
  8. 所有任务执行完毕,打印顺序为1 5 6 2 3 4

加强练习

理解了JS的执行机制,碰到类似题目就可以轻松应对:

// 题目出自https://juejin.im/post/5a6155126fb9a01cb64edb45
console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(3)
        resolve()
    }).then(() => {
        console.log(4)
    })
})

new Promise(resolve => {
    console.log(5)
    resolve()
}).then(() => {
    console.log(6)
}).then(() => {
    console.log(7)
})

new Promise(resolve => {
    console.log(8)
    resolve()
}).then(() => {
    console.log(9)
}).then(() => {
    console.log(10)
})

答案为1 5 8 6 9 7 10 2 3 4