IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 你不知道的HTML -> 正文阅读

[JavaScript知识库]你不知道的HTML

前言

HTML语言在前端开发领域是非常基础且简单的设计型语言,可以不夸张的说,无论是前端小白还是非前端工程师,涉及到编程领域的人几乎没有不会这门语言的。HTML的语言特点是通过结构化的标记节点来描述页面结构,在浏览器中运行该代码时便会形成网页的基本布局,那么HTML的标记节点是如何被浏览器识别并画到网页中的呢?

网页渲染的流程

浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout(重排)——绘制Painting(重绘)

第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。

第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。

第三步,将DOM树和样式表,关联起来,构建一颗Render树(这一过程又称为Attachment)。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。

第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。

第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。

**DOM树的构建是文档加载完成开始的?**构建DOM树是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render树和布局。

**Render树是DOM树和CSSOM树构建完毕才开始构建的吗?**这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。

CSS的解析是从右往左逆向解析的(从DOM树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。

在这里插入图片描述

今天只研究DOM树!

上面的内容简单的介绍了浏览器解释并渲染网页的整个流程,这个流程相信读者也并不会感到陌生,为了更清晰更深入的认识看似简单的HTML,本次以手写HTML解释器的方式将浏览器的神秘面纱揭开。

简单的HTML语言蕴含的知识

首先通过一个简单的HTML结构重温一下HTML语言:

<div id="d">
  你好
  <button class="btn">
    按钮
  </button>
</div>

上面便是日常生活中大家常写的一段基本HTML语法片段,之所以使用标记节点的方式构建页面是因为其天然的嵌套结构可以很好的描述页面中各节点的关系。这样无论是人类还是计算机都可以在不查看显示结果的前提下,很容易的通过阅读该代码了解节点的组成和关系。

浏览器眼中的HTML是什么样的?

对于开发者来说,通过简单的阅读代码便可以读取到代码中蕴含的信息,但是计算机并不是人类,计算机本身并没有语义识别的能力和自我思考的能力,他们只会机械的完成开发者发出的命令,所以在计算机眼中阅读的HTML代码是什么?

答案就是:DOM树!

所以在计算机的眼里,上面写的代码大概是这样的:

[
  {
    type:'node',
    tag:'div',
    attrs:{
      id:'a'
    },
    children:[
      {
        type:'text',
        value:'你好'
      },
      {
        type:'node',
        tag:'button',
        attrs:{
          class:'btn'
        },
        children:[
          {
            type:'text',
        		value:'按钮'
          }
        ]
      }
    ]
  }
]

为什么计算机要把很清晰的标记节点变成结构化的对象呢?HTML标记语言要更简洁更清晰啊?原因非常简单,因为浏览器在运行代码时必须结合系统的堆栈空间来保存关键数据的信息,而HTML的标记结构在计算机的眼中仅仅是这一段字符串而已,并没有其中蕴含的信息,计算机想要知道这段脚本代表的真实含义必须将节点中的关键信息提取出来,那么就需要将提取到的信息分别保存在heap(堆)内存中,这样才能保证在绘制网页时有条理的一个一个的进行绘制。那么问题来了,浏览器是如何把一段标记语言变成DOM树的呢?

揭秘HTML解释器

首先要清楚一件事,浏览器在接收到开发者提交的HTML代码时,到他面前的代码本质上是这样的:

//没错,浏览器得到的仅仅是一段排列组合的字符而已
let code = `
				<div id="root" class="abc"  name="aaa" >
					left
					<span>
						222
						<button>
							123
						</button>
					</span>
					<img src="ddd" />
					right
				</div>
				top
				<div id="d">
					<button id="btn"> xxx</button>
					<div>
						<span>
							xxx
							<button>
								yyy
								123
								<button id="123" >
								 nnn
								 <img/>
								</button>
							</button>
						</span>
					</div>
				</div>
				<br/>
				top-right
				top-left
			`

这样的字符组合对于浏览器来说,有缩进和无缩进没有任何意义,因为字符本身不带有任何的特殊能力。所以浏览器接下来的工作就变得枯燥且乏味了,也就是大学生活中计算机专业同学所必修的课程之一——编译原理。

想办法把字符代码搞成对象

想要从这段字符中提取关键信息的话,必定逃不掉的知识就是编译原理,这里涉及到几个关键的知识点:

  • 词法分析(lexical analysis):进行词法分析的程序或者函数叫作词法分析器(Lexical analyzer,简称Lexer),也叫扫描器(Scanner,例如typescript源码中的scanner.ts),字符流转换成对应的Token流。
  • tokenize:tokenize就是按照一定的规则,例如token令牌(通常代表关键字,变量名,语法符号等),将代码分割为一个个的“串”,也就是语法单元)。涉及到词法解析的时候,常会用到tokennize。
  • 语法分析(parse analysis):是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成语法树,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确。源程序的结构由上下文无关文法描述。

