You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
进程(process)和线程(thread)是操作系统的基本概念。
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。
现代操作系统都是可以同时运行多个任务的,比如:用浏览器上网的同时还可以听音乐。
对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程(准确的说,浏览器是多进程的,这里方便理解只是概括性的描述一下),打开一个 Word 就启动了一个 Word 进程。
有些进程同时不止做一件事,比如 Word,它同时可以进行打字、拼写检查、打印等事情。在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。
由于每个进程至少要做一件事,所以一个进程至少有一个线程。系统会给每个进程分配独立的内存,因此进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
// Let's get hold of those elementsvarouter=document.querySelector('.outer');varinner=document.querySelector('.inner');// Let's listen for attribute changes on the// outer elementnewMutationObserver(function(){console.log('mutate');}).observe(outer,{attributes: true,});// Here's a click listener…functiononClick(){console.log('click');setTimeout(function(){console.log('timeout');},0);Promise.resolve().then(function(){console.log('promise');});outer.setAttribute('data-random',Math.random());}// …which we'll attach to both elementsinner.addEventListener('click',onClick);outer.addEventListener('click',onClick);
本文通过参考多篇文章和视频,根据自己理解的思路进行摘抄整理,补充和总结的关于js事件循环的原理和一些浏览器渲染相关的知识点,对自己来说是一次学习总结,记录自己从零开始一步步理解这些知识点的过程。文章末尾会列出参考文章和视频。
文中如有错误的地方,还请见谅,如果能指出错误,我会很感谢你!
1、进程和线程
1-1 概念
进程(process)和线程(thread)是操作系统的基本概念。
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。
现代操作系统都是可以同时运行多个任务的,比如:用浏览器上网的同时还可以听音乐。
对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程(准确的说,浏览器是多进程的,这里方便理解只是概括性的描述一下),打开一个 Word 就启动了一个 Word 进程。
有些进程同时不止做一件事,比如 Word,它同时可以进行打字、拼写检查、打印等事情。在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。
由于每个进程至少要做一件事,所以一个进程至少有一个线程。系统会给每个进程分配独立的内存,因此进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
1-2 浏览器的多进程架构
以chrome为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能。每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。
我们通常说的js引擎(如v8),负责处理 Javascript 脚本,他所在的线程就是js引擎线程,属于渲染进程的一个线程。
1-3 js是单线程的
JavaScript是一门单线程的语言,因此,JavaScript在同一个时间只能做一件事,单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务,比如说下面这段代码:
代码会依次输出1,2,因为代码是从上到下依次执行,执行完foo(),才继续执行bar(),但是如果foo()中的代码执行的是读取文件或者ajax操作,文件的读取和数据的获取都需要一定时间,这样bar就需要等待foo执行结束才会执行。
JavaScript的单线程,与它的用途有很大关系。JavaScript作为浏览器的脚本语言,主要用来实现与用户的交互,利用JavaScript,可以实现对DOM的各种各样的操作,如果JavaScript是多线程的话,一个线程在一个DOM节点中增加内容,另一个线程要删除这个DOM节点,那么这个DOM节点究竟是要增加内容还是删除呢?这会带来很复杂的同步问题,因此,JavaScript被设计为单线程的。
2、同步与异步
2-1 同步任务
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。在
1-2 js是单线程的
中的示例代码就是一个同步代码。2-2 异步任务
异步任务是指,当遇到耗时较长的任务时,把它挂起等待执行结束,而主线程不被阻塞继续执行后续代码。这样被挂起等待执行结束的任务称为异步任务。异步任务完成后会把回调函数推入一个执行队列,当主线程当前的同步任务执行完成时会检查这个执行队列来执行异步任务的回调函数。(具体js是如果管理和处理这些同步和异步的执行顺序呢?后面会在事件循环小节进行说明)。当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。
2-3 为什么会有同步和异步
因为JavaScript是单线程的,因此同个时间只能处理一个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,造成阻塞。拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验。
JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再执行挂起的任务。这样便产生了同步和异步。
3、事件循环 EventLoop
3-1 Call Stack
3-1-1 Call Stack
Call Stack是一个记录当前代码执行到哪里到一个数据结构。js执行代码时,Call Stack会记录各个任务的进栈和出栈。
考虑如下代码:
执行上面的代码,会有一种被称为main()的方法被执行(在浏览器中也会显示为(anonymous function),可以理解为这段代码本身);然后声明了三个函数,最后调用了printSquare(4)函数;在它内部又依次调用了square(n),multiply(n, n)。所以Call Stack看起来是这样的:
然后接下来的执行顺序是:multiply(n, n) return,multiply(n, n)出栈;square(n) return,square(n)出栈;printSquare(4)执行console.log(squared),console.log(squared)入栈;
console.log(squared)完成,出栈;printSquare(4)出栈;main()出栈;结束。
3-1-2 死循环
如果写了死循环,call stack将会一直被推入无限多个foo(),可能会造成堆栈溢出而卡死。最终当超出一定数量后会导致浏览器报错,杀掉这个进程,提示对应的错误信息。
3-2 阻塞 blocking
上面的代码,当执行
$.getSync('//foo.com')
时,会等待同步的网络请求返回后再执行下一行代码。后面的代码需要等待前面的任务结束才能执行,这就发生了阻塞。阻塞会造成什么问题呢?
我们的代码是跑在浏览器中的。当发生阻塞时,用户的任何操作都不会被立即响应执行,因为主线程被阻塞了(浏览器也不能render,后面浏览器渲染小节会讲到),用户在页面上的交互操作都需要等待之前在阻塞的任务执行完成才能继续往下执行任务。对用户而言就是页面卡住了,任何操作都没有效果。等阻塞结束后,用户之前的操作反而又会被再继续执行,这样就会比较诡异。
解决阻塞问题的方法是使用异步编程(包括回调函数callback function、promise等)。
3-3 异步回调 Async Callback
考虑如下代码:
逐步分析一下代码;call stack首先推入main();然后console.log('hi')进栈,console.log('hi')出栈;setTimeout是异步任务,跳过;console.log('JSConfEU')进栈,console.log('JSConfEU')出栈;main()出栈。5秒后call stack又被推入console.log('there'),然后console.log('there')出栈。
那么setTimeout(cb,5000)是怎么跳过的?它去了哪里?cb又是如何重新进入call stack的呢?看上去那么setTimeout转移到了其他地方等待执行结束,然后把回调函数又返回给js主线程了,但是js是单线程到,又是谁去处理setTimeout异步任务呢?接下来就开始讲一下重点——EVentLoop事件循环
3-4 事件循环 EventLoop
3-4-1 WebAPIs
浏览器不仅仅为js代码提供了运行时环境,还提供了可以进行异步操作的WebAPIs,如:setTimeout/setInterval;DOM(document)对象;XMLHttpRequest对象等。这些WebAPIs可以让js实现异步回调。
3-4-2 任务队列 Task queue (宏任务MacroTask与微任务 Microtask)
简单来说,js执行代码时,各种同步或异步任务会被存入任务队列顺序执行。但是实际情况会复杂很多。
任务队列分为2种:宏任务MacroTask与微任务 Microtask。
常见的宏任务有:同步代码的执行、大多数异步任务(setTimeout、网络请求等)。
常见等微任务有:MutationObserver、Promise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 V8 的垃圾回收过程。
js在执行每个宏任务结束之前会检查微任务队列内是否有待执行的任务,如果有,那么就会依次执行微任务队列的任务。当微任务队列没有待执行的任务,那么当前的宏任务就结束了,当前的宏任务从宏任务队列中删除,js继续执行宏任务队列里的下一个宏任务。js这样循环地检查宏任务事件队列和微任务事件队列来依次执行各个事件任务的机制,被称做事件循环EventLoop。当然事件循环涉及不止js事件的执行,还有很多其他非js事件的执行(如浏览器渲染等
用具体代码分析:
下面开始逐步分析代码:当点击inner div的时候
1、首先触发Click事件(会执行onClick这个回调函数),我们记做
dispatch click
,这是一个宏任务,MacroTasks queue存入dispatch click
任务(注意,dispatch click
任务包含执行onClick
函数及其内部同步代码)。此时的任务队列大概是这个样子:dispatch click
2、js执行
onClick
,Call Stack推入onClick
;Call Stack推入console.log('click')
,console.log('click')
出栈;3、js执行
setTimeout
API,Call Stack推入setTimeout
,setTimeout
API执行,浏览器提供一个timer开始计时,Call Stack推出setTimeout
API。同时因为setTimeout
是一个宏任务,所以,0秒后(实际上是4ms左右,因为W3C在HTML中规定,setTimeout中低于4ms的时间间隔算为4ms)MacroTasks queue存入setTimeout
的回调函数cb(setTimeout)
。dispatch click
,cb(setTimeout)
4、js执行
Promise.resolve.then(cb)
,Call Stack推入Promise.resolve.then(cb)
,Promise.resolve.then(cb)
是一个微任务,因为是立即resolve
了,所以紧接着,Microtask queue存入Promise.resolve.then(cb)
的回调函数cb(Promise then)
,Call Stack推出Promise.resolve.then(cb)
。dispatch click
,cb(setTimeout)
cb(Promise then)
5、js执行
outer.setAttribute()
,Call Stack推入outer.setAttribute()
,触发MutationObserver
,它也是一个微任务,Microtask queue存入MutationObserver
的回调函数cb(mutation observer)
,outer.setAttribute()
出栈。dispatch click
,cb(setTimeout)
cb(Promise then)
,cb(mutation observer)
6、onClick内同步代码执行完了,宏任务
dispatch click
不会立即结束,此时会检查微任务队列是否有任务等待执行,此时微任务队列有2个任务,依次是cb(Promise then)
和cb(mutation observer)
,那么依次执行这2个微任务。7、js执行
cb(Promise then)
,即执行console.log('promise')
。Call Stack推入console.log('promise')
,Call Stack推出console.log('promise')
。微任务队列删除cb(Promise then)
。dispatch click
,cb(setTimeout)
cb(mutation observer)
8、js执行
cb(mutation observer)
,即执行console.log('mutate')
。Call Stack推入console.log('mutate')
,Call Stack推出console.log('mutate')
。微任务队列删除cb(mutation observer)
。此时微任务队列的任务也执行完了,Call Stack推出onClick
。dispatch click
,cb(setTimeout)
9、那么宏任务
dispatch click
是不是结束了呢?答案是没有,因为dispatch click
还有事件冒泡,会触发outer的Click事件,因此onClick函数再次被js执行。于是重复上面的2-8步骤。10、2-8步骤重复完后,任务队列大概是这样的:
dispatch click
,cb(setTimeout)
,cb(setTimeout)
宏任务
dispatch click
结束。宏任务队列删除dispatch click
,由于2-8步骤一共执行了2次,所以宏任务队列还有2个cb(setTimeout)
宏任务。于是,继续执行下一个宏任务。11、js执行第一个
cb(setTimeout)
,即执行console.log('timeout')
,Call stack推入console.log('timeout')
,Call stack推出console.log('timeout')
。此时微任务队列没有待执行的任务。第一个cb(setTimeout)
宏任务结束了。宏任务队列删除第一个cb(setTimeout)
,继续执行第二个cb(setTimeout)
。12、js执行第二个
cb(setTimeout)
,即执行console.log('timeout')
,Call stack推入console.log('timeout')
,Call stack推出console.log('timeout')
。此时微任务队列没有待执行的任务。第二个cb(setTimeout)
宏任务结束了。宏任务队列删除第二个cb(setTimeout)
。13、最终宏任务队列也空了,没有待执行的宏任务了。
再来看一下另一个例子
button元素被添加了2个事件响应回调函数,根据不同的触发方式,各个事件响应的顺序也不同。
用户点击时,触发了2个click事件,他们是分别向主线程推入了回调函数,根据前面的介绍我们知道。DOM事件处理程序是宏任务,所以这2个都是宏任务。对应的log顺序是:
listener 1
、Microtask 1
、listener 2
、Microtask 2
。每个宏任务结束后都会执行相应的微任务。而js调用时,又不太一样,它log的顺序是:
listener 1
、listener 2
、Microtask 1
、Microtask 2
。这是因为,整个script
同步代码作为宏任务,当执行到button.click()
时,会继续执行2个事件监听的回调函数,这2个回调函数也属性本次宏任务,只有2个回调函数都被执行完后,script
才可以退出。这时才会检查微任务事件队列,去执行微任务。4、浏览器渲染
除去网络资源获取的步骤,我们理解的 Web 页面的展示,一般可以分为
构建 DOM 树
、构建渲染树
、布局
、绘制
、渲染层合成
几个步骤。关于这些步骤的细节,推荐阅读下面的扩展阅读,推荐的文章都介绍的比较好。
下面来介绍一下和事件循环有关的一个动画API——requestAnimationFrame
4-1 requestAnimationFrame
通常浏览器渲染页面时,他的频率不会超过显示器刷新的频率,一般都是保持和显示器刷新频率一致(通常60次/秒,大约16.67ms一次)。也就是说,每次显示器刷新画面时,浏览器也更新一次页面。这样保证页面更新与显示器刷新保持同步,到达最流畅的显示效果。
在浏览器渲染进程中,周期性更新页面时,会调用js线程计算最新的页面,然后进行渲染流程。requestAnimationFrame就是在渲染前执行的动画。
定义:
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。用示例说明:
上面的代码是循环调用callback来让Box向前移动1个像素。
requestAnimationFrame是在每次渲染前移动一次,所以它看起来很流畅。显示器每刷新一次,浏览器更新一次页面,同时requestAnimationFrame在浏览器更新页面前让box向前移动1个像素。他们的频率都保持一致。
setTimeout虽然时间间隔是0,但是实际上是4ms左右,它的调用频率是大于浏览器和显示器刷新频率的(通常60次/秒,大约16.67ms一次)。每当浏览器想要更新页面时,setTimeout执行了好几次,计算box的位置时,box已经向前移动了好几个像素,这样box的移动动画就会不流畅,出现跳跃的现象。
requestAnimationFrame在每次渲染前进行计算,显示器每一帧都是先计算更新再进行渲染,即使requestAnimationFrame内的任务耗时较长推迟了渲染,他们的顺序是固定的,效果比setTimeout的不确定性更好,因为不同情况下setTimeout在每一帧出现的次数都不固定。因此,当我们在做动画时,推荐使用requestAnimationFrame。
参考文章:
1、 js中的同步和异步:https://www.cnblogs.com/Yellow-ice/p/10433423.html
2、 nodejs中的异步、非阻塞I/O是如何实现的?:http://47.98.159.95/my_blog/js-async/001.html#%E4%BB%80%E4%B9%88%E6%98%AFi-o%EF%BC%9F
3、JS异步编程有哪些方案?为什么会出现这些方案?:http://47.98.159.95/my_blog/js-async/002.html#%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0%E6%97%B6%E4%BB%A3
4、如何理解EventLoop,共三篇:http://47.98.159.95/my_blog/js-v8/004.html
5、前端高级进阶指南:sl1673495/blogs#37
6、Tasks, microtasks, queues and schedules:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
7、从浏览器多进程到js单线程,js运行机制最全面的一次梳理:https://segmentfault.com/a/1190000012925872
8、从 8 道面试题看浏览器渲染过程与性能优化:https://juejin.im/post/5e143104e51d45414a4715f7#heading-18
9、浏览器层合成与页面渲染优化:https://juejin.im/post/5da52531518825094e373372#heading-1
10、浏览器渲染:http://47.98.159.95/my_blog/browser-render/001.html
参考视频:
1、https://www.bilibili.com/video/BV1K4411D7Jb?from=search&seid=10601633564738187138
2、https://www.bilibili.com/video/BV1ot411i7pD?from=search&seid=10636172763723335375
The text was updated successfully, but these errors were encountered: