[TOC]
****
## 1 簡介
### 1-1 傳統模板引擎
傳統模板引擎編譯生成HTML字符串。
通常在后臺腳本語言中將模板和數據合并成html字符串輸出到前端
或者后臺輸出數據,前端使用模板引擎組合數據和模板生成界面
當數據發生變化的時候,最簡單的就是模板重新渲染,或者模板引擎局部界面重新渲染。
當頁面包含的數據很多的時候,這種代碼操作中包含很多DOM操作,編碼復雜不便維護。而且重新渲染導致性能問題
### 1-2 vdom
虛擬dom則將這個過程分為兩步
第一步編譯模板生成vdom的渲染函數render
在需要的渲染的時候調用渲染函數render組成的樹狀vdom
在mvvm數據綁定的結構中,將視圖部分的渲染組織為vdom的渲染函數。可以優化編碼結構與渲染效率
## 2 模板引擎和vdome
### 2-1 模板引擎簡介
>[info] 下面是一個普通的模板引擎語法。支持循環語句(each) 條件語句(if elseif)和文本填充{}。
~~~
;簡單的模板語法
<div>
<h1>{title}</h1>
<ul>
{each users as user i}
<li class="user-item">
<img src="/avatars/{user.id}" />
<span>NO.{i + 1} - {user.name}</span>
{if user.isAdmin}
I am admin
{elseif user.isAuthor}
I am author
{else}
I am nobody
{/if}
</li>
{/each}
</ul>
</div>
~~~
>[info] 對于上述模板語法,輸入下面數據
~~~
var data = {
title: 'Users List',
users: [
{id: 'user0', name: 'Jerry', isAdmin: true},
{id: 'user1', name: 'Lucy', isAuthor: true},
{id: 'user2', name: 'Tomy'}
]
}
~~~
>[info] 模板引擎解析后生成的html字符串如下
~~~
<div>
<h1>Users List</h1>
<ul>
<li class="user-item">
<img src="/avatars/user0" />
<span>NO.1 - Jerry</span>
I am admin
</li>
<li class="user-item">
<img src="/avatars/user1" />
<span>NO.2 - Lucy</span>
I am author
</li>
<li class="user-item">
<img src="/avatars/user2" />
<span>NO.3 - Tomy</span>
I am nobody
</li>
</ul>
</div>
~~~
>[info] 將字符串插入文檔中即可實現渲染界面
### 2-2 vdom
>[info] 如果上述數據中的`data.title`發生變化,
則需要使用dom操作語法重新修改模板結構
可以參考ReactJs的JSX的做法,
將模板編譯為一個生成vdom的的render函數。
render函數接受傳入的數據生成不同的vdom。
然后可以根據vdom的算法diff和patch來比較局部渲染
~~~
;vdom簡單流程
;模板編譯生成渲染函數render
var render = template(tplString)
;接受初始化數據,返回初始化vdom結果
var root1 = render(state1)
;生成真正的dom,插入文檔中
var dom = root.render()
document.body.appendChild(dom)
;接受變化后的數據,生成另外的vdom
var root2 = render(state2)
;對比兩個vdom
var patches = diff(root1, root2)
;渲染對比結果
patch(dom, patches)
~~~
>[info] 這樣將模板編譯與結果渲染分離,可以重復使用編譯結果,提高執行效率。
>[info] 而將結果渲染分為比較和局部渲染,可以優化代碼的組織結構
>[info] 總體流程如下
~~~
1 模板編譯生成一個render函數,接受數據返回不同的vdom
2 接受數據生成vdom,結合渲染平臺生成真正的dom元素,插入文檔
3 數據變化后,渲染函數接受數據,生成新的vdom
4 新舊的vdom進行diff,然后局部patch到文檔的dom元素中
~~~
>[info] 模板編譯生成render函數結構簡單如下
~~~
function render (state) {
return el('div', {}, [
el('h1', {}, [state.title]),
el('ul', {}, state.users.map(function (user, i) {
return el('li', {"class": "user-item"}, [
el('img', {"src": "/avatars/" + user.id}, []),
el('span', {}, ['No.' + (i + 1) + ' - ' + user.name],
(user.isAdmin
? 'I am admin'
: uesr.isAuthor
? 'I am author'
: '')
])
}))
])
}
~~~
## 3 vdom-templat的實現思路
>[info] 簡單的模板引擎可以適合于正則表達式對相應模板字符串進行替換生成
>[info] 這里使用編譯原理的一部基礎知識,實現把一種語言(模板語法)編譯為另外一種語言(render的javascript函數)
## 4 編譯原理流程
>[info] 1 詞法分析:將輸入的模板分割為詞法單元
>[info] 2 語法分析:接受詞法單元,根據文法規則轉換為抽象語法樹
>[info] 3 代碼生成:遍歷AST,生成render函數體代碼

>[info] 4 可以將這個過程分為詞法分析(lex),語法分析(parser),代碼生成(codegen)三部分。
## 5 模板的文法定義
>[info] 可以使用文法描述模板結構的組成,作為詞法分析與語法分析的基礎
~~~
;模板整體
Stat -> Frag Stat | ε
Frag -> IfStat | EachStat | Node | text
;語句組織
IfStat -> '{if ...}' Stat ElseIfs Else '{/if}'
ElseIfs -> ElseIf ElseIfs | ε
ElseIf -> '{elseif ...}' Stat
Else -> '{else}' Stat | ε
EachStat -> '{each ...}' Stat '{/each}'
;節點組織
Node -> OpenTag NodeTail
OpenTag -> '/[\w\-\d]+/' Attrs
NodeTail -> '>' Stat '/\<[\w\d]+\>/' | '/>'
;節點屬性
Attrs -> Attr Attrs | ε
Attr -> '/[\w\-\d]/+' Value
;節點值
Value -> '=' '/"[\s\S]+"/' | ε
~~~
## 6 詞法分析 lexer
>[info] 模板文法中的基礎詞法單元如下
~~~
module.exports = {
TK_TEXT: 1, // 文本節點
TK_IF: 2, // {if ...}
TK_END_IF: 3, // {/if}
TK_ELSE_IF: 4, // {elseif ...}
TK_ELSE: 5, // {else}
TK_EACH: 6, // {each ...}
TK_END_EACH: 7, // {/each}
TK_GT: 8, // >
TK_SLASH_GT: 9, // />
TK_TAG_NAME: 10, // <div|<span|<img|...
TK_ATTR_NAME: 11, // 屬性名
TK_ATTR_EQUAL: 12, // =
TK_ATTR_STRING: 13, // "string"
TK_CLOSE_TAG: 13, // </div>|</span>|</a>|...
TK_EOF: 100 // end of file
}
~~~
>[info] 使用js的正則表達式引擎實現詞法分析,
解析輸入的模板字符串,生成詞法單元流
~~~
;詞法單元入口
function Tokenizer (input) {
this.input = input
this.index = 0
this.eof = false
}
var pp = Tokenizer.prototype
;詞法單元解析
pp.nextToken = function () {
this.eatSpaces()
return (
this.readCloseTag() ||
this.readTagName() ||
this.readAttrName() ||
this.readAttrEqual() ||
this.readAttrString() ||
this.readGT() ||
this.readSlashGT() ||
this.readIF() ||
this.readElseIf() ||
this.readElse() ||
this.readEndIf() ||
this.readEach() ||
this.readEndEach() ||
this.readText() ||
this.readEOF() ||
this.error()
)
}
~~~
>[info] 其中index標識字符串的位置。
nextToken()跳過所有空白字符串,
然后嘗試匹配不同類型的token
匹配失敗嘗試下一種,成功返回移動index,
上面的簡單模板列子經過詞法分析可以解析生成如下
~~~
{ type: 10, label: 'div' }
{ type: 8, label: '>' }
{ type: 10, label: 'h1' }
{ type: 8, label: '>' }
{ type: 1, label: '{title}' }
{ type: 13, label: '</h1>' }
{ type: 10, label: 'ul' }
{ type: 8, label: '>' }
{ type: 6, label: '{each users as user i}' }
{ type: 10, label: 'li' }
{ type: 11, label: 'class' }
{ type: 12, label: '=' }
{ type: 13, label: 'user-item' }
{ type: 8, label: '>' }
{ type: 10, label: 'img' }
{ type: 11, label: 'src' }
{ type: 12, label: '=' }
{ type: 13, label: '/avatars/{user.id}' }
{ type: 9, label: '/>' }
{ type: 10, label: 'span' }
{ type: 8, label: '>' }
{ type: 1, label: 'NO.' }
{ type: 1, label: '{i + 1} - ' }
{ type: 1, label: '{user.name}' }
{ type: 13, label: '</span>' }
{ type: 2, label: '{if user.isAdmin}' }
{ type: 1, label: 'I am admin\r\n ' }
{ type: 4, label: '{elseif user.isAuthor}' }
{ type: 1, label: 'I am author\r\n ' }
{ type: 5, label: '{else}' }
{ type: 1, label: 'I am nobody\r\n ' }
{ type: 3, label: '{/if}' }
{ type: 13, label: '</li>' }
{ type: 7, label: '{/each}' }
{ type: 13, label: '</ul>' }
{ type: 13, label: '</div>' }
{ type: 100, label: '$' }
~~~
## 7 語法解析parser
>[info] 將語法結構組織為first集合和follow集如下
~~~
FIRST(Stat) = {TK_IF, TK_EACH, TK_TAG_NAME, TK_TEXT}
FOLLOW(Stat) = {TK_ELSE_IF, TK_END_IF, TK_ELSE, TK_END_EACH, TK_CLOSE_TAG, TK_EOF}
FIRST(Frag) = {TK_IF, TK_EACH, TK_TAG_NAME, TK_TEXT}
FIRST(IfStat) = {TK_IF}
FIRST(ElseIfs) = {TK_ELSE_IF}
FOLLOW(ElseIfs) = {TK_ELSE, TK_ELSE}
FIRST(ElseIf) = {TK_ELSE_IF}
FIRST(Else) = {TK_ELSE}
FOLLOW(Else) = {TK_END_IF}
FIRST(EachStat) = {TK_EACH}
FIRST(OpenTag) = {TK_TAG_NAME}
FIRST(NodeTail) = {TK_GT, TK_SLASH_GT}
FIRST(Attrs) = {TK_ATTR_NAME}
FOLLOW(Attrs) = {TK_GT, TK_SLASH_GT}
FIRST(Value) = {TK_ATTR_EQUAL}
FOLLOW(Value) = {TK_ATTR_NAME, TK_GT, TK_SLASH_GT}
~~~
>遞歸下降的語法parser如下
~~~
var Tokenizer = require('./tokenizer')
var types = require('./tokentypes')
;語法解析入口
function Parser (input) {
this.tokens = new Tokenizer(input)
this.parse()
}
var pp = Parser.prototype
;詞法類型判斷
pp.is = function (type) {
return (this.tokens.peekToken().type === type)
}
;語法解析
pp.parse = function () {
this.tokens.index = 0
this.parseStat()
this.eat(types.TK_EOF)
}
;Stat解析
pp.parseStat = function () {
if (
this.is(types.TK_IF) ||
this.is(types.TK_EACH) ||
this.is(types.TK_TAG_NAME) ||
this.is(types.TK_TEXT)
) {
this.parseFrag()
this.parseStat()
} else {
// end
}
}
;Frag解析
pp.parseFrag = function () {
if (this.is(types.TK_IF)) return this.parseIfStat()
else if (this.is(types.TK_EACH)) return this.parseEachStat()
else if (this.is(types.TK_TAG_NAME)) return this.parseNode()
else if (this.is(types.TK_TEXT)) {
var token = this.eat(types.TK_TEXT)
return token.label
} else {
this.parseError('parseFrag')
}
}
;等等其他子解析過程
~~~
>[info] 遞歸下降分析,構建語法的樹狀表示結構AST如下
~~~
Stat: {
type: 'Stat'
members: [IfStat | EachStat | Node | text, ...]
}
IfStat: {
type: 'IfStat'
label: <string>,
body: Stat
elifs: [ElseIf, ...]
elsebody: Stat
}
ElseIf: {
type: 'ElseIf'
label: <string>,
body: Stat
}
EachStat: {
type: 'EachStat'
label: <string>,
body: Stat
}
Node: {
type: 'Node'
name: <string>,
attributes: <object>,
body: Stat
}
~~~
>[info] 可以使用具體嵌套功能的js對象或者數組表示樹狀結構的語法樹
語法樹的構建過程可以在語法分析階段同時進行,
最后上面的模板語法獲得下面的語法樹結構

## 8 代碼生成
>[info] 從js字符串構建新的函數可以使用`new Function`
~~~
var newFunc = new Function('a', 'b', 'return a + b')
newFunc(1, 2) // => 3
~~~
>[info] 可以將語法樹對應字符串作為第三個參數生成render函數
只需要對AST進行遍歷,維護一個數組來保存生成的render函數的代碼
~~~
;代碼生成入口
function CodeGen (ast) {
this.lines = []
this.walk(ast)
this.body = this.lines.join('\n')
}
var pp = CodeGen.prototype
;AST遍歷
pp.walk = function (node) {
if (node.type === 'IfStat') {
this.genIfStat(node)
} else if (node.type === 'Stat') {
this.genStat(node)
} else if (node.type === 'EachStat') {
...
}
...
}
;生成不同render
pp.genIfStat = function (node) {
var expr = node.label.replace(/(^\{\s*if\s*)|(\s*\}$)/g, '')
this.lines.push('if (' + expr + ') {')
if (node.body) {
this.walk(node.body)
}
if (node.elseifs) {
var self = this
_.each(node.elseifs, function (elseif) {
self.walk(elseif)
})
}
if (node.elsebody) {
this.lines.push(indent + '} else {')
this.walk(node.elsebody)
}
this.lines.push('}')
}
pp.genEachStat = function (node) {
var expr = node.label.replace(/(^\{\s*each\s*)|(\s*\}$)/g, '')
var tokens = expr.split(/\s+/)
var list = tokens[0]
var item = tokens[2]
var key = tokens[3]
this.lines.push(
'for (var ' + key + ' = 0, len = ' + list + '.length; ' + key + ' < len; ' + key + '++) {'
)
this.lines.push('var ' + item + ' = ' + list + '[' + key + '];')
if (node.body) {
this.walk(node.body)
}
this.lines.push('}')
}
// ...
~~~
>[info] 其中的lines包含相應的代碼結果
然后生成對應的render函數
~~~
var code = new CodeGen(ast)
var render = new Function('el', 'data', code.body)
~~~
>[info] el作為render函數的渲染目標節點,
data需要的數據,
code.body為解析后的render函數
## 9 diff和patch封裝
>[info] 對于diff和patch,可以將其封裝為setData的api。
每次數據變更,只需要setData就可以更新到DOM元素上
~~~
// vTemplate.compile 編譯模版字符串,返回一個函數
var usersListTpl = vTemplate.compile(tplStr)
// userListTpl 傳入初始數據狀態,返回一個實例
var usersList = usersListTpl({
title: 'Users List',
users: [
{id: 'user0', name: 'Jerry', isAdmin: true},
{id: 'user1', name: 'Lucy', isAuthor: true},
{id: 'user2', name: 'Tomy'}
]
})
// 返回的實例有 dom 元素和一個 setData 的 API
document.appendChild(usersList.dom)
// 需要變更數據的時候,setData 一下即可
usersList.setData({
title: 'Users',
users: [
{id: 'user1', name: 'Lucy', isAuthor: true},
{id: 'user2', name: 'Tomy'}
]
})
~~~
## 參考
[vdom模板引擎](https://segmentfault.com/a/1190000004420078)
[vdom完整代碼](https://github.com/livoras/virtual-template)
- 概述
- 框架結構
- 編譯入口(\entries)
- web-compiler.js(web編譯)
- web-runtime.js(web運行時)
- web-runtime-wih-compiler.js(web編譯運行)
- web-server-renderer.js(web服務器渲染)
- 核心實現 (\core)
- index.js(核心入口)
- config.js(核心配置)
- core\util(核心工具)
- core\observer(雙向綁定)
- core\vdom(虛擬DOM)
- core\global-api(核心api)
- core\instance(核心實例)
- 模板編譯(\compiler)
- compiler\parser(模板解析)
- events.js(事件解析)
- helper.js(解析助手)
- directives\ref.js(ref指令)
- optimizer.js(解析優化)
- codegen.js(渲染生成)
- index.js(模板編譯入口)
- web渲染(\platforms\web)
- compiler(web編譯目錄)
- runtime(web運行時目錄)
- server(web服務器目錄)
- util(web工具目錄)
- 服務器渲染(\server)
- render-stream.js(流式渲染)
- render.js(服務器渲染函數)
- create-renderer.js(創建渲染接口)
- 框架流程
- Vue初始化
- Vue視圖數據綁定
- Vue數據變化刷新
- Vue視圖操作刷新
- 框架工具
- 基礎工具(\shared)
- 模板編譯助手
- 核心實例工具
- Web渲染工具
- 基礎原理
- dom
- string
- array
- function
- object
- es6
- 模塊(Module)
- 類(Class)
- 函數(箭頭)
- 字符串(擴展)
- 代理接口(Proxy)
- 數據綁定基礎
- 數據綁定實現
- mvvm簡單實現
- mvvm簡單使用
- vdom算法
- vdom實現
- vue源碼分析資料