浏览器渲染基本原理(二):JS引擎的工做方式

JS引擎也能够叫作JS解释器node

浏览器的组成

浏览器的核心是两部分:渲染引擎和JavaScript解释器(又称JavaScript引擎)。chrome

(1)渲染引擎promise

渲染引擎的主要做用是,将网页从代码“渲染”为用户视觉上能够感知的平面文档。不一样的浏览器有不一样的渲染引擎。浏览器

以上四步并不是严格按顺序执行,每每第一步还没完成,第二步和第三步就已经开始了。因此,会看到这种状况:网页的HTML代码还没下载完,但浏览器已经显示出内容了。缓存

(2)JavaScript引擎网络

JavaScript引擎的主要做用是,读取网页中的JavaScript代码,对其处理后运行。数据结构

本节主要介绍JavaScript引擎的工做方式。多线程

 

 

<script>标签的工做原理

 

正常的网页加载流程是这样的。

  1. 浏览器一边下载HTML网页,一边开始解析
  2. 解析过程当中,发现<script>标签
  3. 暂停解析,网页渲染的控制权转交给JavaScript引擎
  4. 若是<script>标签引用了外部脚本,就下载该脚本,不然就直接执行
  5. 执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页

 

也就是说,加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。缘由是JavaScript能够修改DOM(好比使用document.write方法),因此必须把控制权让给它,不然会致使复杂的线程竞赛的问题。异步

若是外部脚本加载时间很长(好比一直没法完成下载),就会形成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。async

为了不这种状况,较好的作法是将<script>标签都放在页面底部,而不是头部。这样即便遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少能够看到内容,而不是面对一张空白的页面。

若是某些脚本代码很是重要,必定要放在页面头部的话,最好直接将代码嵌入页面,而不是链接外部脚本文件,这样能缩短加载时间。

将脚本文件都放在网页尾部加载,还有一个好处。在DOM结构生成以前就调用DOM,JavaScript会报错,若是脚本都在网页尾部加载,就不存在这个问题,由于这时DOM确定已经生成了。

 

<head><script>console.log(document.body.innerHTML);
</script></head><body></body>

上面代码执行时会报错,由于此时document.body元素还未生成。

一种解决方法是设定DOMContentLoaded事件的回调函数。

 

 

 

下面是一个window.requestAnimationFrame()对比效果的例子。

// 重绘代价高functiondoubleHeight(element) {
varcurrentHeight=element.clientHeight;
element.style.height= (currentHeight*2) +'px';
}
all_my_elements.forEach(doubleHeight);
// 重绘代价低functiondoubleHeight(element) {
varcurrentHeight=element.clientHeight;
window.requestAnimationFrame(function () {
element.style.height= (currentHeight*2) +'px';
});
}
all_my_elements.forEach(doubleHeight);

 

 

 

 

 

JavaScript虚拟机

JavaScript是一种解释型语言,也就是说,它不须要编译,能够由解释器实时运行这样的好处是运行和修改都比较方便,刷新页面就能够从新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。为了提升运行速度,目前的浏览器都将JavaScript进行必定程度的编译,生成相似字节码(bytecode)的中间代码,以提升运行速度。

早期,浏览器内部对JavaScript的处理过程以下:

  1. 读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。
  2. 对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。
  3. 使用“翻译器”(translator),将代码转为字节码(bytecode)。
  4. 使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。

