Vue 進階系列(三)之Render函式原理及實現
Vue進階系列彙總如下,歡迎閱讀,歡迎加高階前端進階群一起學習(文末)。
Render函式原理
根據第一篇文章介紹的響應式原理,如下圖所示。
在初始化階段,本質上發生在auto run
函式中,然後通過render
函式生成Virtual DOM
,view
根據Virtual DOM
生成Actual DOM
。因為render
函式依賴於頁面上所有的資料data
,並且這些資料是響應式的,所有的資料作為元件render
函式的依賴。一旦這些資料有所改變,那麼render
函式會被重新呼叫。
在更新階段,render
Virtual Dom
,新舊Virtual DOM
之間會進行比較,把diff之後的最小改動應用到Actual DOM
中。
Watcher負責收集依賴,清除依賴和通知依賴。在大型複雜的元件樹結構下,由於採用了精確的依賴追蹤系統,所以會避免元件的過度渲染。
Actual DOM 和 Virtual DOM
Actual DOM 通過document.createElement('div')生成一個DOM節點。
document.createElement('div')
// 瀏覽器原生物件(開銷大)
"[object HTMLDivElement]"
複製程式碼
Virtual DOM 通過 vm.$createElement('div')生成一個JS物件,VDOM物件有一個表示div的tag屬性,有一個包含了所有可能特性的data屬性,可能還有一個包含更多虛擬節點的children列表。
vm.$createElement('div')
// 純JS物件(輕量)
{ tag: 'div', data: { attrs: {}, ...}, children: [] }
複製程式碼
因為Virtual DOM的渲染邏輯和Actual DOM解耦了,所以有能力執行在的非瀏覽器環境中,這就是為什麼Virtual DOM出現之後混合開發開始流行的原因,React Native 和 Weex能夠實現的原理就是這個。
JSX和Template
JSX和Template都是用於宣告DOM和state之間關係的一種方式,在Vue中,Template是預設推薦的方式,但是也可以使用JSX來做更靈活的事。
JSX更加動態化,對於使用程式語言是很有幫助的,可以做任何事,但是動態化使得編譯優化更加複雜和困難。
Template更加靜態化並且對於表示式有更多約束,但是可以快速複用已經存在的模板,模板約束意味著可以在編譯時做更多的效能優化,相對於JSX在編譯時間上有著更多優勢。
例項1:實現example元件
要求使用如下
<example :tags="['h1', 'h2', 'h3']"></example>
複製程式碼
要求輸出如下
<div>
<h1>0</h1>
<h2>1</h2>
<h3>2</h3>
</div>
複製程式碼
上面這個需求可以通過render
函式來做,官方提供了createElement
函式用來生成模板。createElement('div', {}, [...])
可接受的引數如下。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標籤字串,元件選項物件,或者
// 解析上述任何一種的一個 async 非同步函式。必需引數。
'div',
// {Object}
// 一個包含模板相關屬性的資料物件
// 你可以在 template 中使用這些特性。可選引數。
{
},
// {String | Array}
// 子虛擬節點 (VNodes),由 `createElement()` 構建而成,
// 也可以使用字串來生成“文字虛擬節點”。可選引數。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
複製程式碼
知道了用法之後,就可以在render
中返回createElement
生成的虛擬節點,外層是div
,內層是三個錨點標題h1 h2 h3
,所以內層需要遍歷下,使用兩個createElement
就可以完成了。
通常使用h
作為createElement
的別名,這是Vue
的通用慣例,也是JSX
的要求。
實現如下
<!--引用-->
<script src="../node_modules/vue/dist/vue.js"></script>
<!--定義template -->
<div id="app">
<example :tags="['h1', 'h2', 'h3']"></example>
</div>
<script>
// 定義example元件
Vue.component('example', {
props: ['tags'],
render (h) {
// 第二個引數是一個包含模板相關屬性的資料物件,可選引數
// 子虛擬節點(VNodes)引數可以傳入字串或者數字,
// 通過createElement生成,可選引數
return h('div', this.tags.map((tag, i) => h(tag, i)))
}
})
// 例項化
new Vue({ el: '#app' })
</script>
複製程式碼
例項2:實現動態的<example>
元件
要求如下
- 實現一個
Foo
元件渲染<div>foo</div>
,實現一個Bar
元件渲染<div>bar</div>
。 - 實現一個
<example>
元件,根據屬性ok
動態渲染Foo
元件或者Bar
元件。如果屬性ok
是true
,那麼最終的渲染應該是<div>foo</div>
。 - 實現一個按鈕控制屬性
ok
,通過這個屬性讓<example>
在Foo
或者Bar
之間切換。
根據上面的要求,在模板中呼叫<example>
元件,然後定義<button>
元件,同時繫結屬性ok
。
實現如下
<!--引用-->
<script src="../node_modules/vue/dist/vue.js"></script>
<!--定義template -->
<div id="app">
<!--繫結屬性ok-->
<example :ok="ok"></example>
<!--繫結點選事件-->
<button @click="ok = !ok">toggle</button>
</div>
<script>
// 定義Foo
const Foo = {
render (h) {
return h('div', 'foo')
}
}
// 定義Bar
const Bar = {
render (h) {
return h('div', 'bar')
}
}
// 定義example元件
// 根據ok屬性動態切換
Vue.component('example', {
props: ['ok'],
render (h) {
return h(this.ok ? Foo : Bar)
}
})
// 例項化
new Vue({
el: '#app',
data: { ok: true }
})
</script>
複製程式碼
例項3:實現元件
要求如下
- 實現一個
withAvatarURL
函式,要求傳入一個帶有url
屬性的元件,返回一個接收username
屬性的高階元件,這個高階元件主要負責獲取相應的頭像URL。 - 在API返回之前,高階元件將佔位符URL
http://via.placeholder.com/200x200
傳遞給內部元件。
例子如下
const SmartAvatar = withAvatarURL(Avatar)
// 使用這個方式
<smart-avatar username="vuejs"></smart-avatar>
// 替換下面的方式
<avatar url="/path/to/image.png"></avatar>
複製程式碼
withAvatarURL
函式返回一個物件,接收username
屬性,在生命週期created
獲取頭像URL。Avatar
物件接收src
屬性,src
的內容從withAvatarURL
中獲取,然後展示在上。例項化的時候,傳入新定義的元件名SmartAvatar
。
實現如下
<!--引用-->
<script src="../node_modules/vue/dist/vue.js"></script>
<!--定義template-->
<div id="app">
<smart-avatar username="vuejs"></smart-avatar>
</div>
<script>
// 獲取頭像URL
function fetchURL (username, cb) {
setTimeout(() => {
// 獲取頭像並回傳
cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
}, 500)
}
// 傳遞的InnerComponent
const Avatar = {
props: ['src'],
template: `<img :src="src">`
}
function withAvatarURL (InnerComponent) {
return {
props: ['username'],
inheritAttrs: false, // 2.4 only,元件將不會把未被註冊的props呈現為普通的HTML屬性
data () {
return { url: null }
},
created () {
// 獲取頭像URL並回傳給this.url
fetchURL(this.username, url => {
this.url = url
})
},
render (h) {
return h(InnerComponent, {
attrs: this.$attrs, // 2.4 only,獲取到沒有使用的註冊屬性
props: {
src: this.url || 'http://via.placeholder.com/200x200'
}
})
}
}
}
const SmartAvatar = withAvatarURL(Avatar)
// 例項化,新構造元件名為SmartAvatar或smart-avatar
new Vue({
el: '#app',
components: { SmartAvatar }
})
</script>
複製程式碼
本文內容參考自VUE作者尤大的付費視訊
Vue官網之渲染函式 & JSX
交流
本人Github連結如下,歡迎各位Star
我是木易楊,現在是網易高階前端工程師,目前維護了一個高階前端進階群,歡迎加入。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!