浏览器组成、线程及event loop

浏览器组成

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 task queues.来自同一个task source的所有tasks必须放入同一个task queue。通常有4中task source:DOM manipulation task source,user interaction task source,networking task source和history traversal task source。同一个task queue是按先进先出顺序执行的。浏览器可以根据task queue中task source的不同,给予task queue不同的优先级。比如为了及时响应,the user agent could then give keyboard and mouse events preference over other tasks three quarters of the time。即由浏览器决定挑哪一个task queue中的队首task执行。
另外Each event loop has a microtask queue. Microtask execute in order, 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:

  1. 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)
  2. set "currently running task" to "task A"
  3. run "task A"(means run the callback function)
  4. set "currently running task" back to null,and remove "task A" from its task queue
  5. 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;
  6. update rendering
  7. 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的占比。

事件冒泡再次执行的回调仍属于同一个task:

通过js代码触发click事件(targetEle.click())会导致在执行回调的间隙仍在script task中,故stack不会为空,阻塞microtask执行。

posted @ 2017-02-19 13:43  开发之路  阅读(1389)  评论(0编辑  收藏  举报