逐行解释将字节码转为机器码,是很低效的。为了提升运行速度,现代浏览器改成采用“即时编译”(Just In Time compiler,缩写JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,而且把编译结果缓存(inline cache)。一般,一个程序被常常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提高。

不一样的浏览器有不一样的编译策略。有的浏览器只编译最常常用到的部分,好比循环的部分;有的浏览器索性省略了字节码的翻译步骤,直接编译成机器码,好比chrome浏览器的V8引擎。

字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,通常也把虚拟机称为JavaScript引擎。由于JavaScript运行时未必有字节码,因此JavaScript虚拟机并不彻底基于字节码,而是部分基于源码,即只要有可能,就经过JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其余采用虚拟机(好比Java)的语言不尽相同。这样作的目的,是为了尽量地优化代码、提升性能。下面是目前最多见的一些JavaScript虚拟机:

 

单线程模型

含义

首先,明确一个观念:JavaScript只在一个线程上运行,不表明JavaScript引擎只有一个线程。事实上,JavaScript引擎有多个线程,其中单个脚本只能在一个线程上运行,其余线程都是在后台配合。JavaScript脚本在一个线程里运行。这意味着,一次只能运行一个任务,其余任务都必须在后面排队等待。

JavaScript之因此采用单线程,而不是多线程,跟历史有关系。JavaScript从诞生起就是单线程,缘由是不想让浏览器变得太复杂,由于多线程须要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来讲,这就太复杂了。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。

单线程模型带来了一些问题,主要是新的任务被加在队列的尾部,只有前面的全部任务运行结束,才会轮到它执行。若是有一个任务特别耗时,后面的任务都会停在那里等待,形成浏览器失去响应,又称“假死”。为了不“假死”,当某个操做在必定时间后仍没法结束,浏览器就会跳出提示框,询问用户是否要强行中止脚本运行。

若是排队是由于计算量大,CPU忙不过来,倒也算了,可是不少时候CPU是闲着的,由于IO设备(输入输出设备)很慢(好比Ajax操做从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时CPU彻底能够无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去。这种机制就是JavaScript内部采用的Event Loop。

消息队列

JavaScript运行时,除了一根运行线程,系统还提供一个消息队列(message queue),里面是各类须要当前程序处理的消息。新的消息进入队列的时候,会自动排在队列的尾端。

运行线程只要发现消息队列不为空,就会取出排在第一位的那个消息,执行它对应的回调函数。等到执行完,再取出排在第二位的消息,不断循环,直到消息队列变空为止。

每条消息与一个回调函数相联系,也就是说,程序只要收到这条消息,就会执行对应的函数。另外一方面,进入消息队列的消息,必须有对应的回调函数。不然这个消息就会遗失,不会进入消息队列。举例来讲,鼠标点击就会产生一条消息,报告click事件发生了。若是没有回调函数,这个消息就遗失了。若是有回调函数,这个消息进入消息队列。等到程序收到这个消息,就会执行click事件的回调函数。

另外一种状况是setTimeout会在指定时间向消息队列添加一条消息。若是消息队列之中,此时没有其余消息,这条消息会当即获得处理;不然,这条消息会不得不等到其余消息处理完,才会获得处理。所以,setTimeout指定的执行时间,只是一个最先可能发生的时间,并不能保证必定会在那个时间发生。

一旦当前执行栈空了,消息队列就会取出排在第一位的那条消息,传入程序。程序开始执行对应的回调函数,等到执行完,再处理下一条消息。

 

Event Loop

所谓Event Loop,指的是一种内部循环,用来一轮又一轮地处理消息队列之中的消息,即执行对应的回调函数。Wikipedia的定义是:“Event Loop是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。能够就把Event Loop理解成动态更新的消息队列自己。

下面是一些常见的JavaScript任务。

  • 执行JavaScript代码
  • 对用户的输入(包含鼠标点击、键盘输入等等)作出反应
  • 处理异步的网络请求

全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在JavaScript执行进程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入JavaScript执行进程、而进入“任务队列”(task queue)的任务,只有“任务队列”通知主进程,某个异步任务能够执行了,该任务(采用回调函数的形式)才会进入JavaScript进程执行。

以Ajax操做为例,它能够看成同步任务处理,也能够看成异步任务处理,由开发者决定。若是是同步任务,主线程就等着Ajax操做返回结果,再往下执行;若是是异步任务,该任务直接进入“任务队列”,JavaScript进程跳过Ajax操做,直接往下执行,等到Ajax操做有告终果,JavaScript进程再执行对应的回调函数。

也就是说,虽然JavaScript只有一根进程用来执行,可是并行的还有其余进程(好比,处理定时器的进程、处理用户输入的进程、处理网络通讯的进程等等)。这些进程经过向任务队列添加任务,实现与JavaScript进程通讯。

想要理解Event Loop,就要从程序的运行模式讲起。运行之后的程序叫作“进程”(process),通常状况下,一个进程一次只能执行一个任务。若是有不少任务须要执行,不外乎三种解决方法。

  1. 排队。由于一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。

  2. 新建进程。使用fork命令,为每一个任务新建一个进程。

  3. 新建线程。由于进程太耗费资源,因此现在的程序每每容许一个进程包含多个线程,由线程去完成任务。

若是某个任务很耗时,好比涉及不少I/O(输入/输出)操做,那么线程的运行大概是下面的样子。

 

 

上图的绿色部分是程序的运行时间,红色部分是等待时间。能够看到,因为I/O操做很慢,因此这个线程的大部分运行时间都在空等I/O操做的返回结果。这种运行方式称为”同步模式”(synchronous I/O)。

若是采用多线程,同时运行多个任务,那极可能就是下面这样。

 

 

上图代表,多线程不只占用多倍的系统资源,也闲置多倍的资源,这显然不合理。

 

上图主线程的绿色部分,仍是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,而后接着日后运行,因此不存在红色的等待时间。等到I/O程序完成操做,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

能够看到,因为多出了橙色的空闲时间,因此主线程得以运行更多的任务,这就提升了效率。这种运行方式称为”异步模式“(asynchronous I/O)。

这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也所以使它具有了其余语言不具有的优点。若是部署得好,JavaScript程序是不会出现堵塞的,这就是为何node.js平台能够用不多的资源,应付大流量访问的缘由。

若是有大量的异步任务(实际状况就是这样),它们会在“消息队列”中产生大量的消息。这些消息排成队,等候进入主线程。本质上,“消息队列”就是一个“先进先出”的数据结构。好比,点击鼠标就产生一系列消息(各类事件),mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。

 

 

参考连接John Dalziel, The race for speed part 2: How JavaScript compilers workJake Archibald,Deep dive into the murky waters of script loadingMozilla Developer Network, window.setTimeoutRemy Sharp, Throttling function callsAyman Farhat, An alternative to Javascript’s evil setIntervalIlya Grigorik, Script-injected “async scripts” considered harmfulAxel Rauschmayer, ECMAScript 6 promises (1/2): foundationsDaniel Imms, async vs defer attributesCraig Buckler, Load Non-blocking JavaScript with HTML5 Async and DeferDomenico De Felice, How browsers work为这个值。

相关文章
相关标签/搜索