在Vue中,Vue模板對應的就是Vue中的View(視圖)部分,也是Vue重中之一,而在Vue中要了解Vue模板我們就需要從兩個方面來着手,其一是Vue的模板語法,其二就是模板渲染。Vue模板語法是Vue中常用的技術之一,除非在應用程序中不用渲染視圖或者你的程序直接采用的是渲染函數( render() )。相較而言,模板語法較簡單一點,但對於模板的渲染(模板編譯)就會更為復雜一些,如果需要了解模板渲染就需要對Vue的渲染函數,響應式原理之類的要有所了解。當然,如果你跟我一樣是初學者的話,建議你先花一點時間閱讀一下下面幾篇文章:
那咱們接下來先從Vue模板語法開始入手,應該這部分相對來說較簡單一點。
Vue模板語法
先來看一段最簡單的代碼:
1 <!-- App.vue --> 2 <template> 3 <div id="app"> 4 {{ message }} 5 </div> 6 </template>
上面代碼演示的僅僅Vue模板中的一種方式,也是最簡單和最常見的一種模板方式。在Vue中除了上述這種方式之外還有其他幾種方式,較為詳細的可以閱讀《Vue.js 定義組件模板的七種方式》一文。
這段代碼具體的含義是什么暫不說。
在Vue中,模板語法是邏輯和視圖之間的溝通橋梁,使用模板語法編寫的HTML會響應Vue實例中的各種變化,簡單地說,Vue實例中的邏輯可以隨心所欲的渲染在頁面上。正如上面的示例所示,如果我們Vue實例中邏輯讓message
發生變化時,那么瀏覽器客戶端就立即發生變化。
示例是一個最簡單的模板語法,但其中有一個角色是最為重要,那就是**插值(Mustache)標簽,常用{{}}
符號來表示。Vue模板中插值常見的使用方法主要有:文本、原始HTML、屬性、JavaScript表達式、指令和修飾符**等。
文本
插值中最常見的就是文本插值,正如上面的示例中的{{ message }}
,該標簽將會被替代為對應數據對象上message
屬性的值。無論何時,綁定的數據對象上message
屬性發生變化時,插值處的內容都會更新。
但也有一個額外的場景,那就是在模板語法中看是否使用了其他的指令,比如,在模板中要是使用了v-once
指令的話,那么該插值就是一次性地插值。也就是說,當數據改變時,插值處的內容不會更新。其使用如下所示:
1 <!-- App.vue --> 2 <template> 3 <div id="app"> 4 <span v-once>{{ message }}</span> 5 </div> 6 </template>
原始HTML
插值語法中(也就是{{}}
)會將數據解釋為普通文本,而非HTML代碼,為了輸出真正的HTML,需要使用v-html
指令,比如下面這個示例:
1 <!-- App.vue --> 2 <template> 3 <div id="app"> 4 <img alt="Vue logo" src="./assets/logo.png"> 5 <div>{{rawHTML}}</div> 6 <div v-html="rawHTML"></div> 7 </div> 8 </template> 9 10 <script> 11 export default { 12 name: 'app', 13 data () { 14 return { 15 rawHTML: '<span style="color:red;">原始HTML</span>' 16 } 17 } 18 } 19 </script>
效果如下:
注意: 不能使用v-html
來復合局部模板,因為Vue不是基於字符串的模板引擎。另外動態渲染任意的HTML會有一定的危險,因為它很容易導致XSS攻擊。
屬性
插值語法不能作用在HTML元素的屬性上,遇到這種情形需要使用v-bind
指令:
<div v-bind:id="dynamicId"></div>
在布爾特性的情況下,它們的存在即暗示為true
, v-bind 工作起來略有不同,比如:
<button v-bind:disabled="isButtonDisabled">Button</button>
如果 isButtonDisabled 的值是 null 、 undefined 或 false ,則 disabled 特性甚至不會被包含在渲染出來的 <button> 元素中。
JavaScript表達式
在插值語法中,我們還可以使用JavaScript的表達式,比如:
1 {{number + 1}} 2 {{ ok ? 'Yes' : 'No'}} 3 <div v-bind:id="'list-' + id">
這些表達式會在所屬Vue實例的數據作用域下作為JavaScript被解析。有個限制就是,每個綁定都只能包含單個表達式,所以下面的例子都不會生效:
<!-- 這是語句,不是表達式 -->
{{ var a = 1 }}
<!-- 流控制也不會生效,請使用三元表達式 -->
{{ if (ok) { return message } }}
指令
在Vue中有不少內置的指令,常常以v-
前綴的特殊特性,比如前面看到的v-html
、v-once
、v-bind
等。Vue指令的特性的值預期是單個JavaScript表達式。Vue指令的職責是,當表達式的值改變時,將其產生的連帶影響,響應式地作用於DOM。比如下面這個v-if
示例:
<p v-if="seen">現在你看到我了</p>
這里,v-if
指令將根據表達式seen
的值的真假來插入或移除<p>
元素。
在Vue中一些指令可以接收一個參數,在指令名稱之后以冒號(:
)表示,比如前面提到的v-bind
指令:
<a v-bind:href="url">我是一個鏈接</a>
這里的href
是參數,告訴v-bind
指令將該元素的href
屬性與表達式url
的值綁定。另外在V2.6開始,可以用方括號([]
)綁定一個動態參數,比如:
<a v-bind:[attributeName]="url">我是一個鏈接</a>
上面代碼中的attributeName
會被作為一個JavaScript表達式進行動態求值,求得的值將會作為最終的參數來使用。比如,在Vue實例中有一個data
屬性attributeName
,其值為"href"
,那么這個綁寫下將等價於v-bind:href
。同樣地,你可以使用動態參數為一個動態的事件名綁定處理函數:
<a v-on:[eventName]="doSomething"> ... </a>
同樣地,當 eventName
的值為 "focus"
時,v-on:[eventName]
將等價於 v-on:focus
。
當然,在使用動態參數時有一些約束,比如:
- 對動態參數的值的約束: 動態參數預期會求出一個字符串,異常情況下值為
null
。這個特殊的null
值可以被顯性地用於移除綁定。任何其它非字符串類型的值都將會觸發一個警告。 - 對動態參數表達式的約束: 動態參數表達式有一些語法約束,因為某些字符,例如空格和引號,放在 HTML 特性名里是無效的。
使用Vue指令的時候,還可以采用縮寫的方式,比如:
<!-- 完整語法 --> <a v-bind:href="url">...</a> <!-- 縮寫 --> <a :href="url">...</a> <!-- 完整語法 --> <a v-on:click="doSomething">...</a> <!-- 縮寫 --> <a @click="doSomething">...</a>
它們看起來可能與普通的 HTML 略有不同,但 :
與 @
對於特性名來說都是合法字符,在所有支持 Vue 的瀏覽器都能被正確地解析。而且,它們不會出現在最終渲染的標記中。縮寫語法是完全可選的,但隨着你更深入地了解它們的作用,你會慶幸擁有它們。
事實上,在Vue中的指令也有不少,最常見的以及其相應的使用方法可以閱讀下面相關教程:
上面列的都是Vue內置的一些常見指令,除了內置的指令之外,在Vue中可以根據其相應的機制實現一些自定義的指令,比如這兩篇文章中介紹的內容《自定義指令》、《Vue 自定義指令的魅力》。
修飾符
Vue中的指令后面還可以緊跟一個.
指明的特殊后綴,用於指出一個指令應該以特殊方式綁定。比如,.prevent
修飾符告訴v-on
指令對於觸發的事件調用event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...</form>
上面我們僅僅是Vue中模板語法中面上的一些東東,很多同學對上面了解或掌握已經非常的熟悉了。當然也有部分同學和我類似,希望能知道一些更深的東西,比如模板渲染更深層的東西。接下來咱們嘗試一起來嘗試了解一下這方面的的知識點。
模板渲染
Vue的模板渲染相對而言要更為復雜,涉及更多底層的知識,如果你能熟讀Vue源碼,應該更易於理解。如果你和我一樣對於源碼閱讀還有一定的難度,那么我們可以先從別的方面着手,了解模板渲染的一些基本原理。這樣一來,就能更清楚Vue的模板是如何工作的,簡單地說,就是如何渲染(也就是模板編譯)。
在深入了解Vue模板渲染之前,有幾個基礎概念需要先進行了解:AST數據結構、VNode數據結構、createElement
的問題和渲染函數。
AST數據結構
AST是Abstract Syntax Tree首字母的簡寫,即抽象語法樹的意思。是源代碼的抽象語法結構的樹狀表現形式,計算機學科中編譯原理的概念。而Vue源碼中借鑒的是@John Resig的 HTML Parrser對模板進行解析,得到的就是AST代碼。
將<template>
轉換成抽象語法樹(AST)。
其中AST是解析器中一個非常重要的概念。在Vue中,ASTNode主要分為:ASTElement(元素)、ASTText(文本)和ASTExpression(表達式)。用type
屬性區分。
用一個簡單的示例來做個簡單的闡述:
1 <div id="app"> 2 <h1>W3cplus.com</h1> 3 <p>{{ 1 + 1 }}</p> 4 </div>
這段代碼生成的AST如下:
看上去是不是和DOM樹有點類似。
VNode數據結構
VNode是VDOM(虛擬DOM(Virtual DOM))中的概念,是真實DOM元素的簡化版,與真實DOM元素是一一對應的關系。
Vue 2.6中VNode數據結構的定義大致像下面這樣:
1 { 2 this.tag = tag 3 this.data = data 4 this.children = children 5 this.text = text 6 this.elm = elm 7 this.ns = undefined 8 this.context = context 9 this.fnContext = undefined 10 this.fnOptions = undefined 11 this.fnScopeId = undefined 12 this.key = data && data.key 13 this.componentOptions = componentOptions 14 this.componentInstance = undefined 15 this.parent = undefined 16 this.raw = false 17 this.isStatic = false 18 this.isRootInsert = true 19 this.isComment = false 20 this.isCloned = false 21 this.isOnce = false 22 this.asyncFactory = asyncFactory 23 this.asyncMeta = undefined 24 this.isAsyncPlaceholder = false 25 }
Vue中的渲染函數的生成跟這些屬性相關。
document.createElement
的問題
我們為什么不直接使用原生 DOM 元素,而是使用真實 DOM 元素的簡化版 VNode,最大的原因就是 document.createElement
這個方法創建的真實 DOM 元素會帶來性能上的損失。
1 let div = document.createElement('div'); 2 for(let k in div) { 3 console.log(k); 4 }
document.createElement 創建的元素其屬性多達 228
個(包含原型鏈上的屬性),而這些屬性有 90%
多對我們來說都是無用的。VNode 就是簡化版的真實 DOM 元素,關聯着真實的DOM,比如屬性elm
,只包括我們需要的屬性,新增了一些在 diff
過程中需要使用的屬性,例如 isStatic
,就可以用來對比新舊 DOM。這也是為什么要使用虛擬DOM的原因!
如果你想擴展或深入了解有關於虛擬DOM相關的內容,可以閱讀下面這些文章。
渲染函數
render() 即是渲染函數,這個函數是通過編譯模板文件得到的,其運行結果是VNode(虛擬DOM)。在Vue中使用 Vue.compile(template) 方法對模板進行編譯。主要會經歷三個步驟:
- 第一步是將 模板字符串 轉換成 element ASTs(解析器)
- 第二步是對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)
- 第三步是 使用 element ASTs 生成
render
函數代碼字符串(代碼生成器)
其對應的其實就是三個函數:
parse()
函數:用來解析<template>
,即解析器。主要功能是將template
字符串解析成 AST。前面定義了ASTElement
的數據結構,parse
函數就是將template
里的結構(指令,屬性,標簽等)轉換為AST形式存進ASTElement
中,最后解析生成AST。optimize()
函數:用來優化靜態內容,即優化器。主要功能就是標記靜態節點,為后面patch
過程中對比新舊 VNode 樹形結構做優化。被標記為static
的節點在后面的diff
算法中會被直接忽略,不做詳細的比較。這里的靜態內容指的是 和數據沒有關系,不需要每次都刷新的內容。generate()
函數:用來創建render()
字符串,即代碼生成器。主要功能就是根據 AST 結構拼接生成render
函數的字符串。
這三個函數也是compile()
函數中三個核心部分,根據每個步驟所起的作用,繪制了一張草圖:
有了上面這些知識點,繼續探究模板渲染的過程就會更易於理解了。
如果結合我們上一節學習的Vue實例的生命周期圖,那么模板渲染最重要的兩個過程將是 DOM初始化 和 DOM的更新。
簡單地說,模板中的 DOM初始化 部分概括為將el
、template
和render()
函數通過一系列的函數,比如compileToFunctions()
和compile()
函數轉換為render
函數並最終生成真實DOM的過程;而 DOM更新 就是數據發生變化后,DOM進行更新的過程。結合生命周期的圖和前面所掌握的知識點,我們現在來嘗試着將Vue模板渲染過程用圖繪制出來。
上圖是根據自己閱讀相關資料整理的,難免有錯,歡迎路過的大嬸拍正。
如果想正確的繪制出Vue模板渲染過程的路線圖的話,還是需要去嘗試閱讀Vue的源碼。這樣會更清楚,更深層的知識點。擴展閱讀:
小結
這篇文章整理了學習Vue模板的一些心得和筆記。除了介紹了模板語法相關的知識點之外,更多的是花了不少時間去理解Vue模板編譯(渲染)的一個過程。根據相關的文檔和自己的理解把模板渲染的過程繪制成了圖。畢竟沒有熟讀源碼,難免有錯,歡迎路過的大神指正。或者您有這方面的經驗,歡迎在下面的評論中一起分享。