vue parseHTML 函数源码解析AST基本形成

目录
  • AST(抽象语法树)?
    • 子节点
  • Vue中是如何把html(template)字符串编译解析成AST
    • 解析html
    • 代码重新改造
    • 接着解析 html (template)字符串
    • 解析div

AST(抽象语法树)?

在上篇文章中我们已经把整个词法分析的解析过程分析完毕了。

例如有html(template)字符串:

<div id="app">
  <p>{{ message }}</p>
</div>

产出如下:

{
attrs: [" id="app"", "id", "=", "app", undefined, undefined]
end: 14
start: 0
tagName: "div"
unarySlash: ""
}
{
attrs: []
end: 21
start: 18
tagName: "p"
unarySlash: ""
}

看到这不禁就有疑问? 这难道就是AST(抽象语法树)??

非常明确的告诉你答案:No 这不是我们想要的AST,parse 阶段最终生成的这棵树应该是与如上html(template)字符串的结构一一对应的:

├── div
│   ├── p
│   │   ├── 文本

如果每一个节点我们都用一个 javascript 对象来表示的话,那么 div 标签可以表示为如下对象:

{
  type: 1,
  tag: "div"
}

子节点

由于每个节点都存在一个父节点和若干子节点,所以我们为如上对象添加两个属性:parent 和 children ,分别用来表示当前节点的父节点和它所包含的子节点:

{
  type: 1,
  tag:"div",
  parent: null,
  children: []
}

同时每个元素节点还可能包含很多属性 (attributes),所以我们可以为每个节点添加attrsList属性,用来存储当前节点所拥有的属性:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

按照以上思路去描述之前定义的 html 字符串,那么这棵抽象语法树应该长成如下这个样子:

{
  type: 1,
  tag: "div",
  parent: null,
  attrsList: [],
  children: [{
      type: 1,
      tag: "p",
      parent: div,
      attrsList: [],
      children:[
         {
          type: 3,
          tag:"",
          parent: p,
          attrsList: [],
          text:"{{ message }}"
         }
       ]
  }],
}

实际上构建抽象语法树的工作就是创建一个类似如上所示的一个能够描述节点关系的对象树,节点与节点之间通过 parent 和 children 建立联系,每个节点的 type 属性用来标识该节点的类别,比如 type 为 1 代表该节点为元素节点,type 为 3 代表该节点为文本节点。

这里可参考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp

回顾我们所学的 parseHTML 函数可以看出,他只是在生成 AST 中的一个重要环节并不是全部。 那在Vue中是如何把html(template)字符串编译解析成AST的呢?

Vue中是如何把html(template)字符串编译解析成AST

在源码中:

function parse (html) {
  var root;
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      // 省略...
    },
    end: function (){
      // 省略...
    }
  })
  return root
}

可以看到Vue在进行模板编译词法分析阶段调用了parse函数,parse函数返回root,其中root 所代表的就是整个模板解析过后的 AST,这中间还有两个非常重要的钩子函数,之前我们没有讲到的,options.start 、options.end。

接下来重点就来看看他们做了什么。

解析html

假设解析的html字符串如下:

<div></div>

这是一个没有任何子节点的div 标签。如果要解析它,我们来简单写下代码。

function parse (html) {
  var root;
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root) root = element
    },
    end: function (){
      // 省略...
    }
  })
  return root
}

如上: 在start 钩子函数中首先定义了 element 变量,它就是元素节点的描述对象,接着判断root 是否存在,如果不存在则直接将 element 赋值给 root 。当解析这段 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数将被调用,最终 root 变量将被设置为:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

html 字符串复杂度升级: 比之前的 div 标签多了一个子节点,span 标签。

<div>
  <span></span>
</div>

代码重新改造

此时需要把代码重新改造。

function parse (html) {
  var root;
  var currentParent;
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root){
        root = element;
       }else if(currentParent){
        currentParent.children.push(element)
      }
      if (!unary) currentParent = element
    },
    end: function (){
      // 省略...
    }
  })
  return root
}

我们知道当解析如上 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数被调用,root变量被设置为:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

还没完可以看到在 start 钩子函数的末尾有一个 if 条件语句,当一个元素为非一元标签时,会设置 currentParent 为该元素的描述对象,所以此时currentParent也是:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

接着解析 html (template)字符串

接着解析 html (template)字符串,会遇到 span 元素的开始标签,此时root已经存在,currentParent 也存在,所以会将 span 元素的描述对象添加到 currentParent 的 children 数组中作为子节点,所以最终生成的 root 描述对象为:

