前世
| 版本 | 年份 |
|---|---|
| html | 1990 |
| html4 | 1997 |
| html5 | 2014 |
template 作為html5 提供的新標(biāo)簽,意為“模板”。
<template>
<h1>我是放在template標(biāo)簽里的模板</h1>
</template>

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
| 節(jié)點(diǎn)序號(hào) | 節(jié)點(diǎn)類型 | 描述 | 子節(jié)點(diǎn) |
|---|---|---|---|
| 1 | Element | 代表元素 | Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference |
| 2 | Attr | 代表屬性 | Text, EntityReference |
| 3 | Text | 代表元素或?qū)傩灾械奈谋緝?nèi)容 | None |
| 4 | CDATASection | 代表文檔中的 CDATA 部分(不會(huì)由解析器解析的文本) | None |
| 5 | EntityReference | 代表實(shí)體引用 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
| 6 | Entity | 代表實(shí)體 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
| 7 | ProcessingInstruction | 代表處理指令 | None |
| 8 | Comment | 代表注釋 | None |
| 9 | Document | 代表整個(gè)文檔(DOM 樹(shù)的根節(jié)點(diǎn)) | Element, ProcessingInstruction, Comment, DocumentType |
| 10 | DocumentType | 向?yàn)槲臋n定義的實(shí)體提供接口 | None |
| 11 | DocumentFragment | 代表輕量級(jí)的 Document 對(duì)象,能夠容納文檔的某個(gè)部分 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
| 12 | Notation | 代表 DTD 中聲明的符號(hào) | None |
<script type="text/template">
<h1>我是放在script標(biāo)簽里的模板</h1>
</script>
注:<script>設(shè)置了type="text/template",標(biāo)簽里面的內(nèi)容不會(huì)被執(zhí)行,也不會(huì)在頁(yè)面上顯示。這種稱為是微模板,這個(gè)概念后續(xù)廣泛用于單頁(yè)面應(yīng)用程序(SPA)。John Resig對(duì)此進(jìn)行了很好的解釋(https://johnresig.com/blog/javascript-micro-templating)。
今生
一、Vue template 模板編譯
<div id="app">
<h1>我是放在vue template標(biāo)簽里的模板</h1>
<my-tpl></my-tpl>
</div>
<template id="my-tpl">
<h2>現(xiàn)在的時(shí)間是{{time}}</h2>
</template>
<script>
new Vue({
el: '#app',
data() {
return {
time : Date.now()
}
},
template: '#my-tpl'
})
</script>

大家會(huì)想,在使用模板的時(shí)候,經(jīng)常會(huì)使用一些js表達(dá)式或者一些指令等,然后在html語(yǔ)法中這些功能是不存在的,為何在類似Vue的模板中就可以使用呢?這就是通過(guò)模板編譯實(shí)現(xiàn)的。
模板編譯的作用就是將模板解析成渲染函數(shù),渲染函數(shù)的作用就是生成一份vnode。

1、 模板解析(解析器)
- 將模板解析為AST
<div>
<h1>{{title}}</h1>
</div>
通過(guò)vue-template-compiler@2.6.11轉(zhuǎn)換后得到的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.過(guò)濾器解析器
Vue框架主要通過(guò)complier/parser目錄下三個(gè)文件完成
html-parser.js text-parser.js filter-parser.js
主要思路是利用了棧(stack)的先進(jìn)后出/后進(jìn)先出的特性,完成對(duì)模板的解析工作。
//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對(duì)象的生成
2、模板優(yōu)化(優(yōu)化器)
- 遞歸遍歷AST標(biāo)記靜態(tài)節(jié)點(diǎn)
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)
}
}
}
}
遞歸標(biāo)記static / staticRoot的過(guò)程
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)
))
}
當(dāng)模板被解析器解析成AST時(shí),會(huì)根據(jù)不同的元素類型設(shè)置不同的type值。
type: 1 元素節(jié)點(diǎn)
type: 2 帶變量的動(dòng)態(tài)文本節(jié)點(diǎn)
type: 3 不帶變量的純文本節(jié)點(diǎn)
當(dāng)type為3時(shí),很好理解必然是靜態(tài)節(jié)點(diǎn),當(dāng)type為1時(shí)說(shuō)明是一個(gè)元素節(jié)點(diǎn),此時(shí)判斷稍有復(fù)雜。當(dāng)有v-pre即可判斷是一個(gè)靜態(tài)節(jié)點(diǎn),否則就必須滿足以下條件才會(huì)判定是一個(gè)靜態(tài)節(jié)點(diǎn)。
1.不能使用動(dòng)態(tài)的綁定語(yǔ)法(v-/@/:等開(kāi)頭的屬性)
2.不能使用v-if or v-for or v-else指令
3.不能使用內(nèi)置標(biāo)簽(slot/component)
4.不能使用組件,必須是瀏覽器保留標(biāo)簽(div/p 等)
5.節(jié)點(diǎn)的父節(jié)點(diǎn)不能是template標(biāo)簽
6.節(jié)點(diǎn)不能動(dòng)態(tài)節(jié)點(diǎn)的相關(guān)屬性
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生成渲染函數(shù),編譯的最后就是把優(yōu)化的AST樹(shù)轉(zhuǎn)換成可執(zhí)行的代碼
const compiler = require("vue-template-compiler");
const info = compiler.compile("<div></div>");
render: "with(this){return _c('div')}",
_c 函數(shù)定義在 src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

以上過(guò)程 入口文件,源碼如下:
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
}
})
二、模板引擎
通常我們?cè)谫x值界面新的數(shù)據(jù)的時(shí)候,經(jīng)常會(huì)以下方式實(shí)現(xiàn),這也是最原始的實(shí)現(xiàn)??此茮](méi)什么問(wèn)題,但如果數(shù)據(jù)很多很復(fù)雜的情況下,通過(guò)字符串拼接的模式就會(huì)顯得非常麻煩,累贅,最后一定苦不堪言。
const name = "peter"
document.body.innerHTML = "<h1>My name is "+name+"</h1>"
隨著前端應(yīng)用變得日益復(fù)雜的背景下,數(shù)據(jù)與界面分離的必要性越來(lái)越大,很多JS的模板引擎因此而生。如用模板引擎實(shí)現(xiàn)方式,代碼如下:
<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模板引擎函數(shù)
function template(dom, data) {
// do something
//返回拼接好的字符串
}
</script>
模板引擎函數(shù)就是通過(guò)一系列解析拼接過(guò)程,返回一個(gè)可執(zhí)行的渲染函數(shù),主要步驟具體如下:
1、模板獲取
2、將DOM結(jié)構(gòu)與js變量、表達(dá)式等分離,詞法分析生成AST
3、組裝完成的字符串通過(guò)Function生成動(dòng)態(tài)HTML代碼
目前市面上已經(jīng)出了有很多類型的模板引擎,性能對(duì)比如圖所示

baiduTemplate: http://baidufe.github.io/BaiduTemplate/
artTemplate: https://github.com/aui/artTemplate
juicer:https://github.com/PaulGuo/Juicer
doT:http://olado.github.com/doT/
tmpl:https://github.com/BorisMoore/jquery-tmpl
handlebars:http://handlebarsjs.com
easyTemplate:https://github.com/qitupstudios/easyTemplate
underscoretemplate: http://underscorejs.org/