User interface: a. Every part of the browser display, except the window. b. The address bar, back/forward button, bookmarking menu, etc.
Browser Engine: marshals actions between the UI and the rendering engine. This provides a high-level interface to the Rendering Engine. The Browser Engine provides methods to initiate the loading of a URL and other high-level browsing actions (reload, back, forward). The Browser Engine also provides the User interface with various messages relating to error messages and loading progress.
Rendering engine(渲染引擎/内核) : a. Parse HTML and CSS. b. Display parsed content on the screen.
JavaScript interpreter: Used to parse and execute JavaScript code.
浏览器的线程
浏览器是多线程的,它们在内核制控下相互配合以保持同步。一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。
1) javascript引擎是基于事件驱动单线程执行的(可以修改DOM,简单化处理了),必须符合ECMAScript规范。JS引擎一直等待着event loop中任务的到来,然后加以处理(只有当前函数执行栈执行完毕,才会去任务队列中取任务执行)。浏览器无论什么时候都只有一个JS线程在运行JS程序。
2) UI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但是 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,JS对页面的操作即GUI的更新也会被保存在一个队列中,等到JS引擎空闲时才有机会被执行。这就是JS阻塞页面加载。
3) 事件触发线程,当一个事件被触发时该线程会把事件添加到任务队列的队尾,等待JS引擎的处理。这些事件可以来自JavaScript引擎当前执行的代码块调用setTimeout/ajax添加一个任务,也可以来自浏览器其他线程如鼠标点击添加的任务。但由于JS的单线程关系,所有这些事件都得排队等待JS引擎处理。
"任务队列"是一个事件的队列(也可以理解成消息队列),当浏览器的其他线程完触发一项任务时,就在js引擎的"任务队列"中添加一个事件/任务,等js引擎的函数执行栈空闲时就会读取任务队列的事件,并执行该事件对应的回调函数。队列中后面的任务必须等前面的任务执行完才能被执行。该过程会一直循环不停,如下图所示:
Event loops
在HTML规范中有要求浏览器实现。简单总结如下:
为了协调事件,用户接口,脚本,渲染,网络等,浏览器必须使用event loops。有对 browsing contexts和 workers的两种类型。An event loop has one or more and are executed:
- after every callback, as long as no other JavaScript is mid-execution;
- at the end of each task.
task分为两类:
macro-task:script( js codes as a whole in script tag),setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering;
micro-task:process.nextTick, Promises, Object.observe, MutationObserver.
js引擎会把task push到对应的macrotask queue(即task queue)或microtask queue中。在event loop的一个回合中,会先从macrotask queue中取出队首的任务进行执行;执行完毕后,再依次执行microtask queue中的所有任务;如果在执行过程中添加了新的microtask,则会在当前的回合中继续执行,直到全部mircotask执行完毕才进入下一个event loop回合。
An event loop must continually run through the following steps for as long as it exists:
- select the oldest task(task A) in task queues.If task A is null(means task queues is empty),jump to step 5(microtasks steps)
- set "currently running task" to "task A"
- run "task A"(means run the callback function)
- set "currently running task" back to null,and remove "task A" from its task queue
- perform microtask queue
- (a).select the oldest task(task x) in microtask queue
- (b).if task x is null(means microtask queues is empty),jump to step (g)
- (c).set "currently running task" to "task x"
- (d).run "task x"
- (e).set "currently running task" to null,remove "task x" from the microtask queue
- (f).select next oldest task in microtask queue,jump to step(b)
- (g).finish microtask queue;
- update rendering
- next routnd:jump to step 1
microtask执行时间过长会导致macotask任务的阻塞,导致UI渲染的阻塞。nodejs的process.nextTick限制1000个tick任务,以使macrotask得以执行。
案例解析1:
//promises come from ECMAScript rather than HTML
setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i<10000 ; i++ ){ i==9999 && resolve() } console.log(2) }).then(function(){ console.log(5) })then(function(){ console.log(6) }); console.log(3); //整个js代码是script task,属于macrotask,会在当前task queue中执行,在执行过程中碰到setTimeout,会push到task queue中,但要等下一回合执行;第一个then()属于microtask,在本回合执行,返回undefined,第二个then()也会在当前回合执行
案例解析2:js线程一直执行,stack就不为空,浏览器就没有机会取task queue中的UI render任务更新页面。chrome dev tools的断点调试会影响event loop实时更新UI.
Render queue:浏览器在1s中渲染页面60次,每16ms就会往Render queue中添加一个UI render任务。但是浏览器只有在stack为空时才有机会执行该任务。通过setTimeout(cb,0)将任务分割,就是增加UI render 任务被执行的机会,改善用户体验。
可以将计算量大的任务通过setTimeout分割成多个小任务,这样浏览器就有时间执行UI线程。但是不好的地方是计算所要花的时间会更长。分析performance,想办法改善代码(单次小任务中执行更多的计算)以降低cpu idle的占比。
通过js代码触发click事件(targetEle.click())会导致在执行回调的间隙仍在script task中,故stack不会为空,阻塞microtask执行。