浏览器基础

什么是浏览器?


通俗来讲,浏览器就是用来浏览网页用的东西。比如说,火狐、谷歌、ie、safari、欧鹏,对于我们前端来讲我们更多的是关注浏览器的内核。
通用内核:Trident(IE),Gecko(Firefox ),Webkit(Safari),Blink(Chrome );
专⽤用浏览器器:Android WebView、iOS UIWebView、WeChat WebView

浏览器结构


浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储),如下图:


image.png
  • User Interface(用户界面):包括地址栏、后退/前进按钮、书签目录等,也就是你-所看到的除了页面显示窗口之外的其他部分;

  • Browser engine(浏览器引擎):可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心;

  • Rendering engine(渲染引擎):解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,也有人称之为排版引擎,我们常说的浏览器内核主要指的就是渲染引擎;

  • Networking(网络):用来完成网络调用或资源下载的模块,基于TCP协议,如HTTP,Websocket;

  • UI Backend(UI 后端): 用来绘制基本的浏览器窗口内控件,如输入框、按钮、单选按钮等,根据浏览器不同绘制的视觉效果也不同,但功能都是一样的;

  • JavaScript Interpreter(JS解释器 ):用来解释执行JS脚本的模块,如 V8 引擎、JavaScriptCore;

  • JavaScript Interpreter(数据存储):浏览器在硬盘中保存的数据,如,BC(浏览器和server)互通数据 cookie、临时Kv,SessionStorage、持久化数据LocalStorage等各种数据,可通过浏览器引擎提供的API进行调用;

浏览器内核


通常讲的浏览器内核就是指代的浏览器的渲染引擎(Rendering engine)。不同的浏览器使用不同的渲染内核,对HTML/JS/CSS的标准语法的解释也存在差异,导致在显示效果、语法支持度和渲染效率上也存在差别,所以也就导致了网页程序在不同内核的浏览器下的表现和渲染差异,乃至bug都不统一。

内核是如何来处理网页,让页面显示出来的?

如下图所示:


image.png

一个页面的呈现,粗略的说会经过以下这些步骤:

  • DOM 树的构建(Parse HTML)

  • 构建 CSSOM 树(Recaculate Style)

    为什么是 Re-caculate Style 呢?这是因为浏览器本身有 User Agent StyleSheet,所以最终的样式是我们的样式代码样式与用户代理默认样式覆盖/重新计算得到的。

  • 合并 DOM 树与 CSSDOM 树为 Render 树

  • 布局(Layout)

  • 绘制(Paint)

  • 瓦片化(Tiling)

DOM 树的构建

html通过解析之后会转换成DOM,也就是Document Object Model,

Document Object Model也称'文本对象模型',DOM模型是一个树状的结构(tree),每一层拥有node(节点)

image.png

每个节点有不同的类型,每个类型有基于DOM构建出来更专业的模型;每个模型有Object、Attribute、内容部分的Text。DOM存在的意义一是,方便的把html渲染出来,二是,给JavaScript暴露操作的api接口,所以不管我们用什么方式,我们都是在操作DOM,DOM有的功能,我们都可以用,没有的话,无论用什么方式也没用。如下图所示:


image.png
构建 CSSOM 树

CSSOM 树的构建 “原料” 的来源有:外部 CSS 文件、内部样式、内联样式。
当那浏览器拿到css之后,会根据规则解析成styleSheet,


image.png
构建渲染树(Render)

1.把那到的DOM 树与 CSSOM 树融合成渲染树;
2.渲染树只包括渲染页面需要的节点;

排除 <script> <meta> 等功能化、非视觉节点
排除 display: none 的节点

image.png
布局(Layout)

Layout 阶段做的工作:计算并确定页面所有元素的位置、尺寸。

Layout 在 Chrome 开发者工具 Timeline 面板中被归并到 Paint 阶段


image.png

浏览器在发生layout过程中还会去做flow,由于网页是‘流式布局’的,也就是说,网页会从前到后的顺序显示出来的,页面的出现顺序是依据html的顺序从上到下依次排列出来的。如下图所示:

image.png
image.png

当元素某些样式变更/JavaScript 执行某些样式请求,会导致 Layout trashing,又叫做回流(Reflow)

绘制(Paint)

一旦布局(Layout)步骤完成,浏览器便触发 “Paint Setup” 与 “Paint” 事件(渲染引擎底层概念),执行 paint 操作,结合渲染树与布局信息绘制实际像素。

