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
observObserver主要是對 物件屬性 通過 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; }