1. 程式人生 > 實用技巧 >vue的mvvm模式

vue的mvvm模式

1. 渲染專案列表時,“key” 屬性的作用和重要性是什麼?

key的特殊 attribute 主要用在 Vue 的虛擬 DOM 演算法,在新舊 nodes 對比時辨識 VNodes。

如果不使用 key,Vue 會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改/複用相同型別元素的演算法。

而使用 key 時,它會基於 key 的變化重新排列元素順序,並且會移除 key 不存在的元素。

有相同父元素的子元素必須有獨特的 key。重複的 key 會造成渲染錯誤。

2. 如何理解MVVM原理?

MVVM是一種軟體架構模式,

通過輕量的contrl改變資料,vm層監聽資料的變化更新dom樹(JS物件),然後計算出最佳改變檢視方法,再將最終的JS物件對映成真實的DOM,交由瀏覽器去繪製。

不直接改變檢視,而是改變dom樹,(使用Diff演算法)將dom樹對映成真實的DOM。

大大減少的渲染過程的消耗和提高了頁面更新渲染速度。

不管mvc還是將mvvm最終目的都是將model層資料展示到view上。

MVC:Model(模型)、View(檢視)、 Controller(控制器)。檢視可以給控制器發指令,控制器改變/更新資料和檢視。最終資料通過控制器展示到檢視上。

JS操作真實DOM的代價!

用我們傳統的開發模式MVC,原生JS或JQ操作DOM時,瀏覽器會從構建DOM樹開始從頭到尾執行一遍流程。

在一次操作中,我需要更新10個DOM節點,瀏覽器收到第一個DOM請求後並不知道還有9次更新操作,因此會馬上執行流程,最終執行10次。

例如,第一次計算完,緊接著下一個DOM更新請求,這個節點的座標值就變了,前一次計算為無用功。計算DOM節點座標值等都是白白浪費的效能。

即使計算機硬體一直在迭代更新,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現頁面卡頓,影響使用者體驗。

為什麼需要虛擬DOM,它有什麼好處?

Web介面由DOM樹(樹的意思是資料結構)來構建,當其中一部分發生變化時,其實就是對應某個DOM節點發生了變化,

虛擬DOM就是為了解決瀏覽器效能問題而被設計出來的。

如前,若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容儲存到本地一個JS物件中,最終將這個JS物件一次性attch到DOM樹上,再進行後續操作,避免大量無謂的計算量。

所以,用JS物件模擬DOM節點的好處是,頁面的更新可以先全部反映在JS物件(虛擬DOM)上,操作記憶體中的JS物件的速度顯然要更快,等更新完成後,再將最終的JS物件對映成真實的DOM,交由瀏覽器去繪製。

真實DOM和其解析流程?

瀏覽器渲染引擎工作流程都差不多,大致分為5步,建立DOM樹——建立StyleRules——建立Render樹——佈局Layout——繪製Painting

第一步,用HTML分析器,分析HTML元素,構建一顆DOM樹(標記化和樹構建)。

第二步,用CSS分析器,分析CSS檔案和元素上的inline樣式,生成頁面的樣式表。

第三步,將DOM樹和樣式表,關聯起來,構建一顆Render樹(這一過程又稱為Attachment)。每個DOM節點都有attach方法,接受樣式資訊,返回一個render物件(又名renderer)。這些render物件最終會被構建成一顆Render樹。

第四步,有了Render樹,瀏覽器開始佈局,為每個Render樹上的節點確定一個在顯示屏上出現的精確座標。

第五步,Render樹和節點顯示座標都有了,就呼叫每個節點paint方法,把它們繪製出來。

DOM樹的構建是文件載入完成開始的?構建DOM數是一個漸進過程,為達到更好使用者體驗,渲染引擎會盡快將內容顯示在螢幕上。它不必等到整個HTML文件解析完畢之後才開始構建render數和佈局。

Render樹是DOM樹和CSSOM樹構建完畢才開始構建的嗎?這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊載入,一遍解析,一遍渲染的工作現象。

CSS的解析是從右往左逆向解析的(從DOM樹的下-上解析比上-下解析效率高),巢狀標籤越多,解析越慢。


作者:LoveBugs_King
連結:https://www.jianshu.com/p/af0b398602bc
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

3.vue-Diff操作

當dom需改變時,先創新的虛擬dom, 和舊的虛擬dom進行平級比較(4種):

replace (替換):節點變

props(更新):屬性/屬性值變,

text(更新文字):文字內容變,

reorder(重排序):移動/增/刪

得到一個diff結果,一組新舊虛擬dom差異資料。

以diff結果為根據,使用documentFragment (文件片段物件),對真實的dom進行一次更新。

reorder重排序(關於key)

當節點不使用key(唯一),再diff的平級比較中,按順序比較,一旦出現差異,差異後同級的節點全部要被解除安裝,然後再按實際情況添加回去,如果差異出現的在前,後面順序沒問題,這種效率明顯低下。

當節點使用key(唯一),再diff的平級比較中,它能夠根據key,直接找到具體位置進行操作,效率比較高。

在實際程式碼中,會對新舊兩棵樹進行一個深度的遍歷,每個節點都會有一個標記。每遍歷到一個節點就把該節點和新的樹進行對比,如果有差異就記錄到一個物件中。

下面我們建立一棵新樹,用於和之前的樹進行比較,來看看Diff演算法是怎麼操作的。

old Tree new Tree

平層Diff,只有以下4種情況:

1、節點型別變了,例如下圖中的P變成了H3。我們將這個過程稱之為REPLACE。直接將舊節點解除安裝並裝載新節點。舊節點包括下面的子節點都將被解除安裝,如果新節點和舊節點僅僅是型別不同,但下面的所有子節點都一樣時,這樣做效率不高。但為了避免O(n^3)的時間複雜度,這樣是值得的。這也提醒了開發者,應該避免無謂的節點型別的變化,例如執行時將div變成p沒有意義。

2、節點型別一樣,僅僅屬性或屬性值變了。我們將這個過程稱之為PROPS。此時不會觸發節點解除安裝和裝載,而是節點更新。

查詢不同屬性方法

3、文字變了,文字對也是一個Text Node,也比較簡單,直接修改文字內容就行了,我們將這個過程稱之為TEXT

4、移動/增加/刪除 子節點,我們將這個過程稱之為REORDER。看一個例子,在A、B、C、D、E五個節點的B和C中的BC兩個節點中間加入一個F節點。

例子

我們簡單粗暴的做法是遍歷每一個新虛擬DOM的節點,與舊虛擬DOM對比相應節點對比,在舊DOM中是否存在,不同就解除安裝原來的按上新的。這樣會對F後邊每一個節點進行操作。解除安裝C,裝載F,解除安裝D,裝載C,解除安裝E,裝載D,裝載E。效率太低。

粗暴做法

如果我們在JSX裡為陣列或列舉型元素增加上key後,它能夠根據key,直接找到具體位置進行操作,效率比較高。常見的最小編輯距離問題,可以用Levenshtein Distance演算法來實現,時間複雜度是O(M*N),但通常我們只要一些簡單的移動就能滿足需要,降低精確性,將時間複雜度降低到O(max(M,N))即可。

最終Diff出來的結果

對映成真實DOM

虛擬DOM有了,Diff也有了,現在就可以將Diff應用到真實DOM上了。深度遍歷DOM將Diff的內容更新進去。

根據Diff更新DOM 根據Diff更新DOM

我們會有兩個虛擬DOM(js物件,new/old進行比較diff),使用者互動我們操作資料變化new虛擬DOM,old虛擬DOM會對映成實際DOM(js物件生成的DOM文件)通過DOM fragment操作給瀏覽器渲染。當修改new虛擬DOM,會把newDOM和oldDOM通過diff演算法比較,得出diff結果資料表(用4種變換情況表示)。再把diff結果表通過DOMfragment更新到瀏覽器DOM中。

虛擬DOM的存在的意義?vdom 的真正意義是為了實現跨平臺,服務端渲染,以及提供一個性能還算不錯 Dom 更新策略。vdom 讓整個 mvvm 框架靈活了起來

Diff演算法只是為了虛擬DOM比較替換效率更高,通過Diff演算法得到diff演算法結果資料表(需要進行哪些操作記錄表)。原本要操作的DOM在vue這邊還是要操作的,只不過用到了js的DOMfragment來操作dom(統一計算出所有變化後統一更新一次DOM)進行瀏覽器DOM一次性更新。其實DOMfragment我們不用平時發開也能用,但是這樣程式設計師寫業務程式碼就用把DOM操作放到fragment裡,這就是框架的價值,程式設計師才能專注於寫業務程式碼




