mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
del
This commit is contained in:
306
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器API(小实验):动手整理全部API.md
Normal file
306
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器API(小实验):动手整理全部API.md
Normal 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 => js.add(o));
|
||||
let names = Object.getOwnPropertyNames(window)
|
||||
names = names.filter(e => !js.has(e));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这一部分我们已经在JavaScript部分讲解过了(JavaScript对象:你知道全部的对象分类吗),所以这里我就采用手工的方式过滤出来。
|
||||
|
||||
## DOM中的元素构造器
|
||||
|
||||
接下来我们看看已经讲过的DOM部分,DOM部分包含了document属性和一系列的构造器,我们可以用JavaScript的prototype来过滤构造器。
|
||||
|
||||
```
|
||||
names = names.filter( e => {
|
||||
try {
|
||||
return !(window[e].prototype instanceof Node)
|
||||
} catch(err) {
|
||||
return true;
|
||||
}
|
||||
}).filter( e => 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 => js.add(o));
|
||||
names = names.filter(e => !js.has(e));
|
||||
|
||||
names = names.filter( e => {
|
||||
try {
|
||||
return !(window[e].prototype instanceof Node)
|
||||
} catch(err) {
|
||||
return true;
|
||||
}
|
||||
}).filter( e => 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 => windowprops.add(o));
|
||||
names = names.filter(e => !windowprops.has(e));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们还要过滤掉所有的事件,也就是on开头的属性。
|
||||
|
||||
```
|
||||
names = names.filter( e => !e.match(/^on/))
|
||||
|
||||
```
|
||||
|
||||
webkit前缀的私有属性我们也过滤掉:
|
||||
|
||||
```
|
||||
names = names.filter( e => !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 => interfaces.add(o));
|
||||
|
||||
names = names.filter(e => !interfaces.has(e));
|
||||
|
||||
|
||||
```
|
||||
|
||||
这样过滤之后,我们已经过滤掉了所有的事件、Window对象、JavaScript全局对象和DOM相关的属性,但是,竟然还剩余了很多属性!你是不是很惊讶呢?好了,接下来我们才进入今天的正题。
|
||||
|
||||
## 其它属性
|
||||
|
||||
这些既不属于Window对象,又不属于JavaScript语言的Global对象的属性,它们究竟是什么呢?
|
||||
|
||||
我们可以一个一个来查看这些属性,来发现一些我们以前没有关注过的标准。
|
||||
|
||||
首先,我们要把过滤的代码做一下抽象,写成一个函数:
|
||||
|
||||
```
|
||||
function filterOut(names, props) {
|
||||
let set = new Set();
|
||||
props.forEach(o => set.add(o));
|
||||
return names.filter(e => !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 => 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中有大量的属性,这里我用代码做了过滤。得到了以下列表:
|
||||
|
||||
```
|
||||
["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"]
|
||||
|
||||
```
|
||||
|
||||
于是我们把它们也过滤掉:
|
||||
|
||||
```
|
||||
|
||||
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到标准的归类,不同的浏览器环境应该略有不同,欢迎你把自己的结果留言一起讨论。
|
||||
|
||||
|
||||
240
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器CSSOM:如何获取一个元素的准确位置.md
Normal file
240
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器CSSOM:如何获取一个元素的准确位置.md
Normal 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树形结构相关的内容。今天,我们再来学习一类新的API:CSSOM。
|
||||
|
||||
我想,你在最初接触浏览器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标签创建样式表,例如:
|
||||
|
||||
```
|
||||
<style title="Hello">
|
||||
a {
|
||||
color:red;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" title="x" href="data:text/css,p%7Bcolor:blue%7D">
|
||||
|
||||
```
|
||||
|
||||
我们创建好样式表后,还有可能要对它进行一些操作。如果我们以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("about:blank", "_blank" ,"width=100,height=100,left=100,right=100" )
|
||||
|
||||
```
|
||||
|
||||
一些浏览器出于安全考虑没有实现,也不适用于移动端浏览器,这部分你仅需简单了解即可。下面我们来了解一下滚动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("scroll", 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("scroll", 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&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)
|
||||
295
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器DOM:你知道HTML的节点有哪几种吗?.md
Normal file
295
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器DOM:你知道HTML的节点有哪几种吗?.md
Normal 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: <tagname>...</tagname>
|
||||
Text: text
|
||||
Comment: <!-- comments -->
|
||||
DocumentType: <!Doctype html>
|
||||
ProcessingInstruction: <?a 1?>
|
||||
|
||||
```
|
||||
|
||||
我们在编写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 === "p")
|
||||
node.nextSibling();
|
||||
console.log(node);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
比起NodeIterator,TreeWalker多了在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("aaaa"))
|
||||
|
||||
```
|
||||
|
||||
最后我们看一个完整的例子。
|
||||
|
||||
```
|
||||
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("aaaa"))
|
||||
|
||||
```
|
||||
|
||||
这个例子展示了如何使用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&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)
|
||||
162
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器事件:为什么会有捕获过程和冒泡过程?.md
Normal file
162
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器事件:为什么会有捕获过程和冒泡过程?.md
Normal 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是如此成功,以至于今天很多的前端工程师会有一个观点,认为我们能够“点击一个按钮”,实际上并非如此,我们只能够点击鼠标上的按钮或者触摸屏,是操作系统和浏览器把这个信息对应到了一个逻辑上的按钮,再使得它的视图对点击事件有反应。这就引出了我们第一个要讲解的机制:捕获与冒泡。
|
||||
|
||||
## 捕获与冒泡
|
||||
|
||||
很多文章会讲到捕获过程是从外向内,冒泡过程是从内向外,但是这里我希望讲清楚,为什么会有捕获过程和冒泡过程。
|
||||
|
||||
我们刚提到,实际上点击事件来自触摸屏或者鼠标,鼠标点击并没有位置信息,但是一般操作系统会根据位移的累积计算出来,跟触摸屏一样,提供一个坐标给浏览器。
|
||||
|
||||
那么,把这个坐标转换为具体的元素上事件的过程,就是捕获过程了。而冒泡过程,则是符合人类理解逻辑的:当你按电视机开关时,你也按到了电视机。
|
||||
|
||||
所以我们可以认为,捕获是计算机处理事件的逻辑,而冒泡是人类处理事件的逻辑。
|
||||
|
||||
以下代码展示了事件传播顺序:
|
||||
|
||||
```
|
||||
<body>
|
||||
<input id="i"/>
|
||||
</body>
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
document.body.addEventListener("mousedown", () => {
|
||||
console.log("key1")
|
||||
}, true)
|
||||
|
||||
document.getElementById("i").addEventListener("mousedown", () => {
|
||||
console.log("key2")
|
||||
}, true)
|
||||
|
||||
document.body.addEventListener("mousedown", () => {
|
||||
console.log("key11")
|
||||
}, false)
|
||||
|
||||
document.getElementById("i").addEventListener("mousedown", () => {
|
||||
console.log("key22")
|
||||
}, false)
|
||||
|
||||
```
|
||||
|
||||
我们监听了body和一个body的子元素上的鼠标按下事件,捕获和冒泡分别监听,可以看到,最终产生的顺序是:
|
||||
|
||||
- “key1”
|
||||
- “key2”
|
||||
- “key22”
|
||||
- “key11”
|
||||
|
||||
这是捕获和冒泡发生的完整顺序。
|
||||
|
||||
在一个事件发生时,捕获过程跟冒泡过程总是先后发生,跟你是否监听毫无关联。
|
||||
|
||||
在我们实际监听事件时,我建议这样使用冒泡和捕获机制:默认使用冒泡模式,当开发组件时,遇到需要父元素控制子元素的行为,可以使用捕获机制。
|
||||
|
||||
理解了冒泡和捕获的过程,我们再看监听事件的API,就非常容易理解了。
|
||||
|
||||
addEventListener有三个参数:
|
||||
|
||||
- 事件名称;
|
||||
- 事件处理函数;
|
||||
- 捕获还是冒泡。
|
||||
|
||||
事件处理函数不一定是函数,也可以是个JavaScript具有handleEvent方法的对象,看下例子:
|
||||
|
||||
```
|
||||
var o = {
|
||||
handleEvent: event => 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界面。
|
||||
|
||||
|
||||
198
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的(阶段三).md
Normal file
198
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的(阶段三).md
Normal 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语法部分,已经总结了选择器的各种符号,这里再把它列出来,我们回顾一下。
|
||||
|
||||
- 空格: 后代,选中它的子节点和所有子节点的后代节点。
|
||||
- >: 子代,选中它的子节点。
|
||||
- +:直接后继选择器,选中它的下一个相邻节点。
|
||||
- ~:后继,选中它之后所有的相邻节点。
|
||||
- ||:列,选中表格中的一列。
|
||||
|
||||
关于选择器的知识,我会在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。
|
||||
|
||||
除了前进一段的情况,我们还需要处理后退的情况,比如,我们这样一段代码:
|
||||
|
||||
```
|
||||
<a id=b>
|
||||
<span>1<span>
|
||||
<span class=cls>2<span>
|
||||
</a>
|
||||
<span class=cls>3<span>
|
||||
|
||||
```
|
||||
|
||||
当遇到 </a>时,必须使得规则 a#b .cls 回退一步,这样第三个 span 才不会被选中。后代选择器的作用范围是父节点的所有子节点,因此规则是在匹配到本标签的结束标签时回退。
|
||||
|
||||
## 后继选择器“ ~ ”
|
||||
|
||||
接下来我们看下后继选择器,跟后代选择器不同的地方是,后继选择器只作用于一层,我们来看一个例子:
|
||||
|
||||
```
|
||||
.cls~* {
|
||||
border:solid 1px green;
|
||||
}
|
||||
<div>
|
||||
<span>1<span>
|
||||
<span class=cls>2<span>
|
||||
<span>
|
||||
3
|
||||
<span>4</span>
|
||||
<span>
|
||||
<span>5</span>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
这里 .cls 选中了 span 2 然后 span 3 是它的后继,但是span 3的子节点 span 4 并不应该被选中,而span 5也是它的后继,因此应该被选中。
|
||||
|
||||
按照DOM树的构造顺序,4在3和5中间,我们就没有办法像前面讲的后代选择器一样通过激活或者关闭规则来实现匹配。
|
||||
|
||||
但是这里有个非常方便的思路,就是给选择器的激活,带上一个条件:父元素。
|
||||
|
||||
注意,这里后继选择器,当前半段的 .cls 匹配成功时,后续 * 所匹配的所有元素的父元素都已经确定了(后继节点和当前节点父元素相同是充分必要条件)。在我们的例子中,那个div就是后继节点的父元素。
|
||||
|
||||
## 子代选择器“ >”
|
||||
|
||||
我们继续看,子代选择器是如何实现的。
|
||||
|
||||
实际上,有了前面讲的父元素这个约束思路,我们很容易实现子代选择器。区别仅仅是拿当前节点作为父元素,还是拿当前节点的父元素作为父元素。
|
||||
|
||||
```
|
||||
div>.cls {
|
||||
border:solid 1px green;
|
||||
}
|
||||
<div>
|
||||
<span>1<span>
|
||||
<span class=cls>2<span>
|
||||
<span>
|
||||
3
|
||||
<span>4</span>
|
||||
<span>
|
||||
<span>5</span>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
我们看这段代码,当DOM树构造到div时,匹配了CSS规则的第一段,因为是子代选择器,我们激活后面的 .cls选择条件,并且指定父元素必须是当前div。于是后续的构建DOM树构建过程中,span 2 就被选中了。
|
||||
|
||||
## 直接后继选择器“ +”
|
||||
|
||||
直接后继选择器的实现是上述中最为简单的了,因为它只对唯一一个元素生效,所以不需要像前面几种一样反复激活和关闭规则。
|
||||
|
||||
一个最简单的思路是,我们可以把它当作检查元素自身的选择器来处理。即我们把#id+.cls都当做检查某一个元素的选择器。
|
||||
|
||||
另外的一种思路是:给后继选择器加上一个flag,使它匹配一次后失效。你可以尝试一下,告诉我结果。
|
||||
|
||||
## 列选择器“ || ”
|
||||
|
||||
列选择器比较特别,它是专门针对表格的选择器,跟表格的模型建立相关,我们这里不详细讲了。
|
||||
|
||||
## 其它
|
||||
|
||||
我们不要忘记,CSS选择器还支持逗号分隔,表示“或”的关系。这里最简单的实现是把逗号视为两条规则的一种简易写法。
|
||||
|
||||
比如:
|
||||
|
||||
```
|
||||
a#b, .cls {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们当作两条规则来处理:
|
||||
|
||||
```
|
||||
a#b {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
.cls {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
还有一个情况,就是选择器可能有重合,这样,我们可以使用树形结构来进行一些合并,来提高效率:
|
||||
|
||||
```
|
||||
#a .cls {
|
||||
|
||||
}
|
||||
|
||||
#a span {
|
||||
|
||||
}
|
||||
#a>span {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里实际上可以把选择器构造成一棵树:
|
||||
|
||||
<li>#a
|
||||
<ul>
|
||||
- <空格>.cls
|
||||
- <空格>span
|
||||
- >span
|
||||
|
||||
需要注意的是,这里的树,必须要带上连接符。
|
||||
|
||||
## 结语
|
||||
|
||||
这一节我们讲解了CSS计算的过程。CSS计算是把CSS规则应用到DOM树上,为DOM结构添加显示相关属性的过程。在这一节中,我们主要介绍了选择器的几种复合结构应该如何实现。
|
||||
|
||||
在这一步骤之后,我们得到了一棵带有CSS属性的树,为我们后续打下了基础。
|
||||
|
||||
最后留一个问题,你认为CSS语法解析成什么结构,最适合我们进行CSS计算。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/9d/a1fa9a462fb96ae3a70ff4751203d79d.jpg" alt="">
|
||||
225
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段一).md
Normal file
225
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段一).md
Normal 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
|
||||
|
||||
<html>
|
||||
<head><title>301 Moved Permanently</title></head>
|
||||
<body bgcolor="white">
|
||||
<center><h1>301 Moved Permanently</h1></center>
|
||||
<hr><center>openresty</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
这就是一次完整的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&302:永久性与临时性跳转。
|
||||
- 304:跟客户端缓存没有更新。
|
||||
|
||||
- 403:无权限。
|
||||
- 404:表示请求的页面不存在。
|
||||
- 418:It’s 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协议了呢?用到了它们的哪些特性,请留言告诉我吧。
|
||||
265
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段二).md
Normal file
265
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段二).md
Normal 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的千丝万缕的联系,我们需要做不少容错处理。“<?”和“<%”什么的也是必须要支持好的,报了错也不能吭声。
|
||||
|
||||
### 1.词(token)是如何被拆分的
|
||||
|
||||
首先我们来看看一个非常标准的标签,会被如何拆分:
|
||||
|
||||
```
|
||||
<p class="a">text text text</p>
|
||||
|
||||
```
|
||||
|
||||
如果我们从最小有意义单元的定义来拆分,第一个词(token)是什么呢?显然,作为一个词(token),整个p标签肯定是过大了(它甚至可以嵌套)。
|
||||
|
||||
那么,只用p标签的开头是不是合适吗?我们考虑到起始标签也是会包含属性的,最小的意义单元其实是“<p” ,所以“ <p” 就是我们的第一个词(token)。
|
||||
|
||||
我们继续拆分,可以把这段代码依次拆成词(token):
|
||||
|
||||
- <p“标签开始”的开始;
|
||||
- class=“a” 属性;
|
||||
- > “标签开始”的结束;
|
||||
- text text text 文本;
|
||||
- </p>标签结束。
|
||||
|
||||
这是一段最简单的例子,类似的还有什么呢?现在我们可以来来看看这些词(token)长成啥样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/84/f98444aa3ea7471d2414dd7d0f5e3a84.png" alt="">
|
||||
|
||||
根据这样的分析,现在我们讲讲浏览器是如何用代码实现,我们设想,代码开始从HTTP协议收到的字符流读取字符。
|
||||
|
||||
在接受第一个字符之前,我们完全无法判断这是哪一个词(token),不过,随着我们接受的字符越来越多,拼出其他的内容可能性就越来越少。
|
||||
|
||||
比如,假设我们接受了一个字符“ < ” 我们一下子就知道这不是一个文本节点啦。
|
||||
|
||||
之后我们再读一个字符,比如就是 x,那么我们一下子就知道这不是注释和CDATA了,接下来我们就一直读,直到遇到“>”或者空格,这样就得到了一个完整的词(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是我见过唯一一个标准中规定了状态机实现的语言,对大部分语言来说,状态机是一种实现而非定义)。
|
||||
|
||||
这里我们为了理解原理,用这个简单的状态机就足够说明问题了。
|
||||
|
||||
状态机的初始状态,我们仅仅区分 “< ”和 “非<”:
|
||||
|
||||
- 如果获得的是一个非<字符,那么可以认为进入了一个文本节点;
|
||||
- 如果获得的是一个<字符,那么进入一个标签状态。
|
||||
|
||||
不过当我们在标签状态时,则会面临着一些可能性。
|
||||
|
||||
<li>
|
||||
比如下一个字符是“ ! ” ,那么很可能是进入了注释节点或者CDATA节点。
|
||||
</li>
|
||||
<li>
|
||||
如果下一个字符是 “/ ”,那么可以确定进入了一个结束标签。
|
||||
</li>
|
||||
<li>
|
||||
如果下一个字符是字母,那么可以确定进入了一个开始标签。
|
||||
</li>
|
||||
<li>
|
||||
如果我们要完整处理各种HTML标准中定义的东西,那么还要考虑“ ? ”“% ”等内容。
|
||||
</li>
|
||||
|
||||
我们可以看到,用状态机做词法分析,其实正是把每个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。
|
||||
|
||||
由于状态机设计属于编译原理的基本知识,这里我们仅作一个简要的介绍。
|
||||
|
||||
接下来就是代码实现的事情了,在C/C++和JavaScript中,实现状态机的方式大同小异:我们把每个函数当做一个状态,参数是接受的字符,返回值是下一个状态函数。(这里我希望再次强调下,状态机真的是一种没有办法封装的东西,所以我们永远不要试图封装状态机。)
|
||||
|
||||
为了方便理解和试验,我们这里用JavaScript来讲解,图上的data状态大概就像下面这样的:
|
||||
|
||||
```
|
||||
var data = function(c){
|
||||
if(c=="&") {
|
||||
return characterReferenceInData;
|
||||
}
|
||||
if(c=="<") {
|
||||
return tagOpen;
|
||||
}
|
||||
else if(c=="\0") {
|
||||
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=="/") {
|
||||
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=="?") {
|
||||
return bogusCommentState;
|
||||
}
|
||||
else {
|
||||
error();
|
||||
return dataState;
|
||||
}
|
||||
};
|
||||
//……
|
||||
|
||||
```
|
||||
|
||||
这段代码给出了状态机的两个状态示例:data即为初始状态,tagOpenState是接受了一个“ < ” 字符,来判断标签类型的状态。
|
||||
|
||||
这里的状态机,每一个状态是一个函数,通过“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 || "";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
前面我们的词(token)中,以下两个是需要成对匹配的:
|
||||
|
||||
- tag start
|
||||
- tag end
|
||||
|
||||
根据一些编译原理中常见的技巧,我们使用的栈正是用于匹配开始和结束标签的方案。
|
||||
|
||||
对于Text节点,我们则需要把相邻的Text节点合并起来,我们的做法是当词(token)入栈时,检查栈顶是否是Text节点,如果是的话就合并Text节点。
|
||||
|
||||
同样我们来看看直观的解析过程:
|
||||
|
||||
```
|
||||
<html maaa=a >
|
||||
<head>
|
||||
<title>cool</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="a" />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
通过这个栈,我们可以构建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实现了这一过程。
|
||||
|
||||
今天给你留的题目是:在语法和词法的代码,我已经给出了大体的结构,请你试着把内容补充完整吧。
|
||||
118
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段五).md
Normal file
118
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段五).md
Normal 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属性,这份合成后的位图就失效了,我们需要重新绘制所有的元素。
|
||||
|
||||
那么如果我们所有的元素都不合成,会怎样呢?结果就是,相当于每次我们都必须要重新绘制所有的元素,这也不是对性能友好的选择。
|
||||
|
||||
那么好的合成策略是什么呢,好的合成策略是“猜测”可能变化的元素,把它排除到合成之外。
|
||||
|
||||
我们来举个例子:
|
||||
|
||||
```
|
||||
<div id="a">
|
||||
<div id="b">...</div>
|
||||
<div id="c" style="transform:translate(0,0)"></div>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
假设我们的合成策略能够把a、b两个div合成,而不把c合成,那么,当我执行以下代码时:
|
||||
|
||||
```
|
||||
document.getElementById("c").style.transform = "translate(100px, 0)";
|
||||
|
||||
```
|
||||
|
||||
我们绘制的时候,就可以只需要绘制a和b合成好的位图和c,从而减少了绘制次数。这里需要注意的是,在实际场景中,我们的b可能有很多复杂的子元素,所以当合成命中时,性能提升收益非常之高。
|
||||
|
||||
目前,主流浏览器一般根据position、transform等属性来决定合成策略,来“猜测”这些元素未来可能发生变化。
|
||||
|
||||
但是,这样的猜测准确性有限,所以新的CSS标准中,规定了will-change属性,可以由业务代码来提示浏览器的合成策略,灵活运用这样的特性,可以大大提升合成策略的效果。
|
||||
|
||||
## 绘制
|
||||
|
||||
绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”的过程,不过,一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。
|
||||
|
||||
一般最终位图位于显存中,也有一些情况下,浏览器只需要把内存中的一张位图提交给操作系统或者驱动就可以了,这取决于浏览器运行的环境。不过无论如何,我们把任何位图合成到这个“最终位图”的操作称为绘制。
|
||||
|
||||
这个过程听上去非常简单,这是因为在前面两个小节中,我们已经得到了每个元素的位图,并且对它们部分进行了合成,那么绘制过程,实际上就是按照z-index把它们依次绘制到屏幕上。
|
||||
|
||||
然而如果在实际中这样做,会带来极其糟糕的性能。
|
||||
|
||||
有一个一度非常流行于前端群体的说法,讲做CSS性能优化,应该尽量避免“重排”和“重绘”,前者讲的是我们上一课的排版行为,后者模糊地指向了我们本课程三小节讲的三个步骤,而实际上,这个说法大体不能算错,却不够准确。
|
||||
|
||||
因为,实际上,“绘制”发生的频率比我们想象中要高得多。我们考虑一个情况:鼠标划过浏览器显示区域。这个过程中,鼠标的每次移动,都造成了重新绘制,如果我们不重新绘制,就会产生大量的鼠标残影。
|
||||
|
||||
这个时候,限制绘制的面积就很重要了。如果鼠标某次位置恰巧遮盖了某个较小的元素,我们完全可以重新绘制这个元素来完成我们的目标,当然,简单想想就知道,这种事情不可能总是发生的。
|
||||
|
||||
计算机图形学中,我们使用的方案就是“脏矩形”算法,也就是把屏幕均匀地分成若干矩形区域。
|
||||
|
||||
当鼠标移动、元素移动或者其它导致需要重绘的场景发生时,我们只重新绘制它所影响到的几个矩形区域就够了。比矩形区域更小的影响最多只会涉及4个矩形,大型元素则覆盖多个矩形。
|
||||
|
||||
设置合适的矩形区域大小,可以很好地控制绘制时的消耗。设置过大的矩形会造成绘制面积增大,而设置过小的矩形则会造成计算复杂。
|
||||
|
||||
我们重新绘制脏矩形区域时,把所有与矩形区域有交集的合成层(位图)的交集部分绘制即可。
|
||||
|
||||
## 结语
|
||||
|
||||
在这一节课程中,我们讲解了浏览器中的位图操作部分,这包括了渲染、合成和绘制三个部分。渲染过程把元素变成位图,合成把一部分位图变成合成层,最终的绘制过程把合成层显示到屏幕上。
|
||||
|
||||
当绘制完成时,就完成了浏览器的最终任务,把一个URL最后变成了一个可以看的网页图像。当然了,我们对每一个部分的讲解,都省略了大量的细节,比如我们今天讲到的绘制,就有意地无视了滚动区域。
|
||||
|
||||
尽管如此,对浏览器工作原理的感性认识,仍然可以帮助我们理解很多前端技术的设计和应用技巧,浏览器的工作原理和性能部分非常强相关,我们在实践部分的性能优化部分,会再次跟你做一些探讨。
|
||||
|
||||
实际上,如果你认真阅读浏览器系列的课程,是可以用JavaScript实现一个玩具浏览器的,我非常希望学习课程的同学中能有人这样做,一旦你做到了,收益会非常大。这就是我今天留给你的课外作业,你可以尝试一下。
|
||||
|
||||
|
||||
102
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段四).md
Normal file
102
极客时间专栏/geek/重学前端/模块三:浏览器实现原理与API/浏览器:一个浏览器是如何工作的?(阶段四).md
Normal 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排版。这属于进阶版的排版方式了,你可以了解一下。
|
||||
|
||||
你平时喜欢使用方式排版呢,欢迎留言告诉我。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user