资讯专栏INFORMATION COLUMN

重学前端学习笔记(十二)--浏览器工作解析(二)

zxhaaa / 2195人阅读

摘要:状态迁移代码所谓的状态迁移,就是当前状态函数返回下一个状态函数。状态函数通过代码中的函数来输出解析好的词,我们只需要覆盖,即可指定对解析结果的处理方式。词法分析器代码状态函数们至此,字符流被拆成词。

笔记说明

</>复制代码

  1. 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入winter的专栏学习【原文有winter的语音】,如有侵权请联系我,邮箱:kaimo313@foxmail.com
一、概括

</>复制代码

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

二、解析代码 2.1、词(token)是如何被拆分的

</>复制代码

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

接下拆解下面代码:

</>复制代码

  1. text text text

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

class=“a” 属性

> “标签开始”的结束

text text text 文本

标签结束

关于token的解释:

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

2.2、状态机 2.2.1、过程

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

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

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

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

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

可能会遇到的情况:

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

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

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

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

2.2.2、代码化

</>复制代码

  1. // 初始状态
  2. var data = function(c){
  3. if(c=="&") {
  4. return characterReferenceInData;
  5. }
  6. if(c=="<") {
  7. return tagOpen;
  8. }
  9. else if(c=="") {
  10. error();
  11. emitToken(c);
  12. return data;
  13. }
  14. else if(c==EOF) {
  15. emitToken(EOF);
  16. return data;
  17. }
  18. else {
  19. emitToken(c);
  20. return data;
  21. }
  22. };
  23. // tagOpenState 是接受了一个“ < ” 字符,来判断标签类型的状态。
  24. var tagOpenState = function tagOpenState(c){
  25. if(c=="/") {
  26. return endTagOpenState;
  27. }
  28. if(c.match(/[A-Z]/)) {
  29. token = new StartTagToken();
  30. token.name = c.toLowerCase();
  31. return tagNameState;
  32. }
  33. if(c.match(/[a-z]/)) {
  34. token = new StartTagToken();
  35. token.name = c;
  36. return tagNameState;
  37. }
  38. if(c=="?") {
  39. return bogusCommentState;
  40. }
  41. else {
  42. error();
  43. return dataState;
  44. }
  45. };
  46. //……

状态迁移代码:

</>复制代码

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

</>复制代码

  1. var state = data;
  2. var char
  3. while(char = getInput())
  4. state = state(char);

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

词法分析器代码:

</>复制代码

  1. function HTMLLexicalParser(){
  2. // 状态函数们……
  3. function data() {
  4. // ……
  5. }
  6. function tagOpen() {
  7. // ……
  8. }
  9. // ……
  10. var state = data;
  11. this.receiveInput = function(char) {
  12. state = state(char);
  13. }
  14. }

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

三、构建 DOM 树 3.1、用栈实现词->dom树

HTML词法分析器:

</>复制代码

  1. function HTMLSyntaticalParser(){
  2. var stack = [new HTMLDocument];
  3. // receiveInput负责接收词法部分产生的词(token)
  4. this.receiveInput = function(token) {
  5. // 构建dom树算法
  6. }
  7. // emmitToken 来调用
  8. this.getOutput = function(){
  9. return stack[0];
  10. }
  11. }

NODE类:

</>复制代码

  1. function Element(){
  2. this.childNodes = [];
  3. }
  4. function Text(value){
  5. this.value = value || "";
  6. }

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

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

</>复制代码

  1. cool

栈-->dom树:

栈顶元素就是当前节点

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

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

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

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

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

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

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

个人总结

纸上得来终觉浅,绝知此事要躬行。动手很重要,这一部分还是需要敲敲代码协助理解原理,单单看会有点枯燥,我在winter课程的下面留言发现一个不错的html-parser,github地址是这个https://github.com/aimergenge/toy-html-parser,觉得不错的可以支持一波star

lexer.js展示