理论上讲非常的抽象,实际上代码的编译过程翻译成人话就是,先从这一团杂乱的字符中提取关键信息,其中包含:关键字、单词、符号和分隔符等等,并将这些关键信息整理成一张线性表。得到线性表后的计算机程序会根据得到的关键词和符号等结构将程序整理成有规则的结构,然后将关键信息放在置顶的关键位置,这个结果通常是一棵树也被称为AST(abstract syntax tree)抽象语法树。这个过程进行完毕浏览器就成功的将HTML转换成了DOM树。

说干就干,词法分析器来了

本文通过类似AST的方式解决HTML到DOM的完成过程,但生成的结果并不会与AST一致,由于HTML为设计型语言,并不存在编程语言的特性,所以词法分析阶段与其他的语言识别器类似,但后续的语法分析则要简单很多。那么如何实现一个词法分析器呢?

对于浏览器来说,当它阅读开发者提交的HTML代码时实际是按照这样的步骤阅读的:

在这里插入图片描述

如上图,浏览器只能一个字符一个字符进行代码的读取,那么这样阅读之后得到的岂不是一大堆的单个字符?和我们的预期差距非常的大啊!所以接下来参考下图继续看浏览器读取代码的骚操作。
在这里插入图片描述

如上图,也就是说浏览器在阅读字符时也并不是完全不思考,当其阅读到符号内容时,会一个一个的记录单独的符号,而读取到的内容为字符时,若连续阅读的都是字符浏览器会记录从头到尾的所有字符,一旦下一个内容得到的是非字符元素时,浏览器便会收录一个关键字,这样便实现了词法分析器。

用JavaScript做一个词法分析器吧

实现词法分析器的实际算法前,需要准备三个变量来存放一些关键内容

//符号(这里用了一个拼音缩写见谅)
let fh = ['<','>','/','\'','\"','='] 
//分隔符
let space = ['\n','\r','\t',' ']
//系统保留字
let letters = ['div','button','span','img','br','h1','h2']

需要有这些基础内容,在读取字符时才能知道读取到的内容属于哪个范畴方便记录。接下来便是词法分析器的代码实现:

//通过字符识别scanner扫描状态
function getStatus(letter){
  if(space.includes(letter)){
    //扫描到无效字符或分隔符状态
    return 0
  }else if(fh.includes(letter)){
    // 扫描到符号状态
    return 1
  }else{
    // 扫描到字母状态
    return 2
  }
}
//词法分析器
function makeTokens(code){
  let index = 0//扫描索引
  let status = 0//扫描状态
  let lastStatus = 0//上一个字符状态
  let nextStatus = 0//下一个字符状态
  let keyword = ''//扫描到的关键字
  //定义本次字符,上一个字符,下一个字符
  let letter,lastLetter,nextLetter = ''
  //词法分析结果
  let tokens = []
  //开启遍历
  while(index<code.length){
    //获取本次字符
    letter = code[index]
    //获取上一个字符
    if(index>0){
      lastLetter = code[index-1]
    }
    //获取下一个字符
    if(index<code.length-1){
      nextLetter = code[index+1]
    }
    //获取本次状态
    status = getStatus(letter)
    //获取上一次状态
    lastStatus = getStatus(lastLetter)
    //获取下一次状态
    nextStatus = getStatus(nextLetter)
    //分状态判断
    switch (status){
      case 1://扫描到符号时直接装进tokens
        keyword = letter
        tokens.push({
          type:'fh',
          keyword
        })
        keyword = ''
        break;
      case 2://扫描到非符号字符时
        //将字符连接成关键字
        keyword+=letter
        //若下一次扫描到的不是普通字符
        if(nextStatus!=2){
          //判断是否为系统保留字
          if(letters.includes(keyword)){
            //封装系统保留字
            tokens.push({
              type:'word',
              keyword
            })
          }else{
            //若不是系统保留字则封装为其他关键字
            tokens.push({
              type:'other',
              keyword
            })
          }
          //重置关键字防止影响下次扫描
          keyword=''
        }
        break;
      default:
        break;
    }
    //指向下一个字符
    index++
  }
  //返回词法分析结果
  return tokens
}

