浏览器与js运行机制
进程与线程
进程
进程是CPU分配资源的最小单位,它是一个可以自己独立运行且拥有自己资源空间的任务程序;包括程序以及程序所使用的内存及系统资源
线程
线程是CPU调度的最小单位,它就是程序中的一个执行流;也可以理解为一个进程代码的不同执行路径
一个进程中只有一个执行流就是单线程,程序按照顺序执行,前面的处理好才执行后面的
一个进程中有多个执行流就是多线程,多个线程并行执行各自的任务
JS为什么是单线程
多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。而且,如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期
HTML5提出了 Web Worker标准,允许JS脚本创建多个线程,但是子线程完全收主线程控制,而且不得操作DOM,所以它并没改变JS单线程的本质
浏览器多线程
浏览器是多进程
我们没打开一个Tab标签页就会产生一个进程
如果我们打开多个Tab标签页,其中一个Tab标签页崩溃了,影响整个浏览器,那体验肯定是不行的,所以不可能是单进程;每个进程有多个线程都会占用资源,所以我们打开多个标签页可能会卡,谷歌就有标签页限制
浏览器有那些进程
Broswer进程
浏览器的主进程,该进程只有一个,主要是一个协调、主控的作用
负责浏览器的页面展示、交互;(前进后退)
负责页面的管理,创建和销毁其他进程
网络资源的管理,下载等
第三方插件进程
使用插件是才创建,一个插件对应一个进程
GPU进程
该进程只有一个,负责3D的绘制等
Render渲染进程
渲染进程就是我们所说的浏览器内核,内部是多线程
每个tab页面都有一个渲染进程
主要作用是页面渲染、脚本执行,事件处理等
Render进程及它主要的线程
render进程是多线程
GUI渲染线程
主要负责页面的渲染,解析html、css,生成DOM树、CSS规则树,构建Render树,页面的布局绘制
JS引擎线程
JS引擎线程(比如V8引擎)是JS内核;负责解析JavaScript脚本,运行代码
一个render进程中,无论什么时候都只有一个JS线程再运行JS程序
JS引擎线程与GUI渲染线程互斥:
因为JS引擎可以修改DOM树,那么如果JS引擎在执行修改了DOM结构的同时,GUI线程也在渲染页面,那么这样就会导致渲染线程获取的DOM的元素信息可能与JS引擎操作DOM后的结果不一致。
当JS引擎执行的时候,GUI线程需要被冻结,但是GUI的渲染会被保存在一个队列当中,等待JS引擎空闲的时候执行渲染
如果JS引擎正在进行CPU密集型计算,那么JS引擎将会阻塞,长时间不空闲,导致渲染进程一直不能执行渲染,页面就会看起来卡顿卡顿的,渲染不连贯。所以,要尽量避免JS执行时间过长。
事件触发线程
属于浏览器而不是JS引擎
用来控制事件循环,管理事件队列
当js执行碰到事件绑定和异步操作会走事件触发线程,将对应的事件添加到对用的线程中;当事件触发或异步有了结果将它们的回调事件添加到事件队列等待JS引擎事件线程处理
因为JS是单线程,所以事件队列中的事件都要等待JS引擎线程处理
定时触发线程
浏览器定计数器不是在 JS 引擎线程中计数的(JS引擎是单线程,如果处于阻塞线程状态就计不了时,JS引擎线程与GUI渲染线程互斥,GUI进程执行的时候就阻塞了JS引擎线程,就记不了时了)
计时完成后,会添加到事件触发线程的事件队列中
W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步http请求线程
当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行
事件循环机制Event Loop
JS分为同步任务和异步任务,同步任务在主线程也就是JS引擎线程上执行
除了主线程之外,我们的事件触发线程中有一个任务队列,只要异步事件有了结果就会在任务队列总添加它的回调事件;任务队列中由两个队列,一个宏任务队列,一个时微任务队列
1》首先我们的同步任务会进入主执行栈,异步任务交给事件触发线程,异步任务有了结果才会被添加到事件队列中
2》当我们主执行栈中的代码执行之后,会先去微任务队列读取任务,然后执行
3》执行完所有的微任务后又会到微任务队列中读取微任务(因为刚才可能产生了新的微任务),直到没有读取到微任务,然后GUI渲染线程会进行一次渲染(会阻塞js执行)
4》渲染后会去宏任务队列读取宏任务,然后执行
5》宏任务执行完毕会去微任务队列读取任务,有就执行,然后和之前一样,读取微任务直到没有微任务进行渲染,一个事件循环结束
6》渲染页面,再读取、执行宏任务之前,执行完所有微任务之后渲染一次页面
6》重复上面的事情,微任务-渲染-宏任务
宏任务
所有微任务执行完后,下一个宏任务执行前,GUI渲染线程会渲染一次页面
常见宏任务:
setTimeout
setInterval
requestAnimationFrame(浏览器)
微任务
常见微任务
Promise.then()
Promise.catch()
Promise.finally()
process.nextTick (node)
Object.observe
this.$nextTick()
Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。
nextTick会创建一个微任务(或宏任务),将其内部回调推入微任务队列中,vue中一个事件循环中所有dom更新也是一个微任务,dom更新在这个微任务之前进入微任务队列中,是先更新dom再执行this.$next中的代码,所以可以再里面获取到更新后的dom
nextTick异步处理更新队列的逻辑:在下一个的事件循环“tick”中,去刷新队列,依次尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境都不支持,则会采用 setTimeout(fn, 0)
代替;所以this.$nextTick(vue中) 可能是一个微任务也可能时宏任务
我们再来梳理一遍上面从数据变更到 dom 更新之前的整个流程
- 修改响应式数据
- 触发
Object.defineProperty
中的set
- 发布通知
- 触发
Watcher
中的update
方法, update
方法中把Watcher
缓冲到一个队列- 刷新队列的方法(其实就是更新 dom 的方法)传到
nextTick
方法中 nextTick
方法中把传进来的callback
都放在一个数组callbacks
中,然后放在异步队列中去执行
然后这时你调用了 $nextTick
方法,传进来一个获取最新 dom 的回调,这个回调也会推到那个数组 callbacks 中,此时遍历 callbacks 并执行所有回调的动作已经放到了异步队列中,到这(假设你后面没有其他的代码了)所有的同步代码就执行完了,然后开始执行异步队列中的任务,更新 dom 的方法是最先被推进去的,所以就先执行,你传进来的获取最新 dom 的回调是最后传进来的所以最后执行,显而易见,当执行到你的回调的时候,前面更新 dom 的动作都已经完成了,所以现在你的回调就能获取到最新的 dom 了。