</>复制代码

  1. const EOF = void 0
  2. function HTMLLexicalParser (syntaxer) {
  3. let state = data
  4. let token = null
  5. let attribute = null
  6. let characterReference = ""
  7. this.receiveInput = function (char) {
  8. if (state == null) {
  9. throw new Error("there is an error")
  10. } else {
  11. state = state(char)
  12. }
  13. }
  14. this.reset = function () {
  15. state = data
  16. }
  17. function data (c) {
  18. switch (c) {
  19. case "&":
  20. return characterReferenceInData
  21. case "<":
  22. return tagOpen
  23. // perhaps will not encounter in javascript?
  24. // case "":
  25. // error()
  26. // emitToken(c)
  27. // return data
  28. // can be handle by default case
  29. // case EOF:
  30. // emitToken(EOF)
  31. // return data
  32. default:
  33. emitToken(c)
  34. return data
  35. }
  36. }
  37. // only handle right character reference
  38. function characterReferenceInData (c) {
  39. if (c === ";") {
  40. characterReference += c
  41. emitToken(characterReference)
  42. characterReference = ""
  43. return data
  44. } else {
  45. characterReference += c
  46. return characterReferenceInData
  47. }
  48. }
  49. function tagOpen (c) {
  50. if (c === "/") {
  51. return endTagOpen
  52. }
  53. if (/[a-zA-Z]/.test(c)) {
  54. token = new StartTagToken()
  55. token.name = c.toLowerCase()
  56. return tagName
  57. }
  58. // no need to handle this
  59. // if (c === "?") {
  60. // return bogusComment
  61. // }
  62. return error(c)
  63. }
  64. function tagName (c) {
  65. if (c === "/") {
  66. return selfClosingTag
  67. }
  68. if (/[
  69. f
  70. ]/.test(c)) {
  71. return beforeAttributeName
  72. }
  73. if (c === ">") {
  74. emitToken(token)
  75. return data
  76. }
  77. if (/[a-zA-Z]/.test(c)) {
  78. token.name += c.toLowerCase()
  79. return tagName
  80. }
  81. }
  82. function beforeAttributeName (c) {
  83. if (/[
  84. f
  85. ]/.test(c)) {
  86. return beforeAttributeName
  87. }
  88. if (c === "/") {
  89. return selfClosingTag
  90. }
  91. if (c === ">") {
  92. emitToken(token)
  93. return data
  94. }
  95. if (/[""<]/.test(c)) {
  96. return error(c)
  97. }
  98. attribute = new Attribute()
  99. attribute.name = c.toLowerCase()
  100. attribute.value = ""
  101. return attributeName
  102. }
  103. function attributeName (c) {
  104. if (c === "/") {
  105. token[attribute.name] = attribute.value
  106. return selfClosingTag
  107. }
  108. if (c === "=") {
  109. return beforeAttributeValue
  110. }
  111. if (/[
  112. f
  113. ]/.test(c)) {
  114. return beforeAttributeName
  115. }
  116. attribute.name += c.toLowerCase()
  117. return attributeName
  118. }
  119. function beforeAttributeValue (c) {
  120. if (c === """) {
  121. return attributeValueDoubleQuoted
  122. }
  123. if (c === """) {
  124. return attributeValueSingleQuoted
  125. }
  126. if (/
  127. f
  128. /.test(c)) {
  129. return beforeAttributeValue
  130. }
  131. attribute.value += c
  132. return attributeValueUnquoted
  133. }
  134. function attributeValueDoubleQuoted (c) {
  135. if (c === """) {
  136. token[attribute.name] = attribute.value
  137. return beforeAttributeName
  138. }
  139. attribute.value += c
  140. return attributeValueDoubleQuoted
  141. }
  142. function attributeValueSingleQuoted (c) {
  143. if (c === """) {
  144. token[attribute.name] = attribute.value
  145. return beforeAttributeName
  146. }
  147. attribute.value += c
  148. return attributeValueSingleQuoted
  149. }
  150. function attributeValueUnquoted (c) {
  151. if (/[
  152. f
  153. ]/.test(c)) {
  154. token[attribute.name] = attribute.value
  155. return beforeAttributeName
  156. }
  157. attribute.value += c
  158. return attributeValueUnquoted
  159. }
  160. function selfClosingTag (c) {
  161. if (c === ">") {
  162. emitToken(token)
  163. endToken = new EndTagToken()
  164. endToken.name = token.name
  165. emitToken(endToken)
  166. return data
  167. }
  168. }
  169. function endTagOpen (c) {
  170. if (/[a-zA-Z]/.test(c)) {
  171. token = new EndTagToken()
  172. token.name = c.toLowerCase()
  173. return tagName
  174. }
  175. if (c === ">") {
  176. return error(c)
  177. }
  178. }
  179. function emitToken (token) {
  180. syntaxer.receiveInput(token)
  181. }
  182. function error (c) {
  183. console.log(`warn: unexpected char "${c}"`)
  184. }
  185. }
  186. class StartTagToken {}
  187. class EndTagToken {}
  188. class Attribute {}
  189. module.exports = {
  190. HTMLLexicalParser,
  191. StartTagToken,
  192. EndTagToken
  193. }