注:在 Timeline 工具内,Layout 与 Paint 两个过程被统一归并到 Paint 阶段。

image.png
Tiling(瓦片)

当我们打开一个非常大的网页的时候,我们在滚动的时候,由于页面元素的位置发生了改变,不停触发了页面的回流,导致浏览器多次计算页面各个元素它们所处的位置信息,从而导致滚动不流畅。在Chrome 浏览器中,就会把网页拆成很多份瓦片,先把靠近可视区的内容预加载出来,当当页面开始滚动的时候,内容进入可视区,加载一片片的瓦片,就不需要把每个元素的内容在计算出来了,从而提高了页面的流畅性。


image.png

关键渲染路径

关键渲染路径的优化是评价一个优秀前段工程师工作非常重要的一点,其中一个指标就是页面加载时间的长短;其次,要尽可能的减少需要大量计算的layout、reflow和需要调用浏览器接口的绘制,在paint之前每一步的堵塞都会最终延迟paint。


image.png

重排重绘

重排:是引起DOM树重新计算的行为;

重绘:一个元素外观的改变(如color)所触发的浏览器重新绘制的行为;

首先,我们要知道什么样的行为会影响重排?什么样的行为会影响重绘?
只要造成了任何易位置相关的变动都会造成重排,常见的触发重排的操作:

1.DOM树的结构变化,如节点的增减、移动;
2.DOM元素的几何属性变化,如外边距、内边距、边框厚度、宽高、等几何属性;
3.窗口属性的获取和尺寸改变,如offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。

改变了页面颜色的属性只会造成重绘,同时,重排一定会造成重绘,所以相对于重排,重绘的对性能的影响较少。

虽然重排和重绘会对性能会照成影响,但并不是说,我们不去做重排和重绘,毕竟页面的一些交互是必不可少的。我们关注的是如何尽量的减少一些不必要的重排和重绘,来,举栗子look look:

var times = 15000;

// 栗子1 每次过桥+重排+重绘
console.time(1);
for(var i = 0; i < times; i++) {
   document.getElementById('myDiv1').innerHTML += 'a';
}
console.timeEnd(1);

// 栗子2 只过桥
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
   var tmp = document.getElementById('myDiv2').innerHTML;
   str += 'a';
}
document.getElementById('myDiv2').innerHTML = str;
console.timeEnd(2);

// 栗子3 
console.time(3);
var _str = '';
for(var i = 0; i < times; i++) {
 _str += 'a';
}
document.getElementById('myDiv3').innerHTML = _str;
console.timeEnd(3);

// 1: 2874.619ms
// 2: 11.154ms
// 3: 1.282ms

数据是不会撒谎的,看到了吧,虽然结果都是一样的,但因不同的操作方式,但耗时差距简直是天和地的差别,栗子1,每循环一次,都要去获取DOM的id并修改html里的内容,相当于重排、重绘了15000次,耗时也是最长的;栗子二,先声明一个变量 str,每次循环都把值存到变量str中,等循环结束后,一次性把变量str存的值赋给DOM,相当于只进行了一次重排、重绘,结果是效率大大的提高,但由于每次循环都去访问了DOM,还不是最优的方案;栗子3,和栗子2类似,区别是在循环的过程不去访问DOM,结果是性能是最好的。多次访问DOM,多次重排和重绘,耗时简直是个噩梦。
重排和重绘是DOM编程中耗能的主要原因之一,平时涉及DOM编程时除上栗子之外还可以参考以下几点:

  • 尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新)
    同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0)
  • 如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(fragment元素的应用)
  • 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

本地存储

本地储存,我们要通过3个维度来讲解,BS互通数据cookie临时KVsessonStorage持久化数据LocalStorage;

cookie

作用

cookie是纯文本,没有可执行代码。存储数据,当用户访问了某个网站(网页)的时候,我们就可以通过cookie来向访问者电脑上存储数据,或者某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密)。

如何工作

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。
存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那种设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。
所以,cookie适合存储那些客户端和服务端互通通的数据。

特征

1.不同的浏览器存放的cookie位置不一样,也是不能通用的。
2.cookie的存储是以域名形式进行区分的,不同的域下存储的cookie是独立的。
3.我们可以设置cookie生效的域(当前设置cookie所在域的子域),也就是说,我们能够操作的cookie是当前域以及当前域下的所有子域。
4.一个域名下存放的cookie的个数是有限制的,不同的浏览器存放的个数不一样,一般为20个。
5.每个cookie存放的内容大小也是有限制的,不同的浏览器存放大小不一样,一般为4KB。
6.cookie也可以设置过期的时间,默认是会话结束的时候,当时间到期自动销毁。

