Vue實現組件化的基本思路


Vue.js(以下簡稱Vue)是時下流行的前端開發庫,一般搭配其插件Vue-Router,Vuex一起使用,行業中稱為Vue全家桶。
Vue使用了MVVM的理念,將表現層(DOM)和數據層進行了分離,其基本思想是數據和DOM的一體化,操作數據即可變更DOM,表單交互亦可通過v-model指令改變數據,將前端開發者從DOM、數據兩手抓的泥潭中解放出來。
但是,使用這種方法的代價還是明顯的,那就是Vue本身,因為這對大多數開發者而言是一個黑盒子。如果不能大概了解它做了哪些事情,那么有的時候遇到一些問題還是很頭疼的。
本文關於Vue實現組件化的基本思路,總結了一些其中發生的故事,希望對今后的開發學習有所幫助。

最基本的模型

Vue2.0之后並沒有將DOM和數據進行直接綁定,而是采用了VNode類,也就是常說的虛擬DOM。虛擬DOM其實並不神奇,它就是一個JS對象,描述了DOM元素的一些特征,但是它的屬性要比真實DOM少得多,這就使得操作更加方便省時。
數據層位於Vue實例中(以下簡稱VM),該實例的渲染函數執行可得到VNode,然后Vnode通過patch過程渲染成真實DOM,真實DOM又通過注冊事件改變VM,這就是一個基本的模型。

img1

Vue實例的由來

那么VM從何而來呢?考慮以下實際問題:

// App.js
new Vue({
	name: 'App',
	components: { M },
	template: ` <div> <div>swamp</div> <M /> </div>`
}).$mount('#app')

// M.vue
<template>
	<div :class="containerClass">
		<div>I am M</div>
		<ul>
			<li v-for="item in listData">{{ item }}</li>
		</ul>
	</div>
</template>
<script>
export default {
	name: 'M',
	data () {
		return {
			listData: ['A', 'B', 'C', 'D', 'E', 'F'],
			containerClass: 'container'
		}
	}
}
</script>

通過Vue的模板解析(parse)和Vue-loader,上述兩種形式的組件定義都會最終變成一個options對象,這個對象包含了所有我們定義的屬性,template被解析成渲染函數render和靜態渲染函數staticRenderFns(這是Vue針對靜態模板的優化)。上例中App.js通過new Vue得到第一個VM,接着執行render函數得到VNode,緊接着此VNode進入patch階段,模板中的html標簽是可以直接生成dom元素的;但是其中有一個M標簽,這不是一個html標簽,然而它作為一個組件已被注冊在數據的components屬性中,於是Vue拿到這個組件(一個options對象),然后通過Vue.extend(options)生成一個Vue的子類Sub,然后通過實例化這個Sub類得到M組件的VM(第二個VM)。當然,這個VM也會執行render和patch,然后插入到dom中去。

在這里插入圖片描述

初始化patch過程

Vue有兩類重要的Vnode,一種是占位Vnode(placeholder Vnode, 即上文中的M節點),它是一個虛擬的節點,作為M實際內容的父節點存在,這類節點的tag屬性一般為Vue-component-2-M這種形式,而普通Vnode(Actual Vnode)的tag屬性則是普通的html節點名稱,如div, span, ul等。
普通Vnode節點的patch過程很簡單,遞歸patch其子元素(createChildren),然后創建實際節點插入dom即可。
占位Vnode節點的patch過程則比較復雜,包含子元素的遞歸創建過程,上文所述M節點的VM創建即是在這個階段進行的。

在這里插入圖片描述

數據變更時的patch

如果通過某種操作變更了VM的數據,比如上面例子中,我們在M組件中調用this.containerClass = 'common'; this.listData = ['C', 'B', 'F', 'D', 'E', 'G', 'A'],此時M組件會再次render得到全新的VNode(這里再次render的觸發基於雙向數據綁定,這是Vue的一大核心,但不是本文重點),這個全新的VNode將與之前存在的VNode進行比較,得到差異后再patch進dom,從而完成更新。patch的含義是打補丁,用在這種場景再合適不過了。
VNode擁有和DOM類似的樹形結構,在patch過程中,新老VNode進行同層比較:父節點與父節點比,子節點與子節點比,不會跨層比較(這其實是Vue針對樹的編輯距離問題的一種處理,將時間復雜度降低到可以接受的程度)。在本例中,我們主要分析第三層的比較,以此闡述patch過程中diff算法的核心。

在這里插入圖片描述

設老VNode的序列為O,新VNode的序列為N,分別保留指向序列開頭和結尾的指針,稱為Os, Oe, Ns, Ne。
diff的目的是調整O使其變為N(調整老DOM得到新DOM),使用了雙指針算法:
首先是四次比較,比較的目的是發現相同的節點,用修改操作取代創建從而提高效率,這四次比較分別是:

  1. Os - Ns, 如果VNode相同則把兩個指針均向右移動,說明新老節點相同,不做處理;
  2. Oe - Ne, 如果VNode相同則把兩個指針均向左移動,說明新老節點相同,不做處理;
  3. Os - Ne, 如果VNode相同說明在新的序列中這個節點應該被移動到Oe后邊,直接在DOM層面處理移動,然后把Os右移,Ne左移;
  4. Oe - Ns, 如果VNode相同說明在新的序列中這個節點應該被移動到Os前面,直接在DOM層面處理移動,然后把Ns右移,Oe左移;
  5. 如果以上四種情況都沒有找到能夠匹配的VNode,則在序列O中尋找Ns。可知如果采取遍歷O序列的方式,diff算法的時間復雜度為O(min{len(O), len(N)} * len(O));而如果各個VNode擁有key屬性,Vue會事先建立一個[key]-[Vnode]的散列表,依據Ns的key去查表只需要O(1)時間,則diff算法的時間復雜度為O(max{len(O), len(N)})。如果查找成功,就將找到的節點移動到Os的前方,並將Ns右移;
  6. 如果上述情況均不能成功,那就說明O序列中並沒有Ns處的節點,只能創建一個新的節點,把它插入Os之前,並將Ns右移。
    上述循環進行直到Os出現在Oe之前或者Ns出現在Ne之前,此時如果Oe仍然在Os之前或者同一位置,說明原始序列中的這些元素要被刪除;如果Ne仍然在Ns之前或者同一位置,說明原始序列中的這些元素要被新添加,按情況執行對應的操作即可。

依據此算法,上面例子中的情形可以圖示為:
在這里插入圖片描述
以上就是最基本的Vue從定義到組件化到生成DOM的過程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM