模板编译的前世今生(Vue版)

前世

版本 年份
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343