重学前端 12 # 浏览器工作解析(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 重学前端 12 # 浏览器工作解析(二)

一、概括


本文主要聊聊浏览器如何解析请求回来的 HTML 代码以及 DOM 树又是如何构建的。


b0ac114aa01a034f24ef78dab8cfcd47.png

二、解析代码


2.1、词(token)是如何被拆分的

“词”(指编译原理的术语 token,表示最小的有意义的单元),种类大约只有 标签开始属性标签结束注释CDATA节点几种。


接下拆解下面代码:

<p class="a">text text text</p>


这段代码依次拆成词(token):


  • <p“标签开始”的开始
  • class=“a” 属性
  • > “标签开始”的结束
  • text text text 文本
  • </p> 标签结束

关于token的解释:

075ca5fbf0b532f5155ce87349088095.png

HTTP 协议收到的字符流读取字符。每读入一个字符,其实都要做一次决策,而且这些决定是跟“当前状态”有关的。把字符流解析成词(token),最常见的方案就是使用状态机。



2.2、状态机


2.2.1、过程

把部分词(token)的解析画成一个状态机:

5f16b6277cce5420d648094ab7795573.png


具体的可以参考HTML官方文档


状态机的初始状态,我们仅仅区分 “< ”和 “非 <”:


   如果获得的是一个非 < 字符,那么可以认为进入了一个文本节点

   如果获得的是一个 < 字符,那么进入一个标签状态


可能会遇到的情况:


   比如下一个字符是“ ! ” ,那么很可能是进入了注释节点或者 CDATA节点

   如果下一个字符是 “ / ”,那么可以确定进入了一个结束标签

   如果下一个字符是字母,那么可以确定进入了一个开始标签

   如果我们要完整处理各种 HTML 标准中定义的东西,那么还要考虑 “ ? ” “ % ”等内容



2.2.2、代码化

// 初始状态
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;
    }
};
// tagOpenState 是接受了一个“ < ” 字符,来判断标签类型的状态。
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;
    }
};
//……



状态迁移代码:

所谓的状态迁移,就是当前状态函数返回下一个状态函数。

var state = data;
var char
while(char = getInput())
    state = state(char);


状态函数通过代码中的 emitToken 函数来输出解析好的 token(词),我们只需要覆盖 emitToken,即可指定对解析结果的处理方式。

词法分析器代码:

function HTMLLexicalParser(){
    // 状态函数们……
    function data() {
        // ……
    }
    function tagOpen() {
        // ……
    }
    // ……
    var state = data;
    this.receiveInput = function(char) {
        state = state(char);
    }
}

至此,字符流被拆成词(token)。




三、构建 DOM 树


3.1、用栈实现词->dom树


HTML词法分析器:

function HTMLSyntaticalParser(){
    var stack = [new HTMLDocument];
    // receiveInput负责接收词法部分产生的词(token)
    this.receiveInput = function(token) {
        // 构建dom树算法
    }
    // emmitToken 来调用
    this.getOutput = function(){
        return stack[0];
    }
}


NODE类:

function Element(){
    this.childNodes = [];
}
function Text(value){
    this.value = value || "";
}


使用的栈正是用于匹配开始和结束标签的方案。

用上述的栈以及下面的html来进行解析过程分析:

<html maaa=a >
    <head>
        <title>cool</title>
    </head>
    <body>
        <img src="a" />
    </body>
</html>


栈–>dom树:


   栈顶元素就是当前节点

   遇到属性,就添加到当前节点

   遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点

   遇到注释节点,作为当前节点的子节点

   遇到 tag start 就入栈一个节点,当前节点就是这个节点的父节点

   遇到 tag end 就出栈一个节点(还可以检查是否匹配)


本来这里有个视频分析上面html代码的,用栈构造 DOM 树的全过程,这里就用一张图片看一下算了:

990fc8ea468490855125d6698cfc1d9c.png

视频演示了怎样生成右边的结果。更多详情规则可以参考W3C网站关于树的构建部分



拓展


html-parser,github地址是这个https://github.com/aimergenge/toy-html-parser


lexer.js展示