接下来准备一段代码,测试一下词法分析器的结果:

let code = `
				<div id="d">
					hello
					<button class="btn">你好</button>
				</div>
			`
let tokens = makeTokens(code)
console.log(tokens)

运行代码后,会得到一个很大的JSON数组,该数组大致如下:

[
    {
        "type": "fh",
        "keyword": "<"
    },
    {
        "type": "word",
        "keyword": "div"
    },
    {
        "type": "other",
        "keyword": "id"
    },
    {
        "type": "fh",
        "keyword": "="
    },
    {
        "type": "fh",
        "keyword": "\""
    },
    {
        "type": "other",
        "keyword": "d"
    },
    {
        "type": "fh",
        "keyword": "\""
    },
    {
        "type": "fh",
        "keyword": ">"
    },
    {
        "type": "other",
        "keyword": "hello"
    },
    {
        "type": "fh",
        "keyword": "<"
    },
    {
        "type": "word",
        "keyword": "button"
    },
    {
        "type": "other",
        "keyword": "class"
    },
    {
        "type": "fh",
        "keyword": "="
    },
    {
        "type": "fh",
        "keyword": "\""
    },
    {
        "type": "other",
        "keyword": "btn"
    },
    {
        "type": "fh",
        "keyword": "\""
    },
    {
        "type": "fh",
        "keyword": ">"
    },
    {
        "type": "other",
        "keyword": "你好"
    },
    {
        "type": "fh",
        "keyword": "<"
    },
    {
        "type": "fh",
        "keyword": "/"
    },
    {
        "type": "word",
        "keyword": "button"
    },
    {
        "type": "fh",
        "keyword": ">"
    },
    {
        "type": "fh",
        "keyword": "<"
    },
    {
        "type": "fh",
        "keyword": "/"
    },
    {
        "type": "word",
        "keyword": "div"
    },
    {
        "type": "fh",
        "keyword": ">"
    }
]

通过阅读该结构发现,词法分析器的作用就是将不具备关键信息点的完整字符串的关键信息按类别提取并保存到线性表中。

胜利就在前方,DOM树来了

有了词法分析器构造的线性表之后,下一步的任务就是通过阅读线性表识别出HTML应用的节点层级关系并将其填装到一个对象中,此步骤涉及到部分基础算法(遍历树的深度优先算法以及其他基础算法思想),用文字描述很难表现出作者的诚意,所以完整的DOM树构建代码附上(内附完整注释,代码编写匆忙,有不严谨的地方望见谅):

/**
			 * 根据叶子节点id和根对象获取叶子节点的上一个节点(表示其前一个兄弟节点,或其直接父亲节点)
			 * 采用深度优先算法进行树的遍历
			 * @param {Object} nodeIndex 节点的index
			 * @param {Object} deep 节点的deep
			 * @param {Object} doc DOM树对象
			 * @param {Object} obj 节点对象(保留对象)
			 */