设置

客户端设置

document.cookie = '名字=值';
//并且设置了生效域
document.cookie ='username=cfangxu;domain=baike.baidu.com'

注意:客户端可以设置cookie 的下列选项:expires、domain、path、secure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

服务器端设置

不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。
Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):

 Set-Cookie: value[; expires=date][; domain=domain][; 
 path=path][; secure]

注意:一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。
服务端可以设置cookie 的所有选项:expires、domain、path、secure、HttpOnly通过 Set-Cookie 指定的这些可选项只会在浏览器端使用,而不会被发送至服务器端。

读取

我们通过document.cookie来获取当前网站下的cookie的时候,得到的字符串形式的值,它包含了当前网站下所有的cookie(为避免跨域脚本(xss)攻击,这个方法只能获取非 HttpOnly 类型的cookie)。它会把所有的cookie通过一个分号+空格的形式串联起来,例如 username=chenfangxu;job=coding

修改cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除

把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样。

注意

如果只设置一个值,那么算cookie中的value; 设置的两个cookie,key值如果设置的相同,下面的也会把上面的覆盖。

cookie的属性(可选项)

过期时间
如果我们想长时间存放一个cookie。需要在设置这个cookie的时候同时给他设置一个过期的时间。如果不设置,cookie默认是临时存储的,当浏览器关闭进程的时候自动销毁。

注意:document.cookie = '名称=值;expires=' + GMT(格林威治时间)格式的日期型字符串;

一般设置天数: newDate().setDate(oDate.getDate()+5);比当前时间多5天

一个设置cookie时效性的例子:

function setCookie(c_name, value, expiredays){
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
}
使用方法;

setcookie('username','zxy',30); expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以秒为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );max-age有三种可能值:负数、0、正数。

负数:有效期session;

0:删除cookie;

正数:有效期为创建时刻+ max-age;

cookie的域概念

domain指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。

浏览器会把 domain 的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将匹配的 cookie 发送至服务器

document.cookie ="username=cfangxu;path=/;domain=qq.com"

如上:“www.qq.com" 与 "sports.qq.com" 公用一个关联的域名"qq.com",我们如果想让 "sports.qq.com" 下的cookie被 "www.qq.com" 访问,我们就需要用到 cookie 的domain属性,并且需要把path属性设置为 "/"。

服务端设置
Set-Cookie:username=zxy;path=/;domain=qq.com;

注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。

cookie的路径概念(path选项)
cookie 一般都是由于用户访问页面而被创建的,可是并不是只有在创建 cookie 的页面才可以访问这个 cookie。 因为安全方面的考虑,默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问。即path属性可以为服务器特定文档指定cookie,这个属性设置的url且带有这个前缀的url路径都是有效的。

客户端设置

最常用的例子就是让 cookie 在根目录下,这样不管是哪个子页面创建的 cookie,所有的页面都可以访问到了。

document.cookie="username=zxy;path=/"

domain和path总结

domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。

所以domain和path2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

cookie的安全性(secure选项)

通常 cookie 信息都是使用HTTP连接传递数据,这种传递方式很容易被查看,所以 cookie 存储的信息容易被窃取。假如 cookie 中所传递的内容比较重要,那么就要求使用加密的数据传输。

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

document.cookie="username=zxy;secure'

把cookie设置为secure,只保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息。机密且敏感的信息绝不应该在 cookie 中存储或传输,因为 cookie 的整个机制原本都是不安全的。

注意:如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

localStorage(本地存储)

HTML5新方法,不过IE8及以上浏览器都兼容。

特点:

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。

  • 存储的信息在同一域中是共享的。

  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。

  • 大小:据说是5M(跟浏览器厂商有关系)

  • 在非IE下的浏览中可以本地打开。IE浏览器要在服务器中打开。

  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡

  • localStorage受同源策略的限制

设置
lacalStorage.setItem('username','zxy');

获取

localStorage.getItem('username');

删除

localStorage.remove('username');

也可以一次清除所有存储

localStorage.clear();
sessionStorage

其实跟localStorage差不多,也是本地存储,会话本地存储

特点

