js 异步 你还应该知道的那些事儿

in 前端 with 0 comment

提到 js 的异步,可能你上学的时候只是学过异步的操作要放在后面去运行,setTimeout 就是一个最简单的异步。但知道这些是远远不够的,你还应该知道这些:

同步、异步和他们产生的原因

在 js 的世界里,同步和异步和自然世界是完全相反的概念

单线程的 js

js 是一种单线程的语言,但浏览器是多线程的,这样浏览器就只分配一个线程给 js 使用
现在的浏览器都是多 tab 页的,一个 tab 页可以看作一个 进程。这个进程里可以包含多个 线程,例如加载页面时浏览器会分配一个线程去计算 DOM 树、分配一个线程去加载外部资源,同时分配一个线程给 js 进行自上而下的执行
由于 js 单线程的个性,我们就需要理解同步和异步操作在 js 的执行时产生的顺序问题

事件循环机制 Event Loop

由于 js 是单线程,非阻塞性的任务就采用同步执行,阻塞性任务会放进异步中等待,回调函数会放进事件队列
当每次主线程的任务清完以后就回去读取一次事件队列中的任务,并移入主线程执行(称作一个 tick),这就是时间循环机制

同步/异步

同步就是在一个线程上按顺序执行任务,先进先出
异步就是在主线程上发现了一个异步任务,把异步任务移出主线程,放进等待任务队列中
等待任务队列会被浏览器分配线程来监听主任务队列的任务是否执行完毕,当主任务队列执行完毕后,把等待任务队列的任务放进主进程执行

宏任务和微任务

规范中规定任务分类两大类:宏任务和微任务,其中宏任务参与了事件循环,微任务没有参与,直接在 javascript 引擎中执行
每个宏任务结束后, 都要清空所有的微任务

宏任务 macrotasks

任务浏览器nodejs
UI rendering
I/O
setTimeout
setInterval
requestAnimationFrame
setImmediate

微任务 microtasks

任务浏览器nodejs
process.nextTick
Promise
MutationObserver

js 执行顺序

首先 js 自上而下执行任务
每个宏任务执行结束后,都要清空所有的微任务,所以微任务会在下一个 tick 之前全部清空
698814-20180906145003189-254912994

Promise

Promise并不是完全的同步,当在 Excutor 中执行 resolve 或者 reject 的时候,此时是异步操作,会先执行 then/catch 等,当主线程执行完成后,才会再去调用 resolve/reject 把存放的方法执行

async/await

使用这样的组合等待异步执行完成后再执行下面的代码
async 的 function 会返回一个 Promise 对象
按照文档, await 等待的是一个表达式。如果它等到了一个 Promise 对象,那么 await 会阻塞后面的代码,等到 Promise 的 resolve 给出一个结果

举个例子

例题

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

async1()

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})

console.log('script end')

输出结果

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout

解析

  1. 整体代码执行 输出 script start
  2. 遇到 setTimeout 宏任务,放入宏任务队列等待执行
  3. 执行 async1(),返回 Promise(微任务)
    1. 按顺序执行 输出 async1 start
    2. 执行 async2(),等待结果 输出 async2
      await 阻塞后面的代码,放入微任务队列(微1)
  4. new Promise 立即执行
    1. 把 then 放入微任务队列(微2)
    2. 输出 promise1
    3. 遇到 resolve() 跳出
  5. 整体代码执行 输出 script end
  6. 检查微任务队列:有微任务顺序为微1、微2,按顺序执行
  7. 主线程:无任务,执行下一个 tick
  8. 把 setTimeout 放入主线程执行