function getObjByIndex(nodeIndex,deep,doc,obj){
  //目标节点对象
  let target = {}
  let index = 0 //起始索引
  let tree = doc //初始化树对象
  while(index<tree.length){
    // debugger
    //获取跟节点的每个叶子节点
    let item = tree[index]
    //若传入节点的index匹配成功
    if(nodeIndex == item.index){
      //记录返回数据
      target = item
      //若传入节点的深度低于目标节点,需要逐层找到相同层的节点
      while(deep<target.deep){
        target = target.parentNode
      }
      return target
    }
    //若节点具有子代叶子节点
    if(item.children){
      //重置需要遍历内部叶子节点
      let col = 0
      //遍历根节点层的叶子节点
      while(col<item.children.length){
        // console.log(index,i)
        //逐个获取叶子节点
        let obj = item.children[col]
        // 若内层叶子节点匹配成功
        if(nodeIndex == obj.index){
          target = obj
          while(deep<target.deep){
            target = target.parentNode
          }
          return target
        }
        //若子代叶子节点仍然有子元素
        if(obj.children){
          //将遍历条件切换为该节点并重置遍历索引
          item = obj
          col = 0
        }else{
          //若该节点没有子代叶子节点,且当前节点有下一个节点
          if(item.children[col+1]){
            //则直接去下一个节点
            col++
          }else{
            //若该节点无子代节点并且无下一个节点代表当前深度遍历到本条线路的最后一个
            //获取当前item的父节点
            let parent = item.parentNode
            //判断当前遍历层是否为父节点的最后一个子节点,如果有则返回上一层,知道当前节点不是同层最后一个节点
            while(parent.children&&parent.children.indexOf(item) == parent.children.length-1){
              //向上跳跃
              item = parent 
              parent = parent.parentNode
            }
            //若当前父节点没有跳回最上层
            if(parent.children){
              //获取当前item节点所在序号
              col = parent.children.indexOf(item)
              //将item上跳触发新路径的遍历
              item = parent
            }else{
              //若回到跟节点则代表大节点遍历完毕,跳出内层while循环
              col = item.length
            }
            //增加索引
            col++
          }
        }
      }
    }
    //增加单层索引
    index++
  }
  return target
}
//构建节点对象树的实际工具函数
/**
			 * 根据节点信息构建节点对象并插入到DOM树中
			 * @param {Object} deep 节点深度
			 * @param {Object} leafIndex 节点序号(id)
			 * @param {Object} doc DOM树对象
			 * @param {Object} begin 扫描起点
			 * @param {Object} end 扫描终点
			 * @param {Object} status 扫描状态 0为进入节点外部,1为进入起始标签,2为进入终止标签
			 * @param {Object} nodeStatus 节点结构 1单标签,2双标签
			 */
function makeObj(deep,leafIndex,doc,begin,end,status,nodeStatus){
  
  let tag = ''
  //当闭合标签结束时status为1时代表标签头信息,防止误将</xxx>等结束符号记录为有效节点
  if(status == 1){
    //定义属性字符串
    let attrStr = ''
    //遍历扫描范围
    let attrStatus = 0
    while(begin<=end){

      //获取关键字
      let item = tokens[begin]
      let lastItem = tokens[begin-1]
      //当关键字类型为word时代表当前标签名称
      if(item.type == 'word'){
        tag = item.keyword
      }
      //筛选属性数据并拼接到属性字符串中
      if((item.type == 'fh' || item.type == 'other') &&  item.keyword!='<' && item.keyword!='>' && item.keyword!='/'){
        attrStr+=item.keyword
        if(item.keyword=='\''&&lastItem.keyword != '='){
          attrStr += ';' 
        }
        if(item.keyword=='\"'&&lastItem.keyword != '='){
          attrStr += ';' 
        }
      }
      //扫描递增
      begin++
    }
    //定义节点对象
    let obj = {
      type:'node',//节点类型
      deep,//节点深度
      index:leafIndex,//节点id
      tag,//标签名
      attrs:{},//属性
      nodeStatus
    }
    //根据叶子节点id获取他前一个节点的信息
    let target = getObjByIndex(leafIndex-1,deep,doc,obj)
    //封装节点属性
    attrStr.replace(/(\'|\")/g,'').split(';').forEach(item => {
      if(item.trim().length>0){
        let [key,value] = item.split('=')
        obj.attrs[key] = value
      }
    })
    //当上一个节点为空时设置跟节点
    if(Object.keys(target).length == 0){
      obj.parentNode = doc
      doc.push(obj)
    }else{
      //当上一个节点有值时,判断当前节点是否是上一个节点的子节点
      if(deep>target.deep){
        obj.parentNode = target
        //设置子节点
        if(target.children){
          target.children.push(obj)
        }else{
          target.children = [obj]
        }
        //当深度相同时获取上一个节点的父节点并插入兄弟对象
      }else if(deep == target.deep){
        obj.parentNode = target.parentNode
        if(target.parentNode.children){
          target.parentNode.children.push(obj)
        }else{
          doc.push(obj)
        }
      }
    }
  }
}
/**
			 * 根据词法分析结果将线性表整理成树
			 * @param {Object} tokens
			 */
