This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,306 @@
<audio id="audio" title="浏览器API小实验动手整理全部API" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/d9/4bb664d6fefed17357265cd64b824cd9.mp3"></audio>
你好我是winter。今天我们来讲讲浏览器API。
浏览器的API数目繁多我们在之前的课程中已经一起学习了其中几个比较有体系的部分比如之前讲到过的DOM和CSSOM等等。但是如果你留意过会发现我们讲到的API仍然是标准中非常小的一部分。
这里我们不可能把课程变成一本厚厚的API参考手册所以这一节课我设计了一个实验我们一起来给API分分类。
我们按照每个API所在的标准来分类。所以我们用代码来反射浏览器环境中全局对象的属性然后我们用JavaScript的filter方法来逐步过滤掉已知的属性。
接下来我们整理API的方法如下
- 从Window的属性中找到API名称
- 查阅MDN或者Google找到API所在的标准
- 阅读标准手工或者用代码整理出标准中包含的API
- 用代码在Window的属性中过滤掉标准中涉及的API。
重复这个过程我们可以找到所有的API对应的标准。首先我们先把前面已经讲过的API过滤掉。
##JavaScript中规定的API
大部分的API属于Window对象或者说全局对象我们可以用反射来看一看现行浏览器中已经实现的API我这里使用Mac下的Chrome 72.0.3626.121版本。
我们首先调用 Object.getOwnPropertyNames(window)。在我的环境中可以看到共有821个属性。
这里包含了JavaScript标准规定的属性我们做一下过滤
```
{
let js = new Set();
let objects = ["BigInt", "BigInt64Array", "BigUint64Array", "Infinity", "NaN", "undefined", "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Array", "Date", "RegExp", "Promise", "Proxy", "Map", "WeakMap", "Set", "WeakSet", "Function", "Boolean", "String", "Number", "Symbol", "Object", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Float32Array", "Float64Array", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint16Array", "Uint32Array", "Uint8ClampedArray", "Atomics", "JSON", "Math", "Reflect", "escape", "unescape"];
objects.forEach(o =&gt; js.add(o));
let names = Object.getOwnPropertyNames(window)
names = names.filter(e =&gt; !js.has(e));
}
```
这一部分我们已经在JavaScript部分讲解过了JavaScript对象你知道全部的对象分类吗所以这里我就采用手工的方式过滤出来。
## DOM中的元素构造器
接下来我们看看已经讲过的DOM部分DOM部分包含了document属性和一系列的构造器我们可以用JavaScript的prototype来过滤构造器。
```
names = names.filter( e =&gt; {
try {
return !(window[e].prototype instanceof Node)
} catch(err) {
return true;
}
}).filter( e =&gt; e != "Node")
```
这里我们把所有Node的子类都过滤掉再把Node本身也过滤掉这是非常大的一批了。
## Window对象上的属性
接下来我们要找到Window对象的定义我们在下面链接中可以找到。
- [https://html.spec.whatwg.org/#window](https://html.spec.whatwg.org/#window)
这里有一个Window接口是使用WebIDL定义的我们手工把其中的函数和属性整理出来如下
```
window,self,document,name,location,history,customElements,locationbar,menubar, personalbar,scrollbars,statusbar,toolbar,status,close,closed,stop,focus, blur,frames,length,top,opener,parent,frameElement,open,navigator,applicationCache,alert,confirm,prompt,print,postMessage
```
接下来我们编写代码把这些函数和属性从浏览器Window对象的属性中去掉JavaScript代码如下
```
{
let names = Object.getOwnPropertyNames(window)
let js = new Set();
let objects = ["BigInt", "BigInt64Array", "BigUint64Array", "Infinity", "NaN", "undefined", "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Array", "Date", "RegExp", "Promise", "Proxy", "Map", "WeakMap", "Set", "WeakSet", "Function", "Boolean", "String", "Number", "Symbol", "Object", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Float32Array", "Float64Array", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint16Array", "Uint32Array", "Uint8ClampedArray", "Atomics", "JSON", "Math", "Reflect", "escape", "unescape"];
objects.forEach(o =&gt; js.add(o));
names = names.filter(e =&gt; !js.has(e));
names = names.filter( e =&gt; {
try {
return !(window[e].prototype instanceof Node)
} catch(err) {
return true;
}
}).filter( e =&gt; e != "Node")
let windowprops = new Set();
objects = ["window", "self", "document", "name", "location", "history", "customElements", "locationbar", "menubar", " personalbar", "scrollbars", "statusbar", "toolbar", "status", "close", "closed", "stop", "focus", " blur", "frames", "length", "top", "opener", "parent", "frameElement", "open", "navigator", "applicationCache", "alert", "confirm", "prompt", "print", "postMessage", "console"];
objects.forEach(o =&gt; windowprops.add(o));
names = names.filter(e =&gt; !windowprops.has(e));
}
```
我们还要过滤掉所有的事件也就是on开头的属性。
```
names = names.filter( e =&gt; !e.match(/^on/))
```
webkit前缀的私有属性我们也过滤掉
```
names = names.filter( e =&gt; !e.match(/^webkit/))
```
除此之外我们在HTML标准中还能找到所有的接口这些我们也过滤掉
```
let interfaces = new Set();
objects = ["ApplicationCache", "AudioTrack", "AudioTrackList", "BarProp", "BeforeUnloadEvent", "BroadcastChannel", "CanvasGradient", "CanvasPattern", "CanvasRenderingContext2D", "CloseEvent", "CustomElementRegistry", "DOMStringList", "DOMStringMap", "DataTransfer", "DataTransferItem", "DataTransferItemList", "DedicatedWorkerGlobalScope", "Document", "DragEvent", "ErrorEvent", "EventSource", "External", "FormDataEvent", "HTMLAllCollection", "HashChangeEvent", "History", "ImageBitmap", "ImageBitmapRenderingContext", "ImageData", "Location", "MediaError", "MessageChannel", "MessageEvent", "MessagePort", "MimeType", "MimeTypeArray", "Navigator", "OffscreenCanvas", "OffscreenCanvasRenderingContext2D", "PageTransitionEvent", "Path2D", "Plugin", "PluginArray", "PopStateEvent", "PromiseRejectionEvent", "RadioNodeList", "SharedWorker", "SharedWorkerGlobalScope", "Storage", "StorageEvent", "TextMetrics", "TextTrack", "TextTrackCue", "TextTrackCueList", "TextTrackList", "TimeRanges", "TrackEvent", "ValidityState", "VideoTrack", "VideoTrackList", "WebSocket", "Window", "Worker", "WorkerGlobalScope", "WorkerLocation", "WorkerNavigator"];
objects.forEach(o =&gt; interfaces.add(o));
names = names.filter(e =&gt; !interfaces.has(e));
```
这样过滤之后我们已经过滤掉了所有的事件、Window对象、JavaScript全局对象和DOM相关的属性但是竟然还剩余了很多属性你是不是很惊讶呢好了接下来我们才进入今天的正题。
## 其它属性
这些既不属于Window对象又不属于JavaScript语言的Global对象的属性它们究竟是什么呢
我们可以一个一个来查看这些属性,来发现一些我们以前没有关注过的标准。
首先,我们要把过滤的代码做一下抽象,写成一个函数:
```
function filterOut(names, props) {
let set = new Set();
props.forEach(o =&gt; set.add(o));
return names.filter(e =&gt; !set.has(e));
}
```
每次执行完filter函数都会剩下一些属性接下来我们找到剩下的属性来看一看。
### ECMAScript 2018 Internationalization API
在我的浏览器环境中第一个属性是Intl。
查找这些属性来历的最佳文档是MDN当然你也可以使用Google。
总之经过查阅我发现它属于ECMA402标准这份标准是JavaScript的一个扩展它包含了国际化相关的内容
- [http://www.ecma-international.org/ecma-402/5.0/index.html#Title](http://www.ecma-international.org/ecma-402/5.0/index.html#Title)
ECMA402中只有一个全局属性Intl我们也把它过滤掉
```
names = names.filter(e =&gt; e != "Intl")
```
再来看看还有什么属性。
### Streams标准
接下来我看到的属性是: ByteLengthQueuingStrategy。
同样经过查阅它来自WHATWG的Streams标准<br>
[https://streams.spec.whatwg.org/#blqs-class](https://streams.spec.whatwg.org/#blqs-class)
不过跟ECMA402不同Streams标准中还有一些其它属性这里我手工查阅了这份标准并做了整理。
接下来,我们用代码把它们跟 ByteLengthQueuingStrategy 一起过滤掉:
```
names = filterOut(names, ["ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader", "ReadableStreamDefaultController", "ReadableByteStreamController", "ReadableStreamBYOBRequest", "WritableStream", "WritableStreamDefaultWriter", "WritableStreamDefaultController", "TransformStream", "TransformStreamDefaultController", "ByteLengthQueuingStrategy", "CountQueuingStrategy"]);
```
好了,过滤之后,又少了一些属性,我们继续往下看。
### WebGL
接下来我看到的属性是WebGLContextEvent。
显然这个属性来自WebGL标准
- [https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15](https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15)
我们在这份标准中找到了一些别的属性,我们把它一起过滤掉:
```
names = filterOut(names, ["WebGLContextEvent","WebGLObject", "WebGLBuffer", "WebGLFramebuffer", "WebGLProgram", "WebGLRenderbuffer", "WebGLShader", "WebGLTexture", "WebGLUniformLocation", "WebGLActiveInfo", "WebGLShaderPrecisionFormat", "WebGLRenderingContext"]);
```
过滤掉WebGL我们继续往下看。
### Web Audio API
下一个属性是 WaveShaperNode。这个属性名听起来就跟声音有关这个属性来自W3C的Web Audio API标准。
我们来看一下标准:
- [https://www.w3.org/TR/webaudio/](https://www.w3.org/TR/webaudio/)
Web Audio API中有大量的属性这里我用代码做了过滤。得到了以下列表
```
[&quot;AudioContext&quot;, &quot;AudioNode&quot;, &quot;AnalyserNode&quot;, &quot;AudioBuffer&quot;, &quot;AudioBufferSourceNode&quot;, &quot;AudioDestinationNode&quot;, &quot;AudioParam&quot;, &quot;AudioListener&quot;, &quot;AudioWorklet&quot;, &quot;AudioWorkletGlobalScope&quot;, &quot;AudioWorkletNode&quot;, &quot;AudioWorkletProcessor&quot;, &quot;BiquadFilterNode&quot;, &quot;ChannelMergerNode&quot;, &quot;ChannelSplitterNode&quot;, &quot;ConstantSourceNode&quot;, &quot;ConvolverNode&quot;, &quot;DelayNode&quot;, &quot;DynamicsCompressorNode&quot;, &quot;GainNode&quot;, &quot;IIRFilterNode&quot;, &quot;MediaElementAudioSourceNode&quot;, &quot;MediaStreamAudioSourceNode&quot;, &quot;MediaStreamTrackAudioSourceNode&quot;, &quot;MediaStreamAudioDestinationNode&quot;, &quot;PannerNode&quot;, &quot;PeriodicWave&quot;, &quot;OscillatorNode&quot;, &quot;StereoPannerNode&quot;, &quot;WaveShaperNode&quot;, &quot;ScriptProcessorNode&quot;, &quot;AudioProcessingEvent&quot;]
```
于是我们把它们也过滤掉:
```
names = filterOut(names, ["AudioContext", "AudioNode", "AnalyserNode", "AudioBuffer", "AudioBufferSourceNode", "AudioDestinationNode", "AudioParam", "AudioListener", "AudioWorklet", "AudioWorkletGlobalScope", "AudioWorkletNode", "AudioWorkletProcessor", "BiquadFilterNode", "ChannelMergerNode", "ChannelSplitterNode", "ConstantSourceNode", "ConvolverNode", "DelayNode", "DynamicsCompressorNode", "GainNode", "IIRFilterNode", "MediaElementAudioSourceNode", "MediaStreamAudioSourceNode", "MediaStreamTrackAudioSourceNode", "MediaStreamAudioDestinationNode", "PannerNode", "PeriodicWave", "OscillatorNode", "StereoPannerNode", "WaveShaperNode", "ScriptProcessorNode", "AudioProcessingEvent"]);
```
我们继续看下一个属性。
### Encoding标准
在我的环境中,下一个属性是 TextDecoder经过查阅得知这个属性也来自一份WHATWG的标准Encoding
- [https://encoding.spec.whatwg.org/#dom-textencoder](https://encoding.spec.whatwg.org/#dom-textencoder)
这份标准仅仅包含四个接口,我们把它们过滤掉:
```
names = filterOut(names, ["TextDecoder", "TextEncoder", "TextDecoderStream", "TextEncoderStream"]);
```
我们继续来看下一个属性。
### Web Background Synchronization
下一个属性是 SyncManager这个属性比较特殊它并没有被标准化但是我们仍然可以找到它的来源文档
- [https://wicg.github.io/BackgroundSync/spec/#sync-manager-interface](https://wicg.github.io/BackgroundSync/spec/#sync-manager-interface)
这个属性我们就不多说了,过滤掉就好了。
### Web Cryptography API
我们继续看下去,下一个属性是 SubtleCrypto这个属性来自Web Cryptography API也是W3C的标准。
- [https://www.w3.org/TR/WebCryptoAPI/](https://www.w3.org/TR/WebCryptoAPI/)
这份标准中规定了三个Class和一个Window对象的扩展给Window对象添加了一个属性crypto。
```
names = filterOut(names, ["CryptoKey", "SubtleCrypto", "Crypto", "crypto"]);
```
我们继续来看。
### Media Source Extensions
下一个属性是 SourceBufferList它来自于
- [https://www.w3.org/TR/media-source/](https://www.w3.org/TR/media-source/)
这份标准中包含了三个接口这份标准还扩展了一些接口但是没有扩展window。
```
names = filterOut(names, ["MediaSource", "SourceBuffer", "SourceBufferList"]);
```
我们继续看下一个属性。
### The Screen Orientation API
下一个属性是ScreenOrientation它来自W3C的The Screen Orientation API标准
- [https://www.w3.org/TR/screen-orientation/](https://www.w3.org/TR/screen-orientation/)
它里面只有ScreenOrientation一个接口也是可以过滤掉的。
## 结语
到 Screen Orientation API我这里看到还剩300余个属性没有处理剩余部分我想把它留给大家自己来完成。
我们可以看到在整理API的过程中我们可以找到各种不同组织的标准比如
- ECMA402标准来自 ECMA
- Encoding标准来自WHATWG
- WebGL标准来自 Khronos
- Web Cryptography标准来自 W3C
- 还有些API根本没有被标准化。
浏览器环境的API正是这样复杂的环境。我们平时编程面对的环境也是这样的一个环境。
所以面对如此繁复的API我建议在系统掌握DOM、CSSOM的基础上你可以仅仅做大概的浏览和记忆根据实际工作需要选择其中几个来深入学习。
做完这个实验你对Web API的理解应该会有很大提升。
这一节课的问题就是完成所有的API到标准的归类不同的浏览器环境应该略有不同欢迎你把自己的结果留言一起讨论。

View File

@@ -0,0 +1,240 @@
<audio id="audio" title="浏览器CSSOM如何获取一个元素的准确位置" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a9/05/a9d778787d19d67bb2d2d04e00215605.mp3"></audio>
你好我是winter。
在前面的课程中我们已经学习了DOM相关的API狭义的DOM API仅仅包含DOM树形结构相关的内容。今天我们再来学习一类新的APICSSOM。
我想你在最初接触浏览器API的时候应该都有跟我类似的想法“好想要element.width、element.height这样的API啊”。
这样的API可以直接获取元素的显示相关信息它们是非常符合人的第一印象直觉的设计但是偏偏 DOM API 中没有这样的内容。
随着学习的深入我才知道这样的设计是有背后的逻辑的正如HTML和CSS分别承担了语义和表现的分工DOM和CSSOM也有语义和表现的分工。
DOM中的所有的属性都是用来表现语义的属性CSSOM的则都是表现的属性width和height这类显示相关的属性都属于我们今天要讲的CSSOM。
顾名思义CSSOM是CSS的对象模型在W3C标准中它包含两个部分描述样式表和规则等CSS的模型部分CSSOM和跟元素视图相关的View部分CSSOM View
在实际使用中CSSOM View比CSSOM更常用一些因为我们很少需要用代码去动态地管理样式表。
在今天的文章中我来分别为你介绍这两部分的API。
## CSSOM
首先我们来介绍下CSS中样式表的模型也就是CSSOM的本体。
我们通常创建样式表也都是使用HTML标签来做到的我们用style标签和link标签创建样式表例如
```
&lt;style title=&quot;Hello&quot;&gt;
a {
color:red;
}
&lt;/style&gt;
&lt;link rel=&quot;stylesheet&quot; title=&quot;x&quot; href=&quot;data:text/css,p%7Bcolor:blue%7D&quot;&gt;
```
我们创建好样式表后还有可能要对它进行一些操作。如果我们以DOM的角度去理解的话这些标签在DOM中是一个节点它们有节点的内容、属性这两个标签中CSS代码有的在属性、有的在子节点。这两个标签也遵循DOM节点的操作规则所以可以使用DOM API去访问。
但是这样做的后果是我们需要去写很多分支逻辑并且要想解析CSS代码结构也不是一件简单的事情所以这种情况下我们直接使用CSSOM API去操作它们生成的样式表这是一个更好的选择。
我们首先了解一下CSSOM API的基本用法一般来说我们需要先获取文档中所有的样式表
```
document.styleSheets
```
document的styleSheets属性表示文档中的所有样式表这是一个只读的列表我们可以用方括号运算符下标访问样式表也可以使用item方法来访问它有length属性表示文档中的样式表数量。
样式表只能使用style标签或者link标签创建对XML来说还可以使用<!--?xml-styleSheet ... ?-->,咱们暂且不表)。
我们虽然无法用CSSOM API来创建样式表但是我们可以修改样式表中的内容。
```
document.styleSheets[0].insertRule("p { color:pink; }", 0)
document.styleSheets[0].removeRule(0)
```
更进一步我们可以获取样式表中特定的规则Rule并且对它进行一定的操作具体来说就是使用它的cssRules属性来实现
```
document.styleSheets[0].cssRules
```
这里取到的规则列表同样是支持item、length和下标运算。
不过这里的Rules可就没那么简单了它可能是CSS的at-rule也可能是普通的样式规则。不同的rule类型具有不同的属性。
我们在CSS语法部分已经为你整理过at-rule的完整列表多数at-rule都对应着一个rule类型
- CSSStyleRule
- CSSCharsetRule
- CSSImportRule
- CSSMediaRule
- CSSFontFaceRule
- CSSPageRule
- CSSNamespaceRule
- CSSKeyframesRule
- CSSKeyframeRule
- CSSSupportsRule
具体的规则支持的属性建议你可以用到的时候再去查阅MDN或者W3C的文档在我们的文章中仅为你详细介绍最常用的 CSSStyleRule。
CSSStyleRule有两个属性selectorText 和 style分别表示一个规则的选择器部分和样式部分。
selector部分是一个字符串这里显然偷懒了没有设计进一步的选择器模型我们按照选择器语法设置即可。
style部分是一个样式表它跟我们元素的style属性是一样的类型所以我们可以像修改内联样式一样直接改变属性修改规则中的具体CSS属性定义也可以使用cssText这样的工具属性。
此外CSSOM还提供了一个非常重要的方法来获取一个元素最终经过CSS计算得到的属性
```
window.getComputedStyle(elt, pseudoElt);
```
其中第一个参数就是我们要获取属性的元素,第二个参数是可选的,用于选择伪元素。
好了到此为止我们可以使用CSSOM API自由地修改页面已经生效的样式表了。接下来我们来一起关注一下视图的问题。
## CSSOM View
CSSOM View 这一部分的API可以视为DOM API的扩展它在原本的Element接口上添加了显示相关的功能这些功能又可以分成三个部分窗口部分滚动部分和布局部分下面我来分别带你了解一下。
## 窗口 API
窗口API用于操作浏览器窗口的位置、尺寸等。
- moveTo(x, y) 窗口移动到屏幕的特定坐标;
- moveBy(x, y) 窗口移动特定距离;
- resizeTo(x, y) 改变窗口大小到特定尺寸;
- resizeBy(x, y) 改变窗口大小特定尺寸。
此外窗口API还规定了 window.open() 的第三个参数:
```
window.open(&quot;about:blank&quot;, &quot;_blank&quot; ,&quot;width=100,height=100,left=100,right=100&quot; )
```
一些浏览器出于安全考虑没有实现也不适用于移动端浏览器这部分你仅需简单了解即可。下面我们来了解一下滚动API。
## 滚动 API
要想理解滚动首先我们必须要建立一个概念在PC时代浏览器可视区域的滚动和内部元素的滚动关系是比较模糊的但是在移动端越来越重要的今天两者必须分开看待两者的性能和行为都有区别。
### 视口滚动API
可视区域视口滚动行为由window对象上的一组API控制我们先来了解一下
- scrollX 是视口的属性表示X方向上的当前滚动距离有别名 pageXOffset
- scrollY 是视口的属性表示Y方向上的当前滚动距离有别名 pageYOffset
- scroll(x, y) 使得页面滚动到特定的位置有别名scrollTo支持传入配置型参数 {top, left}
- scrollBy(x, y) 使得页面滚动特定的距离,支持传入配置型参数 {top, left}。
通过这些属性和方法我们可以读取视口的滚动位置和操纵视口滚动。不过要想监听视口滚动事件我们需要在document对象上绑定事件监听函数
```
document.addEventListener(&quot;scroll&quot;, function(event){
//......
})
```
视口滚动API是页面的顶层容器的滚动大部分移动端浏览器都会采用一些性能优化它和元素滚动不完全一样请大家一定建立这个区分的意识。
### 元素滚动API
接下来我们来认识一下元素滚动API在Element类参见DOM部分为了支持滚动加入了以下API。
- scrollTop 元素的属性表示Y方向上的当前滚动距离。
- scrollLeft 元素的属性表示X方向上的当前滚动距离。
- scrollWidth 元素的属性,表示元素内部的滚动内容的宽度,一般来说会大于等于元素宽度。
- scrollHeight 元素的属性,表示元素内部的滚动内容的高度,一般来说会大于等于元素高度。
- scroll(x, y) 使得元素滚动到特定的位置有别名scrollTo支持传入配置型参数 {top, left}。
- scrollBy(x, y) 使得元素滚动到特定的位置,支持传入配置型参数 {top, left}。
- scrollIntoView(arg) 滚动元素所在的父元素使得元素滚动到可见区域可以通过arg来指定滚到中间、开始或者就近。
除此之外可滚动的元素也支持scroll事件我们在元素上监听它的事件即可
```
element.addEventListener(&quot;scroll&quot;, function(event){
//......
})
```
这里你需要注意一点元素部分的API设计与视口滚动命名风格上略有差异你在使用的时候不要记混。
## 布局API
最后我们来介绍一下布局API这是整个CSSOM中最常用到的部分我们同样要分成全局API和元素上的API。
### 全局尺寸信息
window对象上提供了一些全局的尺寸信息它是通过属性来提供的我们一起来了解一下来这些属性。
<img src="https://static001.geekbang.org/resource/image/b6/10/b6c7281d86eb7214edf17069f95ae610.png" alt="">
<li>
window.innerHeight, window.innerWidth 这两个属性表示视口的大小。
</li>
<li>
window.outerWidth, window.outerHeight 这两个属性表示浏览器窗口占据的大小,很多浏览器没有实现,一般来说这两个属性无关紧要。
</li>
<li>
window.devicePixelRatio 这个属性非常重要表示物理像素和CSS像素单位的倍率关系Retina屏这个值是2后来也出现了一些3倍的Android屏。
</li>
<li>
window.screen (屏幕尺寸相关的信息)
<ul>
- window.screen.width, window.screen.height 设备的屏幕尺寸。
- window.screen.availWidth, window.screen.availHeight 设备屏幕的可渲染区域尺寸一些Android机器会把屏幕的一部分预留做固定按钮所以有这两个属性实际上一般浏览器不会实现的这么细致。
- window.screen.colorDepth, window.screen.pixelDepth 这两个属性是固定值24应该是为了以后预留。
虽然window有这么多相关信息在我看来我们主要使用的是innerHeight、innerWidth和devicePixelRatio三个属性因为我们前端开发工作只需要跟视口打交道其它信息大概了解即可。
### 元素的布局信息
最后我们来到了本节课一开始提到的问题我们是否能够取到一个元素的宽width和高height
实际上,我们首先应该从脑中消除“元素有宽高”这样的概念,我们课程中已经多次提到了,有些元素可能产生多个盒,事实上,只有盒有宽和高,元素是没有的。
所以我们获取宽高的对象应该是“盒”于是CSSOM View为Element类添加了两个方法
- getClientRects();
- getBoundingClientRect()。
getClientRects 会返回一个列表,里面包含元素对应的每一个盒所占据的客户端矩形区域,这里每一个矩形区域可以用 x, y, width, height 来获取它的位置和尺寸。
getBoundingClientRect 这个API的设计更接近我们脑海中的元素盒的概念它返回元素对应的所有盒的包裹的矩形区域需要注意这个API获取的区域会包括当overflow为visible时的子元素区域。
根据实际的精确度需要我们可以选择何时使用这两个API。
这两个API获取的矩形区域都是相对于视口的坐标这意味着这些区域都是受滚动影响的。
如果我们要获取相对坐标,或者包含滚动区域的坐标,需要一点小技巧:
```
var offsetX = document.documentElement.getBoundingClientRect().x - element.getBoundingClientRect().x;
```
如这段代码所示,我们只需要获取文档跟节点的位置,再相减即可得到它们的坐标。
这两个API的兼容性非常好定义又非常清晰建议你如果是用JavaScript实现视觉效果时尽量使用这两个API。
## 结语
今天我们一起学习了CSSOM这一类型的API。我们首先就说到了就像HTML和CSS分别承担了语义和表现的分工DOM和CSSOM也有语义和表现的分工。
CSSOM是CSS的对象模型在W3C标准中它包含两个部分描述样式表和规则等CSS的模型部分CSSOM和跟元素视图相关的View部分CSSOM View
最后留给你一个问题写好欢迎留言来讨论请找一个网页用我们今天讲的API把页面上的所有盒的轮廓画到一个canvas元素上。
# 猜你喜欢
[<img src="https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg" alt="unpreview">](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&amp;utm_source=app&amp;utm_medium=chongxueqianduan&amp;utm_campaign=163-presell)

View File

@@ -0,0 +1,295 @@
<audio id="audio" title="浏览器DOM你知道HTML的节点有哪几种吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a1/67/a16b18d28c0998406b2c8ebe703f1b67.mp3"></audio>
你好我是winter。
今天我们进入浏览器API的学习, 这一节课我们来学习一下DOM API。
DOM API是最早被设计出来的一批API也是用途最广的API所以早年的技术社区常常用DOM来泛指浏览器中所有的API。不过今天这里我们要介绍的DOM指的就是狭义的文档对象模型。
## DOM API介绍
首先我们先来讲一讲什么叫做文档对象模型。
顾名思义文档对象模型是用来描述文档这里的文档是特指HTML文档也用于XML文档但是本课不讨论XML。同时它又是一个“对象模型”这意味着它使用的是对象这样的概念来描述HTML文档。
说起HTML文档这是大家最熟悉的东西了我们都知道HTML文档是一个由标签嵌套而成的树形结构因此DOM也是使用树形的对象模型来描述一个HTML文档。
DOM API大致会包含4个部分。
- 节点DOM树形结构中的节点相关API。
- 事件触发和监听事件相关API。
- Range操作文字范围相关API。
- 遍历遍历DOM需要的API。
事件相关API和事件模型我们会用单独的课程讲解所以我们本篇文章重点会为你介绍节点和遍历相关API。
DOM API 数量很多我希望给你提供一个理解DOM API设计的思路避免单靠机械的方式去死记硬背。
### 节点
DOM的树形结构所有的节点有统一的接口Node我们按照继承关系给你介绍一下节点的类型。
<img src="https://static001.geekbang.org/resource/image/6e/f6/6e278e450d8cc7122da3616fd18b9cf6.png" alt="">
在这些节点中除了Document和DocumentFrangment都有与之对应的HTML写法我们可以看一下。
```
Element: &lt;tagname&gt;...&lt;/tagname&gt;
Text: text
Comment: &lt;!-- comments --&gt;
DocumentType: &lt;!Doctype html&gt;
ProcessingInstruction: &lt;?a 1?&gt;
```
我们在编写HTML代码并且运行后就会在内存中得到这样一棵DOM树HTML的写法会被转化成对应的文档模型而我们则可以通过JavaScript等语言去访问这个文档模型。
这里我们每天都需要用到要重点掌握的是Document、Element、Text节点。
DocumentFragment也非常有用它常常被用来高性能地批量添加节点。因为Comment、DocumentType和ProcessingInstruction很少需要运行时去修改和操作所以有所了解即可。
### Node
Node是DOM树继承关系的根节点它定义了DOM节点在DOM树上的操作首先Node提供了一组属性来表示它在DOM树中的关系它们是
- parentNode
- childNodes
- firstChild
- lastChild
- nextSibling
- previousSibling
从命名上我们可以很清晰地看出这一组属性提供了前、后、父、子关系有了这几个属性我们可以很方便地根据相对位置获取元素。当然Node中也提供了操作DOM树的API主要有下面几种。
- appendChild
- insertBefore
- removeChild
- replaceChild
这个命名跟上面一样我们基本可以知道API的作用。这几个API的设计可以说是饱受诟病。其中最主要的批评是它不对称——只有before没有after而jQuery等框架都对其做了补充。
实际上appendChild和insertBefore的这个设计是一个“最小原则”的设计这两个API是满足插入任意位置的必要API而insertAfter则可以由这两个API实现出来。
我个人其实不太喜欢这个设计对我而言insertAt(pos) 更符合审美一些。当然,不论喜不喜欢,这个标准已经确定,我们还是必须要掌握它。
这里从设计的角度还想要谈一点那就是所有这几个修改型的API全都是在父元素上操作的比如我们要想实现“删除一个元素的上一个元素”必须要先用parentNode获取其父元素。
这样的设计是符合面向对象的基本原则的。还记得我们在JavaScript对象部分讲的对象基本特征吗“拥有哪些子元素”是父元素的一种状态所以修改状态应该是父元素的行为。这个设计我认为是DOM API中好的部分。
到此为止Node提供的API已经可以很方便大概吧地对树进行增、删、遍历等操作了。
除此之外Node还提供了一些高级API我们来认识一下它们。
- compareDocumentPosition 是一个用于比较两个节点中关系的函数。
- contains 检查一个节点是否包含另一个节点的函数。
- isEqualNode 检查两个节点是否完全相同。
- isSameNode 检查两个节点是否是同一个节点实际上在JavaScript中可以用“===”。
- cloneNode 复制一个节点如果传入参数true则会连同子元素做深拷贝。
DOM标准规定了节点必须从文档的create方法创建出来不能够使用原生的JavaScript的new运算。于是document对象有这些方法。
- createElement
- createTextNode
- createCDATASection
- createComment
- createProcessingInstruction
- createDocumentFragment
- createDocumentType
上面的这些方法都是用于创建对应的节点类型。你可以自己尝试一下。
## Element 与 Attribute
Node提供了树形结构上节点相关的操作。而大部分时候我们比较关注的是元素。Element表示元素它是Node的子类。
元素对应了HTML中的标签它既有子节点又有属性。所以Element子类中有一系列操作属性的方法。
我们需要注意对DOM而言Attribute和Property是完全不同的含义只有特性场景下两者才会互相关联这里在后面我会详细讲解今天的文章里我就不展开了
首先我们可以把元素的Attribute当作字符串来看待这样就有以下的API
- getAttribute
- setAttribute
- removeAttribute
- hasAttribute
如果你追求极致的性能还可以把Attribute当作节点
- getAttributeNode
- setAttributeNode
此外如果你喜欢property一样的访问attribute还可以使用 attributes 对象,比如 document.body.attributes.class = “a” 等效于 document.body.setAttribute(“class”, “a”)。
### 查找元素
document节点提供了查找元素的能力。比如有下面的几种。
- querySelector
- querySelectorAll
- getElementById
- getElementsByName
- getElementsByTagName
- getElementsByClassName
我们需要注意getElementById、getElementsByName、getElementsByTagName、getElementsByClassName这几个API的性能高于querySelector。
而 getElementsByName、getElementsByTagName、getElementsByClassName 获取的集合并非数组,而是一个能够动态更新的集合。
我们看一个例子:
```
var collection = document.getElementsByClassName('winter');
console.log(collection.length);
var winter = document.createElement('div');
winter.setAttribute('class', 'winter')
document.documentElement.appendChild(winter)
console.log(collection.length);
```
在这段代码中我们先获取了页面的className为winter的元素集合不出意外的话应该是空。
我们通过console.log可以看到集合的大小为0。之后我们添加了一个class为winter的div这时候我们再看集合可以发现集合中出现了新添加的元素。
这说明浏览器内部是有高速的索引机制来动态更新这样的集合的。所以尽管querySelector系列的API非常强大我们还是应该尽量使用getElement系列的API。
## 遍历
前面已经提到过通过Node的相关属性我们可以用JavaScript遍历整个树。实际上DOM API中还提供了NodeIterator 和 TreeWalker 来遍历树。
比起直接用属性来遍历NodeIterator 和 TreeWalker 提供了过滤功能,还可以把属性节点也包含在遍历之内。
NodeIterator的基本用法示例如下
```
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);
var node;
while(node = iterator.nextNode())
{
console.log(node);
}
```
这个API的设计非常老派这么讲的原因主要有两点一是循环并没有类似“hasNext”这样的方法而是直接以nextNode返回null来标志结束二是第二个参数是掩码这两个设计都是传统C语言里比较常见的用法。
放到今天看这个迭代器无法匹配JavaScript的迭代器语法而且JavaScript位运算并不高效掩码的设计就徒增复杂性了。
这里请你注意一下这个例子中的处理方法通常掩码型参数我们都是用按位或运算来叠加。而针对这种返回null表示结束的迭代器我使用了在while循环条件中赋值来保证循环次数和调用next次数严格一致但这样写可能违反了某些编码规范
我们再来看一下TreeWalker的用法。
```
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false)
var node;
while(node = walker.nextNode())
{
if(node.tagName === &quot;p&quot;)
node.nextSibling();
console.log(node);
}
```
比起NodeIteratorTreeWalker多了在DOM树上自由移动当前节点的能力一般来说这种API用于“跳过”某些节点或者重复遍历某些节点。
总的来说我个人不太喜欢TreeWalker和NodeIterator这两个API建议需要遍历DOM的时候直接使用递归和Node的属性。
## Range
Range API 是一个比较专业的领域,如果不做富文本编辑类的业务,不需要太深入。这里我们就仅介绍概念和给出基本用法的示例,你只要掌握即可。
Range API 表示一个HTML上的范围这个范围是以文字为最小单位的所以Range不一定包含完整的节点它可能是Text节点中的一段也可以是头尾两个Text的一部分加上中间的元素。
我们通过 Range API 可以比节点 API 更精确地操作 DOM 树,凡是 节点 API 能做到的Range API都可以做到而且可以做到更高性能但是 Range API 使用起来比较麻烦,所以在实际项目中,并不常用,只有做底层框架和富文本编辑对它有强需求。
创建Range一般是通过设置它的起止来实现我们可以看一个例子
```
var range = new Range(),
firstText = p.childNodes[1],
secondText = em.firstChild
range.setStart(firstText, 9) // do not forget the leading space
range.setEnd(secondText, 4)
```
此外,通过 Range 也可以从用户选中区域创建这样的Range用于处理用户选中区域:
```
var range = document.getSelection().getRangeAt(0);
```
更改 Range 选中区段内容的方式主要是取出和插入分别由extractContents和insertNode来实现。
```
var fragment = range.extractContents()
range.insertNode(document.createTextNode(&quot;aaaa&quot;))
```
最后我们看一个完整的例子。
```
var range = new Range(),
firstText = p.childNodes[1],
secondText = em.firstChild
range.setStart(firstText, 9) // do not forget the leading space
range.setEnd(secondText, 4)
var fragment = range.extractContents()
range.insertNode(document.createTextNode(&quot;aaaa&quot;))
```
这个例子展示了如何使用range来取出元素和在特定位置添加新元素。
## 总结
在今天的文章中我们一起了解了DOM API的内容。DOM API大致会包含4个部分。
- 节点DOM树形结构中的节点相关API。
- 事件触发和监听事件相关API。
- Range操作文字范围相关API。
- 遍历遍历DOM需要的API。
DOM API中还提供了NodeIterator 和 TreeWalker 来遍历树。比起直接用属性来遍历NodeIterator 和 TreeWalker 提供了过滤功能,还可以把属性节点也包含在遍历之内。
除此之外我们还谈到了Range的一些基础知识点这里你掌握即可。
最后我给你留了一个题目请你用DOM API来实现遍历整个DOM树把所有的元素的tagName打印出来。
### 补充阅读:命名空间
我们本课介绍的所有API特意忽略了命名空间。
在HTML场景中需要考虑命名空间的场景不多。最主要的场景是SVG。创建元素和属性相关的API都有带命名空间的版本
<li>document
<ul>
- createElementNS
- createAttributeNS
- getAttributeNS
- setAttributeNS
- getAttributeNodeNS
- setAttributeNodeNS
- removeAttributeNS
- hasAttributeNS
- attributes.setNamedItemNS
- attributes.getNamedItemNS
- attributes.removeNamedItemNS
若要创建Document或者Doctype也必须要考虑命名空间问题。DOM要求从document.implementation来创建。
- document.implementation.createDocument
- document.implementation.createDocumentType
除此之外,还提供了一个快捷方式,你也可以动手尝试一下。
- document.implementation.createHTMLDocument
# 猜你喜欢
[<img src="https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg" alt="unpreview">](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&amp;utm_source=app&amp;utm_medium=chongxueqianduan&amp;utm_campaign=163-presell)

View File

@@ -0,0 +1,162 @@
<audio id="audio" title="浏览器事件:为什么会有捕获过程和冒泡过程?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/a1/3d74436115f82450711614b8fe564fa1.mp3"></audio>
你好我是winter。这一节课我们进入了浏览器的部分一起来学习一下事件。
## 事件概述
在开始接触具体的API之前我们要先了解一下事件。一般来说事件来自输入设备我们平时的个人设备上输入设备有三种
- 键盘;
- 鼠标;
- 触摸屏。
**这其中触摸屏和鼠标又有一定的共性它们被称作pointer设备所谓pointer设备是指它的输入最终会被抽象成屏幕上面的一个点。**但是触摸屏和鼠标又有一定区别,它们的精度、反应时间和支持的点的数量都不一样。
我们现代的UI系统都源自WIMP系统。WIMP即Window Icon Menu Pointer四个要素它最初由施乐公司研发后来被微软和苹果两家公司应用在了自己的操作系统上关于这个还有一段有趣的故事我附在文末了
WIMP是如此成功以至于今天很多的前端工程师会有一个观点认为我们能够“点击一个按钮”实际上并非如此我们只能够点击鼠标上的按钮或者触摸屏是操作系统和浏览器把这个信息对应到了一个逻辑上的按钮再使得它的视图对点击事件有反应。这就引出了我们第一个要讲解的机制捕获与冒泡。
## 捕获与冒泡
很多文章会讲到捕获过程是从外向内,冒泡过程是从内向外,但是这里我希望讲清楚,为什么会有捕获过程和冒泡过程。
我们刚提到,实际上点击事件来自触摸屏或者鼠标,鼠标点击并没有位置信息,但是一般操作系统会根据位移的累积计算出来,跟触摸屏一样,提供一个坐标给浏览器。
那么,把这个坐标转换为具体的元素上事件的过程,就是捕获过程了。而冒泡过程,则是符合人类理解逻辑的:当你按电视机开关时,你也按到了电视机。
所以我们可以认为,捕获是计算机处理事件的逻辑,而冒泡是人类处理事件的逻辑。
以下代码展示了事件传播顺序:
```
&lt;body&gt;
&lt;input id="i"/&gt;
&lt;/body&gt;
```
```
document.body.addEventListener("mousedown", () =&gt; {
console.log("key1")
}, true)
document.getElementById("i").addEventListener("mousedown", () =&gt; {
console.log("key2")
}, true)
document.body.addEventListener("mousedown", () =&gt; {
console.log("key11")
}, false)
document.getElementById("i").addEventListener("mousedown", () =&gt; {
console.log("key22")
}, false)
```
我们监听了body和一个body的子元素上的鼠标按下事件捕获和冒泡分别监听可以看到最终产生的顺序是
- “key1”
- “key2”
- “key22”
- “key11”
这是捕获和冒泡发生的完整顺序。
在一个事件发生时,捕获过程跟冒泡过程总是先后发生,跟你是否监听毫无关联。
在我们实际监听事件时,我建议这样使用冒泡和捕获机制:默认使用冒泡模式,当开发组件时,遇到需要父元素控制子元素的行为,可以使用捕获机制。
理解了冒泡和捕获的过程我们再看监听事件的API就非常容易理解了。
addEventListener有三个参数
- 事件名称;
- 事件处理函数;
- 捕获还是冒泡。
事件处理函数不一定是函数也可以是个JavaScript具有handleEvent方法的对象看下例子
```
var o = {
handleEvent: event =&gt; console.log(event)
}
document.body.addEventListener("keydown", o, false);
```
第三个参数不一定是bool值也可以是个对象它提供了更多选项。
- once只执行一次。
- passive承诺此事件监听不会调用preventDefault这有助于性能。
- useCapture是否捕获否则冒泡
实际使用,在现代浏览器中,还可以不传第三个参数,我建议默认不传第三个参数,因为我认为冒泡是符合正常的人类心智模型的,大部分业务开发者不需要关心捕获过程。除非你是组件或者库的使用者,那就总是需要关心冒泡和捕获了。
## 焦点
我们讲完了pointer事件是由坐标控制而我们还没有讲到键盘事件。
键盘事件是由焦点系统控制的,一般来说,操作系统也会提供一套焦点系统,但是现代浏览器一般都选择在自己的系统内覆盖原本的焦点系统。
焦点系统也是视障用户访问的重要入口,所以设计合理的焦点系统是非常重要的产品需求,尤其是不少国家对可访问性有明确的法律要求。
在旧时代有一个经典的问题是如何去掉输入框上的虚线框这个虚线框就是Windows焦点系统附带的UI表现。
现在Windows的焦点已经不是用虚线框表示了但是焦点系统的设计几十年间没有太大变化。
焦点系统认为整个UI系统中有且仅有一个“聚焦”的元素所有的键盘事件的目标元素都是这个聚焦元素。
Tab键被用来切换到下一个可聚焦的元素焦点系统占用了Tab键但是可以用JavaScript来阻止这个行为。
浏览器API还提供了API来操作焦点
```
document.body.focus();
document.body.blur();
```
其实原本键盘事件不需要捕获过程但是为了跟pointer设备保持一致也规定了从外向内传播的捕获过程。
## 自定义事件
除了来自输入设备的事件还可以自定义事件实际上事件也是一种非常好的代码架构但是DOM API中的事件并不能用于普通对象所以很遗憾我们只能在DOM元素上使用自定义事件。
自定义事件的代码示例如下来自MDN
```
var evt = new Event("look", {"bubbles":true, "cancelable":false});
document.dispatchEvent(evt);
```
这里使用Event构造器来创造了一个新的事件然后调用dispatchEvent来在特定元素上触发。<br>
我们可以给这个Event添加自定义属性、方法。
注意这里旧的自定义事件方法使用document.createEvent和initEvent已经被废弃。
## 总结
今天这一节课,我们讲了浏览器中的事件。
我们分别介绍了事件的捕获与冒泡机制、焦点机制和自定义事件。
捕获与冒泡机制来自pointer设备输入的处理捕获是计算机处理输入的逻辑冒泡是人类理解事件的思维捕获总是在冒泡之前发生。
焦点机制则来自操作系统的思路用于处理键盘事件。除了我们讲到的这些随着输入设备的不断丰富还有很多新的事件加入如Geolocation和陀螺仪等。
最后给你留个小问题。请你找出你所知道的所有事件类型,和它们的目标元素类型。
## WIMP的小故事
WIMP是由Alan Kay主导设计的这位巨匠同时也是面向对象之父和Smalltalk语言之父。
乔布斯曾经受邀参观施乐他见到当时的WIMP界面认为非常惊艳不久后就领导苹果研究了新一代麦金塔系统。
后来在某次当面对话中乔布斯指责比尔盖茨抄袭了WIMP的设计盖茨淡定地回答“史蒂夫我觉得应该用另一种方式看待这个问题。这就像我们有个叫施乐的有钱邻居当我闯进去想偷走电视时却发现你已经这么干了。”
但是不论如何苹果和微软的数十代操作系统极大地发展了这个体系才有了我们今天的UI界面。

View File

@@ -0,0 +1,198 @@
<audio id="audio" title="浏览器:一个浏览器是如何工作的(阶段三)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/b3/b9ecca12e8fa1bfc3802b31aedc388b3.mp3"></audio>
你好我是winter。
在上一节课中我已经讲了浏览器的DOM构建过程但是这个构建的DOM实际上信息是不全的它只有节点和属性不包含任何的样式信息。
我们这一节课就来讲讲浏览器是如何把CSS规则应用到节点上并给这棵朴素的DOM树添加上CSS属性的。
## 整体过程
首先我们还是要感性地理解一下这个过程。
首先CSS选择器这个名称可能会给你带来一定的误解觉得好像CSS规则是DOM树构建好了以后再进行选择并给它添加样式的。实际上这个过程并不是这样的。
我们回忆一下我们在浏览器第一节课讲的内容浏览器会尽量流式处理整个过程。我们上一节课构建DOM的过程是从父到子从先到后一个一个节点构造并且挂载到DOM树上的那么这个过程中我们是否能同步把CSS属性计算出来呢
答案是肯定的。
在这个过程中,我们依次拿到上一步构造好的元素,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整。所以,从这个角度看,所谓的选择器,应该被理解成“匹配器”才更合适。
我在CSS语法部分已经总结了选择器的各种符号这里再把它列出来我们回顾一下。
- 空格: 后代,选中它的子节点和所有子节点的后代节点。
- &gt;: 子代,选中它的子节点。
- +:直接后继选择器,选中它的下一个相邻节点。
- ~:后继,选中它之后所有的相邻节点。
- ||:列,选中表格中的一列。
关于选择器的知识我会在CSS的部分继续讲解。这里我们主要介绍浏览器是如何实现这些规则的。
不知道你有没有发现这里的选择器有个特点那就是选择器的出现顺序必定跟构建DOM树的顺序一致。这是一个CSS设计的原则即保证选择器在DOM树构建到当前节点时已经可以准确判断是否匹配不需要后续节点信息。
也就是说,未来也不可能会出现“父元素选择器”这种东西,因为父元素选择器要求根据当前节点的子节点,来判断当前节点是否被选中,而父节点会先于子节点构建。
理解了CSS构建的大概过程我们下面来看看具体的操作。
首先我们必须把CSS规则做一下处理。作为一门语言CSS需要先经过词法分析和语法分析变成计算机能够理解的结构。
这部分具体的做法属于编译原理的内容这里就不做赘述了。我们这里假设CSS已经被解析成了一棵可用的抽象语法树。
我们在之前的CSS课程中已经介绍过 compound-selector 的概念,一个 compound-selector 是检查一个元素的规则,而一个复合型选择器,则是由数个 compound-selector 通过前面讲的符号连接起来的。
## 后代选择器 “空格”
我们先来分析一下后代选择器,我们来一起看一个例子:
```
a#b .cls {
width: 100px;
}
```
可以把一个CSS选择器按照 compound-selector 来拆成数段,每当满足一段条件的时候,就前进一段。
比如在上面的例子中当我们找到了匹配a#b的元素时,我们才会开始检查它所有的子代是否匹配 .cls。
除了前进一段的情况,我们还需要处理后退的情况,比如,我们这样一段代码:
```
&lt;a id=b&gt;
&lt;span&gt;1&lt;span&gt;
&lt;span class=cls&gt;2&lt;span&gt;
&lt;/a&gt;
&lt;span class=cls&gt;3&lt;span&gt;
```
当遇到 &lt;/a&gt;时,必须使得规则 a#b .cls 回退一步,这样第三个 span 才不会被选中。后代选择器的作用范围是父节点的所有子节点,因此规则是在匹配到本标签的结束标签时回退。
## 后继选择器“ ~ ”
接下来我们看下后继选择器,跟后代选择器不同的地方是,后继选择器只作用于一层,我们来看一个例子:
```
.cls~* {
border:solid 1px green;
}
&lt;div&gt;
&lt;span&gt;1&lt;span&gt;
&lt;span class=cls&gt;2&lt;span&gt;
&lt;span&gt;
3
&lt;span&gt;4&lt;/span&gt;
&lt;span&gt;
&lt;span&gt;5&lt;/span&gt;
&lt;/div&gt;
```
这里 .cls 选中了 span 2 然后 span 3 是它的后继但是span 3的子节点 span 4 并不应该被选中而span 5也是它的后继因此应该被选中。
按照DOM树的构造顺序4在3和5中间我们就没有办法像前面讲的后代选择器一样通过激活或者关闭规则来实现匹配。
但是这里有个非常方便的思路,就是给选择器的激活,带上一个条件:父元素。
注意,这里后继选择器,当前半段的 .cls 匹配成功时,后续 * 所匹配的所有元素的父元素都已经确定了后继节点和当前节点父元素相同是充分必要条件。在我们的例子中那个div就是后继节点的父元素。
## 子代选择器“ &gt;”
我们继续看,子代选择器是如何实现的。
实际上,有了前面讲的父元素这个约束思路,我们很容易实现子代选择器。区别仅仅是拿当前节点作为父元素,还是拿当前节点的父元素作为父元素。
```
div&gt;.cls {
border:solid 1px green;
}
&lt;div&gt;
&lt;span&gt;1&lt;span&gt;
&lt;span class=cls&gt;2&lt;span&gt;
&lt;span&gt;
3
&lt;span&gt;4&lt;/span&gt;
&lt;span&gt;
&lt;span&gt;5&lt;/span&gt;
&lt;/div&gt;
```
我们看这段代码当DOM树构造到div时匹配了CSS规则的第一段因为是子代选择器我们激活后面的 .cls选择条件并且指定父元素必须是当前div。于是后续的构建DOM树构建过程中span 2 就被选中了。
## 直接后继选择器“ +”
直接后继选择器的实现是上述中最为简单的了,因为它只对唯一一个元素生效,所以不需要像前面几种一样反复激活和关闭规则。
一个最简单的思路是,我们可以把它当作检查元素自身的选择器来处理。即我们把#id+.cls都当做检查某一个元素的选择器。
另外的一种思路是给后继选择器加上一个flag使它匹配一次后失效。你可以尝试一下告诉我结果。
## 列选择器“ || ”
列选择器比较特别,它是专门针对表格的选择器,跟表格的模型建立相关,我们这里不详细讲了。
## 其它
我们不要忘记CSS选择器还支持逗号分隔表示“或”的关系。这里最简单的实现是把逗号视为两条规则的一种简易写法。
比如:
```
a#b, .cls {
}
```
我们当作两条规则来处理:
```
a#b {
}
```
```
.cls {
}
```
还有一个情况,就是选择器可能有重合,这样,我们可以使用树形结构来进行一些合并,来提高效率:
```
#a .cls {
}
#a span {
}
#a&gt;span {
}
```
这里实际上可以把选择器构造成一棵树:
<li>#a
<ul>
- &lt;空格&gt;.cls
- &lt;空格&gt;span
- &gt;span
需要注意的是,这里的树,必须要带上连接符。
## 结语
这一节我们讲解了CSS计算的过程。CSS计算是把CSS规则应用到DOM树上为DOM结构添加显示相关属性的过程。在这一节中我们主要介绍了选择器的几种复合结构应该如何实现。
在这一步骤之后我们得到了一棵带有CSS属性的树为我们后续打下了基础。
最后留一个问题你认为CSS语法解析成什么结构最适合我们进行CSS计算。
<img src="https://static001.geekbang.org/resource/image/a1/9d/a1fa9a462fb96ae3a70ff4751203d79d.jpg" alt="">

View File

@@ -0,0 +1,225 @@
<audio id="audio" title="浏览器:一个浏览器是如何工作的?(阶段一)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cb/a6/cbdd6ce0312b24bf861674b002a5aba6.mp3"></audio>
对于前端开发来说,我们平时与浏览器打交道的时间是最多的。可浏览器对前端同学来说更多像一个神秘黑盒子的存在。我们仅仅知道它能做什么,而不知道它是如何做到的。
在我面试和接触过的前端开发者中70%的前端同学对这部分的知识内容只能达到“一知半解”的程度。甚至还有一部分同学会质疑这部分知识是否重要:这与我们的工作相关吗,学多了会不会偏移前端工作的方向?
事实上,我们这里所需要了解的浏览器工作原理只是它的大致过程,这部分浏览器工作原理不但是前端面试的常考知识点,它还会辅助你的实际工作,学习浏览器的内部工作原理和个中缘由,对于我们做性能优化、排查错误都有很大的好处。
在我们的课程中,我也会控制浏览器相关知识的粒度,把它保持在“给前端工程师了解浏览器”的水准,而不是详细到“给浏览器开发工程师实现浏览器”的水准。
那么,我们今天开始,来共同思考一下。一个浏览器到底是如何工作的。
实际上对浏览器的实现者来说他们做的事情就是把一个URL变成一个屏幕上显示的网页。
这个过程是这样的:
1. 浏览器首先使用HTTP协议或者HTTPS协议向服务端请求页面
1. 把请求回来的HTML代码经过解析构建成DOM树
1. 计算DOM树上的CSS属性
1. 最后根据CSS属性对元素逐个进行渲染得到内存中的位图
1. 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度;
1. 合成之后,再绘制到界面上。
<img src="https://static001.geekbang.org/resource/image/63/4c/6391573a276c47a9a50ae0cbd2c5844c.jpg" alt="">
我们在开始详细介绍之前要建立一个感性认识。我们从HTTP请求回来开始这个过程并非一般想象中的一步做完再做下一步而是一条流水线。
从HTTP请求回来就产生了流式的数据后续的DOM树构建、CSS计算、渲染、合成、绘制都是尽可能地流式处理前一步的产出即不需要等到上一步骤完全结束就开始处理上一步的输出这样我们在浏览网页时才会看到逐步出现的页面。
首先我们来介绍下网络通讯的部分。
## HTTP协议
浏览器首先要做的事就是根据URL把数据取回来取回数据使用的是HTTP协议实际上这个过程之前还有DNS查询不过这里就不详细展开了。
我先来了解下HTTP的标准。
HTTP标准由IETF组织制定跟它相关的标准主要有两份
<li>
HTTP1.1 [https://tools.ietf.org/html/rfc2616](https://tools.ietf.org/html/rfc2616)
</li>
<li>
HTTP1.1 [https://tools.ietf.org/html/rfc7234](https://tools.ietf.org/html/rfc7234)
</li>
HTTP协议是基于TCP协议出现的对TCP协议来说TCP协议是一条双向的通讯通道HTTP在TCP的基础上规定了Request-Response的模式。这个模式决定了通讯必定是由浏览器端首先发起的。
大部分情况下浏览器的实现者只需要用一个TCP库甚至一个现成的HTTP库就可以搞定浏览器的网络通讯部分。HTTP是纯粹的文本协议它是规定了使用TCP协议来传输文本格式的一个应用层协议。
下面我们试着用一个纯粹的TCP客户端来手工实现HTTP一下
## 实验
我们的实验需要使用telnet客户端这个客户端是一个纯粹的TCP连接工具安装方法
首先我们运行telnet连接到极客时间主机在命令行里输入以下内容
```
telnet time.geekbang.org 80
```
这个时候TCP连接已经建立我们输入以下字符作为请求
```
GET / HTTP/1.1
Host: time.geekbang.org
```
按下两次回车,我们收到了服务端的回复:
```
HTTP/1.1 301 Moved Permanently
Date: Fri, 25 Jan 2019 13:28:12 GMT
Content-Type: text/html
Content-Length: 182
Connection: keep-alive
Location: https://time.geekbang.org/
Strict-Transport-Security: max-age=15768000
&lt;html&gt;
&lt;head&gt;&lt;title&gt;301 Moved Permanently&lt;/title&gt;&lt;/head&gt;
&lt;body bgcolor=&quot;white&quot;&gt;
&lt;center&gt;&lt;h1&gt;301 Moved Permanently&lt;/h1&gt;&lt;/center&gt;
&lt;hr&gt;&lt;center&gt;openresty&lt;/center&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这就是一次完整的HTTP请求的过程了我们可以看到在TCP通道中传输的完全是文本。
在请求部分,第一行被称作 request line它分为三个部分HTTP Method也就是请求的“方法”请求的路径和请求的协议和版本。
在响应部分,第一行被称作 response line它也分为三个部分协议和版本、状态码和状态文本。
紧随在request line或者response line之后是请求头/响应头,这些头由若干行组成,每行是用冒号分隔的名称和值。
在头之后,以一个空行(两个换行符)为分隔,是请求体/响应体请求体可能包含文件或者表单数据响应体则是HTML代码。
## HTTP协议格式
根据上面的分析我们可以知道HTTP协议大概可以划分成如下部分。
<img src="https://static001.geekbang.org/resource/image/3d/a1/3db5e0f362bc276b83c7564430ecb0a1.jpg" alt="">
我们简单看一下在这些部分中path是请求的路径完全由服务端来定义没有很多的特别内容而version几乎都是固定字符串response body是我们最熟悉的HTML我在后面会有专门的课程介绍这里也就不多讲了。
下面我们就来逐个给你介绍其它部分。
## HTTP Method方法
我们首先来介绍一下request line里面的方法部分。这里的方法跟我们编程中的方法意义类似表示我们此次HTTP请求希望执行的操作类型。方法有以下几种定义
- GET
- POST
- HEAD
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
浏览器通过地址栏访问页面都是GET方法。表单提交产生POST方法。
HEAD则是跟GET类似只返回响应头多数由JavaScript发起。
PUT和DELETE分别表示添加资源和删除资源但是实际上这只是语义上的一种约定并没有强约束。
CONNECT现在多用于HTTPS和WebSocket。
OPTIONS和TRACE一般用于调试多数线上服务都不支持。
## HTTP Status code状态码和 Status text状态文本
接下来我们看看response line的状态码和状态文本。常见的状态码有以下几种。
- 1xx临时回应表示客户端请继续。
<li>2xx请求成功。
<ul>
- 200请求成功。
- 301&amp;302永久性与临时性跳转。
- 304跟客户端缓存没有更新。
- 403无权限。
- 404表示请求的页面不存在。
- 418Its a teapot. 这是一个彩蛋来自ietf的一个愚人节玩笑。[超文本咖啡壶控制协议](https://tools.ietf.org/html/rfc2324)
- 500服务端错误。
- 503服务端暂时性错误可以一会再试。
对我们前端来说1xx系列的状态码是非常陌生的原因是1xx的状态被浏览器HTTP库直接处理掉了不会让上层应用知晓。
2xx系列的状态最熟悉的就是200这通常是网页请求成功的标志也是大家最喜欢的状态码。
3xx系列比较复杂301和302两个状态表示当前资源已经被转移只不过一个是永久性转移一个是临时性转移。实际上301更接近于一种报错提示客户端下次别来了。
304又是一个每个前端必知必会的状态产生这个状态的前提是客户端本地已经有缓存的版本并且在Request中告诉了服务端当服务端通过时间或者tag发现没有更新的时候就会返回一个不含body的304状态。
## HTTP Head (HTTP头)
HTTP头可以看作一个键值对。原则上HTTP头也是一种数据我们可以自由定义HTTP头和值。不过在HTTP规范中规定了一些特殊的HTTP头我们现在就来了解一下它们。
在HTTP标准中有完整的请求/响应头规定,这里我们挑几个重点的说一下:
我们先来看看Request Header。
<img src="https://static001.geekbang.org/resource/image/2b/a2/2be3e2457f08bdf624837dfaee01e4a2.png" alt="">
接下来看一下Response Header。<br>
<img src="https://static001.geekbang.org/resource/image/ef/c9/efdeadf27313e08bf0789a3b5480f7c9.png" alt="">
这里仅仅列出了我认为比较常见的HTTP头这些头是我认为前端工程师应该做到不需要查阅看到就可以知道意思的HTTP头。完整的列表还是请你参考我给出的rfc2616标准。
## HTTP Request Body
HTTP请求的body主要用于提交表单场景。实际上HTTP请求的body是比较自由的只要浏览器端发送的body服务端认可就可以了。一些常见的body格式是
- application/json
- application/x-www-form-urlencoded
- multipart/form-data
- text/xml
我们使用HTML的form标签提交产生的HTML请求默认会产生 application/x-www-form-urlencoded 的数据格式当有文件上传时则会使用multipart/form-data。
## HTTPS
在HTTP协议的基础上HTTPS和HTTP2规定了更复杂的内容但是它基本保持了HTTP的设计思想使用上的Request-Response模式。
我们首先来了解下HTTPS。HTTPS有两个作用一是确定请求的目标服务端身份二是保证传输的数据不会被网络中间节点窃听或者篡改。
HTTPS的标准也是由RFC规定的你可以查看它的详情链接
[https://tools.ietf.org/html/rfc2818](https://tools.ietf.org/html/rfc2818)
HTTPS是使用加密通道来传输HTTP的内容。但是HTTPS首先与服务端建立一条TLS加密通道。TLS构建于TCP协议之上它实际上是对传输的内容做一次加密所以从传输内容上看HTTPS跟HTTP没有任何区别。
## HTTP 2
HTTP 2是HTTP 1.1的升级版本,你可以查看它的详情链接。
- [https://tools.ietf.org/html/rfc7540](https://tools.ietf.org/html/rfc7540)
HTTP 2.0 最大的改进有两点一是支持服务端推送二是支持TCP连接复用。
服务端推送能够在客户端发送第一个请求到服务端时,提前把一部分内容推送给客户端,放入缓存当中,这可以避免客户端请求顺序带来的并行度不高,从而导致的性能问题。
TCP连接复用则使用同一个TCP连接来传输多个HTTP请求避免了TCP连接建立时的三次握手开销和初建TCP连接时传输窗口小的问题。
>
Note: 其实很多优化涉及更下层的协议。IP层的分包情况和物理层的建连时间是需要被考虑的。
## 结语
在这一节内容中我们一起学习了浏览器的第一步工作也就是“浏览器首先使用HTTP协议或HTTPS协议向服务端请求页面”的这一过程。
在这个过程中掌握HTTP协议是重中之重。我从一个小实验开始带你体验了一次完整的HTTP请求过程。我们一起先分析了HTTP协议的结构。接下来我分别介绍了HTTP方法、HTTP状态码和状态文本、HTTP Head和HTTP Request Body几个重点需要注意的部分。
最后我还介绍了HTTPS和HTTP 2这两个补充版本以便你可以更好地熟悉并理解新的特性。
你在工作中是否已经开始使用HTTPS和HTTP 2协议了呢用到了它们的哪些特性请留言告诉我吧。

View File

@@ -0,0 +1,265 @@
<audio id="audio" title="浏览器:一个浏览器是如何工作的?(阶段二)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5b/e5/5b7f5be24f8dcbd66d7ec4ec4a96efe5.mp3"></audio>
你好我是winter今天我们继续来看浏览器的相关内容。
我在上一篇文章中简要介绍了浏览器的工作大致可以分为6个阶段我们昨天讲完了第一个阶段也就是通讯的部分浏览器使用HTTP协议或者HTTPS协议向服务端请求页面的过程。
今天我们主要来看两个过程如何解析请求回来的HTML代码DOM树又是如何构建的。<br>
<img src="https://static001.geekbang.org/resource/image/34/5a/34231687752c11173b7776ba5f4a0e5a.png" alt="">
## 解析代码
我们在前面讲到了HTTP的构成但是我们有一部分没有详细讲解那就是Response的body部分这正是因为HTTP的Response的body就要交给我们今天学习的内容去处理了。
HTML的结构不算太复杂我们日常开发需要的90%的“词”指编译原理的术语token表示最小的有意义的单元种类大约只有标签开始、属性、标签结束、注释、CDATA节点几种。
实际上有点麻烦的是由于HTML跟SGML的千丝万缕的联系我们需要做不少容错处理。“&lt;?”和“&lt;%”什么的也是必须要支持好的,报了错也不能吭声。
### 1.词token是如何被拆分的
首先我们来看看一个非常标准的标签,会被如何拆分:
```
&lt;p class=&quot;a&quot;&gt;text text text&lt;/p&gt;
```
如果我们从最小有意义单元的定义来拆分第一个词token是什么呢显然作为一个词token整个p标签肯定是过大了它甚至可以嵌套
那么只用p标签的开头是不是合适吗我们考虑到起始标签也是会包含属性的最小的意义单元其实是“&lt;p” ,所以“ &lt;p” 就是我们的第一个词token
我们继续拆分可以把这段代码依次拆成词token
- &lt;p“标签开始”的开始
- class=“a” 属性;
- &gt; “标签开始”的结束;
- text text text 文本;
- &lt;/p&gt;标签结束。
这是一段最简单的例子类似的还有什么呢现在我们可以来来看看这些词token长成啥样子
<img src="https://static001.geekbang.org/resource/image/f9/84/f98444aa3ea7471d2414dd7d0f5e3a84.png" alt="">
根据这样的分析现在我们讲讲浏览器是如何用代码实现我们设想代码开始从HTTP协议收到的字符流读取字符。
在接受第一个字符之前我们完全无法判断这是哪一个词token不过随着我们接受的字符越来越多拼出其他的内容可能性就越来越少。
比如,假设我们接受了一个字符“ &lt; ” 我们一下子就知道这不是一个文本节点啦。
之后我们再读一个字符,比如就是 x那么我们一下子就知道这不是注释和CDATA了接下来我们就一直读直到遇到“&gt;”或者空格这样就得到了一个完整的词token了。
实际上我们每读入一个字符其实都要做一次决策而且这些决定是跟“当前状态”有关的。在这样的条件下浏览器工程师要想实现把字符流解析成词token最常见的方案就是使用状态机。
### 2.状态机
绝大多数语言的词法部分都是用状态机实现的。那么我们来把部分词token的解析画成一个状态机看看
<img src="https://static001.geekbang.org/resource/image/8b/b0/8b43d598bc1f83a8a1e7e8f922013ab0.png" alt="">
当然了我们这里的分析比较粗略真正完整的HTML词法状态机比我们描述的要复杂的多。更详细的内容你可以参考[HTML官方文档](https://html.spec.whatwg.org/multipage/parsing.html#tokenization)HTML官方文档规定了80个状态顺便一说HTML是我见过唯一一个标准中规定了状态机实现的语言对大部分语言来说状态机是一种实现而非定义
这里我们为了理解原理,用这个简单的状态机就足够说明问题了。
状态机的初始状态,我们仅仅区分 “&lt; ”和 “非&lt;”:
- 如果获得的是一个非&lt;字符,那么可以认为进入了一个文本节点;
- 如果获得的是一个&lt;字符,那么进入一个标签状态。
不过当我们在标签状态时,则会面临着一些可能性。
<li>
比如下一个字符是“ ! ” 那么很可能是进入了注释节点或者CDATA节点。
</li>
<li>
如果下一个字符是 “/ ”,那么可以确定进入了一个结束标签。
</li>
<li>
如果下一个字符是字母,那么可以确定进入了一个开始标签。
</li>
<li>
如果我们要完整处理各种HTML标准中定义的东西那么还要考虑“ ? ”“% ”等内容。
</li>
我们可以看到,用状态机做词法分析,其实正是把每个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。
由于状态机设计属于编译原理的基本知识,这里我们仅作一个简要的介绍。
接下来就是代码实现的事情了在C/C++和JavaScript中实现状态机的方式大同小异我们把每个函数当做一个状态参数是接受的字符返回值是下一个状态函数。这里我希望再次强调下状态机真的是一种没有办法封装的东西所以我们永远不要试图封装状态机。
为了方便理解和试验我们这里用JavaScript来讲解图上的data状态大概就像下面这样的
```
var data = function(c){
if(c==&quot;&amp;&quot;) {
return characterReferenceInData;
}
if(c==&quot;&lt;&quot;) {
return tagOpen;
}
else if(c==&quot;\0&quot;) {
error();
emitToken(c);
return data;
}
else if(c==EOF) {
emitToken(EOF);
return data;
}
else {
emitToken(c);
return data;
}
};
var tagOpenState = function tagOpenState(c){
if(c==&quot;/&quot;) {
return endTagOpenState;
}
if(c.match(/[A-Z]/)) {
token = new StartTagToken();
token.name = c.toLowerCase();
return tagNameState;
}
if(c.match(/[a-z]/)) {
token = new StartTagToken();
token.name = c;
return tagNameState;
}
if(c==&quot;?&quot;) {
return bogusCommentState;
}
else {
error();
return dataState;
}
};
//……
```
这段代码给出了状态机的两个状态示例data即为初始状态tagOpenState是接受了一个“ &lt; ” 字符,来判断标签类型的状态。
这里的状态机每一个状态是一个函数通过“if else”来区分下一个字符做状态迁移。这里所谓的状态迁移就是当前状态函数返回下一个状态函数。
这样,我们的状态迁移代码非常的简单:
```
var state = data;
var char
while(char = getInput())
state = state(char);
```
这段代码的关键一句是“ state = state(char) ”不论我们用何种方式来读取字符串流我们都可以通过state来处理输入的字符流这里用循环是一个示例真实场景中可能是来自TCP的输出流。
状态函数通过代码中的 emitToken 函数来输出解析好的token我们只需要覆盖 emitToken即可指定对解析结果的处理方式。
词法分析器接受字符的方式很简单,就像下面这样:
```
function HTMLLexicalParser(){
//状态函数们……
function data() {
// ……
}
function tagOpen() {
// ……
}
// ……
var state = data;
this.receiveInput = function(char) {
state = state(char);
}
}
```
至此我们就把字符流拆成了词token了。
## 构建DOM树
接下来我们要把这些简单的词变成DOM树这个过程我们是使用栈来实现的任何语言几乎都有栈为了给你跑着玩我们还是用JavaScript来实现吧毕竟JavaScript中的栈只要用数组就好了。
```
function HTMLSyntaticalParser(){
var stack = [new HTMLDocument];
this.receiveInput = function(token) {
//……
}
this.getOutput = function(){
return stack[0];
}
}
```
我们这样来设计HTML的语法分析器receiveInput负责接收词法部分产生的词token通常可以由emitToken来调用。
在接收的同时即开始构建DOM树所以我们的主要构建DOM树的算法就写在receiveInput当中。当接收完所有输入栈顶就是最后的根节点我们DOM树的产出就是这个stack的第一项。
为了构建DOM树我们需要一个Node类接下来我们所有的节点都会是这个Node类的实例。
在完全符合标准的浏览器中不一样的HTML节点对应了不同的Node的子类我们为了简化就不完整实现这个继承体系了。我们仅仅把Node分为Element和Text如果是基于类的OOP的话我们还需要抽象工厂来创建对象
```
function Element(){
this.childNodes = [];
}
function Text(value){
this.value = value || &quot;&quot;;
}
```
前面我们的词token以下两个是需要成对匹配的
- tag start
- tag end
根据一些编译原理中常见的技巧,我们使用的栈正是用于匹配开始和结束标签的方案。
对于Text节点我们则需要把相邻的Text节点合并起来我们的做法是当词token入栈时检查栈顶是否是Text节点如果是的话就合并Text节点。
同样我们来看看直观的解析过程:
```
&lt;html maaa=a &gt;
&lt;head&gt;
&lt;title&gt;cool&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;img src=&quot;a&quot; /&gt;
&lt;/body&gt;
&lt;/html&gt;
```
通过这个栈我们可以构建DOM树
- 栈顶元素就是当前节点;
- 遇到属性,就添加到当前节点;
- 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
- 遇到注释节点,作为当前节点的子节点;
- 遇到tag start就入栈一个节点当前节点就是这个节点的父节点
- 遇到tag end就出栈一个节点还可以检查是否匹配
我在文章里面放了一个视频你可以点击查看用栈构造DOM树的全过程。
<video poster="https://static001.geekbang.org/resource/image/7c/12/7cf7a46496b2c19ae78d263bcd75ef12.png" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/53121b57-16cf0ec72ae-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/035b80dfcd0240fe8b7a602696c89317/107c22eb4ef14186bcd223b40ca3f54c-9469c087299d3945df8190acc979f573-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/035b80dfcd0240fe8b7a602696c89317/107c22eb4ef14186bcd223b40ca3f54c-4c6364b3a4d8ed2e8ef6071a8fc26a06-hd.m3u8" type="application/x-mpegURL"></video>
当我们的源代码完全遵循XHTML这是一种比较严谨的HTML语法这非常简单问题然而HTML具有很强的容错能力奥妙在于当tag end跟栈顶的start tag不匹配的时候如何处理。
于是这又有一个极其复杂的规则幸好W3C又一次很贴心地把全部规则都整理地很好我们只要翻译成对应的代码就好了以下这个网站呈现了全部规则。你可以点击查看。
- [http://www.w3.org/html/wg/drafts/html/master/syntax.html#tree-construction](http://www.w3.org/html/wg/drafts/html/master/syntax.html#tree-construction)
## 结语
好了总结一下。在今天的文章中我带你继续探索了浏览器的工作原理我们主要研究了解析代码和构建DOM树两个步骤。在解析代码的环节里我们一起详细地分析了一个词token被拆分的过程并且给出了实现它所需要的一个简单的状态机。
在构建DOM树的环节中基本思路是使用栈来构建DOM树为了方便你动手实践我用JavaScript实现了这一过程。
今天给你留的题目是:在语法和词法的代码,我已经给出了大体的结构,请你试着把内容补充完整吧。

View File

@@ -0,0 +1,118 @@
<audio id="audio" title="浏览器:一个浏览器是如何工作的?(阶段五)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/2b/ace04adb813dd27c56805ed9a234e72b.mp3"></audio>
你好我是winter。我们的浏览器系列已经进行到最后一篇。
在之前的几篇文章中我们已经经历了把URL变成字符流把字符流变成词token把词token流构造成DOM树把不含样式信息的DOM树应用CSS规则变成包含样式信息的DOM树并且根据样式信息计算了每个元素的位置和大小。
那么,我们最后的步骤,就是根据这些样式信息和大小信息,为每个元素在内存中渲染它的图形,并且把它绘制到对应的位置。
## 渲染
首先我们来谈谈渲染这个词渲染也是个外来词它是英文词render的翻译render这个词在英文里面有“导致”“变成”的意思也有“粉刷墙壁”的意思。
在计算机图形学领域里英文render这个词是一个简写它是特指把模型变成位图的过程。我们把render翻译成“渲染”是个非常有意思的翻译中文里“渲染”这个词是一种绘画技法是指沾清水把墨涂开的意思。
所以render翻译成“渲染”我认为是非常高明的对render这个过程用国画的渲染手法来概括是颇有神似的。
我们现在的一些框架也会把“从数据变成HTML代码的过程”称为render其实我觉得这是非常具有误导性的我个人是非常不喜欢这种命名方式当然了所谓“文无第一”在自然语言的范围里我们很难彻底否定这种用法的合理性。
不过,在本篇文章中,我们可以约定一下,本文中出现的“渲染”一词,统一指的是它在图形学的意义,也就是把模型变成位图的过程。
这里的位图就是在内存里建立一张二维表格把一张图片的每个像素对应的颜色保存进去位图信息也是DOM树中占据浏览器内存最多的信息我们在做内存占用优化时主要就是考虑这一部分
浏览器中渲染这个过程就是把每一个元素对应的盒变成位图。这里的元素包括HTML元素和伪元素一个元素可能对应多个盒比如inline元素可能会分成多行。每一个盒对应着一张位图。
这个渲染过程是非常复杂的,但是总体来说,可以分成两个大类:图形和文字。
盒的背景、边框、SVG元素、阴影等特性都是需要绘制的图形类。这就像我们实现HTTP协议必须要基于TCP库一样这一部分我们需要一个底层库来支持。
一般的操作系统会提供一个底层库比如在Android中有大名鼎鼎的Skia而Windows平台则有GDI一般的浏览器会做一个兼容层来处理掉平台差异。
这些盒的特性如何绘制,每一个都有对应的标准规定,而每一个的实现都可以作为一个独立的课题来研究,当年圆角+虚线边框,可是难倒了各个浏览器的工程师。考虑到这些知识互相都比较独立,对前端工程师来说也不是特别重要的细节,我们这里就不详细探究了。
盒中的文字,也需要用底层库来支持,叫做字体库。字体库提供读取字体文件的基本能力,它能根据字符的码点抽取出字形。
字形分为像素字形和矢量字形两种。通常的字体会在6px 8px等小尺寸提供像素字形比较大的尺寸则提供矢量字形。矢量字形本身就需要经过渲染才能继续渲染到元素的位图上去。目前最常用的字体库是Freetype这是一个C++编写的开源的字体库。
在最普遍的情况下,渲染过程生成的位图尺寸跟它在上一步排版时占据的尺寸相同。
但是理想和现实是有差距的,很多属性会影响渲染位图的大小,比如阴影,它可能非常巨大,或者渲染到非常遥远的位置,所以为了优化,浏览器实际的实现中会把阴影作为一个独立的盒来处理。
注意,我们这里讲的渲染过程,是不会把子元素绘制到渲染的位图上的,这样,当父子元素的相对位置发生变化时,可以保证渲染的结果能够最大程度被缓存,减少重新渲染。
## 合成
合成是英文术语compositing的翻译这个过程实际上是一个性能考量它并非实现浏览器的必要一环。
我们上一小节中讲到,渲染过程不会把子元素渲染到位图上面,合成的过程,就是为一些元素创建一个“合成后的位图”(我们把它称为合成层),把一部分子元素渲染到合成的位图上面。
看到这句话,我想你一定会问问题,到底是为哪些元素创建合成后的位图,把哪些子元素渲染到合成的位图上面呢?
这就是我们要讲的合成的策略。我们前面讲了,合成是一个性能考量,那么合成的目标就是提高性能,根据这个目标,我们建立的原则就是最大限度减少绘制次数原则。
我们举一个极端的例子。如果我们把所有元素都进行合成比如我们为根元素HTML创建一个合成后的位图把所有子元素都进行合成那么会发生什么呢
那就是一旦我们用JavaScript或者别的什么方式改变了任何一个CSS属性这份合成后的位图就失效了我们需要重新绘制所有的元素。
那么如果我们所有的元素都不合成,会怎样呢?结果就是,相当于每次我们都必须要重新绘制所有的元素,这也不是对性能友好的选择。
那么好的合成策略是什么呢,好的合成策略是“猜测”可能变化的元素,把它排除到合成之外。
我们来举个例子:
```
&lt;div id=&quot;a&quot;&gt;
&lt;div id=&quot;b&quot;&gt;...&lt;/div&gt;
&lt;div id=&quot;c&quot; style=&quot;transform:translate(0,0)&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
```
假设我们的合成策略能够把a、b两个div合成而不把c合成那么当我执行以下代码时
```
document.getElementById(&quot;c&quot;).style.transform = &quot;translate(100px, 0)&quot;;
```
我们绘制的时候就可以只需要绘制a和b合成好的位图和c从而减少了绘制次数。这里需要注意的是在实际场景中我们的b可能有很多复杂的子元素所以当合成命中时性能提升收益非常之高。
目前主流浏览器一般根据position、transform等属性来决定合成策略来“猜测”这些元素未来可能发生变化。
但是这样的猜测准确性有限所以新的CSS标准中规定了will-change属性可以由业务代码来提示浏览器的合成策略灵活运用这样的特性可以大大提升合成策略的效果。
## 绘制
绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”的过程,不过,一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。
一般最终位图位于显存中,也有一些情况下,浏览器只需要把内存中的一张位图提交给操作系统或者驱动就可以了,这取决于浏览器运行的环境。不过无论如何,我们把任何位图合成到这个“最终位图”的操作称为绘制。
这个过程听上去非常简单这是因为在前面两个小节中我们已经得到了每个元素的位图并且对它们部分进行了合成那么绘制过程实际上就是按照z-index把它们依次绘制到屏幕上。
然而如果在实际中这样做,会带来极其糟糕的性能。
有一个一度非常流行于前端群体的说法讲做CSS性能优化应该尽量避免“重排”和“重绘”前者讲的是我们上一课的排版行为后者模糊地指向了我们本课程三小节讲的三个步骤而实际上这个说法大体不能算错却不够准确。
因为,实际上,“绘制”发生的频率比我们想象中要高得多。我们考虑一个情况:鼠标划过浏览器显示区域。这个过程中,鼠标的每次移动,都造成了重新绘制,如果我们不重新绘制,就会产生大量的鼠标残影。
这个时候,限制绘制的面积就很重要了。如果鼠标某次位置恰巧遮盖了某个较小的元素,我们完全可以重新绘制这个元素来完成我们的目标,当然,简单想想就知道,这种事情不可能总是发生的。
计算机图形学中,我们使用的方案就是“脏矩形”算法,也就是把屏幕均匀地分成若干矩形区域。
当鼠标移动、元素移动或者其它导致需要重绘的场景发生时我们只重新绘制它所影响到的几个矩形区域就够了。比矩形区域更小的影响最多只会涉及4个矩形大型元素则覆盖多个矩形。
设置合适的矩形区域大小,可以很好地控制绘制时的消耗。设置过大的矩形会造成绘制面积增大,而设置过小的矩形则会造成计算复杂。
我们重新绘制脏矩形区域时,把所有与矩形区域有交集的合成层(位图)的交集部分绘制即可。
## 结语
在这一节课程中,我们讲解了浏览器中的位图操作部分,这包括了渲染、合成和绘制三个部分。渲染过程把元素变成位图,合成把一部分位图变成合成层,最终的绘制过程把合成层显示到屏幕上。
当绘制完成时就完成了浏览器的最终任务把一个URL最后变成了一个可以看的网页图像。当然了我们对每一个部分的讲解都省略了大量的细节比如我们今天讲到的绘制就有意地无视了滚动区域。
尽管如此,对浏览器工作原理的感性认识,仍然可以帮助我们理解很多前端技术的设计和应用技巧,浏览器的工作原理和性能部分非常强相关,我们在实践部分的性能优化部分,会再次跟你做一些探讨。
实际上如果你认真阅读浏览器系列的课程是可以用JavaScript实现一个玩具浏览器的我非常希望学习课程的同学中能有人这样做一旦你做到了收益会非常大。这就是我今天留给你的课外作业你可以尝试一下。

View File

@@ -0,0 +1,102 @@
<audio id="audio" title="浏览器:一个浏览器是如何工作的?(阶段四)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ea/6d/ea4ebf7748c39f63d10d11703ea3b66d.mp3"></audio>
你好我是winter。
我们书接上文。浏览器进行到这一步我们已经给DOM元素添加了用于展现的CSS属性接下来浏览器的工作就是确定每一个元素的位置了。我们的基本原则仍然不变就是尽可能流式地处理上一步骤的输出。
在构建DOM树和计算CSS属性这两个步骤我们的产出都是一个一个的元素但是在排版这个步骤中有些情况下我们就没法做到这样了。
尤其是表格相关排版、Flex排版和Grid排版它们有一个显著的特点那就是子元素之间具有关联性。
## 基本概念
首先我们先来介绍一些基本概念,使你可以感性地认识一下我们平常说的各种术语。
**“排版”**这个概念最初来自活字印刷,是指我们把一个一个的铅字根据文章顺序,放入板框当中的步骤,排版的意思是确定每一个字的位置。
在现代浏览器中,仍然借用了这个概念,但是排版的内容更加复杂,包括文字、图片、图形、表格等等,我们把浏览器确定它们位置的过程,叫作排版。
浏览器最基本的排版方案是**正常流排版**,它包含了顺次排布和折行等规则,这是一个跟我们提到的印刷排版类似的排版方案,也跟我们平时书写文字的方式一致,所以我们把它叫做正常流。
浏览器的文字排版遵循公认的文字排版规范,文字排版是一个复杂的系统,它规定了行模型和文字在行模型中的排布。行模型规定了行顶、行底、文字区域、基线等对齐方式。(你还记得小时候写英语的英语本吗?英语本上的四条线就是一个简单的行模型。)
此外,浏览器支持不同语言,因为不同语言的书写顺序不一致,所以浏览器的文本排版还支持双向文字系统。
浏览器又可以支持元素和文字的混排,元素被定义为占据长方形的区域,还允许边框、边距和留白,这个就是所谓的**盒模型**。
在正常流的基础上,浏览器还支持两类元素:绝对定位元素和浮动元素。
<li>
绝对定位元素把自身从正常流抽出直接由top和left等属性确定自身的位置不参加排版计算也不影响其它元素。绝对定位元素由position属性控制。
</li>
<li>
浮动元素则是使得自己在正常流的位置向左或者向右移动到边界并且占据一块排版空间。浮动元素由float属性控制。
</li>
除了正常流浏览器还支持其它排版方式比如现在非常常用的Flex排版这些排版方式由外部元素的display属性来控制注意display同时还控制元素在正常流中属于inline等级还是block等级
## 正常流文字排版
我们会在CSS部分详细介绍正常流排版的行为我们这里主要介绍浏览器中的正常流。正常流是唯一一个文字和盒混排的排版方式我们先从文字来讲起。
要想理解正常流,我们首先要回忆一下自己如何在纸上写文章。
首先,纸是有固定宽度的,虽然纸有固定高度,但是我们可以通过下一页纸的方式来接续,因此我们不存在写不下的场景。
我们书写文字的时候,是从左到右依次书写,每一个字跟上一个字都不重叠,文字之间有一定间距,当写满一行时,我们换到下一行去继续写。
书写中文时,文字的上、下、中轴线都对齐,书写英文时,不同字母的高度不同,但是有一条基线对齐。
实际上浏览器环境也很类似。但是因为浏览器支持改变排版方向,不一定是从左到右从上到下,所以我们把文字依次书写的延伸方向称为主轴或者主方向,换行延伸的方向,跟主轴垂直交叉,称为交叉轴或者交叉方向。
我们一般会从某个字体文件中获取某个特定文字的相关信息。我们获取到的信息大概类似下面:
<img src="https://static001.geekbang.org/resource/image/06/01/0619d38f00d539f7b6773e541ce6fa01.png" alt="">
纵向版本:
<img src="https://static001.geekbang.org/resource/image/c3/96/c361c7ff3a11216c139ed462b9d5f196.png" alt="">
这两张图片来自著名开源字体解析库freetype实际上各个库对字体的理解大同小异我们注意一下advance代表每一个文字排布后在主轴上的前进距离它跟文字的宽/高不相等,是字体中最重要的属性。
除了字体提供的字形本身包含的信息文字排版还受到一些CSS属性影响如line-height、letter-spacing、word-spacing等。
在正常流的文字排版中多数元素被当作长方形盒来排版而只有display为inline的元素是被拆成文本来排版的还有一种run-in元素它有时作为盒有时作为文字不太常用这里不详细讲了
display值为inline的元素中的文字排版时会被直接排入文字流中inline元素主轴方向的margin属性和border属性例如主轴为横向时的margin-left和margin-right也会被计算进排版前进距离当中。
注意,当没有强制指定文字书写方向时,在左到右文字中插入右到左向文字,会形成一个双向文字盒,反之亦然。
这样,即使没有元素包裹,混合书写方向的文字也可以形成一个盒结构,我们在排版时,遇到这样的双向文字盒,会先排完盒内再排盒外。
## 正常流中的盒
在正常流中display不为inline的元素或者伪元素会以盒的形式跟文字一起排版。多数display属性都可以分成两部分内部的排版和是否inline带有inline-前缀的盒,被称作行内级盒。
根据盒模型一个盒具有margin、border、padding、width/height等属性它在主轴方向占据的空间是由对应方向的这几个属性之和决定的而vertical-align属性决定了盒在交叉轴方向的位置同时也会影响实际行高。
所以,浏览器对行的排版,一般是先行内布局,再确定行的位置,根据行的位置计算出行内盒和文字的排版位置。
块级盒比较简单,它总是单独占据一整行,计算出交叉轴方向的高度即可。
## 绝对定位元素
position属性为absolute的元素我们需要根据它的包含块来确定位置这是完全跟正常流无关的一种独立排版模式逐层找到其父级的position非static元素即可。
## 浮动元素排版
float元素非常特别浏览器对float的处理是先排入正常流再移动到排版宽度的最左/最右(这里实际上是主轴的最前和最后)。
移动之后float元素占据了一块排版的空间因此在数行之内主轴方向的排版距离发生了变化直到交叉轴方向的尺寸超过了浮动元素的交叉轴尺寸范围主轴排版尺寸才会恢复。float元素排布完成后float元素所在的行需要重新确定位置。
## 其它的排版
CSS的每一种排版都有一个很复杂的规定实际实现形式也各不相同。比如如Flex排版支持了flex属性flex属性将每一行排版后的剩余空间平均分配给主轴方向的width/height属性。浏览器支持的每一种排版方式都是按照对应的标准来实现的。
## 结语
这一部分我们以正常流为主介绍了浏览器的排版基本概念及一些算法。这里我主要介绍了正常流中的文字排版、正常流中的盒、绝对定位元素、浮动元素排版这几种情况。最后我还简单介绍了一下Flex排版。这属于进阶版的排版方式了你可以了解一下。
你平时喜欢使用方式排版呢,欢迎留言告诉我。