vue 快速入門 系列 —— 模板
其他章節請看:
模板
前面提到 vue 中的虛擬 dom 主要做兩件事:
- 提供與真實節點對應的 vNode
- 新舊 vNode 對比,尋找差異,然後更新視圖
①、vNode 從何而來?
前面也說了聲明式框架只需要我們描述狀態
與 dom
之間的映射關係。狀態
到視圖
的轉換,框架會給我們做。
②、用什麼描述狀態與 dom 之間的映射關係?
Tip:jQuery 是命令式的框架,現代的 vue、react屬於聲明式框架。
簡介
首先公布問題 ② 的答案:用模板描述狀態與 dom 之間的映射關係。
於是我們知道這三者之間的關係:
狀態 –> 模板 –> dom
模板編譯器
請先看一個模板的示例:
<span>Message: {{ msg }}</span>
<h1 v-if="awesome">Vue is awesome!</h1>
<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
v-if
、v-for
、{{}}
是什麼?html 中根本不存在這些東西。
我們知道 javascript 程式碼只有 javascript 引擎認識,同理,模板也只有類似模板引擎的東西認識它。
在 vue 中,類似模板引擎的叫做模板編譯器。通過模板編譯器將模板編譯成渲染函數,而執行渲染函數就會使用當前最新的狀態生成一份 vnode。
模板 — 編譯 –> 渲染函數 — 執行 –> vNode
至此,問題 ① 的答案顯而易見了,vNode 由渲染函數生成。
模板和虛擬 dom 所處位置
我們根據上文,能輕易的知道模板
所處位置:
狀態 –> 模板
subgraph a[模板]
模板 — 編譯 –> 渲染函數 — 執行 –> vNode
end
vNode –> 視圖
在 虛擬 dom 的作用 中,我們知道虛擬 dom
所處位置:
狀態 –> a
subgraph a[虛擬 dom]
vNode
patch
end
a –> 視圖
最後,我們將這兩個圖合併成一個即可:
狀態 –> 模板
subgraph a[模板]
模板 — 編譯 –> 渲染函數
end
渲染函數 — 執行 –> b
subgraph b[虛擬 dom]
vNode
patch
end
b –> 視圖
Tip: 將渲染函數指向虛擬 dom
,是因為 vue 官網有這麼一句話:「虛擬 DOM」是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼
模板是如何編譯成渲染函數,以及為什麼執行渲染函數就可以生成 vNode?請繼續看下文。
渲染函數
將模板編譯成渲染函數,只需要 3 步:
- 解析器:將HTML字元串轉換為
AST
AST
就是一個普通的 javascript 對象,描述了該節點的資訊以及子節點的資訊,類似 vNode
- 優化器:遍歷 AST,標記靜態節點,用於提高性能
<p>hello</p>
是靜態節點,渲染之後不會再改變<p>{{hello}}</p>
不是靜態節點,因為狀態會影響它
- 生成器:使用
AST
生成渲染函數
- 執行渲染函數就會根據現在的狀態生成一份虛擬 dom(
vNode
)
- 執行渲染函數就會根據現在的狀態生成一份虛擬 dom(
為什麼是這 3 步?不重要,這只是一種演算法而已。
Tip:倘若我們能理解這 3 步確實能將模板編譯成渲染函數,而渲染函數執行後能生成 vNode。那麼 vue 中模板這一部分,也算是入門了。
分析
我們採用最直接的方法,即運行一段程式碼,看看 AST
是什麼?優化器
做了什麼?渲染函數
是什麼?渲染函數又是如何生成 vNode
的?
程式碼很簡單,一個 html 頁面,裡面引入 vue.js
,然後在 vue.js
中打上一個斷點(輸入 debugger),最後運行 test.html
:
// test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src='vue.js'></script>
</head>
<body>
<!-- 模板 -->
<div id='app'>
<p title='testTitle' @click='say'>number: {{num}}</p>
</div>
<!-- /模板 -->
<script>
const app = new Vue({
el: '#app',
data: {
num: 0
},
methods: {
say(){
this.num += 1;
}
}
})
</script>
</body>
</html>
// vue.js
// 打上斷點(行{1})
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
debugger // {1}
// 解析器
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 優化器
optimize(ast, options);
}
// 生成器
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
AST
執行完 var ast = parse(template.trim(), options);
,ast
為:
// ast:
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}"
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
}
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
]
}
於是我們知道,AST
就是一個普通的 javascript 對象,類似虛擬節點或 dom Node,裡面有節點的類型、屬性、子節點等等。
優化器的作用
將 ast 交給優化器處理後(optimize(ast, options);
),ast
為:
// 優化器:(在上一步的基礎上增加 static 和 staticRoot 兩個屬性)
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}",
"static":false
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
},
"static":false,
"staticRoot":false
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
],
"static":false,
"staticRoot":false
}
優化器給 ast
增加 static
和 staticRoot
兩個屬性,用於標記靜態節點。
生成器
接著將 ast
交給生成器處理(var code = generate(ast, options);
),code
為:
// code
{"render":"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('p',{attrs:{\"title\":\"testTitle\"},on:{\"click\":say}},[_v(\"number: \"+_s(num))])])}","staticRenderFns":[]}
將 code.render
字元串格式化:
// code.render
with(this) {
return _c(
'div',
{
attrs: {
"id": "app"
}
},
[
_c(
'p',
{
attrs: {
"title": "testTitle"
},
on: {
"click": say
}
},
[
_v("number: " + _s(num))
]
)
]
)
}
code.render
這個字元串導出到外界,會放到一個函數中,這個函數就是渲染函數。
不理解?沒關係,我們先看另一個示例:
new Function ([arg1[, arg2[, …argN]],] functionBody)
const obj = {name: 'ph'}
const code = `with(this){console.log('hello: ' + name)}`
const renderFunction = new Function(code)
renderFunction.call(obj)
// 等同於
const obj = {name: 'ph'}
function renderFunction(){
with(this){console.log('hello: ' + name)}
}
renderFunction.call(obj) // hello: ph
這下理解了吧。我們將 code.render
指向的字元串導出到外界,外界利用 new Function()
創建渲染函數。
前面提到執行渲染函數會生成 vNode
。看看 code.render
就能知曉,裡面出現的 _v
和 _c
,分別用於生成元素類型的 vNode 和文本類型的 vNode。請看相關源碼:
// 創建文本類型的 vNode
target._v = createTextVNode;
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 創建元素類型的 vNode
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
...
return _createElement(context, tag, data, children, normalizationType)
}
Tip: 關於 vue 中解析器、優化器和生成器裡面具體是如何實現的,本系列就不展開了。
其他章節請看: