相關vue原理之虛擬dom參考資料:javascript
3. Vue官方《渲染函數文檔》java
4. Vue渲染函數源碼解析node
最近學習Vue過程當中對虛擬Dom以及Render函數原理不是很理解,雖然官方有介紹,但仍是沒能深刻理解,因此找了點資料來補補,這一切都是爲了深刻掌握Vue知識。react
-----------分割線--------------
git
今天咱們學習的目的是瞭解和學習Vue的render
函數。若是想要更好的學習Vue的render
函數相關的知識,咱們有必要重溫一下Vue中的一些基本概念。那麼先上一張圖,這張圖從宏觀上展示了Vue總體流程:github
從上圖中,不難發現一個Vue的應用程序是如何運行起來的,模板經過編譯生成AST,再由AST生成Vue的render
函數(渲染函數),渲染函數結合數據生成Virtual DOM樹,Diff和Patch後生成新的UI。從這張圖中,能夠接觸到Vue的一些主要概念:算法
watcher
,這個watcher
將會在組件render
的時候收集組件所依賴的數據,並在依賴有更新的時候,觸發組件從新渲染。你根本不須要寫shouldComponentUpdate
,Vue會自動優化並更新要更新的UI。上圖中,render
函數能夠做爲一道分割線,render
函數的左邊能夠稱之爲編譯期,將Vue的模板轉換爲渲染函數。render
函數的右邊是Vue的運行時,主要是基於渲染函數生成Virtual DOM樹,Diff和Patch。編程
Vue推薦在絕大多數狀況下使用template
來建立你的HTML。然而在一些場景中,須要使用JavaScript的編程能力和建立HTML,這就是render
函數,它比template
更接近編譯器。
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
複製代碼
在HTML層,咱們決定這樣定義組件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
複製代碼
當咱們開始寫一個經過level
的prop
動態生成heading
標籤的組件,你可能很快想到這樣實現:
<!-- HTML -->
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
<!-- Javascript -->
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
複製代碼
在這種場景中使用 template
並非最好的選擇:首先代碼冗長,爲了在不一樣級別的標題中插入錨點元素,咱們須要重複地使用 <slot></slot>
。
雖然模板在大多數組件中都很是好用,可是在這裏它就不是很簡潔的了。那麼,咱們來嘗試使用 render
函數重寫上面的例子:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name 標籤名稱
this.$slots.default // 子組件中的陣列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
複製代碼
簡單清晰不少!簡單來講,這樣代碼精簡不少,可是須要很是熟悉 Vue 的實例屬性。在這個例子中,你須要知道當你不使用 slot
屬性向組件中傳遞內容時,好比 anchored-heading
中的 Hello world!
,這些子元素被存儲在組件實例中的 $slots.default
中。
對Vue的一些概念和渲染函數的基礎有必定的瞭解以後,咱們須要對一些瀏覽器的工做原理有一些瞭解,這樣對咱們學習render
函數是很重要的。好比下面的這段HTML代碼:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
複製代碼
當瀏覽器讀到這些代碼時,它會創建一個DOM節點樹來保持追蹤,若是你會畫一張家譜樹來追蹤家庭成員的發展同樣。
HTML的DOM節點樹以下圖所示:
每一個元素都是一個節點。每片文字也是一個節點。甚至註釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹同樣,每一個節點均可以有孩子節點 (也就是說每一個部分能夠包含其它的一些部分)。
高效的更新全部這些節點會是比較困難的,不過所幸你沒必要再手動完成這個工做了。你只須要告訴 Vue 你但願頁面上的 HTML 是什麼,這能夠是在一個模板裏:
<h1>{{ blogTitle }}</h1>
複製代碼
或者一個渲染函數裏:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
複製代碼
在這兩種狀況下,Vue 都會自動保持頁面的更新,即使 blogTitle
發生了改變。
在Vue 2.0中,渲染層的實現作了根本性改動,那就是引入了虛擬DOM。
Vue的編譯器在編譯模板以後,會把這些模板編譯成一個渲染函數。而函數被調用的時候就會渲染而且返回一個虛擬DOM的樹。
當咱們有了這個虛擬的樹以後,再交給一個Patch函數,負責把這些虛擬DOM真正施加到真實的DOM上。在這個過程當中,Vue有自身的響應式系統來偵測在渲染過程當中所依賴到的數據來源。在渲染過程當中,偵測到數據來源以後就能夠精確感知數據源的變更。到時候就能夠根據須要從新進行渲染。當從新進行渲染以後,會生成一個新的樹,將新的樹與舊的樹進行對比,就能夠最終得出應施加到真實DOM上的改動。最後再經過Patch函數施加改動。
簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在應該狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。
Vue支持咱們經過data
參數傳遞一個JavaScript對象作爲組件數據,而後Vue將遍歷此對象屬性,使用Object.defineProperty
方法設置描述對象,經過存取器函數能夠追蹤該屬性的變動,Vue建立了一層Watcher
層,在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的setter
被調用時,會通知Watcher
從新計算,從而使它關聯的組件得以更新,以下圖:
有關於Vue的響應式相關的內容,能夠閱讀下列文章:
Object.defineproperty
對於Vue自帶的響應式系統,並非我們今天要聊的東西。咱們仍是回到Vue的虛擬DOM中來。對於虛擬DOM,我們來看一個簡單的實例,就是下圖所示的這個,詳細的闡述了模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM
的一個過程
其實Vue中的虛擬DOM仍是很複雜的,我也是隻知其一;不知其二,若是你想深刻的瞭解,能夠閱讀@JoeRay61的《Vue原理解析之Virtual DOM》一文。
經過前面的學習,咱們初步瞭解到Vue經過創建一個虛擬DOM對真實DOM發生的變化保持追蹤。好比下面這行代碼:
return createElement('h1', this.blogTitle)
複製代碼
createElement
到底會返回什麼呢?其實不是一個實際的 DOM 元素。它更準確的名字多是 createNodeDescription
,由於它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。咱們把這樣的節點描述爲「虛擬節點 (Virtual Node)」,也常簡寫它爲「VNode」。「虛擬 DOM」是咱們對由 Vue 組件樹創建起來的整個 VNode 樹的稱呼。
Vue組件樹創建起來的整個VNode樹是惟一的。這意味着,下面的render
函數是無效的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 錯誤-重複的 VNodes
myParagraphVNode, myParagraphVNode
])
}
複製代碼
若是你真的須要重複不少次的元素/組件,你可使用工廠函數來實現。例如,下面這個例子 render
函數完美有效地渲染了 20
個重複的段落:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
複製代碼
上圖展現的是獨立構建時的一個渲染流程圖。
繼續使用上面用到的模板到真實DOM過程的一個圖:
這裏會涉及到Vue的另外兩個概念:
HTML字符串 → render函數 → VNode → 真實DOM節點
render函數 → VNode → 真實DOM節點
運行時構建的包,會比獨立構建少一個模板編譯器。在$mount
函數上也不一樣。而$mount
方法又是整個渲染過程的起始點。用一張流程圖來講明:
由此圖能夠看到,在渲染過程當中,提供了三種渲染模式,自定義render
函數、template
、el
都可以渲染頁面,也就是對應咱們使用Vue時,三種寫法:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement (
'h' + this.level, // tag name標籤名稱
this.$slots.default // 子組件中的陣列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
複製代碼
let app = new Vue({
template: `<div>{{ msg }}</div>`,
data () {
return {
msg: ''
}
}
})
複製代碼
let app = new Vue({
el: '#app',
data () {
return {
msg: 'Hello Vue!'
}
}
})
複製代碼
這三種渲染模式最終都是要獲得render
函數。只不過用戶自定義的render
函數省去了程序分析的過程,等同於處理過的render
函數,而普通的template
或者el
只是字符串,須要解析成AST,再將AST轉化爲render
函數。
記住一點,不管哪一種方法,都要獲得
render
函數。
咱們在使用過程當中具體要使用哪一種調用方式,要根據具體的需求來。
若是是比較簡單的邏輯,使用template
和el
比較好,由於這兩種都屬於聲明式渲染,對用戶理解比較容易,但靈活性比較差,由於最終生成的render
函數是由程序經過AST解析優化獲得的;而使用自定義render
函數至關於人已經將邏輯翻譯給程序,可以勝任複雜的邏輯,靈活性高,但對於用戶的理解相對差點。
在使用render
函數,其中還有另外一個須要掌握的部分,那就是createElement
。接下來咱們須要熟悉的是如何在createElement
函數中生成模板。那麼咱們分兩個部分來對createElement
進行理解。
createElement
能夠是接受多個參數:
第一個參數對於createElement
而言是一個必須的參數,這個參數能夠是字符串string
、是一個對象object
,也能夠是一個函數function
。
<div id="app">
<custom-element></custom-element>
</div>
Vue.component('custom-element', {
render: function (createElement) {
return createElement('div')
}
})
let app = new Vue({
el: '#app'
})
複製代碼
上面的示例,給createElement
傳了一個String
參數'div'
,即傳了一個HTML標籤字符。最後會有一個div
元素渲染出來:
接着把上例中的String
換成一個Object
,好比:
Vue.component('custom-element', {
render: function (createElement) {
return createElement({
template: `<div>Hello Vue!</div>`
})
}
})
複製代碼
上例傳了一個{template: '<div>Hello Vue!</div>'}
對象。此時custom-element
組件渲染出來的結果以下:
除此以外,還能夠傳一個Function
,好比:
Vue.component('custom-element', {
render: function (createElement) {
var eleFun = function () {
return {
template: `<div>Hello Vue!</div>`
}
}
return createElement(eleFun())
}
})
複製代碼
最終獲得的結果和上圖是同樣的。這裏傳了一個eleFun()
函數給createElement
,而這個函數返回的是一個對象。
createElement
是一個可選參數,這個參數是一個Object
。來看一個小示例:
<div id="app">
<custom-element></custom-element>
</div>
Vue.component('custom-element', {
render: function (createElement) {
var self = this
// 第一個參數是一個簡單的HTML標籤字符 「必選」
// 第二個參數是一個包含模板相關屬性的數據對象 「可選」
return createElement('div', {
'class': {
foo: true,
bar: false
},
style: {
color: 'red',
fontSize: '14px'
},
attrs: {
id: 'boo'
},
domProps: {
innerHTML: 'Hello Vue!'
}
})
}
})
let app = new Vue({
el: '#app'
})
複製代碼
最終生成的DOM,將會帶一些屬性和內容的div
元素,以下圖所示:
createElement
還有第三個參數,這個參數是可選的,能夠給其傳一個String
或Array
。好比下面這個小示例:
<div id="app">
<custom-element></custom-element>
</div>
Vue.component('custom-element', {
render: function (createElement) {
var self = this
return createElement(
'div', // 第一個參數是一個簡單的HTML標籤字符 「必選」
{
class: {
title: true
},
style: {
border: '1px solid',
padding: '10px'
}
}, // 第二個參數是一個包含模板相關屬性的數據對象 「可選」
[
createElement('h1', 'Hello Vue!'),
createElement('p', '開始學習Vue!')
] // 第三個參數是傳了多個子元素的一個數組 「可選」
)
}
})
let app = new Vue({
el: '#app'
})
複製代碼
最終的效果以下:
其實從上面這幾個小例來看,不難發現,以往咱們使用Vue.component()
建立組件的方式,均可以用render
函數配合createElement
來完成。你也會發現,使用Vue.component()
和render
各有所長,正如文章開頭的一個示例代碼,就不適合Vue.component()
的template
,而使用render
更方便。
接下來看一個小示例,看看template
和render
方式怎麼建立相同效果的一個組件:
<div id="app">
<custom-element></custom-element>
</div>
Vue.component('custom-element', {
template: `<div id="box" :class="{show: show}" @click="handleClick">Hello Vue!</div>`,
data () {
return {
show: true
}
},
methods: {
handleClick: function () {
console.log('Clicked!')
}
}
})
複製代碼
上面Vue.component()
中的代碼換成render
函數以後,能夠這樣寫:
Vue.component('custom-element', {
render: function (createElement) {
return createElement('div', {
class: {
show: this.show
},
attrs: {
id: 'box'
},
on: {
click: this.handleClick
}
}, 'Hello Vue!')
},
data () {
return {
show: true
}
},
methods: {
handleClick: function () {
console.log('Clicked!')
}
}
})
複製代碼
最後聲明一個Vue實例,並掛載到id
爲#app
的一個元素上:
let app = new Vue({
el: '#app'
})
複製代碼
簡單的來看一下createElement
解析的過程,這部分須要對JS有一些功底。否則看起來有點蛋疼:
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
// 兼容不傳data的狀況
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 若是alwaysNormalize是true
// 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 調用_createElement建立虛擬節點
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context, tag, data, children, normalizationType) {
/**
* 若是存在data.__ob__,說明data是被Observer觀察的數據
* 不能用做虛擬節點的data
* 須要拋出警告,並返回一個空節點
*
* 被監控的data不能被用做vnode渲染的數據的緣由是:
* data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做
*/
if (data && data.__ob__) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 當組件的is屬性被設置爲一個falsy的值
// Vue將不會知道要把這個組件渲染成什麼
// 因此渲染一個空節點
if (!tag) {
return createEmptyVNode()
}
// 做用域插槽
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根據normalizationType的值,選擇不一樣的處理方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 若是標籤名是字符串類型
if (typeof tag === 'string') {
let Ctor
// 獲取標籤名的命名空間
ns = config.getTagNamespace(tag)
// 判斷是否爲保留標籤
if (config.isReservedTag(tag)) {
// 若是是保留標籤,就建立一個這樣的vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 若是找到了這個標籤的定義,就以此建立虛擬組件節點
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常建立一個vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 當tag不是字符串的時候,咱們認爲tag是組件的構造類
// 因此直接建立
} else {
vnode = createComponent(tag, data, context, children)
}
// 若是有vnode
if (vnode) {
// 若是有namespace,就應用下namespace,而後返回vnode
if (ns) applyNS(vnode, ns)
return vnode
// 不然,返回一個空節點
} else {
return createEmptyVNode()
}
}
}
複製代碼
簡單的梳理了一個流程圖,能夠參考下
這部分代碼和流程圖來自於@JoeRay61的《Vue原理解析之Virtual DOM》一文。
在使用Vue模板的時候,咱們能夠在模板中靈活的使用v-if
、v-for
、v-model
和<slot>
之類的。但在render
函數中是沒有提供專用的API。若是在render
使用這些,須要使用原生的JavaScript來實現。
在render
函數中可使用if/else
和map
來實現template
中的v-if
和v-for
。
<ul v-if="items.length">
<li v-for="item in items">{{ item }}</li>
</ul>
<p v-else>No items found.</p>
複製代碼
換成render
函數,能夠這樣寫:
Vue.component('item-list',{
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map((item) => {
return createElement('item')
}))
} else {
return createElement('p', 'No items found.')
}
}
})
<div id="app">
<item-list :items="items"></item-list>
</div>
let app = new Vue({
el: '#app',
data () {
return {
items: ['大漠', 'W3cplus', 'blog']
}
}
})
複製代碼
獲得的效果以下:
render
函數中也沒有與v-model
相應的API,若是要實現v-model
相似的功能,一樣須要使用原生JavaScript來實現。
<div id="app">
<el-input :name="name" @input="val => name = val"></el-input>
</div>
Vue.component('el-input', {
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.name
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
},
props: {
name: String
}
})
let app = new Vue({
el: '#app',
data () {
return {
name: '大漠'
}
}
})
複製代碼
刷新你的瀏覽器,能夠看到效果以下:
這就是深刻底層要付出的,儘管麻煩了一些,但相對於 v-model
來講,你能夠更靈活地控制。
你能夠從this.$slots
獲取VNodes列表中的靜態內容:
render: function (createElement) {
// 至關於 `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
複製代碼
還能夠從this.$scopedSlots
中得到能用做函數的做用域插槽,這個函數返回VNodes:
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
複製代碼
若是要用渲染函數向子組件中傳遞做用域插槽,能夠利用VNode數據中的scopedSlots
域:
<div id="app">
<custom-ele></custom-ele>
</div>
Vue.component('custom-ele', {
render: function (createElement) {
return createElement('div', [
createElement('child', {
scopedSlots: {
default: function (props) {
return [
createElement('span', 'From Parent Component'),
createElement('span', props.text)
]
}
}
})
])
}
})
Vue.component('child', {
render: function (createElement) {
return createElement('strong', this.$scopedSlots.default({
text: 'This is Child Component'
}))
}
})
let app = new Vue({
el: '#app'
})
複製代碼
若是寫習慣了template
,而後要用render
函數來寫,必定會感受好痛苦,特別是面對複雜的組件的時候。不過咱們在Vue中使用JSX可讓咱們回到更接近於模板的語法上。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
複製代碼
將
h
做爲createElement
的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,若是在做用域中h
失去做用,在應用中會觸發報錯。
回過頭來看,Vue中的渲染核心關鍵的幾步流程仍是很是清晰的:
new Vue
,執行初始化$mount
方法,經過自定義render
方法、template
、el
等生成render
函數Watcher
監聽數據的變化render
函數執行生成VNode對象patch
方法,對比新舊VNode對象,經過DOM Diff算法,添加、修改、刪除真正的DOM元素至此,整個new Vue
的渲染過程完畢。
而這篇文章,主要把精力集中在render
函數這一部分。學習了怎麼用render
函數來建立組件,以及瞭解了其中createElement
。
最後要說的是,上文雖然以學習render
函數,但文中涉及了Vue很多的知識點,也有點零亂。初學者本身根據本身獲取所要的知識點。因爲本人也是初涉Vue相關的知識點,若是文章中有不對之處,煩請路過的大神拍正。
原文轉自: 大漠