JS 事件调度
前言
我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。
单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作(如果允许多线程,一个线程对某一个dom进行添加属性操作,另一个线程对该线程进行删除操作,显然是不可行的)。
单线程在保证了执行顺序的同时也限制了javascript的效率,Web Worker
是 HTML5 标准的一部分,这一规范定义了一套 API,允许我们在 js 主线程之外开辟新的 Worker 线程,并将一段 js 脚本运行其中,它赋予了开发者利用 js 操作多线程的能力。多用来进行复杂运算,操作 DOM 的行为是不可行的。
Event Loop

js事件循环
执行栈
所有的JavaScript代码在运行时都是在上下文中进行的。那么,上下文是什么呢?简单来说,它就是当前JavaScript代码被解析和执行时所在环境的抽象概念。
- 全局上下文:最外层的上下文,也是最基础的上下文。根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,它就是我们常说的window对象。一个程序中只能存在一个全局上下文。
- 函数上下文:函数被调用时会被创建,一个程序中可以存在多个函数上下文。
- eval函数上下文:略
执行栈(也叫调用栈或上下文栈),用于存储在代码执行期间创建的所有上下文.它是LIFO(Last in, First out,后进先出)结构。
当函数运行时,创建一个栈帧运来记录函数运行时的相关信息,执行时创建,return后销毁。ECMAScript程序的执行流就是通过这个执行栈进行控制的。
js的另一大特点是非阻塞,那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?
实现这一点的关键在于下面要说的这项机制——任务队列(Task Queue)。
任务队列
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为任务队列。
被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。
Js 中,异步会有两类任务队列:宏任务队列(macro tasks)和微任务队列(microtasks)。
宏任务队列可以有多个,微任务队列只有一个。
- 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
- 微任务:process.nextTick (node.js中进程相关的对象), Promise, Object.observer, MutationObserver。
Promise 比较奇葩,promise.then/cath /finally的回调属于微,回调之前的代码体中的属于宏
在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列就是 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
promise1是微任务,setTimeout是宏任务,为什么看起来是微任务先执行?
网上也确有微任务优先于宏任务的说法,在我看来是不准确的 why?
script其实也存在于task中,当JS stack执行script
时,我们的task是存在run script
的全局任务的,也就是说setTimeout callback会被推入task,等待下一次取宏任务时再执行。当script执行完后,会取本次微任务队列中的promise执行。
Tasks, microtasks, queues and schedules - JakeArchibald.com
---
浏览器架构
现代浏览器都是多进程架构设计。Chrome:1个浏览器主进程、1个GPU进程、多个渲染进程和多个插件进程。
进程 | 控制 |
---|---|
浏览器 | 控制应用中的 “Chrome” 部分,包括地址栏,书签,回退与前进按钮。以及处理 web 浏览器不可见的特权部分,如网络请求与文件访问。 |
渲染 | 控制标签页内网站展示。 |
插件 | 控制站点使用的任意插件,如 Flash。 |
GPU | 处理独立于其它进程的 GPU 任务。GPU 被分成不同进程,因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。 |
通过上面的了解,我们知道在Chrome浏览器中,每次新开一个标签页,都会创建一个新的渲染进程,而渲染进程在标签页中又是扮演着重要的角色,负责标签页内发生的所有事情。其核心工作就是将HTML、CSS和JavaScript转换为用户与之交互的网页。
渲染进程包括多个线程工作:
- 主线程:运行JavaScript、DOM、CSS、样式布局计算
- 工作线程:运行Web Worker,Service Worker
- 合成线程:将图层分成图块,并发送绘制命令发送给浏览器进程(生成页面,显示在显示器上)
- 光栅线程:将图块转换成位图并发送到 GPU
而在主线程解析HTML时,遇到标记时,就会暂停HTML的解析,开始加载、解析并执行JavaScript代码。这样就造成了HTML解析的阻塞,而JavaScript代码的执行又是为什么会阻塞HTML解析呢?这是因为JavaScript代码里可以通过类似document.write()
的方法改写文档,这样就会导致HTML文档整体结构的变化。
小结
event loop 运行机制:
- 在执行栈中执行一个宏任务。
- 执行过程中遇到微任务,将微任务添加到微任务队列中。
- 当前宏任务执行完毕,立即执行微任务队列中的任务。
- 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
- 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。