function makeTokensToTree(tokens){
  let i = 0 //索引
  let status = 0 //状态 0代表进入标签外节点 1代表起始节点 2代表结束节点
  let nextKeyword //下一个关键字
  let lastKeyword
  let nextType //下一个token类型
  let lastType
  let doc = [] //文档对象
  let deep = 0 //节点深度
  let leafIndex = 0 //叶子节点id
  let begin = 0 //扫描起始点
  let end = 0 //扫描重点
  //遍历tokens列表
  while(i<tokens.length){
    //获取当次节点对象
    let { type,keyword } = tokens[i]
    //获取下一个节点(如果有的话)
    if(i<tokens.length-1){
      nextKeyword = tokens[i+1].keyword
      nextType = tokens[i+1].type
    }
    if(i>0){
      lastKeyword = tokens[i-1].keyword
      lastType = tokens[i-1].type
    }
    //如果是符号节点
    if(type == 'fh'){
      //当符号以<开头代表起始标签
      if(keyword == '<'&& nextKeyword!= '/'){
        //设置状态为1
        status = 1
        //每次起始时增加深度以便记录连续嵌套标签的深度
        deep++
        //设置开始节点的扫描起点
        begin = i
        //递增叶子节点id
        leafIndex++
      }
      //当符号以</开头代表结束标签
      if(keyword == '<'&& nextKeyword == '/'){
        // 设置状态为2代表进入结束节点
        status = 2
        //设置结束节点的扫描起点
        begin = i
        //节点结束后深度-1保证同层节点深度相同
        deep--
      }
      //当符号以/>结束时代表自结束标签的结束
      if(keyword == '>'&& lastKeyword == '/'){
        //设置扫描终点
        end = i
        //制作单标签对象结构
        makeObj(deep,leafIndex,doc,begin,end,status,1)
        //深度上浮保证下一个节点为兄弟节点
        deep--
        //设置状态为进入节点外
        status = 0
      }
      //当符号扫描到>时代表一个闭合标签结束
      if(keyword == '>'&& lastKeyword != '/'){
        // 记录扫描结束节点
        end = i
        makeObj(deep,leafIndex,doc,begin,end,status,2)
        //扫描结束后改变状态
        status = 0
      }
    }else{
      //当扫描到节点外部时,处理文本节点
      if(status == 0){
        let obj = {
          type:'text',
          value:keyword,
          // parentDeep:deep,
          deep:deep+1,
          // parentIndex:leafIndex,
          index:leafIndex+1
        }
        let target = getObjByIndex(leafIndex,deep,doc,obj)
        obj.parentNode = target
        if(target.children){
          target.children.push(obj)
        }else{
          if(target == doc){
            target.push(obj)
          }else{
            target.children = [obj]
          }
        }
        leafIndex++
      }
    }
    i++
  }
  return doc
}

小贴士:

这里所有的树的遍历均采用while循环的方式处理主要目的是防止递归函数潜在的执行栈溢出风险。

试试我们的成果吧

在语法分析器和词法分析器的组合下,将下面的HTML结构整理成DOM树的形态:

let code = `
				<div id="d">
					hello
					<button class="btn">你好</button>
					<img src="url"/>
				</div>
				一段意外的文本 <br/>
				<div id="root">
					<span>
						<h1>我是标题</h1>
						<button>点我</button>
					</span>
				</div>
			`
let tokens = makeTokens(code)
let tree = makeTokensToTree(tokens)
console.log(tree)

不出意外的话接下来控制台中会得到如下的结果:

在这里插入图片描述

看到这个结果,是不是才意识到,原来,浏览器其实并不认识HTML,一切的代码都是一段字符描述而已,程序执行引擎在运行程序时做了大量的基础工作,才让开发者编写的带有逻辑语法和关键字的编程语言真正的跑起来,这时是不是该致敬一下语法编译器的开发者们!!

