浏览器工作原理

在浏览器中输入一个URL按回车之后发生了哪些事情?

  • 查看浏览器中是否有缓存存在。即查看浏览器中是否曾记录过当前的域名和IP
  • 查看操作系统中是否有映射。 /etc/hosts
  • 若没有就会将域名发送给LDNS本地域名服务器(如学校、或小区)
  • 若LDNS无缓存,直接访问root DNS server 获取顶级的域名地址(如.com)
  • 向顶级域名服务器访问获取返回权限域名服务器地址。如(baidu.com)
  • 访问权限域名服务器访问获取到IP地址(如www.baidu.com)- 发起TCP三次握手建立连接
  • 发送HTTP请求
  • 服务器相应之后浏览器解析拿到的HTML代码,并请求HTML中的资源(图片、JS等)

浏览器工作原理

整个过程就是由URL转换为bitmap的过程。

  1. 从先发出请求到服务器,然后服务器返回 HTML
  2. 浏览器对HTML进行文本分析或编译,构建DOM树。
  3. 计算CSS属性,对DOM树上对应着哪些CSS规则,哪些规则会发生叠加,或者发生覆盖,将计算后的最终结果挂载到DOM树上,得到一个带样式的DOM树。
  4. 然后进行排版。即计算产生的盒子的位置及大小等等,计算出来。
  5. 最后渲染成位图(bitmap),然后经过操作系统或硬件(显卡)驱动的API完成视图的显示。

状态机

有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。

  • 每一个状态都是一个机器
    • 所有的这些机器接收的输入都是一致的,如制定接收为string,就不能接收一个obj
  • 每一个机器都知道下一个状态
    • 每个机器都有确定的下一个状态(Moore)
    • 每个机器根据输入决定下一个状态。(Mealy)每个状态应该回到哪里,在编写程序时就已经决定好了

如自动门,有开和关两种状态,当读取到开门信号时,状态就会切换为open,当接收到close信号时,就会把状态切换成close。

JS中实现一个Mealy状态机

1
2
3
4
5
6
7
8
9
10
// 每一个函数都是一个状态
function state(input) { // 函数的参数就是输入
// 在函数中,可以自由编写处理每个input的逻辑
return next // 返回下一个状态
}

// 调用
while(input) {
state = state(input) // 使状态机接收输入并完成状态切换。把状态机的返回值作为下一个状态,使得state永远保持当前状态.
}

不使用状态机查找字符串IO

1
2
3
4
5
6
7
8
9
10
11
12
13
function match(state) {
let foundI = false
for(let item of state) {
if(item == 'I') {
foundI = true
} else if(foundI && item == 'O') {
return true
} else {
foundI = false
}
}
return false
}

使用状态机查找字符串IO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function match(string) {
let state = start
for(let item of string) {
start = start(item)
}
return state === end
}

function start(str) {
if(str === 'I') {
return foundO
} else {
return start
}
}

function foundO(str) {
if(str === 'O') {
return end
} else {
return start
}
}

function end() {
return end
}

match('BIOMIND')

TCP/IP协议、HTTP

特点
  • 灵活可扩展。如header里增加了content-type指定文件的编码类型,除了文本也可以传输图片以及音视频。
  • 可靠传输。 继承了TCP可靠的特性。
  • 应用层协议

详见web协议详解

发送HTTP请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// server.js
const http = require('http')

http.createServer((req, res) => {
let body = []
req.on('error', (err) => {
console.log('err', err)
}).on('data', (chunk) => {
body.push(chunk.toString())
}).on('end', () => {
body = (Buffer.concat([ Buffer.from(body.toString()) ])).toString()
console.log("body", body)
res.writeHead(200, { 'Content-Type': 'text/html'})
res.end('hello world\n')
})
}).listen(8088)

console.log('server started')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// client.js
const net = require('net');

class Request {
constructor(props) {
this.method = props.method || 'GET',
this.host = props.host
this.port = props.port
this.path = props.path || '/'
this.body = props.body || {}
this.headers = props.headers || {}
if(!this.headers['Content-Type']) {
this.headers['Content-Type'] = 'application/x-www-form-urlencoded'
}

if(this.headers['Content-Type'] === 'application/json') {
this.bodyText = JSON.stringify(this.body)

} else if(this.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
this.bodyText = Object.keys(this.body).map(key => `key=${encodeURIComponent(this.body[key])}`).join('&')
}
this.headers['Content-length'] = this.bodyText.length
}

send(connection) {
return new Promise((resolve, reject) => {
const parser = new ResponseParser
if(connection) {
connection.write(this.toString())
} else {
connection = net.createConnection({
host: this.host,
port: this.port
}, () => {
connection.write(this.toString())
})
}

connection.on('data', (data) => {
console.log(data.toString())
parser.recceive(data.toString())
if(parser.isFinished) {
resolve(parser.response)
connection.end()
}
})

connection.on('error', (err) => {
reject(err)
connection.end()
})

})
}

toString() {
return `${this.method} ${this.path} HTTP/1.1\r
${Object.keys(this.headers).map(key => `${key}: ${this.headers[key]}`).join('\r\n')}\r
\r
${this.bodyText}`

}
}

