模板引擎
vue 中如何解析模板,指令如何處理
問題:
模板是什么
render函數
render函數與 vdom
模板是什么
<div id='app'>
<div>
<input v-model="title"/>
<button v-on:click="add">submit</button>
</div>
<ul>
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
這是一個模板
那么模板是什么呢?
- 本質是字符串,是以字符串存在的,只不過像html
- 有邏輯,比如判斷,循環這些,如v-if,v-for等,怎么會有邏輯呢,之前寫html就沒邏輯
- 與html格式很像,但有很大區別。首先html在語法上是不認識v-if,v-for這些的。第二個是html是靜態的,沒有邏輯,vue是動態的,有邏輯的。它們只是格式很像
- 但是最終模板還是要轉換為html來顯示的。
那么它是怎么做到的呢?
首先模板最終必須轉換成js代碼,因為:
- 模板有邏輯,有v-if,v-for。必須用js才能實現(圖靈完備的語言)
- 模板要轉化為html渲染頁面,必須用js才能實現
因此,模板最終要轉換成一個js函數(render函數,也就是渲染函數)
首先了解下with
var obj = {
name: 'zhangsan',
age: 20,
getAddress: function(){
alert('beijing');
}
}
// 不用with
function fn(){
alert(obj.name);
alert(obj.age);
obj.getAddress();
}
fn();
// 使用width
function fn1(){
with(obj){
alert(name);
alert(age);
getAddress();
}
}
fn1();
在實際開發中,盡量不要使用with。
fn是我們正常的使用。
fn1使用with的情況。
兩個是同樣的效果,用with的里面,都不寫是誰的屬性,是誰統一的,用with包起來。
這個可讀性可能沒那么強。
render函數
template 模板 通過 Compile 編譯 得到 render函數。
compile 編譯可以分成 parse、optimize 與 generate 三個階段,最終需要得到 render function。
compile 編譯過程,現階段可以不需要完全掌握,只需要了解 解析的大致流程即可。
簡單的例子
我們看最簡單的一段模板
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="../lib/vue-2.4.0.js"></script>
</head>
<body>
<div id="app">
<p>{{price}}</p>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
price:100
}
})
</script>
</body>
</html>
把模板摘出來
<div id="app">
<p>{{price}}</p>
</div>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./vue-2.5.13.js"></script>
</head>
<body>
<div id="app">
<p>{{price}}</p>
</div>
<script>
/**
* _c : 創建dom標簽
* _v : 創建文本節點
* _s : toString
*/
var vm = new Vue({
el: '#app',
data: {
price: 100
}
})
// 這個模板最終生成的函數是下面這個
// 以下是手寫的 render 函數
function render() {
with(this) { // this 就是 vm
return _c(
'div',
{
attrs: {'id': 'app'}
},
[
_c('p', [_v(_s(price))])
]
)
}
}
// 把函數翻譯一下,也就是不用 with
function render1() {
return vm._c(
'div',
{
attrs: {'id': 'app'}
},
[
vm._c('p', [vm._v(vm._s(vm.price))])
]
)
}
</script>
</body>
</html>
這個this就是vm這個實例,這個_c就是vm._c,用來創建dom標簽的。
第一個參數是個div。
第二個參數是個對象,對象里面有屬性。
第三個參數是個數組,數組里面只有一個元素,這個_c肯定也是vm._c。
這個_c第一個參數是p,第二個參數是個數組,里面也是一個數組,_v(_s(price)),這里面的price肯定是vm.price,就是data.price。
然后前面的_s就是vm._s,就是toString函數。
_v也是vm._v,用來創建文本節點的。
總結:
模板中所有信息都包含在render函數中。
this即vm
price即this.price即vm.price即data.price。
_c即this._c即vm._c
todo-list demo的render函數
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<!-- 模板 -->
<div id="app">
<input v-model='title'/>
<button v-on:click='add'>submit</button>
<ul v-for='item in list'>
{{item}}
</ul>
</div>
<!-- 源碼 -->
<script src="./vue-2.6.10.js"></script>
<script>
var data = {
title: '',
list: []
}
// 初始化 vue 實例
var vm = new Vue({
el: '#app',
data: data,
methods: {
add: function(){
this.list.push(this.title);
this.title = ''
}
}
})
</script>
</body>
</html>
然后通過這個例子,看看vue的render函數是什么樣子的,在源碼搜索code.render,然后打印出來。
這是與上面模板對應的 render 函數。
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
},
[
_c('input', {
directives: [{ // 當title發生變化,會賦值給value,從而響應到input
name: "model",
rawName: "v-model",
value: (title),
expression: "title"
}],
domProps: {
"value": (title)
},
on: { // v-model相關 input 里面的數據改變,事件監聽到,會賦值給 title
"input": function($event) {
if ($event.target.composing) return;
title = $event.target.value
}
}
}),
_v(" "),
_c('button', {
on: {
// 綁定 click 事件
"click": add
}
},
[_v("submit")]
),
_v(" "),
_l(
// v-for相關
(list),
function(item) {
return _c('ul', [_v("\n " + _s(item) + "\n ")])
}
)
], 2)
}
這就是這個 demo 所對應的 render 函數。
在創建input的時候第二個參數有個directives。叫做指令,指令名字是model。value是title,也就是vm.title。
后面_v(''),_v表示創建文本節點,主要是input和button有個換行,如果不換行,就不會有 _v去創建一個空的文本節點。
_l 返回的是一個數組,針對list返回li的標簽。
現在可以根據 todo-list demo 的 render 函數,回顧一下 render 函數中:
- v-model 是怎么實現的?
- v-on 是怎么實現的?
- v-for 是怎么實現的?
渲染-vue的模板如何被渲染成html
以上已經解決了模板中“邏輯”的問題(v-for v-if)
還剩下模板生成 html 的問題
另外,vm._c 是什么?render 函數返回了什么?
先復習一下 vdom 的知識
這里 vm._c 跟 snabbdom 里面的h()函數非常相似。
vm._c 返回 vnode。那么 render 函數返回的也是 vnode。
可以這樣理解:
- vm._c 其實就相當於 snabbdom 中的 h 函數
- render 函數執行之后,返回的是 vnode
vue的模板如何被渲染成html
vm._update(vnode) {
const prevVnode = vm._vnode;
vm._vnode = vnode;
if(!prevVnode){
vm.$el = vm._patch_(vm.$el, vnode); // 第一次沒有值
} else {
vm.$el = vm._patch_(prevVnode, vnode); // 有值的情況
}
}
function updateComponent(){
// vm._render即上面的render函數,返回vnode
vm._update(vm._render())
}
render 函數和 vdom
- updateComponent 中實現了 vdom 的 patch
- 頁面首次渲染執行 updateComponent
- data 中每次修改屬性,執行 updateComponent
問題解答
模板是什么?
模板:字符串,有邏輯,嵌入 JS 變量……
模板必須轉換為 JS 代碼(有邏輯、渲染 html、JS 變量)
render 函數執行是返回 vnode
updateComponent
- updateComponent 中實現了 vdom 的 patch
- 頁面首次渲染執行 updateComponent
- data 中每次修改屬性,執行 updateComponent
補充:vue的整個實現流程簡易說明
- 第一步:解析模板成 render 函數
- 第二步:響應式開始監聽
- 第三步:首次渲染,顯示頁面,且綁定依賴
- 第四步:data 屬性變化,觸發 rerender
1. 把模板解析為 render 函數
- 運用 with
- 模板中的所有信息都被 render 函數包含
- 模板中用到的 data 中的屬性,都變成了 JS 變量
- 模板中的 v-model v-for v-on 都變成了 JS 邏輯
- render 函數返回 vnode
2. 響應式開始監聽
- Object.defineProperty
- 將data 的屬性代理到 vm 上
3. 首次渲染,顯示頁面且綁定依賴
- 初次渲染,執行 updateComponent, 執行 vm._render()
- 執行 render 函數,會訪問到 data 下的數據
- 會被響應式的 get 方法監聽到
- 執行 updateComponent,會走到 vdom 的 patch 方法
- patch 將 vnode 渲染成 DOM,初次渲染完成
4. data 屬性變化,觸發 rerender
- 修改屬性,被響應式的 set 監聽到
- set 中執行 updateComponent
- updateComponent 重新執行 vm._render()
- 生成的 vnode 和 prevVnode ,通過 patch 進行對比
- 渲染到 html 中