本小节主要记录了网页中一些其他的知识点:

  • 模型
  • 网页对象
  • 存储对象
  • 通信对象

一、模型

1.1 浏览器模型

浏览器的核心是两部分:渲染引擎和 JavaScript引擎。

1.1.1 渲染引擎

渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。渲染引擎处理网页,通常分成四个阶段:

  1. 解析代码:HTML代码解析为 DOM,CSS代码解析为 CSSOM(CSS Object Model)。
  2. 对象合成:将 DOM和 CSSOM合成一棵渲染树。
  3. 布局:计算出渲染树的布局(layout)。
  4. 绘制:将渲染树绘制到屏幕。

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

来看一下非常重要的重流和重绘。渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。

页面生成以后,脚本操作和样式表操作都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,如改变窗口大小、页面滚动、设置鼠标悬停a:hover效果、在输入框中输入文本等。

重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。

作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如重绘table布局和flex布局,开销都会比较大。

1.1.2 JavaScript引擎

JavaScript引擎的主要作用是,读取网页中的JavaScript代码,对其处理后运行。早期浏览器内部对 JavaScript 的处理过程如下:

  1. 对代码进行词法分析,将代码分解成词元。
  2. 对词元进行语法分析,将代码整理成语法树。
  3. 使用翻译器,将代码转为字节码。
  4. 使用解释器,将字节码转为机器码。

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

字节码不能直接运行,而是运行在一个虚拟机之上,一般也把虚拟机称为JavaScript引擎。并非所有的 JavaScript虚拟机运行时都有字节码,有的JavaScript虚拟机基于源码,即只要有可能,就通过 JIT编译器直接把源码编译成机器码运行,省略字节码步骤。这样做的目的,是为了尽可能地优化代码、提高性能。

1.2 script 元素

浏览器加载 JavaScript 脚本,主要通过<script>元素完成。正常的网页加载流程是这样的:

  1. 渲染引擎一边下载HTML网页,一边开始解析。
  2. 解析过程中发现<script>元素后暂停解析,把网页的控制权转交给JavaScript引擎。
  3. JavaScript引擎下载脚本后执行代码。
  4. 执行完毕后,把网页控制权交还渲染引擎,恢复往下解析 HTML 网页。

浏览器会并行下载脚本,但是执行还是按照定义的顺序来执行。

较好的做法是将<script>标签都放在页面底部而不是头部的原因如下:

  • 如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
  • 因为在 DOM 结构生成之前就调用 DOM 节点JS会报错,如果脚本都在网页尾部加载就不存在这个问题。

对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。

<script>标签中还常见如下属性:

  • defer属性的作用是延迟脚本的执行,等到 DOM 加载生成后再执行脚本。该属性的运行流程如下:
  1. 渲染引擎一边下载HTML网页,一边开始解析。
  2. 解析过程中,发现带有defer属性的<script>标签,继续往下解析 HTML 网页,同时开启另一个进程并行下载<script>元素加载的外部脚本。
  3. 渲染引擎完成解析 HTML 网页,此时再交由JavaScript引擎执行已经下载完成的脚本。

defer属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,依旧保证脚本的执行顺序。

  • async属性的作用是使用另一个进程下载脚本,下载时不会阻塞渲染。该属性的运行流程如下:
  1. 渲染引擎一边下载HTML网页,一边开始解析。
  2. 解析过程中,发现带有async属性的<script>标签,继续往下解析 HTML 网页,同时开启另一个进程并行下载<script>标签中的外部脚本。
  3. 脚本下载完成,渲染引擎暂停解析 HTML 网页,交由JavaScript引擎执行。
  4. 脚本执行完毕,JavaScript引擎交由渲染引擎恢复解析 HTML 网页。

async属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。

一般来说,如果脚本之间有依赖关系,就使用defer属性;如果脚本之间没有依赖关系,就使用async属性。如果同时使用和deferasync属性,浏览器行为由async属性决定。

二、网页对象

2.1 window

window对象指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。

对象属性

网页对象属性

  • window.document:指向document对象
  • window.location:指向Location对象
  • window.localStorage:指向本地储存的localStorage数据
  • window.sessionStorage:指向本地储存的sessionStorage数据

