Skip to main content

Event Loop

Event Loop

https://github.com/aooy/blog/issues/5

视频 1 主线程:javascript 运行的地方,渲染发生的地方,dom 存在的地方

  • requestAnimationFrame

浏览器的事件循环

1.执行栈和事件队列

js 代码执行会讲不同的变量存于内存中的不同位置:堆(heap)栈(stack).其中,堆中放着一些对象,栈中放着基础变量和对象指针.执行栈不同于这个栈 当调用一个方法的时候,js 会生成一个与这个方法对应的执行环境(context),也叫执行上下文.这个执行环境储存着这个方法的私有作用域,上层作用域的指向,以及方法参数.js 单线程的,同一时间只能执行一个方法,这些方法排队在一个单独的地方,叫做执行栈

js 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

2. 微任务(microtask)和宏任务(macrotask)

macro-task 大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render
  • MessageChannel
  • postMessage

micro-task 大概包括:

  • process.nextTick
  • Promise
  • Async/Await(实际就是 promise)
  • MutationObserver(html5 新特性)

3. 浏览器 demo

async/await

  1. async 后面跟同步代码
async后面跟同步代码
export default function demoAsync1(params) {
async function async1() {
console.log('a')
await async2()
console.log('b')
}
async function async2() {
console.log('c')
}
async1()
new Promise(function (resolve) {
console.log('d')
resolve()
}).then(function () {
console.log('e')
})
}
解析
输出: a=>c=>d=>e=>b await 后面跟同步代码,例如 await 1 ;相当于直接把await 后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码),
  1. async 后面跟异步调用
async后面异步调用
export default function demo4(params) {
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 start')
return Promise.resolve().then((res) => {
console.log('async2 end')
})
//对于 return 的理解:1.首先then返回一个新的promise,
//所以返回的Promise.resolve(x),x代码await后面的代码
//返回的内容,理解成一段js代码,会放到宏任务中
}
async1()
setTimeout(function () {
console.log('setTimeout-1')
}, 0)
setTimeout(function () {
console.log('setTimeout-2')
}, 0)
Promise.resolve()
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
.then(function () {
console.log('promise4')
})

new Promise((resolve) => {
console.log('new promise')
resolve()
})
.then(function () {
console.log('new promise1')
})
.then(function () {
console.log('new promise2')
})
.then(function () {
console.log('new promise3')
})
}
解析
await 后面跟异步调用,该异步执行完,相当于内部函数调用栈结束,并不会立即执行await后面的代码,而是等待下一次循环在执行
  1. demo 微任务一次全部执行完,宏任务一个个执行. 一个宏任务里面有多个微任务,会先把里面的微任务都执行了再去执行下一个宏任务
async && promise.resolve
export default function demo2(params) {
// console.log(`此进程的 pid 是 ${process.pid}`)
setTimeout(function () {
console.log('setTimeout')
}, 0)

const async1 = async () => {
console.log('async1 start')
await async2()
console.log('async1 end')
}
const async2 = async () => {
console.log('async2')
}

async1()
setTimeout(() => {
console.log('timer1')
Promise.resolve()
.then(function () {
console.log('setTimeout-promise1-1')
})
.then(function () {
console.log('setTimeout-promise1-2')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve()
.then(function () {
console.log('setTimeout-promise2-1')
})
.then(function () {
console.log('setTimeout-promise2-2')
})
}, 0)

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

new Promise((resolve) => {
resolve(1)
Promise.resolve().then(() => console.log(2))
Promise.resolve().then(() => Promise.resolve().then(() => console.log(3)))
console.log(4)
}).then((t) => console.log(t))
}
Demo
export default function demo3(params) {
new Promise(function (resolve, reject) {
// setTimeout(() => reject(new Error('fail')), 1000);
reject('fail')
})
.then(
(res) => {
console.log('res:', res)
},
(rej) => {
console.log('rej:', rej)
}
)
.catch((e) => {
console.log(1111)
console.log(e)
})
.finally((fin) => {
console.log('finally', fin)
})
}
Demo
export default function demo4(params) {
console.log('script start')
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 start')
return Promise.resolve().then(() => {
console.log('async2 end')
})
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('new promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
console.log('script end')
}

addEventListener 添加两个事件

在代码编译执行时,和事件点击时结果不一样

demo5
<button id="btn">开始</button>
<script>
console.log('script运行时=====')
const btn = document.getElementById('btn')
btn.addEventListener('click', function () {
Promise.resolve().then(function () {
console.log('Microtask 1')
})
console.log('Listener 1')
})
btn.addEventListener('click', function () {
Promise.resolve().then(function () {
console.log('Microtask 2')
})
console.log('Listener 2')
})
btn.click()
console.log('下面就是事件点击的结果!')
</script>

setTimeout,promise 队列执行

setTimeout 是一个个执行 promise 是队列执行,要一次执行完

demo

node 的事件循环

node 中事件循环的实现是依靠的 libuv 引擎

1.宏任务和微任务

macro-task 大概包括:

  • setTimeout
  • setInterval
  • setImmediate
  • script(整体代码)
  • I/O 操作等。

micro-task 大概包括:

  • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  • new Promise().then(回调)等。

2.时间循环模型

   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming:
│ │ poll │<─────┤ connections,
│ └─────────────┬─────────────┘ │ data, etc.
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

过程: 输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O 事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...

  • 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。
  • I/O 事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些 I/O 回调。
  • 闲置阶段(idle, prepare):仅系统内部使用。
  • 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检查阶段(check):setImmediate() 回调函数在这里执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)。

3.三大重点阶段

日常开发中的绝大部分异步任务都是在 poll、check、timers 这 3 个阶段处理的,所以我们来重点看看。

  1. timers

    timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。 一旦执行一个阶段里的一个宏任务(setTimeout,setInterval 和 setImmediate)就立刻执行微任务队列,这就跟浏览器端运行一致

demo2
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function () {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function () {
console.log('promise2')
})
}, 0)

//timer1=>promise1=>timer2=>promise2
  1. poll poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下: img

    1. 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。

    2. 如果没有定时器, 会去看回调函数队列。

      1. 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

      2. 如果 poll 队列为空时,会有两件事发生 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段。

  2. check check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

  3. process.nextTick process.nextTick 是一个独立于 eventLoop 的任务队列。

    在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行

process.nextTick
setTimeout(() => {
console.log('setTimeout1')
}, 0)
setTimeout(() => {
console.log('setTimeout2')
}, 0)
setImmediate(() => {
console.log('timeout1')
Promise.resolve().then(() => console.log('promise resolve'))
process.nextTick(() => console.log('next tick1'))
})
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick2'))
})
setImmediate(() => console.log('timeout3'))
setImmediate(() => console.log('timeout4'))
//timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

node 和 浏览器 eventLoop 的主要区别

两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而 nodejs 中的微任务是在不同阶段之间执行的。