本文主要整理浏览器中的 JavaScript 运行机制。
另外注意一点,由于历史原因,本文中提到的作对比的 Node,指的是 v11 以下的版本! v11 以上的版本中,事件循环表现已经和浏览器一致了!具体见另一篇整理的《Node 事件循环机制》
首先我们得先理解一下计算机中常见的进程和线程概念。做一个形象的比喻:
进程是一个工厂,工厂有独立的资源 -> 系统分配的内存(独立的一块内存)
各个工厂相互独立 -> 进程之间相互独立
线程是工厂中的工人,多个工人协作完成任务 -> 多个线程在进程中协作完成任务
工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
用正式术语总结一下:
到这里让,我们回到浏览器中,对浏览器运行作出一个正确的理解:
总结一下,我们常说 JavaScript 是一门的单线程语言,这话没错,但放在浏览器的世界中,还需要其他进程/线程的配合,才能让一个页面完美运行。
对前端来说,最重要的是理清渲染进程的运行机制,渲染进程是多线程的,页面的渲染,JavaScript 的执行,事件的循环这些线程都在这个进程下打配合。
下面分析一下渲染进程中主要的线程:
GUI 渲染线程
JS 引擎线程
事件触发线程
定时触发器线程
setInterval
与 setTimeout
所在线程异步 AJAX 请求线程
这里再多说几句解释:
事件循环(Event Loop)是前端领域中非常重要的一个概念,因为 JavaScript 是单线程工作的,而 JavaScript 执行的任务有同步和异步之分。事件循环机制是用于协调同步和异步之间的流程。
以下面的代码进行举例理解:
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
function foo() {
console.log('foo')
setTimeout(baz, 0)
bar()
}
foo()
// foo
// bar
// baz
这段代码中,foo()
、bar()
是同步任务,所以会先执行,setTimeout
是异步任务,所以在 "0ms" 后,事件触发线程将 baz()
放入任务队列中,等待主线程空闲后,再取出baz()
这个回调函数,放入执行栈,进行消化。
下面的流程图完整的展示了这个过程:
这里 stack 是主线程的执行栈,heap 是代码中用到的数据(各种对象数组),callback queue 就是任务队列,所有经由 Web API 这些异步线程处理完后的回调事件将统统放在这个队列里,等待主线程空闲时捞起。
对于任务队列,我们需要进一步进行细分,目前有两类任务
区别在于,微任务在本轮Event Loop的所有任务结束后执行,即栈清空后,先执行微任务,再检查任务队列,继续压入栈中执行。
所以同样的异步场景,微任务要比宏任务先执行:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
// Promise1
// setTimeout1
// Promise2
// setTimeout2
不过这里有一点要注意的,浏览器和 Node 中对于微任务的处理方式有所不同。因为 Node 中的事件循环是跟浏览器完全不是一个东西,是由 libuv 进行实现的。
这些知识点在后面整理 Node 的事件循环机制中再作深入整理。
这一节简单讲讲浏览器拿到数据后的渲染过程,详细内容可以另开一篇讲解。
浏览器内核拿到内容后,渲染大概可以划分成以下几个步骤:
所有详细步骤都已经略去,渲染完毕后就是load
事件了,之后就是自己的 JS 逻辑处理了。
这里值得一提的是,在头部引入 CSS 资源的细节:
另外还有图层合成(composite)的知识点,后面再整理。
另外值得注意的是并不是每一轮的事件循环都会触发渲染,浏览器是很聪明的,它有可能会合并一些操作,将多次变更一次性渲染上去。
这里从这篇文章从 event loop 规范探究 javaScript 异步及浏览器更新渲染时机 - 前端 中得知一些结论,先放出来供参考,后续有需要作深入研究一下事件循环和渲染之间的关系:
这里贴一些奇奇怪怪的关于事件循环的代码,对比浏览器和 Node,以作理解:
(注意⚠️:以下代码运行在 v11 以下的版本,v11 以上版本已经跟浏览器行为一致)
// 案例1
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise')
})
})
setTimeout(function(){
console.log(3);
})
// Node: 1 2 3 promise
// 浏览器: 1 2 promise 3
// 案例2
setTimeout(() => {
console.log(1)
Promise.resolve(1).then(function(){
console.log('promise')
})
}, 0)
setTimeout(function(){
console.log(3);
},0)
Promise.resolve(1).then(function(){
console.log(4)
setTimeout(() => {
console.log(5)
}, 0)
})
console.log(2)
// Node: 2 4 1 3 promise 5
// 浏览器:2 4 1 promise 3 5
这篇把微任务和宏任务概念讲得非常清,可参考:
其他参考: