美文网首页
模板编译的前世今生

模板编译的前世今生

作者: Napster99 | 来源:发表于2020-11-06 15:35 被阅读0次

    前世

    版本 年份
    html 1990
    html4 1997
    html5 2014

    template 作为html5 提供的新标签,意为“模板”。

    <template>
        <h1>我是放在template标签里的模板</h1>
    </template>
    
    template.png
    const tpl = document.querySelector('template')
    console.log(tpl.childNodes)          //NodeList []
    console.log(tpl.content.childNodes)  //NodeList(3) [text, h1, text]
    console.log(t.nodeType)  //1 - Element
    console.log(t.content.nodeType)  //11 - DocumentFragment
    
    节点序号 节点类型 描述 子节点
    1 Element 代表元素 Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference
    2 Attr 代表属性 Text, EntityReference
    3 Text 代表元素或属性中的文本内容 None
    4 CDATASection 代表文档中的 CDATA 部分(不会由解析器解析的文本) None
    5 EntityReference 代表实体引用 Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
    6 Entity 代表实体 Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
    7 ProcessingInstruction 代表处理指令 None
    8 Comment 代表注释 None
    9 Document 代表整个文档(DOM 树的根节点) Element, ProcessingInstruction, Comment, DocumentType
    10 DocumentType 向为文档定义的实体提供接口 None
    11 DocumentFragment 代表轻量级的 Document 对象,能够容纳文档的某个部分 Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
    12 Notation 代表 DTD 中声明的符号 None
    <script type="text/template">
         <h1>我是放在script标签里的模板</h1>
    </script>
    

    注:<script>设置了type="text/template",标签里面的内容不会被执行,也不会在页面上显示。这种称为是微模板,这个概念后续广泛用于单页面应用程序(SPA)。John Resig对此进行了很好的解释(https://johnresig.com/blog/javascript-micro-templating)。

    今生

    一、Vue template 模板编译

       <div id="app">
         <h1>我是放在vue template标签里的模板</h1>
         <my-tpl></my-tpl>
      </div>
      <template id="my-tpl">
        <h2>现在的时间是{{time}}</h2>
      </template>
    <script>
      new Vue({
        el: '#app',
        data() {
          return {
               time : Date.now()
            }
        },
        template: '#my-tpl'
     })
    </script>
    
    模板渲染过程

    大家会想,在使用模板的时候,经常会使用一些js表达式或者一些指令等,然后在html语法中这些功能是不存在的,为何在类似Vue的模板中就可以使用呢?这就是通过模板编译实现的。
    模板编译的作用就是将模板解析成渲染函数,渲染函数的作用就是生成一份vnode。


    模板编译流程

    1、 模板解析(解析器)

    • 将模板解析为AST
    <div>
      <h1>{{title}}</h1>
    </div>
    

    通过vue-template-compiler@2.6.11转换后得到的AST

    {
      "type": 1,  //1 元素类型  2 变量text  3 普通文本(普通文字/空格/换行) ...
      "tag": "div",
      "attrsList": [],
      "attrsMap": {},
      "rawAttrsMap": {},
      "children": [
        {
          "type": 1,
          "tag": "h1",
          "attrsList": [],
          "attrsMap": {},
          "rawAttrsMap": {},
          "parent": "[Circular ~]",
          "children": [
            {
              "type": 2,
              "expression": "_s(title)",
              "tokens": [
                {
                  "@binding": "title"
                }
              ],
              "text": "{{title}}",
              "start": 12,
              "end": 21,
              "static": false
            }
          ],
          "start": 8,
          "end": 26,
          "plain": true,
          "static": false,
          "staticRoot": false
        }
      ],
      "start": 0,
      "end": 33,
      "plain": true,
      "static": false,
      "staticRoot": false
    }
    
    解析器具体分为以下几种类型

    1.HTML解析器
    2.文本解析器
    3.过滤器解析器

    Vue框架主要通过complier/parser目录下三个文件完成
    html-parser.js text-parser.js filter-parser.js

    主要思路是利用了栈(stack)的先进后出/后进先出的特性,完成对模板的解析工作。

    //complier/parser/index.js
    const stack = []
    parseHTML(template, {
        start (tag, attrs, unary, start, end) {
          stack.push(element)
        },
        end (tag, start, end) {
          const element = stack[stack.length - 1]
          // pop stack
          stack.length -= 1
          closeElement(element)
        },
        chars (text: string, start: number, end: number) {},
        comment (text: string, start, end) {}
    })
    
    export function parseHTML(html, options) {
      const stack = [];
      const expectHTML = options.expectHTML;
      const isUnaryTag = options.isUnaryTag || no;
      const canBeLeftOpenTag = options.canBeLeftOpenTag || no;
      let index = 0;
      let last, lastTag;
      while (html) {
          if (options.chars && text) {
            options.chars(text, index - text.length, index);
          }
          // End tag:
          const endTagMatch = html.match(endTag);
          parseEndTag(endTagMatch[1], curIndex, index);
          // Start tag:
          const startTagMatch = parseStartTag();
          handleStartTag(startTagMatch);
      }
    
      function handleStartTag(match) {
        if (options.start) {
          options.start(tagName, attrs, unary, match.start, match.end);
        }
      }
    
      function parseEndTag(tagName, start, end) {
        options.end(tagName, start, end);
      }
    }
    
    

    至此完成ASTElement对象的生成

    2、模板优化(优化器)

    • 递归遍历AST标记静态节点
    optimize(ast, options)
    
    //complier/optimizer.js
    function markStatic (node: ASTNode) {
      node.static = isStatic(node)
      if (node.type === 1) {
        // do not make component slot content static. this avoids
        // 1. components not able to mutate slot nodes
        // 2. static slot content fails for hot-reloading
        if (
          !isPlatformReservedTag(node.tag) &&
          node.tag !== 'slot' &&
          node.attrsMap['inline-template'] == null
        ) {
          return
        }
        for (let i = 0, l = node.children.length; i < l; i++) {
          const child = node.children[i]
          markStatic(child)
          if (!child.static) {
            node.static = false
          }
        }
        if (node.ifConditions) {
          for (let i = 1, l = node.ifConditions.length; i < l; i++) {
            const block = node.ifConditions[i].block
            markStatic(block)
            if (!block.static) {
              node.static = false
            }
          }
        }
      }
    }
    
    function markStaticRoots (node: ASTNode, isInFor: boolean) {
      if (node.type === 1) {
        if (node.static || node.once) {
          node.staticInFor = isInFor
        }
        // For a node to qualify as a static root, it should have children that
        // are not just static text. Otherwise the cost of hoisting out will
        // outweigh the benefits and it's better off to just always render it fresh.
        if (node.static && node.children.length && !(
          node.children.length === 1 &&
          node.children[0].type === 3
        )) {
          node.staticRoot = true
          return
        } else {
          node.staticRoot = false
        }
        if (node.children) {
          for (let i = 0, l = node.children.length; i < l; i++) {
            markStaticRoots(node.children[i], isInFor || !!node.for)
          }
        }
        if (node.ifConditions) {
          for (let i = 1, l = node.ifConditions.length; i < l; i++) {
            markStaticRoots(node.ifConditions[i].block, isInFor)
          }
        }
      }
    }
    

    递归标记static / staticRoot的过程

    function isStatic (node: ASTNode): boolean {
      if (node.type === 2) { // expression
        return false
      }
      if (node.type === 3) { // text
        return true
      }
      return !!(node.pre || (
        !node.hasBindings && // no dynamic bindings
        !node.if && !node.for && // not v-if or v-for or v-else
        !isBuiltInTag(node.tag) && // not a built-in
        isPlatformReservedTag(node.tag) && // not a component
        !isDirectChildOfTemplateFor(node) &&
        Object.keys(node).every(isStaticKey)
      ))
    }
    

    当模板被解析器解析成AST时,会根据不同的元素类型设置不同的type值。
    type: 1 元素节点
    type: 2 带变量的动态文本节点
    type: 3 不带变量的纯文本节点
    当type为3时,很好理解必然是静态节点,当type为1时说明是一个元素节点,此时判断稍有复杂。当有v-pre即可判断是一个静态节点,否则就必须满足以下条件才会判定是一个静态节点。

    1.不能使用动态的绑定语法(v-/@/:等开头的属性)
    2.不能使用v-if or v-for or v-else指令
    3.不能使用内置标签(slot/component)
    4.不能使用组件,必须是浏览器保留标签(div/p 等)
    5.节点的父节点不能是template标签
    6.节点不能动态节点的相关属性

    function isDirectChildOfTemplateFor (node: ASTElement): boolean {
      while (node.parent) {
        node = node.parent
        if (node.tag !== 'template') {
          return false
        }
        if (node.for) {
          return true
        }
      }
      return false
    }
    
    

    3、代码生成(代码生成器)

    • 使用AST生成渲染函数,编译的最后就是把优化的AST树转换成可执行的代码
    const compiler = require("vue-template-compiler");
    const info = compiler.compile("<div></div>");
    

    render: "with(this){return _c('div')}",
    _c 函数定义在 src/core/instance/render.js

    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    
    jscode转换流程
    以上过程 入口文件,源码如下:src/compiller/index.js
    /* @flow */
    
    import { parse } from './parser/index'
    import { optimize } from './optimizer'
    import { generate } from './codegen/index'
    import { createCompilerCreator } from './create-compiler'
    
    // `createCompilerCreator` allows creating compilers that use alternative
    // parser/optimizer/codegen, e.g the SSR optimizing compiler.
    // Here we just export a default compiler using the default parts.
    export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    

    二、模板引擎

    通常我们在赋值界面新的数据的时候,经常会以下方式实现,这也是最原始的实现。看似没什么问题,但如果数据很多很复杂的情况下,通过字符串拼接的模式就会显得非常麻烦,累赘,最后一定苦不堪言。

    const name = "peter"
    document.body.innerHTML = "<h1>My name is "+name+"</h1>"
    

    随着前端应用变得日益复杂的背景下,数据与界面分离的必要性越来越大,很多JS的模板引擎因此而生。如用模板引擎实现方式,代码如下:

    <script id="tpl" type="text/template">
      <h1>My name is <%= name %></h1>
    </script>
    <script>
    const tpl = document.getElementById('tpl').innerHTML;
    template(tpl, {name: "peter"});  //template模板引擎函数
    
    function template(dom, data) {
      // do something
     //返回拼接好的字符串
    } 
    </script>
    

    模板引擎函数就是通过一系列解析拼接过程,返回一个可执行的渲染函数,主要步骤具体如下:
    1、模板获取
    2、将DOM结构与js变量、表达式等分离,词法分析生成AST
    3、组装完成的字符串通过Function生成动态HTML代码

    目前市面上已经出了有很多类型的模板引擎,性能对比如图所示

    模板引擎负荷测试

    baiduTemplate: http://baidufe.github.io/BaiduTemplate/

    artTemplate: https://github.com/aui/artTemplate

    juicerhttps://github.com/PaulGuo/Juicer

    doThttp://olado.github.com/doT/

    tmplhttps://github.com/BorisMoore/jquery-tmpl

    handlebars:http://handlebarsjs.com

    easyTemplatehttps://github.com/qitupstudios/easyTemplate

    underscoretemplate: http://underscorejs.org/

    mustache:https://github.com/janl/mustache.js

    kissytemplate:https://github.com/ktmud/kissy

    相关文章

      网友评论

          本文标题:模板编译的前世今生

          本文链接:https://www.haomeiwen.com/subject/uydgvktx.html