用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。也就是说只要这个浏览器窗口没有关闭,即使刷新页面或进入同源另一页面,数据仍然存在。关闭窗口后,sessionStorage即被销毁,或者在新窗口打开同源的另一个页面,sessionStorage也是没有的。

cookie,localStorage,sessionStorage区别

相同:在本地(浏览器端)存储数据。

不同

localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。

sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。

localStorage是永久存储,除非手动删除。

sessionStorage当会话结束(当前页面关闭的时候,自动销毁)

cookie的数据会在每一次发送http请求的时候,同时发送给服务器而localStorage、sessionStorage不会。

web worker


已下关于web worker内容引用阮一峰的文章

一、概述

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker 有以下几个使用注意点。

1.同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

2.DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以试用navigator对象和location对象。

3.通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

4.脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest对象发出AJAX 请求。

5.文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

二、基本用法

主线程

主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。

 var worker = new Worker('work.js');

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。

然后,主线程调用worker.postMessage()方法,向Worker 发消息。

worker.postMessage('Hellow worker');
worker.postMessage({method:'echo',args:['work']);

worker.postMessage()方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。

接着,主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息。

worker.onmessage = function(event) {
    console.log('Received message ' + event.data)
    doSomething();
}

function doSomething() {
 // 执行任务
 worker.postMessage('Work done!');
}

上面代码中,事件对象的data属性可以获取 Worker 发来的数据。

Worker 完成任务以后,主线程就可以把它关掉。

worker.terminate();
Worker 线程

Worker 线程内部需要有一个监听函数,监听message事件。

self.addEventListener('message', function (e) {
   self.postMessage('You said: ' + e.data);
}, false);

上面代码中,self代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。

// 写法一
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message',function(e){
   this.postMessage('You said:'+ e.data)
},false)

除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法用来向主线程发送消息。

根据主线程发来的数据,Worker 线程可以调用不同的方法,下面是一个例子。

self.addEventListener('message', function (e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg);
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

上面代码中,self.close()用于在 Worker内部关闭自身。

Worker 加载脚本

Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()

importScripts('script1.js');

该方法可以同时加载多个脚本。

importScripts('script1.js', 'script2.js');
错误处理

主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。

worker.onerror(function (event) {
  console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''));
});

// 或者
worker.addEventListener('error', function (event) {
  // ...
});

Worker 内部也可以监听error事件。

关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

// 主线程
worker.terminate();
// Worker 线程
self.close();

三、数据通信

前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。

主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。

// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker 线程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。

如果要直接转移数据的控制权,就要使用下面的写法。

// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

四、同页面的 Web Worker

通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。

<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

上面是一段嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,上例是app/worker。

然后,读取这一段嵌入页面的脚本,用 Worker 来处理。

var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data === 'some message'
};

上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就做到了,主线程和 Worker 的代码都在同一个网页上面。

五、实例:Worker 线程完成轮询

有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) { ... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。

六、实例: Worker 新建 Worker

Worker 线程内部还能再新建 Worker 线程(目前只有 Firefox 浏览器支持)。下面的例子是将一个计算密集的任务,分配到10个 Worker。

主线程代码如下。

var worker = new Worker('worker.js');
worker.onmessage = function (event) {
  document.getElementById('result').textContent = event.data;
};

Worker 线程代码如下。

// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

 // start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker('core.js');
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i + 1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += event.data;
  pending_workers -= 1;
  if (pending_workers <= 0)
    postMessage(result); // finished!
 }

上面代码中,Worker 线程内部新建了10个 Worker 线程,并且依次向这10个 Worker 发送消息,告知了计算的起点和终点。计算任务脚本的代码如下。

// core.js
var start;
onmessage = getStart;
function getStart(event) {
  start = event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
     // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();
}

七、API

主线程

浏览器原生提供Worker()构造函数,用来供主线程生成 Worker 线程。

var myWorker = new Worker(jsUrl, options);

Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 线程
self.name // myWorker

Worker()构造函数返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下。

Worker.onerror:指定 error 事件的监听函数。
Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
Worker.postMessage():向 Worker 线程发送消息。
Worker.terminate():立即终止 Worker 线程。
Worker 线程

Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

Worker 线程有一些自己的全局属性和方法。

self.name: Worker 的名字。该属性只读,由构造函数指定。
self.onmessage:指定message事件的监听函数。
self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
self.close():关闭 Worker 线程。
self.postMessage():向产生这个 Worker 线程发送消息。
self.importScripts():加载 JS 脚本。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容