前言
这几在看Vue的源码与其相关的博客,看到了关于Vue异步更新策略和nextTick的诸多文章,奈何功力不够深厚,看的是有点蒙蔽。主要原因是这些个模块,需要对JS的一些运行机制和Event Loop
(事件循环)有一定的了解,于是决定再一次深入的去了解这些知识。
在后来几天的学习中,当下就是总结了这几个蒙蔽点(你可先尝试自我回答一下):
- JS作为一个单线程语言,是如何实现并发的执行(定时器,http请求)任务的?
- 什么是主线程/
call stack
(执行栈)? - 什么是
task queue
(任务队列)? - 什么是(
task/macrotask
)宏/(microtask
)微事件? - 所谓的事件循环,在浏览器运行层面来说,究竟是什么?
- 每次事件循环,都干了什么事情?
当时在学习的过程中,我就如下图这样的感觉:

带着上面这些个问题,于是开始这几天的展开式的学习,从Event Loop
的概念,在到浏览器层面的实际运行原理。
本篇参考诸多文章,借鉴了里面的很多原话,都在文章的末尾都一一列出了。
渲染进程
我猜大部分做前端的,都知道Event Loop
(事件循环)的概念。但是很多人,对它的了解非常的片面。要想知道这个概念究竟是什么,就要浏览器是如何运行的说起。
首先,浏览器是多进程执行的,但是对于我们研究最重要的,就是浏览器多个进程中的渲染进程,在浏览器的运行中,每一个页面都有独立渲染进程。这个进程分别由如下几个线程在工作:
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时器触发线程
- 异步http请求线程
上面这几个线程,保证我们整个页面(应用)的完整运行。
JS引擎线程
JS引擎线程负责解析Javascript脚本,运行代码,V8引擎就是在这个线程上运行的。
- 这条线程,也就是在事件循环中,咱们常说的主线程和
call stack
(执行栈)。所有的任务(函数),最终都会进入这里来执行。 - 只要执行栈空了,它就会不断的去访问
task queue
(任务队列),一旦任务队列中有可以执行的函数,就会压入栈内执行。 - 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程的,所有JS是单线程的
现在出现了两个词:call stack
(执行栈),task queue
(任务队列),这里先来解释一下,什么是执行栈。
call stack(执行栈)
栈是一个先进后出的数据结构,所有的函数都会逐一的进入到这里面执行,一旦执行完毕就会退出这个栈。
function fun3() {
console.log('run');
throw Error('err');
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
这里我特意在fun3
抛出了一个异常,我们来看一下浏览器的输出:

上面这列出来的一个个函数,就是一个执行栈。这里我用一个更详细的图解来表示一下执行栈的运行过程:

上面这个图解,是对执行栈运行过程的分布演示。这个执行栈,就是我们JS真正运行的地方,函数的调用会在这里形成一个调用栈,在里面是一个个执行的,必须得等到栈顶的函数执行完毕退出,才能继续执行下面的函数。一旦这个栈为空,它就会去task queue
(任务队列)看有没有待执行的任务(函数)。
那么我们常说的任务队列,究竟又是一个啥玩意呢?
事件触发线程
首先,这里还要强调一下上面的提到的,一个页面的运行,是需要多个线程配合支持的。
咱们常说的任务队列,就是由这个事件触发线程来维护的。当时,我看到这个就蒙蔽了……尼玛,JS不是单线程吗?这条事件触发线程是怎么回事?
JS的确是还是单线程执行的。这个事件触发线程属于浏览器而不是JS引擎,这是用来控制事件循环,并且管理着一个任务队列(task queue),然而对本身的JS程序,没有任何控制权限,最终任务队列里的函数,还是得交回执行栈去执行。
task queue(任务队列)
那么这个线程维护的这个task queue
究竟是干嘛的呢?
上面在说call stack
(执行栈)的时候,咱们提到了,一旦执行栈里面被清空了,它就会来看任务队列中是否有需要执行的任务(函数)。这个任务队列可能存放着延期执行的回调函数,类似setTimeout
,setInterval
(并不是说setTimeout和setInterval在这里面,而是他们的回调函数),还可能存放着Ajax请求结果的回调函数等等。
这里先看下具体代码:
console.log('1');
setTimeout(() => {
console.log('2');
}, 1000);
$.ajax(/*.....*/)
现在我们来图解一下,整个运行过程(图画的比较丑,别建议):
- 第一步,console.log方法进入执行栈,执行完毕后退出。

- 第二步,执行setTimeout方法。大家知道延迟,是需要去读数的(你可以理解为计时),当到了时间,就让回调进入到任务队列里面,去等待执行。然而,这个读数的工作是谁在做呢?首先肯定不是JS引擎线程在做,因为执行栈一次只能执行一个任务,如果在执行栈中去读数,必然会造成阻塞,所以渲染进程中,有专门的定时器触发线程来负责读数,到了时间,就把回调交给任务队列。

- 第三步,发起Ajax请求,请求的过程也是在其他线程并行执行(http请求线程)的,请求有了结果以后,回调函数加入事件触发线程的任务队列。


所以,现在应该明白call stack
(执行栈),task queue
(任务队列)是怎么一个工作状态了吧。这里说一句不专业的话,但是你可以这么去理解:
在浏览器环境下的JS程序运行中,其实并不是单线程去完成所有任务的,如定时器的读数,http的请求,都是交给其他线程去完成,这样才能保证JS线程不阻塞。
定时器触发线程
上面我们提到在执行setTimeout
和setInterval
的时候,如果让JS引擎线程去读数的话,必然会造成阻塞。这也是不符合实际需求的,所以这件读数的事情,浏览器把它交给了渲染进程中的定时器触发线程。
一旦,代码中出现timer
类型API,就会交给这个线程去执行,这样JS引擎线程,就可以继续干别的事情。等到时间一到,这个线程就会将对应的回调,交给事件触发线程所维护的task queue
(任务队列)并加入其队尾,一旦执行栈为空,就会拿出来执行。
但是这里要提一点,就算执行栈为空也不一定能马上执行这个回调,因为task queue
(任务队列)中可能还有很多的待执行函数,所以定时器只能让它到了时间的加入到task queue
中,但不一定能够准时的执行。
异步http请求线程
这个线程就是专门负责http请求工作的。简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到任务队列,等待js引擎线程来执行。
GUI渲染线程
这个线程要重点说一下。首先这个GUI渲染线程和JS引擎线程是互斥的,说白了就是这两个同一时间,只能有一个在运行,JS引擎线程会阻塞GUI渲染线程,这也是为什么JS执行时间长了,会导致页面渲染不连贯的原因。
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和Rendert树
- JS负责操作DOM对象,GUI负责渲染DOM(最耗费性能的地方),GUI线程会在每次循环中,合并所有的UI修改,也是浏览器对渲染的性能优化。
- GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
小结
通过上面的这些理论,脑海里应该大致知道浏览器层面的JS是如何去工作了的吧。现在应该可以回答一开上面提出的部分问题了:
- JS作为一个单线程语言,是如何实现并发的执行(定时器,http请求)任务的?答案:因为浏览器提供了定时器触发线程和异步Http请求线程,来分担这些会造成主线程阻塞的工作。
- 什么是主线程/
call stack
(执行栈)?答案:执行栈是JS引擎线程(主线程)中的一个先进后出执行JS程序的地方。一次只允许一个函数在执行,一旦栈被清空,将会轮询任务队列,将任务队列中的函数逐一压如栈内执行 - 什么是
task queue
(任务队列)?答案:任务队列是在事件触发线程中,一个存放异步事件回调的地方。当定时器任务,异步请求任务在其他线程执行完毕时,就会将加入队列的队尾,然后被执行栈逐一执行。
OK,现在已经解决三个问题,接下来我们继续解决剩下的三个问题。这个三个问题,就是从JS事件循环机制的角度来研究了。
Event Loop(事件循环)
我相信大部分搞前端的,都应该知道这玩意。但是,我发现并不是每个人都能说清楚这个东西。彻底了解这个,对于我们处理开发中许多异步问题和阅读源码,是很多有帮助的。
首先,我们先开看一张图(此图出自于Event Loop的规范和实现):

我觉得如果你看完了上面渲染进程相关知识,在看这个图,应该是能理解百分之70了吧,剩下百分之是因为里面出现了microtask queue
(微任务队列)和Promise
,mutation observer
的相关字眼。
我觉得,在开始了解Event Loop
之前,有必要提出两个问题:
- 为什么要有
Event Loop
? Event Loop
的每一个循环,干了些什么事?
宏/微任务
针对上面给出这个图出现的一个新词microtask
,来展开进行学习。
首先,先来看一段代码:
setTimeout(() => {
console.log(1);
});
Promise.resolve().then(() => {
console.log(2);
});
console.log(3);
输出的结果:3,2,1
在还没有接触的Event Loop
之前,看到这个结果的时候,说实话,我是很懵逼的。

OK,到这里,我们需要先知道两个概念:task
(任务),microtask
(微任务);
这里提一点,网上很多博客说到了一个
macrotask
(宏任务)其实跟这个task
(任务)是一个东西。你可以参考换一下HTML5规范的文档,里面甚至没有macrotask
这个词,“宏”这个概念,只是为了更好区分任务和微任务的关系。
通过仔细阅读文档得知,这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。
task主要包含:主代码
、setTimeout
、setInterval
、setImmediate
、I/O
、UI交互事件
microtask主要包含:Promise
、process.nextTick
、MutaionObserver
这里提一点:Promise的then方法,会将传入的回调函数,加入到
microtask queue
中。
然后接下来,你需要知道Event Loop
的每个循环的流程:
- 执行一次最旧的task
- 然后检测
microtask
(微任务),直到所有微任务清空为止 - 执行UI render(JS引擎线程被挂起等待,GUI渲染线程开始运行)
现在带着渲染进程的知识,结合这个流程,来捋一遍上面代码:
- 第一轮循环,主代码是一个
task
,于是它进入JS引擎线程中的执行栈开始执行。 setTimeout
方法被调用,也进入执行栈,将一个延时的异步事件交给了定时器触发线程去读数,然后它马上退出执行栈。Promise.resolve()
进入执行栈,返回一个Promise
,退出执行栈。then()
方法进入执行栈,将一个函数加入了microtask queue
(微任务队列),退出执行栈。console.log(3)
进入执行栈,输出3,退出执行栈。- 此时,主代码已经执行完毕,第一个
task
,退出执行栈。 - 然后,执行栈去看
microtask queue
,发现一个()=>{ console.log(2) }
函数,压入执行栈,输出2,退出执行栈。 - 此时,
microtask queue
被清空,切到GUI线程,看是有需要变动UI的,第一轮循环完毕。 - 第二轮循环。在第一轮循环的代码执行中,
setTimeout
发起的定时器是在定时器触发线程并发进行,读数完毕,回调交给事件触发线程中的task queue
。所以,此时任务队列中有一个待执行的task
。 - 执行栈将这个
task
压入执行栈执行,输出1,然后退出执行栈。 - 整个
Event Loop
,继续重复上面的流程执行。
Event Loop
的不断循环,保证了我们的JS代码同步和异步代码的有序执行。
现在回答一下上面提出的两个问题:
- 因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,Event Loop这样的方案应运而生。
task
=>microtask
=>GUI
重点说一下microtask
(微任务)
ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念。在浏览器上,主要有两个微任务API:
Promise.then
mutation observer
第一个大家应该都熟悉,第二个呢,我之前也不知道,是后来再看Vue的nextTick源码中看到的,有兴趣的同学可以去了解一下这个API。
这里主要说一下微任务和宏任务的不同点,和相同点。
不同点:
- 宏任务:
- 异步的任务是需要在其他线程上去执行的,主要是为了保证主线程不阻塞。
- 宏任务的回调函数,是先保存在任务队列中的,也就是事件触发线程上。
- 一次循环,只执行一个
task
- 微任务:
- 微任务它不是异步任务,它会直接将回调函数加入
microtask queue
(微任务队列)。 - 每次前一个
task
执行完毕,然后所有的microtask
都要被执行完。
- 微任务它不是异步任务,它会直接将回调函数加入
相同点:
- 他们的回调函数,都不会本轮循环中立即执行。
- 没有回调函数,它们都将失去意义。
现在再来把最开始提出的后三个问题回顾一下,应该有一个大致的概念了吧。
最后
其实本来是想写,结合Event Loop来理解Vue的异步批量更新以及nextTcik的,但是后面发现Event Loop这块写的太多了,于是就分开写了。。。
但是,我相信你看完上面的全部内容,在面试的时候,或者碰到异步相关问题的时候,都应该能够应付了。其实这个Event Loop
中还有一些用户交互事件没详细讲到,有兴趣的可以自行研究一下。
Tips:如果有错误或者有歧义的地方,可以在直接指出。
本篇参考的资料:
「硬核JS」一次搞懂JS运行机制(这篇博客中,说到关于浏览器进程和线程的知识,讲解的非常详细,同时对Event Loop也是总结的非常好)
Event Loop的规范和实现(这个主要讲Event Loop,也是非常的通俗易懂,里面的许多案例值得参考)