工具对象属性:

  • window.locationbar:地址栏对象,对象的visible属性是一个布尔值,表示该组件是否可见。
  • window.menubar:菜单栏对象,对象的visible属性是一个布尔值,表示该组件是否可见。
  • window.toolbar:工具栏对象,对象的visible属性是一个布尔值,表示该组件是否可见。
  • window.statusbar:状态栏对象,对象的visible属性是一个布尔值,表示该组件是否可见。

对象事件

window.onload属性事件发生在文档在浏览器窗口加载完毕时

2.2 Location

Location对象是浏览器提供的原生对象,提供URL相关的信息和操作方法。通过window.locationdocument.location属性,可以拿到这个对象。

对象属性

  • Location.href:整个 URL。如果对它写入新的 URL 地址,浏览器会立刻跳转到这个新地址
  • Location.origin:URL 的协议、主机名和端口。
  • Location.protocol:当前URL的协议,包括冒号
  • Location.hostname:当前URL的主机(不包括端口)。而Location.host包括端口
  • Location.port:当前URL的端口号
  • Location.pathname:URL 的路径部分,从根路径/开始
  • Location.search:查询字符串部分,从问号?开始。

辅助方法

  • encodeURI()方法用于转码整个URL。它的参数是一个字符串,代表整个 URL。
  • decodeURI()方法用于整个URL的解码。它接受一个参数,就是转码后的URL。

三、存储对象

3.1 Cookie

Cookie是服务器保存在浏览器的一小段文本信息,浏览器每次向服务器发出请求,就会自动附上这段信息。不同浏览器对Cookie数量和大小的限制是不一样的。一般来说单个域名设置的 Cookie 不应超过30个,每个Cookie的大小不能超过4KB。超过限制以后Cookie将被忽略,不会被设置。

Cookie 有一些主要用途:

  • 对话(session)管理:保存登录、购物车等需要记录的信息。
  • 个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等。

cookie的属性

cookie除了使用K-V形式设置Cookie的名字Cookie的值外,它还有以下属性:

  • Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个Cookie。它的值是UTC格式,可以使用Date.prototype.toUTCString()进行格式转换。如果不设置该属性或设为null,Cookie只在当前会话有效,浏览器窗口一旦关闭该Cookie就会被删除。
  • Max-Age属性指定从现在开始Cookie存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了ExpiresMax-Age,那么Max-Age的值将优先生效。
  • Domain属性指定浏览器发出HTTP请求时,哪些域名要附带这个Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个Cookie。如果指定了该属性,那么子域名也会附带这个 Cookie。
  • Path属性指定浏览器发出HTTP请求时,哪些路径要附带这个Cookie。只要浏览器发现,Path属性是HTTP请求路径的开头一部分,就会在头信息里面带上这个Cookie。比如PATH属性是/,那么请求/docs路径也会包含它。
  • Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。
  • HttpOnly属性指定该Cookie无法通过JS脚本拿到,主要让document.cookieXMLHttpRequest Request API拿不到该属性。这样就防止该Cookie被脚本读到。只有浏览器发出HTTP请求时,才会带上该Cookie。

cookie的交互

cookie的生成:在浏览器保存Cookie,就要在HTTP回应的头信息里面,放置一个或多个Set-Cookie字段。一个Set-Cookie字段可同时包括该cookie的多个属性,没有次序的要求。这个过程是服务端手动处理的。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

cookie的发送:向浏览器发送Cookie,就要在HTTP回应的头信息里面,放置一个或多个Cookie字段。一个Cookie字段可同时包含多个 Cookie,使用分号分隔。这个过程是浏览器自动处理的。

Cookie: name=value; name2=value2; name3=value3

3.2 Storage

Storage接口用于脚本在浏览器保存数据,保存的数据都以“键值对”的形式存在。sessionStorage保存的数据用于浏览器的一次会话,当会话结束数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。目前每个域名的存储上限视浏览器而定,Chrome是2.5MB,Firefox和Opera是5MB。