作者:LoveBugs_King
連結:https://www.jianshu.com/p/af0b398602bc
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

4.Vue的響應式原理

當Compile解析dom檢測到相關字串 進行訂閱者初始化,新增到dep(一個負責管理訂閱者的物件)

observer監聽變數,變數發生變化時,通知dep, dep遍歷通知訂閱者,訂閱者呼叫物件的更新檢視的函式(新建新的虛擬dom,然後比對舊的虛擬dom,diff演算法,計算出更新檢視的方法)更新檢視。

observer監聽器(監聽資料變化並通過dep釋出任務)

observer接受需要監聽data,通過dep(每個屬性一個dep)通知訂閱者,

遍歷data屬性 Object.defineProperty給屬性新增set和get方法,set方法被觸發時,通過dep給訂閱者釋出任務。

dep訂閱器(儲存訂閱者,釋出任務)

Dep 扮演的角色是排程中心/訂閱器,主要的作用就是收集觀察者Watcher和通知觀察者目標更新。

每個屬性擁有自己的訊息訂閱器dep,用於存放所有訂閱了該屬性的觀察者物件,

當資料發生改變時,會遍歷觀察者列表(dep.subs),通知所有的watch,讓訂閱者執行自己的update邏輯。

watcher訂閱者(訂閱任務並呼叫檢視更新方法)

訂閱任務並呼叫檢視更新方法

complie解析器(解析dom結構)

遍歷所有子節點,識別節點的值{{}}、繫結的事件v-on、繫結的屬性值v-model。

為節點新增事件監聽,

為帶有v-model添屬性的節點加input事件,並賦值給-model繫結的變數,

為節點的值繫結的變數、v-model定的變數 初始化為訂閱者。

雙向繫結如何實現:

1、我們需要一個方法來識別檢視中哪個元素被設定了雙向繫結。

2、我們需要監視檢視和資料的變化。

3、我們需要將所有變化傳播到繫結檢視或者資料。

幾種實現資料雙向繫結的做法:1、釋出者-訂閱者模式(backbone.js)、髒值檢查(angular.js)、資料劫持(vue.js)。

釋出者-訂閱者模式

data物件中變數作為models, 遍歷(被雙向繫結的物件)訂閱/取消訂閱 對應的任務,當models屬性值發生變化時會觸發釋出對應任務, 訂閱者收到任務通知,進行一系列處理並響應到檢視層。

髒值檢查

和上種方式類似,但在資料驅動檢視變化,不是改完值手動觸發set函式觸發檢視更新,而是通過setInterval()定時輪詢檢測資料變化觸發set函式。

資料劫持

vue.js是通過資料劫持(object.defineProperty()的set和get)結合釋出者-訂閱者方式,在資料變動時釋出訊息給訂閱者,觸發相應回撥監聽。

1、實現一個數據監聽器Observer,能夠對資料物件的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者。

2、實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模版替換資料,以及繫結相應的更新函式。

3、實現一個watcher,做為連線Observer和Compile的橋樑,能夠訂閱並收到每個屬性變動的通知。執行指令繫結的相應回撥函式,從而更新檢視。

4、mvvm入口函式,整合以上三者。


維護訂閱器

訂閱器

維護一個訂閱器,負責例項化訂閱者,當初始化和更新時,呼叫相關函式。

Dep為一個建構函式,有subs陣列儲存訂閱者,addSub和notify兩個函式,addSub負責在初始化訂閱者初始化時(當Compile解析dom檢測到相關字串 進行訂閱者初始化)新增到訂閱器中,notify負責在觀察到資料更新時被觸發 去呼叫訂閱者的更新函式。

實現Observe

observ

Observer主要是對 物件屬性 通過 defineProperty()進行監聽,getter時幫助訂閱者初始化時加入訂閱器,setter時更新物件屬性及通知訂閱者呼叫函式更新訂閱者值。

一個建構函式Observer,一個觸發函式observe。

observe函式判斷引數是否是物件,是的話例項化一個Observe物件(物件屬性 遍歷遞迴 時判斷子屬性值 是否是物件)。

Observe接受一個物件,有Walk、defineReactive兩個函式,Walk負責遍歷物件每個屬性呼叫defineReactive。defineReactive負責遞迴每個物件屬性 設定監聽器。

