《Vue不看源码懂原理》系列——Vue模板编译

咱们在Vue中会使用一些变量,表达式,指令来填充模板,可是这些语法在HTML中是不存在的,那么Vue是如何对这样的模板进行编译的呢?javascript

模板编译

模板编译的主要做用是将Vue模板编译为渲染函数,首先将模板解析成AST(抽象语法树),而后使用AST生成渲染函数。html

首先咱们要知道Vue每次渲染,都会生成一份新的vNode与旧的vNode进行对比,在生成渲染函数以前还会遍历一遍AST,为全部的静态节点作一个编辑,在从新渲染时,不会生成新得节点,而是直接克隆已存在的以前的静态节点。java

因此整体过程是:将模板解析成AST=>遍历AST标记静态节点=>使用AST生成渲染函数正则表达式

在这里插入图片描述

模板解析成AST

在这一步骤中,须要通过解析器将模板解析AST,而后还须要通过优化器,遍历AST找出静态节点并标记。算法

解析器

在解析器内部还分红了文本解析器,HTML解析器和过滤器解析器。express

其中核心部分是HTML解析器,做用是用来解析字符串模板。变量解析器用于解析带有模板的文本变量,而不带用变量的文本节点就是刚才所说的静态节点,不须要解析。过滤器解析器用来解析过滤器。解析结果AST是一种以节点为结构的树形结构的对象,一个对象表示一个节点,对象的属性用来保存节点所须要的数据。数组

解析模板例如:函数

<div>
  <p>{{name}}</p>
</div>
复制代码

解析成AST以后:post

//里面的内容后续会解释
{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
    {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
        type: 2,
        text: "{{name}}",
        static: false,
        expression: "_s(name)"
      }]
    }
  ]
}
复制代码

解析器在解析HTML的过程当中会不断触发各类钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。性能

例如:

parseHTML(template, {
    start (tag, attrs, unary) {
        // 每当解析到标签的开始位置时,触发该函数
    },
    end () {
        // 每当解析到标签的结束位置时,触发该函数
    },
    chars (text) {
        // 每当解析到文本时,触发该函数
    },
    comment (text) {
        // 每当解析到注释时,触发该函数
    }
})
复制代码

咱们简单举一个例子来讲明上述方法是如何构建AST节点的:

<div><p>我是一个节点</p></div>
复制代码

首先,解析器会将html模板做为一段字符串模板从前向后进行解析,解析到<div>时,会触发一个标签开始的钩子函数start();而后解析到<p>时,又触发一次钩子函数start();接着解析到我是一个节点这行文本,此时触发了文本钩子函数chars();而后解析到</p>,触发了标签结束的钩子函数end();接着继续解析到</div>,此时又触发一次标签结束的钩子函数end(),解析结束。

start()函数你能够看做为HTML解析函数,他的三个参数分别是分别是tag、attrs和unary,分别表明标签名、标签的属性以及是不是自闭合标签。

而文本节点的解析函数chars和注释节点的解析函数comment都只有一个参数text。这是由于构建元素节点须要知道标签名、属性和是不是自闭合元素,而构建注释节点和文本节点时只须要知道文本内容便可。 咱们将上面的parseHTML()扩充一下:

//咱们模拟一个建立AST元素类型节点的函数
function createASTElement (tag, attrs, parent) {
    // 返回的是一个节点对象
    return {
        type: 1, // 指定节点类型 1.元素节点
        tag, // 指定节点
        attrsList: attrs, // 指定节点属性
        parent, // 指定是不是自闭合标签
        children: []
    }
}
parseHTML(template, {
    start (tag, attrs, unary) {
        // 每当解析到标签的开始位置时,触发该函数
        // 将标签名、标签的属性以及是不是自闭合标签传入
        let element = createASTElement(tag, attrs, currentParent)
    },
    end () {
        // 每当解析到标签的结束位置时,触发该函数
    },
    chars (text) {
        // 每当解析到文本时,触发该函数 
        // 返回的是一个文本节点对象 
        // 文本分两种类型 2.带变量的动态文本节点 3.不带变量的纯文本节点
        let element = {type: 3, text}
    },
    comment (text) {
        // 每当解析到注释时,触发该函数
        // 返回的是一个注释节点对象,注释文本和文本的区别是打上了isComment标记
        let element = {type: 3, text, isComment: true}
    }
})
复制代码