作业(反向生成HTML)

这个简单吧~既然HTML语法解释器实现起来稍微复杂一点点,那么若将生成的DOM对象反向变回HTML代码是不是就简单很多了?

提前公布答案吧:

/**
			 * DOM结构转HTML代码
			 * 
			 * @param {Object} tree DOM树对象
			 */
function DOM2HTML(tree){
  // console.log(tree)
  //总起始索引
  let index = 0
  //生成的HTML代码
  let str = ''
  //跳出条件
  while(index<tree.length){
    // debugger
    //获取所有根节点
    let item = tree[index]
    //从根节点中提取不同结构的节点
    if(item.type== 'node'){
      if(item.nodeStatus == 2){
        str+=makeSpace(item.deep)+`<${item.tag} ${makeAttrs(item)} >\n`
      }else{
        str+=makeSpace(item.deep)+`<${item.tag} ${makeAttrs(item)} />\n`
      }
    }else if(item.type == 'text'){
      str+=makeSpace(item.deep)+item.value+'\n'
    }
    //若节点存在子元素
    if(item.children){
      //遍历每一个根节点的子树
      let col = 0
      //跳出条件
      while(col<item.children.length){
        //获取每一个子节点
        let obj = item.children[col]
        //根据子节点生成头部标签和内容
        if(obj.type== 'node'){
          if(obj.nodeStatus == 2){
            str+=makeSpace(obj.deep)+`<${obj.tag} ${makeAttrs(obj)} >\n`
          }else{
            str+=makeSpace(obj.deep)+`<${obj.tag} ${makeAttrs(obj)}/>\n`
          }
        }else if(obj.type == 'text'){
          str+=makeSpace(obj.deep)+obj.value+'\n'
        }
        //若子节点存在子代节点
        if(obj.children){
          //进入子节点并重置序号
          item = obj
          col = 0
        }else{
          //若子节点不存在任何叶子节点,并且其并不是最后一个节点
          if(item.children[col+1]){
            //若子节点为双标签则生成闭合结尾
            if(obj.type== 'node'&&obj.nodeStatus == 2){
              // console.log(`</${obj.tag}>`)
              str+=makeSpace(obj.deep)+`</${obj.tag}>\n`
            }
            //向后移动指针
            col++
          }else{
            //获取当前节点层的父节点
            let parent = item.parentNode
            //若子节点不存在下一个邻居节点,判断子节点是否为当前层最后一个节点
            while(parent.children&&parent.children.indexOf(item) == parent.children.length-1){
              //若为最后一个节点则生成闭合结尾
              if(item.nodeStatus == 2){
                str+=makeSpace(item.deep)+`</${item.tag}>\n`
              }
              //若节点为最后一个节点则逐层向上,直到节点不是最后一个节点
              item = parent 
              parent = parent.parentNode
            }
            //若节点有子代元素则继续遍历
            if(parent.children){
              //若节点为双标签则生成闭合节点
              if(item.nodeStatus == 2){
                str+=makeSpace(item.deep)+`</${item.tag}>\n`
              }
              //若该节点层仍有后续兄弟节点
              col = parent.children.indexOf(item)
              //向上跳
              item = parent
            }else{
              //若parent没有children代表已经跳到根节点则退出循环
              col = item.length
            }
            col++
          }
        }

      }
    }
    //生成父节点的闭合结尾标签
    if(item.type== 'node'&& item.nodeStatus == 2){
      str+=makeSpace(item.deep)+`</${item.tag}>\n`
    }
    index++
  }
  return str
}

这段代码便是根据上面的解释器构造的DOM树反向生成HTML的实现(附效果图)

在这里插入图片描述

尾声

后面的内容可能会结合本次的语法解释器,将生成的结果通过canvas渲染到画布上来模拟真正的浏览器绘制网页过程,让同学们更清晰的看到浏览器的实际工作内容,从而激发出大家的编程思想,喜欢的同学请加关注点赞收藏并分享。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-12 16:23:29  更:2022-05-12 16:24:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 19:56:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码