const EOF = void 0
function HTMLLexicalParser (syntaxer) {
  let state = data
  let token = null
  let attribute = null
  let characterReference = ''
  this.receiveInput = function (char) {
    if (state == null) {
      throw new Error('there is an error')
    } else {
      state = state(char)
    }
  }
  this.reset = function () {
    state = data
  }
  function data (c) {
    switch (c) {
      case '&':
        return characterReferenceInData
      case '<':
        return tagOpen
      // perhaps will not encounter in javascript?
      // case '\0':
      //   error()
      //   emitToken(c)
      //   return data
      //  can be handle by default case
      // case EOF:
      //   emitToken(EOF)
      //   return data
      default:
        emitToken(c)
        return data
    }
  }
  // only handle right character reference
  function characterReferenceInData (c) {
    if (c === ';') {
      characterReference += c
      emitToken(characterReference)
      characterReference = ''
      return data
    } else {
      characterReference += c
      return characterReferenceInData
    }
  }
  function tagOpen (c) {
    if (c === '/') {
      return endTagOpen
    }
    if (/[a-zA-Z]/.test(c)) {
      token = new StartTagToken()
      token.name = c.toLowerCase()
      return tagName
    }
    // no need to handle this
    // if (c === '?') {
    //   return bogusComment
    // }
    return error(c)
  }
  function tagName (c) {
    if  (c === '/') {
      return selfClosingTag
    }
    if  (/[\t \f\n]/.test(c)) {
      return beforeAttributeName
    }
    if (c === '>') {
      emitToken(token)
      return data
    }
    if (/[a-zA-Z]/.test(c)) {
      token.name += c.toLowerCase()
      return tagName
    }
  }
  function beforeAttributeName (c) {
    if (/[\t \f\n]/.test(c)) {
      return beforeAttributeName
    }
    if (c === '/') {
      return selfClosingTag
    }
    if (c === '>') {
      emitToken(token)
      return data
    }
    if (/["'<]/.test(c)) {
      return error(c)
    }
    attribute = new Attribute()
    attribute.name = c.toLowerCase()
    attribute.value = ''
    return attributeName
  }
  function attributeName (c) {
    if (c === '/') {
      token[attribute.name] = attribute.value
      return selfClosingTag
    }
    if (c === '=') {
      return beforeAttributeValue
    }
    if (/[\t \f\n]/.test(c)) {
      return beforeAttributeName
    }
    attribute.name += c.toLowerCase()
    return attributeName
  }
  function beforeAttributeValue (c) {
    if (c === '"') {
      return attributeValueDoubleQuoted
    }
    if (c === "'") {
      return attributeValueSingleQuoted
    }
    if (/\t \f\n/.test(c)) {
      return beforeAttributeValue
    }
    attribute.value += c
    return attributeValueUnquoted
  }
  function attributeValueDoubleQuoted (c) {
    if (c === '"') {
      token[attribute.name] = attribute.value
      return beforeAttributeName
    }
    attribute.value += c
    return attributeValueDoubleQuoted
  }
  function attributeValueSingleQuoted (c) {
    if (c === "'") {
      token[attribute.name] = attribute.value
      return beforeAttributeName
    }
    attribute.value += c
    return attributeValueSingleQuoted
  }
  function attributeValueUnquoted (c) {
    if (/[\t \f\n]/.test(c)) {
      token[attribute.name] = attribute.value
      return beforeAttributeName
    }
    attribute.value += c
    return attributeValueUnquoted
  }
  function selfClosingTag (c) {
    if (c === '>') {
      emitToken(token)
      endToken = new EndTagToken()
      endToken.name = token.name
      emitToken(endToken)
      return data
    }
  }
  function endTagOpen (c) {
    if (/[a-zA-Z]/.test(c)) {
      token = new EndTagToken()
      token.name = c.toLowerCase()
      return tagName
    }
    if (c === '>') {
      return error(c)
    }
  }
  function emitToken (token) {
    syntaxer.receiveInput(token)
  }
  function error (c) {
    console.log(`warn: unexpected char '${c}'`)
  }
}
class StartTagToken {}
class EndTagToken {}
class Attribute {}
module.exports = {
  HTMLLexicalParser,
  StartTagToken,
  EndTagToken
}

syntaxer.js展示

const { StartTagToken, EndTagToken } = require('./lexer')
class HTMLDocument {
  constructor () {
    this.isDocument = true
    this.childNodes = []
  }
}
class Node {}
class Element extends Node {
  constructor (token) {
    super(token)
    for (const key in token) {
      this[key] = token[key]
    }
    this.childNodes = []
  }
  [Symbol.toStringTag] () {
    return `Element<${this.name}>`
  }
}
class Text extends Node {
  constructor (value) {
    super(value)
    this.value = value || ''
  }
}
function HTMLSyntaticalParser () {
  const stack = [new HTMLDocument]
  this.receiveInput = function (token) {
    if (typeof token === 'string') {
      if (getTop(stack) instanceof Text) {
        getTop(stack).value += token
      } else {
        let t = new Text(token)
        getTop(stack).childNodes.push(t)
        stack.push(t)
      }
    } else if (getTop(stack) instanceof Text) {
      stack.pop()
    }
    if (token instanceof StartTagToken) {
      let e = new Element(token)
      getTop(stack).childNodes.push(e)
      return stack.push(e)
    }
    if (token instanceof EndTagToken) {
      return stack.pop()
    }
  }
  this.getOutput = () => stack[0]
}
function getTop (stack) {
  return stack[stack.length - 1]
}
module.exports = {
  HTMLSyntaticalParser
}


目录
相关文章
|
8天前
|
机器学习/深度学习 编解码 前端开发
探索无界:前端开发中的响应式设计深度解析####
【10月更文挑战第29天】 在当今数字化时代,用户体验的优化已成为网站与应用成功的关键。本文旨在深入探讨响应式设计的核心理念、技术实现及最佳实践,揭示其如何颠覆传统布局限制,实现跨设备无缝对接,从而提升用户满意度和访问量。通过剖析响应式设计的精髓,我们将一同见证其在现代Web开发中的重要地位与未来趋势。 ####
34 7
|
10天前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计深度解析与实践####
【10月更文挑战第29天】 本文深入探讨了响应式设计的核心理念,即通过灵活的布局、媒体查询及弹性图片等技术手段,使网站能够在不同设备上提供一致且优质的用户体验。不同于传统摘要概述,本文将以一次具体项目实践为引,逐步剖析响应式设计的关键技术点,分享实战经验与避坑指南,旨在为前端开发者提供一套实用的响应式设计方法论。 ####
35 4
|
11天前
|
前端开发 JavaScript API
前端开发的秘密花园:这些技巧让你轻松应对各种浏览器兼容性问题!
【10月更文挑战第31天】前端开发是一个充满创意与挑战的领域,追求极致用户体验的同时,浏览器兼容性问题却时常阻碍我们前进。本文将介绍几种解决浏览器兼容性的最佳实践:使用CSS前缀、Autoprefixer工具、现代JavaScript特性与Babel转译、Polyfill与Feature Detection、响应式设计以及跨域问题处理。掌握这些技巧,助你轻松应对各种兼容性难题,创建更稳定、用户友好的网页应用。
25 3
|
10天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
16天前
|
域名解析 缓存 网络协议
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
|
19天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
54 1
|
30天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
127 2
|
30天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
37 0
|
30天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
30天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。

推荐镜像

更多