我是三钻,一个在《技术银河》中等你们一起来终生漂泊学习。
点赞是力量,关注是认可,评论是关爱!下期再见 !
浏览器工作原理是一块非常重要的内容,我们经常看到的 重绘
、重排
或者一些讲解CSS属性的时候,都会用到一些浏览器工作原理的知识来讲解。理论化学习浏览器工作原理,效果不是很大,而且很枯燥,所以这里我们从零开始用 JavaScript
来实现一个浏览器。
通过自己实现一遍简单的浏览器,我们会对浏览器的基本原理有更为深刻的理解。
我们主要需要完成的是从 URL 请求到 Bitmap 页面展示的整个流程就可以了。
浏览器流程:
URL
部分,经过 HTTP
请求,然后解析返回内容,然后提取 HTML
内容HTML
后,我们可以通过文本分析(parse),然后把HTML的文本编程一个 DOM
树DOM
树是光秃秃的,下一步我们进行 CSS 计算(CSS computing),最终把 CSS 挂载在这个 DOM 树上因为这个处理字符串是整个的浏览器里面贯穿使用的技巧,如果不会用这个状态机,后面实现和读浏览器实现的代码会非常吃力。所以这里我们先讲讲什么是有限状态机。
Mealy 状态机:
// 每个函数是一个状态
function state (input) { // 函数参数就是输入
// 在函数中,可以自由地编写代码,处理每个状态的逻辑
return next; // 返回值作为下一个状态
}
/** ========= 以下是调试 ========= */
while (input) {
// 获取输入
state = state(input); // 把状态机的返回值作为下一个状态
}
input
state = state(input)
,来让状态机接收输入来完成状态切换Mealy
型状态机,返回值一定是根据 input
返回下一个状态Moore
型状态机,返回值是与 input
没有任何关系,都是固定的状态返回我们首先了解一下,在不使用状态机的情况下来实现一些字符串的处理方式:
第一问题:在一个字符串中,找到字符“a”
function match(string) {
for (let letter of string) {
if (letter == 'a') return true;
}
return false;
}
console.log(match('I am TriDiamond'));
第二个问题:不准使用正则表达式,纯粹用 JavaScript 的逻辑实现:在一个字符串中,找到字符“ab”
「直接寻找 a
和 b
,都找到时返回」
/**
* 直接寻找 `a` 和 `b`,都找到时返回
* @param {*} string 被匹配的字符
*/
function matchAB(string) {
let hasA = false;
for (let letter of string) {
if (letter == 'a') {
hasA = true;
} else if (hasA && letter == 'b') {
return true;
} else {
hasA = false;
}
}
return false;
}
console.log( matchAB('hello abert'));
第三个问题:不准使用正则表达式,纯粹用 JavaScript 的逻辑实现:在一个字符串中,找到字符“abcdef”
方法一:「使用暂存空间,移动指针来检测」
/**
* 使用暂存空间,移动指针来检测
* @param {*} match 需要匹配的字符
* @param {*} string 被匹配的字符
*/
function matchString(match, string) {
const resultLetters = match.split(''); // 需要匹配的字符拆解成数组来记录
const stringArray = string.split(''); // 把被匹配的字符串内容也拆解成数组
let index = 0; // 匹配字符串的指针
for (let i = 0; i <= stringArray.length; i++) {
// 因为要保证字符的绝对匹配,如 “ab” 不能是 "abc",不能是 "a b"
// 所以这里需要两个字符必须是有顺序的关系的
if (stringArray[i] == resultLetters[index]) {
// 如果找到一个字符是吻合的,就 index + 1 找下一个字符
index++;
} else {
// 如果下一个字符不吻合,就重置重新匹配
index = 0;
}
// 如果已经匹配完所有的字符了,直接可以返回 true
// 证明字符中含有需要寻找的字符
if (index > resultLetters.length - 1) return true;
}
return false;
}
console.log('方法1', matchString('abcdef', 'hello abert abcdef'));
方法二:「使用 substring
和匹配字符的长度来截取字符,看是否等于答案」
/**
* 通用字符串匹配 - 参考方法2(使用substring)
* @param {*} match 需要匹配的字符
* @param {*} string 被匹配的字符
*/
function matchWithSubstring(match, string) {
for (let i = 0; i < string.length - 1; i++) {
if (string.substring(i, i + match.length) === match) {
return true;
}
}
return false;
}
console.log('方法2', matchWithSubstring('abcdef', 'hello abert abcdef'));
方法三:「逐个查找,直到找到最终结果」
/**
* 逐个查找,直到找到最终结果
* @param {*} string 被匹配的字符
*/
function match(string) {
let matchStatus = [false, false, false, false, false, false];
let matchLetters = ['a', 'b', 'c', 'd', 'e', 'f'];
let statusIndex = 0;
for (let letter of string) {
if (letter == matchLetters[0]) {
matchStatus[0] = true;
statusIndex++;
} else if (matchStatus[statusIndex - 1] && letter == matchLetters[statusIndex]) {
matchStatus[statusIndex] = true;
statusIndex++;
} else {
matchStatus = [false, false, false, false, false, false];
statusIndex = 0;
}
if (statusIndex > matchLetters.length - 1) return true;
}
return false;
}
console.log('方法3', match('hello abert abcdef'));
这里我们使用状态机的方式来实现:在一个字符串中,找到字符“abcdef”
start
和 end
matchedA
就是已经匹配中 a
字符了,以此类推start
f
字符,所以 matchedE
成功后,可以直接返回 结束状态end
end
这个结束状态,也被称为陷阱方法 (Trap),因为状态转变结束了,所以让状态一直停留在这里,知道循环结束/**
* 状态机字符串匹配
* @param {*} string
*/
function match(string) {
let state = start;
for (let letter of string) {
state = state(letter); // 状态切换
}
return state === end; // 如果最后的状态函数是 `end` 即返回 true
}
function start(letter) {
if (letter === 'a') return matchedA;
return start;
}
function end(letter) {
return end;
}
function matchedA(letter) {
if (letter === 'b') return matchedB;
return start(letter);
}
function matchedB(letter) {
if (letter === 'c') return matchedC;
return start(letter);
}
function matchedC(letter) {
if (letter === 'd') return matchedD;
return start(letter);
}
function matchedD(letter) {
if (letter === 'e') return matchedE;
return start(letter);
}
function matchedE(letter) {
if (letter === 'f') return end(letter);
return start(letter);
}
console.log(match('I am abcdef'));
问题升级:用状态机实现字符串“abcabx”的解析
/**
* 状态机匹配字符串
* @param {*} string 被匹配的字符
*/
function match(string) {
let state = start;
for (let letter of string) {
state = state(letter);
}
return state === end;
}
function start(letter) {
if (letter === 'a') return matchedA;
return start;
}
function end(letter) {
return end;
}
function matchedA(letter) {
if (letter === 'b') return matchedB;
return start(letter);
}
function matchedB(letter) {
if (letter === 'c') return matchedC;
return start(letter);
}
function matchedC(letter) {
if (letter === 'a') return matchedA2;
return start(letter);
}
function matchedA2(letter) {
if (letter === 'b') return matchedB2;
return start(letter);
}
function matchedB2(letter) {
if (letter === 'x') return end;
return matchedB(letter);
}
console.log('result: ', match('abcabcabx'));
HTTP
require('http')
TCP
Internet
4G/5G/Wi-Fi
require('net')
在我们编写自己的浏览器之前,我们首先建立一个node.js服务端。
首先我们编写一个 node.js
的服务端:
const http = require('http');
http
.createServer((request, response) => {
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk.toString());
})
.on('end', () => {
body = Buffer.concat(body).toString();
console.log('body', body);
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(' Hello World\n');
});
})
.listen(8080);
console.log('server started');
在编写我们的客户端代码之前,我们需要先了解一下 HTTP 协议。
我们先来看看 HTTP 协议的 request 部分:
POST / HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
field1=aaa&code=x%3D1
request line
,包含了三个部分:
Headers
key: value
格式body
部分:
Content-Type
来决定的接下来我们就可以开始编写代码了!
class Request {
constructor(options) {
// 首先在 constructor 赋予需要使用的默认值
this.method = options.method || 'GET';
this.host = options.host;
this.port = options.port || 80;
this.path = options.path || '/';
this.body = options.body || {};
this.headers = options.headers || {};
if (!this.headers['Content-Type']) {
this.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
// 根据 Content-Type 转换 body 的格式
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('&');
}
// 自动计算 body 内容长度,如果长度不对,就会是一个非法请求
this.headers['Content-Length'] = this.bodyText.length;
}
// 发送请求的方法,返回 Promise 对象
send() {
return new Promise((resolve, reject) => {
//......
});
}
}
/**
* 请求方法
*/
void (async function () {
let request = new Request({
method: 'POST',
host: '127.0.0.1',
port: '8080',
path: '/',
headers: {
['X-Foo2']: 'custom',
},
body: {
name: 'tridiamond',
},
});
let response = await request.end();
console.log(response);
})();
// 发送请求的方法,返回 Promise 对象
send() {
return new Promise((resolve, reject) => {
const parser = new ResponseParser();
resolve('');
});
}
recieveChar
函数来对每个字符进行处理class ResponseParser {
constructor() {}
receive(string) {
for (let i = 0; i < string.length; i++) {
this.receiveChar(string.charAt(i));
}
}
receiveChar(char) {}
}
在接下来的部分,我们需要在代码中解析 HTTP Response 中的内容,所以我先来了解一下 HTTP Response 中的内容。
HTTP/1.1 200 OK
Content-Type: text/html
Date: Mon, 23 Dec 2019 06:46:19 GMT
Connection: keep-alive
Transfer-Encoding: chunked
26
Hello World0
status line
与 request line 相反
chunked body
(是 Node 默认返回的一种格式)这里我们开始实战,通过实现 send 函数中的逻辑来真正发送请求到服务端。
通过以上思路,我们来实现代码:
// 发送请求的方法,返回 Promise 对象
send(connection) {
return new Promise((resolve, reject) => {
const parser = new ResponseParser();
// 先判断 connection 是否有传送过来
// 没有就根据,Host 和 port 来创建一个 TCP 连接
// `toString` 是把请求参数按照 HTTP Request 的格式组装
if (connection) {
connection.write(this.toString());
} else {
connection = net.createConnection(
{
host: this.host,
port: this.port,
},
() => {
connection.write(this.toString());
}
);
}
// 监听 connection 的 data
// 然后原封不动传给 parser
// 如果 parser 已经结束的话,我们就可以进行 resolve
// 最后断开连接
connection.on('data', data => {
console.log(data.toString());
parser.receive(data.toString());
if (parser.isFinished) {
resolve(parser.response);
connection.end();
}
});
// 监听 connection 的 error
// 如果请求出现错误,首先 reject 这个promise
// 然后断开连接,避免占着连接的情况
connection.on('error', err => {
reject(err);
connection.end();
});
});
}
/**
* 组装 HTTP Request 文本内容
*/
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}`;
}
现在我们来具体实现 RequestParser类的代码。
/**
* Response 解析器
*/
class ResponseParser {
constructor() {
this.state = this.waitingStatusLine;
this.statusLine = '';
this.headers = {};
this.headerName = '';
this.headerValue = '';
this.bodyParser = null;
}
receive(string) {
for (let i = 0; i < string.length; i++) {
this.state = this.state(string.charAt(i));
}
}
receiveEnd(char) {
return receiveEnd;
}
/**
* 等待状态行内容
* @param {*} char 文本
*/
waitingStatusLine(char) {
if (char === '\r') return this.waitingStatusLineEnd;
this.statusLine += char;
return this.waitingStatusLine;
}
/**
* 等待状态行结束
* @param {*} char 文本
*/
waitingStatusLineEnd(char) {
if (char === '\n') return this.waitingHeaderName;
return this.waitingStatusLineEnd;
}
/**
* 等待 Header 名
* @param {*} char 文本
*/
waitingHeaderName(char) {
if (char === ':') return this.waitingHeaderSpace;
if (char === '\r') return this.waitingHeaderBlockEnd;
this.headerName += char;
return this.waitingHeaderName;
}
/**
* 等待 Header 空格
* @param {*} char 文本
*/
waitingHeaderSpace(char) {
if (char === ' ') return this.waitingHeaderValue;
return this.waitingHeaderSpace;
}
/**
* 等待 Header 值
* @param {*} char 文本
*/
waitingHeaderValue(char) {
if (char === '\r') {
this.headers[this.headerName] = this.headerValue;
this.headerName = '';
this.headerValue = '';
return this.waitingHeaderLineEnd;
}
this.headerValue += char;
return this.waitingHeaderValue;
}
/**
* 等待 Header 行结束
* @param {*} char 文本
*/
waitingHeaderLineEnd(char) {
if (char === '\n') return this.waitingHeaderName;
return this.waitingHeaderLineEnd;
}
/**
* 等待 Header 内容结束
* @param {*} char 文本
*/
waitingHeaderBlockEnd(char) {
if (char === '\n') return this.waitingBody;
return this.waitingHeaderBlockEnd;
}
/**
* 等待 body 内容
* @param {*} char 文本
*/
waitingBody(char) {
console.log(char);
return this.waitingBody;
}
}
最后我们来实现 Body 内容的解析逻辑。
/**
* Response 解析器
*/
class ResponseParser {
constructor() {
this.state = this.waitingStatusLine;
this.statusLine = '';
this.headers = {};
this.headerName = '';
this.headerValue = '';
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(''),
};
}
receive(string) {
for (let i = 0; i < string.length; i++) {
this.state = this.state(string.charAt(i));
}
}
receiveEnd(char) {
return receiveEnd;
}
/**
* 等待状态行内容
* @param {*} char 文本
*/
waitingStatusLine(char) {
if (char === '\r') return this.waitingStatusLineEnd;
this.statusLine += char;
return this.waitingStatusLine;
}
/**
* 等待状态行结束
* @param {*} char 文本
*/
waitingStatusLineEnd(char) {
if (char === '\n') return this.waitingHeaderName;
return this.waitingStatusLineEnd;
}
/**
* 等待 Header 名
* @param {*} char 文本
*/
waitingHeaderName(char) {
if (char === ':') return this.waitingHeaderSpace;
if (char === '\r') {
if (this.headers['Transfer-Encoding'] === 'chunked') {
this.bodyParser = new ChunkedBodyParser();
}
return this.waitingHeaderBlockEnd;
}
this.headerName += char;
return this.waitingHeaderName;
}
/**
* 等待 Header 空格
* @param {*} char 文本
*/
waitingHeaderSpace(char) {
if (char === ' ') return this.waitingHeaderValue;
return this.waitingHeaderSpace;
}
/**
* 等待 Header 值
* @param {*} char 文本
*/
waitingHeaderValue(char) {
if (char === '\r') {
this.headers[this.headerName] = this.headerValue;
this.headerName = '';
this.headerValue = '';
return this.waitingHeaderLineEnd;
}
this.headerValue += char;
return this.waitingHeaderValue;
}
/**
* 等待 Header 行结束
* @param {*} char 文本
*/
waitingHeaderLineEnd(char) {
if (char === '\n') return this.waitingHeaderName;
return this.waitingHeaderLineEnd;
}
/**
* 等待 Header 内容结束
* @param {*} char 文本
*/
waitingHeaderBlockEnd(char) {
if (char === '\n') return this.waitingBody;
return this.waitingHeaderBlockEnd;
}
/**
* 等待 body 内容
* @param {*} char 文本
*/
waitingBody(char) {
this.bodyParser.receiveChar(char);
return this.waitingBody;
}
}
/**
* Chunked Body 解析器
*/
class ChunkedBodyParser {
constructor() {
this.state = this.waitingLength;
this.length = 0;
this.content = [];
this.isFinished = false;
}
receiveChar(char) {
this.state = this.state(char);
}
/**
* 等待 Body 长度
* @param {*} char 文本
*/
waitingLength(char) {
if (char === '\r') {
if (this.length === 0) this.isFinished = true;
return this.waitingLengthLineEnd;
} else {
// 转换十六进制长度
this.length *= 16;
this.length += parseInt(char, 16);
}
return this.waitingLength;
}
/**
* 等待 Body line 结束
* @param {*} char 文本
*/
waitingLengthLineEnd(char) {
if (char === '\n') return this.readingTrunk;
return this.waitingLengthLineEnd;
}
/**
* 读取 Trunk 内容
* @param {*} char 文本
*/
readingTrunk(char) {
this.content.push(char);
this.length--;
if (this.length === 0) return this.waitingNewLine;
return this.readingTrunk;
}
/**
* 等待新的一行
* @param {*} char 文本
*/
waitingNewLine(char) {
if (char === '\r') return this.waitingNewLineEnd;
return this.waitingNewLine;
}
/**
* 等待新的一行结束
* @param {*} char 文本
*/
waitingNewLineEnd(char) {
if (char === '\n') return this.waitingLength;
return this.waitingNewLineEnd;
}
}
这里我们就实现了浏览器的 HTTP Request 请求,HTTP Response 解析的过程的代码。
下一篇文我们来一起实现 HTTP 解析并且构建 DOM 树,然后进行 CSS 计算。
小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。
《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。
《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书
《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火