对象属性

  • StorageEvent.storageArea:返回键值对所在的整个对象。也就是说可以从这个属性上拿到当前域名储存的所有键值对。
  • StorageEvent.url:表示原始触发storage事件的那个网页的网址。
  • StorageEvent.key:表示发生变动的键名。如果storage事件是由clear()方法引起,该属性返回null
  • StorageEvent.newValue:表示新的键值。如果storage事件是由clear()方法引起,该属性返回null
  • StorageEvent.oldValue:表示旧的键值。如果该键值对是新增的,该属性返回null

对象方法

  • Storage.key()接受一个整数作为参数(从0开始),返回该位置对应的键值。
  • Storage.getItem()方法用于读取数据。它只有一个参数:键名。如果键名不存在,该方法返回null
  • Storage.setItem()方法用于存入数据。它接受两个参数:第一个是键名;第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。
  • Storage.removeItem()方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。
  • Storage.clear()方法用于清除所有保存的数据。

四、通信对象

4.1 跨域通信

所谓“同源”指的是“三个相同”:

  • 协议相同
  • 域名相同
  • 端口相同

常见的跨源通行方式有如下:

WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。具体使用情况后续再补充。

JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单易用,老式浏览器全部支持,服务端改造非常小。去电就是JSONP只能发GET请求。

  • 第一步,网页添加一个<script>元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。
<script src="http://api.foo.com?callback=bar"></script>

注意请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器客户端的回调函数名称。

  • 第二步,服务器收到请求后,拼接一个字符串,将JSON数据放在函数名里面,作为字符串返回(bar({...}))。
  • 第三步,客户端会将服务器返回的字符串,作为代码解析。因为浏览器认为这是script标签请求的脚本内容。这时客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的JSON数据。作为参数的JSON数据被视为 JavaScript对象而不是字符串,因此避免了使用JSON.parse的步骤。
function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

CORS

CORS是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,属于跨源AJAX请求的根本解决方法。它允许浏览器向跨域的服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。相比JSONP只能发GET请求,CORS允许任何类型的请求。

CORS通信与普通的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求。因此实现 CORS 通信的关键是服务器。只要服务器实现了CORS接口就可跨域通信。

CORS 请求分成两类:简单请求和非简单请求。只要同时满足以下两大条件就属于简单请求,凡是不同时满足上面两个条件就属于非简单请求。非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

  • 请求方法是以下三种方法之一:
  • HEAD
  • GET
  • POST
  • HTTP的头信息不超出以下几种字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

CORS简单请求

简单请求发送:

对于简单请求,浏览器直接发出CORS请求。具体来说就是在头信息之中,增加一个Origin字段。

GET /cors HTTP/1.1
Host: api.alice.com
Origin: http://api.bob.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

简单请求响应:

服务器收到查询请求以后,检查了Origin字段以后,确认允许跨源请求就可以做出回应,返回的响应会多出几个头信息字段。如果服务器否定了查询请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,或者明确表示请求不符合条件。浏览器发现这个回应的头信息没有包含Access-Control-Allow-Origin字段就知道出错了,从而抛出一个错误。该错误被XMLHttpRequestonerror回调函数捕获。注意这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头:

  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*来表示接受任意域名的请求。注意如果服务器要求浏览器发送Cookie,该字段就不能设为星号,必须指定明确的、与请求网页一致的域名。
  • Access-Control-Allow-Credentials:该字段可选。表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true即表示服务器明确许可,浏览器可以把Cookie包含在请求中,一起发给服务器。设为true同时要求开发者必须在AJAX请求中打开withCredentials属性,否则即使服务器要求发送 Cookie,浏览器也不会发送。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时XMLHttpRequest对象的getResponseHeader()方法只能拿到服务器返回头的6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定getResponseHeader('FooBar')可以返回FooBar字段的值。

CORS非简单请求

非简单请求发送:

非简单请求的CORS请求,会在正式通信之前增加一次HTTP查询请求。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和头信息字段。只有得到肯定答复后浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

这是为了防止这些新增的请求,对传统的没有CORS支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量DELETEPUT请求,这些传统的表单不可能跨域发出的请求。

4.2 XMLHttpRequest

XMLHttpRequest对象是AJAX的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XMLHttp,它实际上可以使用多种协议(比如fileftp),发送任何格式的数据(包括字符串和二进制)。下面是XMLHttpRequest对象简单用法的完整例子:

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function(){
  // 通信成功时,状态值为4
  if (xhr.readyState === 4){
    if (xhr.status === 200){
      console.log(xhr.responseText);
    } else {
      console.error(xhr.statusText);
    }
  }
};