syntaxer.js展示

</>复制代码

  1. const { StartTagToken, EndTagToken } = require("./lexer")
  2. class HTMLDocument {
  3. constructor () {
  4. this.isDocument = true
  5. this.childNodes = []
  6. }
  7. }
  8. class Node {}
  9. class Element extends Node {
  10. constructor (token) {
  11. super(token)
  12. for (const key in token) {
  13. this[key] = token[key]
  14. }
  15. this.childNodes = []
  16. }
  17. [Symbol.toStringTag] () {
  18. return `Element<${this.name}>`
  19. }
  20. }
  21. class Text extends Node {
  22. constructor (value) {
  23. super(value)
  24. this.value = value || ""
  25. }
  26. }
  27. function HTMLSyntaticalParser () {
  28. const stack = [new HTMLDocument]
  29. this.receiveInput = function (token) {
  30. if (typeof token === "string") {
  31. if (getTop(stack) instanceof Text) {
  32. getTop(stack).value += token
  33. } else {
  34. let t = new Text(token)
  35. getTop(stack).childNodes.push(t)
  36. stack.push(t)
  37. }
  38. } else if (getTop(stack) instanceof Text) {
  39. stack.pop()
  40. }
  41. if (token instanceof StartTagToken) {
  42. let e = new Element(token)
  43. getTop(stack).childNodes.push(e)
  44. return stack.push(e)
  45. }
  46. if (token instanceof EndTagToken) {
  47. return stack.pop()
  48. }
  49. }
  50. this.getOutput = () => stack[0]
  51. }
  52. function getTop (stack) {
  53. return stack[stack.length - 1]
  54. }
  55. module.exports = {
  56. HTMLSyntaticalParser
  57. }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/55023.html

相关文章

  • 重学前端学习笔记)--览器工作解析

    摘要:状态迁移代码所谓的状态迁移,就是当前状态函数返回下一个状态函数。状态函数通过代码中的函数来输出解析好的词,我们只需要覆盖,即可指定对解析结果的处理方式。词法分析器代码状态函数们至此,字符流被拆成词。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入wi...

    LucasTwilight 评论0 收藏0
  • 重学前端学习笔记)--览器工作解析

    摘要:状态迁移代码所谓的状态迁移,就是当前状态函数返回下一个状态函数。状态函数通过代码中的函数来输出解析好的词,我们只需要覆盖,即可指定对解析结果的处理方式。词法分析器代码状态函数们至此,字符流被拆成词。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入wi...

    zilu 评论0 收藏0
  • 重学前端学习笔记)--选择器的机制

    摘要:优先级第一优先级无连接符号第二优先级空格第三优先级复杂选择器的连接符号空格表示选中所有符合条件的后代节点。后代表示选中符合条件的子节点。直接后继表示选中对应列中符合条件的单元格。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入winter的专栏学习【...

    acrazing 评论0 收藏0
  • 重学前端学习笔记)--选择器的机制

    摘要:优先级第一优先级无连接符号第二优先级空格第三优先级复杂选择器的连接符号空格表示选中所有符合条件的后代节点。后代表示选中符合条件的子节点。直接后继表示选中对应列中符合条件的单元格。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入winter的专栏学习【...

    jeffrey_up 评论0 收藏0

发表评论

0条评论

zxhaaa

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<