Q/A

每次defineReactive都會new Dep()再在getter中初始化push訂閱者,Dep中怎麼會有所有訂閱者?

其實只有一個訂閱者,每次都會例項一個Dep,有多個Dep。

setter時迴圈訂閱器中每個訂閱者呼叫update函式,update函式做了什麼事情?

監聽器更新資料時觸發的更新函式判斷 new/old資料是否相同,不相同就把舊Value賦予新值,並在全域性執行回撥函式(傳入新舊值)

實現訂閱者

訂閱者

每個訂閱者例項有4個物件屬性,cb(監聽器更新資料時觸發的函式)vm(元件物件),exp(繫結的屬性key)value(繫結的屬性值)。run和get兩個函式,run為監聽器更新資料時觸發的更新函式判斷 new/old資料是否相同,不相同就把舊Value賦予新值,並在全域性執行回撥函式(傳入新舊值)。get為初始化時把自己新增進訂閱器Dep()中。

實現Compile

解析器主要作用是 遍歷遞迴解析dom節點,解析到雙向繫結的指令,將初始化的資料初始化到檢視中,例項化訂閱器並繫結更新函式。

第一部分

Compile建構函式有3個屬性,vm(全域性環境)el(html最高節點)fragment用來存放dom節點(我們資料更新dom時需要多次操作dom,通過createDocumentFragment建立一個虛擬父節點fragment,把dom移入fragment進行操作,操作完了直接替換整個dom(一次性替換操作效率更高比一次次操作塊70%)。

init()呼叫了nodeToFragment、compileElement、compile三個函式。

nodeToFragment,把dom塞入fragment虛擬父節點。

compileElement,遍歷遞迴fragment中dom,判斷是元素節點的話執行compile函式,是文字節點且有'{{}}'的話執行compileText函式。如果節點有子節點繼續遞迴執行compileElement。

compile,對dom節點的屬性節點進行遍歷,若有"v-"相關欄位屬性name,若有":on"相關欄位則繫結的是事件,執行compileEvent事件,否則執行compileMdole事件

第二部分

"{{}}"對應的compileText函式,負責初始化節點textContent資料,並新增一個訂閱者。

"v-on:"對應的compileEvent函式,負責取得事件名和事件值 通過addEventListener監聽函式觸發執行對應事件。

"v-model"對應compileModel函式,負責初始化節點value資料,並新增一個訂閱者,再通過對node.addEventListener('input', function(...))在input資料變化時實時改變物件資料

第三部分

mvvm入口

入口

我們把整個流程結合起來看一遍

入口建構函式,需要一個資料物件data,需要一個函式物件methods(當data中資料變化時呼叫)。

有一個proxyKeys函式,作用,在訪問selfVue的屬性時代理為selfVue.data屬性(this.data.name = 'canfoo'我們可以用更簡潔的方式 this.name = 'canfoo'),也是通過遍歷每個data屬性為每個屬性新增監聽器object.defineProperty(),在get內把對this.key的訪問替換成this.data.key的屬性值來處理。

監聽器observe對資料物件進行監聽。

例項化compile物件,把節點傳入,在compile會對dom節點進行遍歷遞迴,處理3種情況。1、"{{}}",初始化節點texteContent數值,例項化一個訂閱者。2、"v-model",初始化節點value數值,例項化一個訂閱者,並監聽input事件實時對資料更新。3、"v-on:"把對應事件名和methods中事件進行繫結監聽addEventListener。

例項化訂閱者Watcher,會在初始化時把自己新增進訂閱器Dep(),在資料更新時會通過this.c.call()觸發傳進來的函式 處理資料。

所有事情處理好後執行mounted函式。



作者:LoveBugs_King
連結:https://www.jianshu.com/p/70b06d82ccfc
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

5.簡易實現Object.defineProperty下的繫結

var data = {
    name: ''
  };
  // Data Bindings
  Object.defineProperty(data, 'name', {
    get : function(){},
    set : function(newValue){
      // 頁面響應處理
      document.getElementById('name').innerText = newValue
      data.name = value
    },
    enumerable : true,
    configurable : true
  });
  // 頁面DOM listener
  document.getElementById('name').onchange = function(e) {
    data.name = e.target.value;
  }