Event Loop

4,497 阅读4分钟

概述

相信很多人都看过Philip Roberts在JS-Conf上关于Event Loop的演讲.

这篇文章就是用来记录观后感.

Event Loop

术语

JSModel

Call Stack

调用堆栈: 它是一个用于记录函数调用的数据结构(后进先出)。 当我们调用一个函数时候, 就会将其推入到堆栈中, 当一个函数返回时候, 就会将其推出堆栈的顶部. 另外注意同步代码才会按照顺序马上进入Call Stack. "异步代码" 后面会讲.

如果堆栈长时间被占用或者堵塞就会导致我们常说的blocking script.

看下面这段代码: 我们理解Call Stack的执行过程.


function foo (b) {
  var a = 5
  return a * b + 10
}

function bar (x) {
  var y = 3
  return foo(x * y)
}

console.log(bar(6)) // 100

Satck

// 执行过程如下
1. 首先找到即将开始执行的main函数
2. 从console.log(bar(6)) 开始执行代码, 它被推到调用栈的底部.
3. 然后bar()被推到console.log(bar(6))的顶部
4. 之后foo()被推到bar()的顶部, 但是当它执行后立即返回, 被推出堆栈.
5. 接着将bar()弹出
6. 最后在console.log()被弹出,把返回值100打印在输出台.


以下图一个错误堆栈为例可能更明了:

Error Stack

有时候我们把一个函数递归调用多次, 会进入一个死循环. 而对于Chrome浏览器来说, 栈的大小是有限制的(16000桢), 于是它会抛出一个Max Stack错误.

Max Stack

Heap

堆: 对象是分配在堆里面. 说到堆内存就需要了解栈内存, 引用类型,基本类型.

引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的。如下图所示:栈内存中存放的只是该对象的访问地址,在堆内存中为这个值分配空间。由于这种值的大小不固定,因此不能把它们保存到栈内存中。但内存地址大小的固定的,因此可以将内存地址保存在栈内存中。 这样,当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

Heap and Stack

Quene

队列: JS运行时候包含一个消息队列, 它是要处理的消息列表(事件)和待执行的回调函数.

Quene是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈(Call Stack)一清空,Quene上第一位的事件就自动进入主线程。但是,由于存在"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能进入到事件队列,读取事件,执行事件相应的回调.

基本上来说,这些事件是响应外部异步事件而排队的(例如鼠标被点击或接收到对HTTP请求的响应,当然如果一个事件没有回调, 那么事件是不会进入Quene排队的.

这里我觉得重点是理解什么样的代码才会进入到Quene, 我个人喜欢称呼"异步代码". 我们都知道JS是单线程语音, 如果都采用同步不支持异步的话, 你可以想象加载资源时候堆栈会堵塞成什么样子. 所以JS支持异步回调. 也许有人会问为什么异步代码不可以直接进入Call Stack, 其实想想既然是异步代码那么进入到Call Stack的顺序就是随机插入的显然不合理.

// 到了时间才会推入到队列

console.log(1)
setTimeout(() => {
  console.log(2)
}, 200)
setTimeout(() => {
  console.log(3)
}, 0)
console.log(4)

// 1 4 3 2 

浏览器端异步事件: DOM事件,http请求,setTimeout等异步事件。

Event Loop

事件循环的基本工作是查看堆栈和任务队列,当队列看到堆栈为空时,将队列中的第一件event推送到堆栈。在处理任何其他消息之前,会先处理当前事件的回调。

Event Loop

题外话

关于队列: 其实分为macro-task/micro-task.

// 测试代码

function execOrder () {
  setTimeout(() => console.log('timeout'), 0)
  let promise = new Promise((resolve, reject) => {
	console.log('Promise')
	resolve()
  })
  promise
  	.then(() => {
  		console.log('resolved')
  	})
  console.log('hi')
}

// 执行顺序: Promise hi resolved timeout