class ResponseParser {
constructor() {
this.WAITING_STATUS_LINE = 0
this.WAITING_STATUS_LINE_END = 1
this.WAITING_HEADER_NAME = 2
this.WAITING_HEADER_SPACE = 3
this.WAITING_HEADER_VALUE = 4
this.WAITING_HEADER_LINT_END = 5
this.WAITING_HEADER_BLOCK_END = 6
this.WAITING_BODY = 7

this.current = this.WAITING_STATUS_LINE
this.statusLine = ''
this.headers = {}
this.headerName = ''
this.headerVal = ''
this.bodyParser = null
}

get isFinished() {
return this.bodyParser && this.bodyParser.isFinished
}

get response() {
this.statusLine.match(/HTTP\/1.1 ([0-9]+) ([\s\S]+)/)
return {
statusCode: RegExp.$1,
statusText: RegExp.$2,
headers: this.headers,
body: this.bodyParser.content.join('')
}
}

recceive(str) {
for(let i = 0; i < str.length; i++) {
this.reveiveChar(str.charAt(i))
}
}

reveiveChar(char) {
if(this.current === this.WAITING_STATUS_LINE) {
if(char === '\r') {
this.current = this.WAITING_STATUS_LINE_END
} else {
this.statusLine += char
}
} else if(this.current === this.WAITING_STATUS_LINE_END) {
if(char === '\n') {
this.current = this.WAITING_HEADER_NAME
}
} else if(this.current === this.WAITING_HEADER_NAME) {
if(char === ':') {
this.current = this.WAITING_HEADER_SPACE
} else if(char === '\r') {
this.current = this.WAITING_HEADER_BLOCK_END
if(this.headers['Transfer-Encoding'] === 'chunked') {
this.bodyParser = new TrunkedBodyParser()
}
} else {
this.headerName += char
}
} else if(this.current === this.WAITING_HEADER_SPACE) {
if(char === ' ') {
this.current = this.WAITING_HEADER_VALUE
}
} else if(this.current === this.WAITING_HEADER_VALUE) {
if(char === '\r') {
this.current = this.WAITING_HEADER_LINT_END
this.headers[this.headerName] = this.headerVal
this.headerName = ''
this.headerVal = ''
} else {
this.headerVal += char
}
} else if(this.current === this.WAITING_HEADER_LINT_END) {
if(char === '\n') {
this.current = this.WAITING_HEADER_NAME
}
} else if(this.current === this.WAITING_HEADER_BLOCK_END) {
if(char === '\n') {
this.current = this.WAITING_BODY
}
} else if(this.current === this.WAITING_BODY) {
this.bodyParser.receiveChar(char)
}
}
}

class TrunkedBodyParser {
constructor() {
this.WAITING_LENGTH = 0
this.WAITING_LENGTH_LINE_END = 1
this.READING_THUNK = 2
this.WAITING_NEW_LINE = 3
this.WAITING_NEW_LINE_END = 4
this.length = 0
this.content = []
this.isFinished = false
this.current = this.WAITING_LENGTH
}

receiveChar(char) {
if(this.current === this.WAITING_LENGTH) {
if(char === '\r') {
if(this.length === 0) {
this.isFinished = true
}
this.current = this.WAITING_LENGTH_LINE_END
} else {
this.length *= 16
this.length += parseInt(char, 16)
}
} else if(this.current === this.WAITING_LENGTH_LINE_END) {
if(char === '\n') {
this.current = this.READING_THUNK
}
} else if(this.current === this.READING_THUNK) {
this.content.push(char)
this.length --;
if(this.length === 0) {
this.current = this.WAITING_NEW_LINE
}
} else if(this.current === this.WAITING_NEW_LINE) {
if(char === '\r') {
this.current = this.WAITING_NEW_LINE_END
}
} else if(this.current === this.WAITING_NEW_LINE_END) {
if(char === '\n') {
this.current = this.WAITING_LENGTH
}
}
}

}

void async function() {
let request = new Request({
method: 'POST',
host: 'localhost',
port: '8088',
path: '/',
headers: {
['X-Foo2']: 'customed'
},
body: {
name: 'Cinaiet'
}
})

let response = await request.send()

console.log(response)

}()

解析HTML

HTML 词法分析

  • 开始标签
  • 结束标签
  • 自封闭标签
  1. 判断标签起始位置是否为’<’
  2. 判断下一个字符是否为’/‘,如果是则进入结束标签的计算,如果是字符串则+=当前的tagName
  3. 进入结束标签后,+=当前的tagName取到当前的tagName
  4. 若在tagName中匹配到空格、tab、禁止符、换行符等,则进入attribute标签匹配
  5. 若遇到空格或者’/‘等表示当前标签结束,进入endAttribute

语法分析

HTML的语法是由一个个的状态机去实现的。
详见HTML tokenizetion

  • 使用栈建立节点的父子关系构建DOM树
  • 遇到开始标签时压入栈
  • 遇到结束标签时出栈
  • 自封闭节点不做处理或者理解为入栈后立即出栈
  • 任何元素的父元素都是它入栈前的栈顶元素
  • 文本节点与自封闭标签的处理类似
  • 需要合并多个文本节点

ele| inline | id | class | tagName|
| — | — | — | — | — |
| div div #id | 0 | 1 | 0| 2|
| div #my #id | 0 | 2 | 0|1 |
| div #id| 0 | 1 | 0 | 1 |

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28


function specificity(selector) {
let p = [0, 0, 0, 0]
const selectorParts = selector.split(' ')
for(let part of selectorParts) {
if(part.charAt(0) == '#') {
p[1] += 1
} else if(part.charAt(0) == '.') {
p[2] +=1
} else {
p[3] += 1
}
}
return p
}

function compare(sp1, sp2) {
if(sp1[0] - sp2[0]) {
return sp1[0] - sp2[0]
}
if(sp1[1] - sp2[1])
return sp1[1] - sp2[1]
if(sp1[2] - sp2[2])
return sp1[2] - sp2[2]

return sp1[3] - sp2[3]
}

`

排版

  1. 预处理,如px或者数字转为Number类型便于运算、宽高auto或者为空时,设置为null
  2. flex布局的样式给其添加默认值,如align-item、flexWrap等。