可是使用上述方式建立的节点虽然带有节点对象信息,可是是扁平的,没有层级关系,而Vue使用了出入栈的方式来构建一个AST结构对象,为以前的扁平数据实现层级关系。

每次解析HTML,都会使用一个栈来存储维护,当触发start()函数时,将当前构建的节点推入栈中;每当触发钩子函数end()时,就从栈中弹出上一个节点。举个例子:

<div>
    <h1>我是h1</h1>
    <p>我是文本</p>
</div>
复制代码
  1. 模板的开始位置是div的开始标签,此时发现栈是空的,这说明div节点是根节点,由于它没有父节点。最后,将div节点推入栈中,并将模板字符串中的div开始标签从模板中截取掉
    在这里插入图片描述
  2. 钩子函数里会忽略空格,同时会在模板中将这些空格截取掉。接下来发现是h1的开始标签,因而会触发钩子函数start,会先构建一个h1节点。此时发现栈里存的最近一个节点是div节点,这说明h1节点的父节点是div,因而将h1添加到div的子节点中(也就是children中),而且将h1节点推入栈中,同时从模板中将h1的开始标签截取掉。
    \[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PphkP0lG-1585107481902)(https://s1.ax1x.com/2020/03/25/8XUvYn.jpg)\]
  3. 这时模板的开始位置是一段文本,因而会触发钩子函数chars。先构建一个文本节点,此时发现栈中的最后一个节点是h1,这说明文本节点的父节点是h1,因而将文本节点添加到h1节点的子节点中。因为文本节点没有子节点,因此文本节点不会被推入栈中。最后,将文本从模板中截取掉。
    在这里插入图片描述
  4. 这时模板的开始位置是h1结束标签,因而会触发钩子函数end。end触发后,会把栈中最后一个节点(也就是h1)弹出来。
    在这里插入图片描述
  5. 第2个标签是p标签和h1标签同理,会先构建一个p节点,因为第4步已经从栈中弹出了一个节点h1,因此此时栈中的最近一个节点是div,因而将p推入div的子节点中,最后将p推入到栈中,从模板中截取掉。而后会同样构建文本节点,截取,最后根据p结束标签触发钩子函数end,把p节点弹出来。
    在这里插入图片描述
  6. 最后开始位置是div的结束标签,因而会触发钩子函数end。其逻辑与以前同样,把栈中的最后一个节点div弹出来,并将div的结束标签从模板中截取掉。HTML解析器已经运行完毕,这时咱们会发现栈已经空了,而咱们获得了一个完整的带层级关系的AST语法树
    \[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qOVcHowI-1585107971471)(https://s1.ax1x.com/2020/03/25/8Xw1JA.jpg)\]

其中对开始标签,结束标签,还有标签属性的解析基本是使用了大量正则表达式:去解析<div </div> : class=这样的字符串,去断定这是一个什么标签该去触发什么函数,不作过多描述。

这个过程如何解析HTML中的注释,条件注释,DOCTYPE,文本?

HTML中的注释,判断<!--,经过indexOf找到注释结束位置-->的下标,而后将结束位置前的字符都截取掉。条件注释注释用提早的表达式判断<,条件注释会被直接截取掉。DOCTYPE直接匹配这段字符,根据它的length属性来决定要截取多长的字符串。文本咱们只须要找到>与下一个<在什么位置,这以前的全部字符都属于文本。

节点不完整?

<div><p></div>
复制代码

在上面的代码中,p标签没有结束标签,那么当HTML解析器解析到div的结束标签时,发现栈内元素倒是p标签。就会从栈顶向栈底遍历寻找到div标签,在找到div标签以前遇到的全部其余标签都会标记为忘记闭合的标签,在非生产环境下在控制台打印警告提示。

文本解析器

为何文本解析器要单独说,由于文本其实分两种类型,一种是纯文本,另外一种是带变量的文本。

Hello name
	Hello {{name}}
复制代码

若是是纯文本,不须要进行任何处理;但若是是带变量的文本,那么须要使用文本解析器进一步解析。由于带变量的文本在使用虚拟DOM进行渲染时,须要将变量替换成变量中的值。

  1. 第一步要作的事情就是使用正则表达式来判断文本是不是带变量的文本,也就是检查文本中是否包含{{xxx}}这样的语法。
  2. 咱们建立一个数组,把变量左边的文本添加到数组中,而后把变量改为_s(变量名)这样的函数形式也添加到数组中。若是变量后面还有变量,则重复以上动做。
  3. 数组元素的顺序和文本的顺序是一致的,此时将这些数组元素用+连起来变成字符串(_s(变量名)是Vue中对应的解析变量函数,会返回该变量的值)

优化器

静态节点:

<p>我就是一个纯文本的静态节点</p>
复制代码

优化器则是将解析完的AST进行遍历,找出静态节点并标记,在下次更新对比虚拟DOM的vNode时,若是发现这两个节点是静态节点,则直接跳过更新节点的流程。达到进一步避免一些无用的DOM操做来提高性能,由于静态节点在首次渲染后必定不会改变。

AST生成渲染函数

代码生成器

代码生成器是将解析完的AST转化为渲染函数须要的内容,这个内容叫代码字符串,例如:

<div>
  <p>{{name}}</p>
</div>
复制代码
// 解析为AST
{
  tag: "div"
  type: 1,
  staticRoot: false, // 是否为根静态节点(根静态节点下的所欲节点会认为是静态节点)
  static: false, // 是否为根静态节点
  plain: true,
  parent: undefined,
  attrsList: [], // 元素属性
  attrsMap: {},
  children: [
      {
      tag: "p"
      type: 1, // 
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...}, // 全部子节点会带有父节点信息
      attrsList: [],
      attrsMap: {},
      children: [{
          type: 2,
          text: "{{name}}",
          static: false,
          expression: "_s(name)"
      }]
    }
  ]
}
复制代码
// 解析完的AST生成代码字符串
`with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])}`
复制代码

以后将这串代码字符串传到Vue的渲染函数中,渲染函数根据参数结构,调用相关的建立vNode的方法(生成后的代码字符串中看到了有几个函数调用 _c,_v,_s,这是Vue内部的一些渲染函数,_c能够建立元素类型的vNode,_v能够建立文本类型的vNode,_e能够建立注释类型的vNode)最后组成一份虚拟DOM结构。

咱们拿_c来解释一下这个字符串的结构:

在这里插入图片描述
将其分解来看,拿建立元素类型的函数_c()来讲,图中1和3是第一个参数:HTML标签名,图中2和4是第三个参数:children,这个函数存在第二个可选项参数:元素上使用的属性所对应的数据对象,例如:

<p title="biaoti">name</p>
复制代码
with(this){
  return _c(
    'p', // 标签名
    {
      attrs:{"title":"biaoti"},
    }, // 属性
    [_v("name")] // 子节点
  )
}
复制代码

代码生成器的整体逻辑其实就是递归ATS,而后根据ATS结构拼出这样的_c('div',[_c('p',[_v(_s(name))])]) 字符串,再将其传入渲染函数执行。

至于具体的AST转换过程就不作深刻解释,会令文章显得枯燥。

总结

咱们以上简单讲述了Vue对模板编译的总体流程:解析器(模板字符串转换成AST),优化器(标记静态节点)和代码生成器(将AST装换成带结构的代码字符串)。

解析器经过使用一个栈来维护节点,每从模板字符串中截取一个节点字符串,就将其推入栈中,同时构建一个AST节点,一直到结束节点在将其推出栈,如此循环最后构建出一套带有结构的AST对象。

优化器是经过遍历AST节点,对其中的静态节点作标记,同时最后标记处根静态节点,节省部分没必要要的性能消耗。

代码生成器也是经过遍历去拼出一个渲染函数执行的代码字符串,遍历的过程根据不一样的节点类型type调用不一样的生成字符串方法,最后拼出一个完整的 render 函数须要的代码字符串。

后续还有两篇:

《Vue不看源码懂原理》系列——Vue的diff算法不难懂(直接传送)

《Vue不看源码懂原理》系列——Vue的实例函数和指令解密(下周)

之有一篇用心总结的《Javascript垃圾回收原理》没太有响应,我以为你们能够看一看,耐心一下的话比较好理解。

点个赞,我加油

点关注,不迷路,哈哈哈

相关文章
相关标签/搜索