xhr.onerror = function (e) {
  console.error(xhr.statusText);
};

xhr.open('GET', '/endpoint', true);
xhr.send(null);

对象属性

  • XMLHttpRequest.withCredentials属性是一个布尔值,表示跨域请求时用户信息(比如Cookie和认证的HTTP头信息)是否会包含在请求之中,默认为false,即向example.com发出跨域请求时,不会发送example.com设置在本机上的Cookie(如果有的话)。如果需要跨域AJAX请求发送Cookie,需要withCredentials属性设为true。注意同源的请求不需要设置这个属性。
  • XMLHttpRequest.upload属性返回一个对象,表示上传文件的相关事件。主要就是监听这个对象的各种事件。
  • XMLHttpRequest.timeout属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果就会自动终止。如果该属性等于0,就表示没有时间限制。
  • XMLHttpRequest.readyState返回一个整数,表示实例对象的当前状态。它可能返回以下值:
  • 0,表示XMLHttpRequest实例已经生成,但是实例的open()方法还没有被调用。
  • 1,表示open()方法已经调用,但是实例的send()方法还没有调用,仍然可以使用实例的setRequestHeader()方法,设定HTTP请求的头信息。
  • 2,表示实例的send()方法已经调用,并且服务器返回的头信息和状态码已经收到。
  • 3,表示正在接收服务器传来的数据体(body 部分)。这时如果实例的responseType属性等于text或者空字符串,responseText属性就会包含已经收到的部分信息。
  • 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。
  • XMLHttpRequest.status属性返回一个整数,表示服务器回应的HTTP状态码。
  • XMLHttpRequest.responseType属性是一个字符串,表示服务器返回数据的类型。
  • XMLHttpRequest.response属性表示服务器返回的数据体(即HTTP回应的 body 部分),它可能是任何数据类型(如字符串、对象、二进制对象等)。
  • XMLHttpRequest.responseURL属性是字符串,表示发送数据的服务器的网址。
  • XMLHttpRequest.responseText属性返回从服务器接收到的字符串。只有HTTP请求完成接收以后,该属性才会包含完整的数据。

对象事件

  • XMLHttpRequest.ontimeout:timeout事件的监听函数
  • XMLHttpRequest.onloadstart:loadstart事件(请求发出)的监听函数
  • XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数
  • XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数
  • XMLHttpRequest.onabort:abort 事件(请求中止,如用户调用了abort()方法)的监听函数
  • XMLHttpRequest.onerror:error 事件(请求失败)的监听函数
  • XMLHttpRequest.onload:load事件(请求成功完成)的监听函数
  • XMLHttpRequest.onreadystatechange:readystatechange事件(实例的readyState属性变化)的监听函数

对象方法

  • XMLHttpRequest.open()方法用于指定HTTP请求的参数,或者说初始化XMLHttpRequest实例对象。注意如果对使用过open()方法的AJAX请求,再次使用这个方法,等同于调用abort(),即终止请求。
  • XMLHttpRequest.overrideMimeType()方法用来指定MIME类型,覆盖服务器返回的MIME类型,从而让浏览器进行不一样的处理。比如服务器返回的数据类型是text/xml,由于种种原因浏览器解析不成功报错,这时就拿不到数据。为了拿到原始数据可以把MIME类型改成text/plain,这样浏览器就不会去自动解析,从而可拿到原始文本。注意该方法必须在send()方法之前调用。
  • XMLHttpRequest.setRequestHeader()方法用于设置浏览器发送的HTTP请求的头信息。如果该方法多次调用且设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。注意该方法必须在send()方法之前调用。
  • XMLHttpRequest.getResponseHeader()方法返回HTTP头信息指定字段的值,如果还没有收到服务器回应或者指定字段不存在,则返回null
  • XMLHttpRequest.send()方法用于实际发出HTTP请求。
  • XMLHttpRequest.abort()方法用来终止已经发出的HTTP请求。调用这个方法以后,readyState属性变为4,status属性变为0。

至于其他的还有File对象、Blob对象、ArrayBuffer对象等,到时候随用随查吧。