模板编译的前世今生

avatar
@https://www.tuya.com/

作者: 涂鸦-寒汀

来涂鸦工作: job.tuya.com/


前世

版本年份
html1990
html41997
html52014

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

<template>
    <h1>我是放在template标签里的模板</h1>
</template>

template标签在css中默认自带display:none,不会在页面上显示,也不会发起任何请求。

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

节点类型对照表

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

注:

今生

一、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)

codegen.png 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代码

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

模板引擎负荷测试

baiduTemplate:  baidufe.github.io/BaiduTempla…

artTemplate: github.com/aui/artTemp…

 juicergithub.com/PaulGuo/Jui…

doTolado.github.com/doT/

tmplgithub.com/BorisMoore/…

handlebars:handlebarsjs.com

easyTemplategithub.com/qitupstudio…

underscoretemplate: underscorejs.org/

mustache:github.com/janl/mustac…

kissytemplate:github.com/ktmud/kissy


来涂鸦工作: job.tuya.com/