{
  type: 1,
  tag:"div",
  parent: null,
  attrsList: []
  children: [{
     type: 1,
     tag:"span",
     parent: div,
     attrsList: [],
     children:[]
  }],
}

到目前为止好像没有问题,但是当html(template)字符串复杂度在升级,问题就体现出来了。

<div>
 <span></span>
 <p></p>
</div>

在之前的基础上 div 元素的子节点多了一个 p 标签,到解析span标签的逻辑都是一样的,但是解析 p 标签时候就有问题了。

注意这个代码:

if (!unary) currentParent = element

在解析 p 元素的开始标签时,由于 currentParent 变量引用的是 span 元素的描述对象,所以p 元素的描述对象将被添加到 span 元素描述对象的 children 数组中,被误认为是 span 元素的子节点。而事实上 p 标签是 div 元素的子节点,这就是问题所在。

为了解决这个问题,就需要我们额外设计一个回退的操作,这个回退的操作就在end钩子函数里面实现。

解析div

这是一个什么思路呢?举个例子在解析div 的开始标签时:

stack = [{tag:"div"...}]

在解析span 的开始标签时:

stack = [{tag:"div"...},{tag:"span"...}]

在解析span 的结束标签时:

stack = [{tag:"div"...}]

在解析p 的开始标签时:

stack = [{tag:"div"...},{tag:"p"...}]

在解析p 的标签时:

这样的一个回退操作看懂了吗? 这就能保证在解析p开始标签的时候,stack中存储的是p标签父级元素的描述对象。

接下来继续改造我们的代码。

function parse (html) {
  var root;
  var currentParent;
  var stack = [];
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root){
        root = element;
       }else if(currentParent){
        currentParent.children.push(element)
      }
      if (!unary){
          currentParent = element;
          stack.push(currentParent);
       }
    },
    end: function (){
      stack.pop();
      currentParent = stack[stack.length - 1]
    }
  })
  return root
}

通过上述代码,每当遇到一个非一元标签的结束标签时,都会回退 currentParent 变量的值为之前的值,这样我们就修正了当前正在解析的元素的父级元素。

以上就是根据 parseHTML 函数生成 AST 的基本方式,但实际上还不完美在Vue中还会去处理一元标签,文本节点和注释节点等等。

接下来你是否迫不及待要进入到源码部分去看看了? 但Vue这块代码稍微复杂点,我们还需要有一些前期的预备知识。

以上就是vue parseHTML 函数源码解析AST基本形成的详细内容,更多关于vue parseHTML 函数AST的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue编程三部曲之将template编译成AST示例详解

    目录 前言 编译准备 源码编译链式调用 compileToFunctions parse 解析 template 标签匹配相关的正则 stack advance while 解析开始标签 解析结束标签 当前 template < 不在第一个字符串 处理 stack 栈中剩余未处理的标签 生成 AST start 钩子函数 end 钩子函数 为什么回退? 解析 <p> 解析 </p> chars 钩子函数 commit 钩子函数 番外(可跳过) createASTElement

  • Vue编译器AST抽象语法树源码分析

    目录 引言 baseCompile主要核心代码 如何写一个程序来识别 Token parse 函数解析模板字符串 引言 接上篇  Vue编译器源码分析compile 解析 baseCompile主要核心代码 // `createCompilerCreator` allows creating compilers that use alternative // parser/optimizer/codegen, e.g the SSR optimizing compiler. // Here we

  • Vue AST源码解析第一篇

    讲完了数据劫持原理和一堆初始化,现在是DOM相关的代码了. 上一节是从这个函数开始的: // Line-3924 Vue.prototype._init = function(options) { // 大量初始化 // ... // Go! if (vm.$options.el) { vm.$mount(vm.$options.el); } }; 弄完data属性的数据绑定后,开始处理el属性,也就是挂载的DOM节点,这里的vm.$options.el也就是传进去的'#app'字符串. 有一个

  • Vue3 AST解析器-源码解析

    目录 1.生成 AST 抽象语法树 2.创建 AST 的根节点 3.解析子节点 4.解析模板元素 Element 5.示例:模板元素解析 上一篇文章Vue3 编译流程-源码解析中,我们从 packges/vue/src/index.ts 的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile 函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v

  • vue parseHTML 函数源码解析AST基本形成

    目录 AST(抽象语法树)? 子节点 Vue中是如何把html(template)字符串编译解析成AST 解析html 代码重新改造 接着解析 html (template)字符串 解析div AST(抽象语法树)? 在上篇文章中我们已经把整个词法分析的解析过程分析完毕了. 例如有html(template)字符串: <div id="app"> <p>{{ message }}</p> </div> 产出如下: { attrs: [&q

  • vue parseHTML函数源码解析 AST预备知识

    目录 正文 createASTElement函数 解析指令所用正则 parse 函数中的变量 正文 接上章节:parseHTML 函数源码解析AST 基本形成 在正式扎进Vue parse源码之前,我们先了解下他周边的工具函数, 这能帮我们快速的去理解阅读. 还记得我们在上章节讲的element元素节点的描述对象吗? var element = { type: 1, tag: tag, parent: null, attrsList: attrs, children: [] } 在源码中定义了一

  • vue parseHTML函数源码解析start钩子函数

    目录 正文 platformGetTagNamespace 源码 isForbiddenTag 函数 addIfCondition是什么 processIfConditions 源码 正文 接上章节:parseHTML 函数源码解析 AST 预备知识 现在我们就可以愉快的进入到Vue start钩子函数源码部分了. start: function start(tag, attrs, unary) { // check namespace. // inherit parent ns if ther

  • vue parseHTML 函数源码解析

    目录 正文 函数开头定义的一些常量和变量 while 循环 textEnd ===0 parseStartTag 函数解析开始标签 总结: 正文 接上篇: Vue编译器源码分析AST 抽象语法树 function parseHTML(html, options) { var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpen

  • Django contrib auth authenticate函数源码解析

    引言 django提供了一个默认的auth系统用于用户的登录和授权,并提供了一定的扩展性,允许开发者自行定义多个验证后台,每个验证后台必须实现authenticate函数,并返回None或者User对象. 默认的后台是django.contrib.auth.backends.ModelBackend,该后台通过用户名和密码进行用户的验证,以settings.AUTH_USER_MODEL作为模型.但是在实际的开发中,相信大家都不会固定的使用用户名以及同一个model进行验证,比如,不同的角色需要

  • jQuery each函数源码分析

    jQuery.each方法用于遍历一个数组或对象,并对当前遍历的元素进行处理,在jQuery使用的频率非常大,下面就这个函数做了详细讲解: 代码 /*! * jQuery源码分析-each函数 * jQuery版本:1.4.2 * * ---------------------------------------------------------- * 函数介绍 * * each函数通过jQuery.extend函数附加到jQuery对象中: * jQuery.extend({ * each:

  • FilenameUtils.getName 函数源码分析

    目录 一.背景 二.源码分析 2.1 问题1:为什么需要 NonNul 检查 ? 2.1.1 怎么检查的? 2.1.2 为什么要做这个检查呢? 2.2 问题2: 为什么不根据当前系统类型来获取分隔符? 三.Zoom Out 3.1 代码健壮性 3.2 代码严谨性 3.3 如何写注释 四.总结 一.背景 最近用到了 org.apache.commons.io.FilenameUtils#getName 这个方法,该方法可以传入文件路径,获取文件名. 简单看了下源码,虽然并不复杂,但和自己设想略有区

  • Vue中之nextTick函数源码分析详解

    1. 什么是Vue.nextTick()? 官方文档解释如下: 在下次DOM更新循环结束之后执行的延迟回调.在修改数据之后立即使用这个方法,获取更新后的DOM. 2. 为什么要使用nextTick? <!DOCTYPE html> <html> <head> <title>演示Vue</title> <script src="https://tugenhua0707.github.io/vue/vue1/vue.js"&

  • Vue之Watcher源码解析(1)

    上一节最后再次调用了mount函数,我发现竟然跳到了7000多行的那个函数,之前我还说因为声明早了被覆盖,看来我错了! 就是这个函数: // Line-7531 Vue$3.prototype.$mount = function(el, hydrating) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) }; 第一步query就不用看了,el此时是一个DO

  • Vue之Watcher源码解析(2)

    接着上节Vue Watcher源码的话,继续探讨,目前是这么个过程: 函数大概是这里: // line-3846 Vue.prototype._render = function() { // 获取参数 try { // 死在这儿 vnode = render.call(vm._renderProxy, vm.$createElement); } catch (e) { // 报render错误 } // return empty vnode in case the render functio

随机推荐