Vue 響應式數據 什麼是響應式數據:數據變了,視圖能更新,反之視圖更新,數據要不要更新,不歸響應式數據管。 Vue 在內部實現了一個最核心的defineReactive方法,藉助了Object.defineProperty,核心就是劫持屬性(只會劫持已經存在的屬性),把所有的屬性,重新的添加了 g ...
目錄
- Vue 響應式數據
- Vue 中如何進行依賴收集
- Vue 中模板編譯原理
- Vue 生命周期鉤子
- Vue 組件 data 為什麼必須是個函數?
- nextTick 原理
- set 方法實現原理
- 虛擬 dom 的作用
- diff 演算法的實現原理
- Vue 中 key 的作用和原理
- vue 初渲染流程
- vue 更新流程 依賴收集實現過程
- vue 非同步更新的實現流程
- 組件的初始化流程
- keep-alive 實現原理
Vue 響應式數據
什麼是響應式數據:數據變了,視圖能更新,反之視圖更新,數據要不要更新,不歸響應式數據管。
Vue 在內部實現了一個最核心的defineReactive
方法,藉助了Object.defineProperty
,核心就是劫持屬性(只會劫持已經存在的屬性),把所有的屬性,重新的添加了 getter 和 setter,因此在用戶取值和設置值的時候,可以進行一些操作。
- 對象:多層對象需要通過遞歸來實現劫持。
- 數組:考慮性能原因沒有用 defineProperty 對數組的每一項進行劫持,而是選擇重寫數組的(push,shift,pop,unshift,sort,splice,reverse)方法,數組中如果是對象數據類型也會進行遞歸劫持,數組的索引和長度變化是無法監控到的。
Vue 中如何進行依賴收集
- 每個屬性都擁有自己的 dep 屬性,存放他所依賴的 watcher,當屬性變化後會通知自己對應的 watcher 去更新
- 預設在初始化時會調用 render 函數,此時會觸發屬性依賴收集 dep.depend()
- 當屬性發生修改時會觸發 watcher 更新 dep.notify()
Vue 在初始化的時候會進行掛載$mount
操作,會進行編譯操作,最終會走到render function
,當組件進行渲染時會去取值,取值getter
時,調用dep.depend()收集這個 watcher,存放在Dep
中,當我們去更改值setter
,調用dep.notify()去通知這個 watcher 去更新,實際上 watcher 中存放的就是組件的update
函數.更新的時候,就會走到虛擬 dom 相關的方法。
Vue 中模板編譯原理
模板編譯原理實際上就是 將 template 轉換成 render 函數
,大致可分為以下三步:
- 將 template 模板轉換成 ast 語法樹 - parserHTML
- 定義一個 stack 棧,存放標簽的父子關係
- 通過正則匹配模板字元串,不停的解析,不停的刪除,直至字元串解析完成,
- 得到 ast 樹,(存放標簽名,子節點,及屬性列表)
- 對靜態語法做靜態標記 static,會遞歸遍歷子節點進行標記,組件和插槽不屬於靜態語法 - markUp
- 只有在第一次編譯時,會進行靜態標記,不是每次渲染都標記
- 靜態標記主要是用來做 diff 優化的,靜態節點跳過 diff 操作
- 子節點有一個變化,父節點都不是靜態的
- 生成代碼,核心就是拼接字元串(_c,_v,_s),最終加上with語法 - codeGen
Vue 生命周期鉤子
- Vue 的生命周期鉤子就是回調函數而已,當創建組件實例的過程中會調用對應的鉤子方法。
- 內部會對鉤子函數進行處理,將鉤子函數維護成數組的形式
- 首先會採用策略模式,對 hook 進行合併 mergeHook(),合併成隊列,然後依次調用
function mergeHook(parentVal, childVal) {
const res = childVal // 兒子有
? parentVal
? parentVal.concat(childVal) // 父親也有,就是合併
: Array.isArray(childVal) // 兒子是數組
? childVal
: [childVal] // 不是數組包裝成數組
: parentVal;
return res ? dedupeHooks(res) : res;
}
- beforeCreate 在實例初始化 init 之後,數據初始化(data observer)之前調用,拿不到響應式的狀態,可以拿到$on、$events 以及一些父子關係。在當前階段 data、methods、computed 以及 watch 上的數據和方法都不能被訪問。
- created 數據初始化完畢後調用,實例已經創建完成。完成數據觀測(data observer),屬性和方法的運算,可以直接用響應式數據。但是沒有$el,不能進行 dom 操作。
- beforeMount 在掛載開始之前被調用(在 mountComponent 方法中被調用):之後相關的 render 函數首次被調用。
- mounted el 被新創建的真實的 vm.$el 替換,並掛載到實例上後調用該鉤子。此階段可以獲取渲染後的節點。
- beforeUpdate 數據更新前調用,在創建 Watcher 時會傳一個 before 方法,它裡面會調用 beforeUpdate 鉤子,每次頁面更新都會去調用當前的渲染 watcher,會判斷有沒有 before 方法,有的話就會調用 beforeUpdate, 發生在虛擬 DOM 重新渲染和打補丁 patch 之前。然後再去執行 watcer.run()真實的更新方法。
- updated 執行完 watcer.run()之後,調用 updated 鉤子,表示 dom 已完成更新。 (執行數據更改導致的虛擬 DOM 重新渲染和打補丁)。註意避免在此期間更新數據,因為可能會導致為無限迴圈的更新。
- beforeDestroy 實例銷毀之前調用。僅作為實例即將的信號,實例仍然完全可用。之後會進行一系列的卸載操作。執行真正的卸載(從父節點中移除、清空自己的 watcher、卸載所有的屬性、標記當前組件銷毀狀態、把虛擬節點也銷毀掉、然後調 destroyed)。可以在這時進行一些收尾工作如清除定時器等。
- destroyed 實例銷毀後調用。移除所有的事件監聽器(否則會導致記憶體泄漏),銷毀所有子實例。設置當前虛擬節點的父節點為 null。該鉤子在伺服器端渲染期間不被調用。
Vue 組件 data 為什麼必須是個函數?
組件復用,需要每個組件中都有自己的 data,這樣組件之間才不會相互干擾,組件中的 data 如果寫成對象形式,就使多個組件實例會共用一份 data,一個數據變化後,會影響其他實例中的數據。
因此每次使用組件時都會對組件進行實例化操作後,調用 data 函數返回一個對象作為組件的數據源。這樣可以保證多個組件間數據互不影響。
而根實例(new Vue())採用單例模式,且不需要任何的合併操作,所以根實例的 data 屬性可以是函數,也可以是對象,實際上源碼中根本的判斷條件為 vm 屬性,只有根才有 vm 屬性,組件和 mixin 都沒有 vm 屬性,因此可以作為判斷條件,區分 data 是否為函數。並給出相關報錯信息。
nextTick 原理
當用戶修改了數據後並不會馬上更新視圖,更新 DOM 時是非同步執行的,只要偵聽到數據變化,Vue 將開啟一個任務隊列,並緩衝同一時間迴圈中發生的所有數據變更。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。而 $nextTick 中的方法會被放到更新隊列的後面,在下次 DOM 更新迴圈結束之後執⾏延遲回調,視圖需要等隊列中所有任務完成之後,再統一進行更新。在修改數據之後使⽤ $nextTick,則可以在回調中獲取更新後的 DOM。
Vue 在內部對非同步隊列嘗試使用原生的 Promise.then(微任務)、MutationObserver 和 setImmediate,如果執行環境不支持,則會採用 setTimeout(fn, 0)(巨集任務)代替。
set 方法實現原理
- 如果目標不存在或者是原始類型,直接報錯,cannot set reactive property on undefined,null,or primitive value
- 如果是數組,Vue.set(arr,1,100),調用重寫的
target.splice(key,1,val)
方法,可以更新視圖 - 如果是對象,看這個對象本身有沒有這個值,如果有就直接更新就好,因為他本身就是響應式的,
- 如果是根實例,或者根數據 data 時,會報錯提示 應該在初始化時聲明該數據
- 如果不是響應式數據,也不需要將其定義成響應式屬性 Vue.set({},'age',18),相當於這個對象本身就不是響應式的,就直接賦值,也不需要更新視圖
- 最後就把調用的屬性定義成響應式的即可。調用
defineReactive(ob.value,key,val)
- 通知視圖更新
ob.dep.notify()
因此 Vue.set 實際上就是兩個方法的集合,target.splice(key,1,val)
和 defineReactive(ob.value,key,val)
,
虛擬 dom 的作用
是什麼:Virtual DOM 就是用 js 對象來描述真實 DOM 結構,是對真實 DOM 的抽象。
為什麼:由於直接操作 DOM 性能低,但是 js 層的操作效率高,可以將 DOM 操作轉化成對象操作,最終通過 diff 演算法比對新舊 vdom 的差異進行更新 DOM(減少了對真實 DOM 的操作)。
邊操作 dom 邊獲取視圖,每次操作 dom 都可能會引起 dom 的迴流和重繪,導致性能不高,有了 vdom 就可以把所有的操作都放在 vdom 上,最終把更新和一系列的邏輯批量的同步到真實 dom 上,
好處:虛擬 DOM 不依賴真實平臺環境從而也可以實現跨平臺。比如 nodejs 就沒有 Dom,想要實現 SSR 就需要藉助 Vdom
diff 演算法的實現原理
Vue 的 diff 演算法是平級比較,不考慮跨級比較的情況。內部採用深度遞歸的方式 + 雙指針的方式進行比較(雙指針分別指向新舊的結尾)。
- 先比較是否是相同節點,判斷屬性 key + tag
- 相同節點比較屬性,並復用老節點
- 比較兒子節點,考慮老節點和新節點兒子的情況
- 優化比較:頭頭、尾尾、頭尾、尾頭
- 比對查找進行復用
diff 的複雜度 是 O(n),當一方子元素的頭尾相等時,結束迴圈,(因為同層比較,內部只有一層迴圈).子元素嵌套時,遞歸同層比較
如果不能匹配到的話,就會根據當前的老的索引 key 創建一個映射表,拿新的去裡面找,如果能找到就復用,找不到就創建新的,最終把老的多餘的刪掉,
Vue 中 key 的作用和原理
- Vue 在 patch 過程中通過 key 可以判斷兩個虛擬節點是否是相同節點。 (可以復用老節點)
- 無 key 會導致更新的時候出問題,比如 unshift 變成 push 效果,並更新所有節點,有 key 時,就可以節點復用,僅做節點的移動即可。
- 儘量不要採用索引作為 key,而是使用數據的唯一標識
vue 初渲染流程
- vue 初始化流程 _init:
- 預設會調用 vue._init 方法將用戶的參數掛在到$options 選項上,vm.$options。(vue 調用的方法使用原型擴展的形式)
- vue 會根據用戶的參數進行數據的初始化,data props computed watch 等 ,在外界是無法訪問的,可以通過 vm._data 訪問到用戶的數據。
- 對數據進行觀測,對象(遞歸使用 Object.defineProperty),數組(方法重寫,切片編程),劫持到用戶的操作,觀測的目的是用戶修改數據時 -> 更新視圖
- 將數據代理到 vm 對象上,vm.xxx => vm._data.xxx
- vue 掛載流程 $mount:
- 判斷用戶是否傳入了 el 屬性, 內部會調用$mount 方法,用戶也可以自行調用該方法
- 處理模板優先順序 render / template / outerHTML
- 將模板編譯成函數, 步驟: parseHTML 解析模板 -> ast 語法樹, generate 解析語法樹生成 code -> new Function 生成 render 函數
- 通過 render 方法,生成虛擬 dom + 真實的數據 => 真實的 dom
- 根據虛擬節點渲染真實的節點
vue 更新流程 依賴收集實現過程
- vue 中使用了觀察者模式,預設組件渲染的時候,會創建一個 watcher,並且會渲染視圖
- 當渲染視圖的時候,會取 data 中的數據,會走每個屬性的 get 方法,就讓這個屬性的 dep 記錄 watcher
- 同時讓 watcher 也記住 dep,dep 和 watcher 是多對多的關係,因為一個屬性可以對應多個視圖,一個視圖對應多個數據
- 如果數據發生變化,會通知對應屬性的 dep,一次通知存放的 watcher 去更新
一個屬性對應一個 dep, 一個 dep 對應多個 watcher(數據多頁面共用)
一個組件對應一個 watcher,一個 watcher 可以對應多個 dep(多個屬性)
觀察者模式: dep 收集 watcher,變化時一次通知,watcher 是觀察者,dep 是被觀察者
dep 用來收集渲染邏輯(watcher),watcher 中存放的是組件的 update 函數。數據變化通知 dep 中的 watcher 去執行對應的 update 方法
頁面重新渲染邏輯:只有當頁面模板中用到的數據(就是寫在 render 中的數據) 發生改變時,才會調用 update 方法
vue 非同步更新的實現流程
開啟一個非同步隊列並將更新的 watcher 去重,將用戶的$nextTick 和內部的更新邏輯, 合併為一個 Promise.then,依次執行(多個 nextTick 是一個 promise.then)
nextTick 用一個非同步任務,將多個方法維持一個隊列里,執行時機遵循 js 的 eventloop 機制,具體的執行時機 ,要看底層用的是那個方法,因為 vue 考慮了瀏覽器的相容性,vue 中對 nextTick 做了很多相容性處理,promise 微任務 > MutationObserver(h5 的 api 微任務) > setImmediate > setTimeout
組件的初始化流程
- 第一步:創造組件的虛擬節點,創建虛擬節點的時候,內部會去調用 Vue.extend 方法,產生組件的構造函數 Ctor
- 第二步:給組件添加鉤子函數,data.hook = {init},合併 mergeOptions (自己的組件.proto = 全局的組件),最終返回了一個虛擬節點
- 第三步:頁面開始渲染,渲染的時候,會去調用 patch 方法,並且根據當前的虛擬節點,轉換成真實節點,這時會去調用 createElm,創造真實節點。
- 第四步:創造真實節點的時候發現,如果這個節點是組件,就會調用組件的 createCompontent => 調用 hook.init 方法,
- 第五步: 此時 init 方法,會 new Ctor(),之後會進行子組件的初始化操作 this._init
- 第六步:最終再去調用組件的掛載操作$mount,產生一個$el 真實節點,對應組件模板渲染後的結果。
- 第七步:將組件的 vnode.componentInstance.$el 插入到父標簽中
keep-alive 實現原理
keep-alive 組件是一個抽象組件, 也是一個虛擬組件, 不會被記錄到父子組件關係當中,一般用在路由組件的外層, 主要為了緩存組件, 為頻繁掛載銷毀,提供緩存功能節約性能,
- 包含 include 屬性,添加白名單,表示那些組件需要緩存,切換過後才會進行緩存,並不是將白名單中的 name 直接全部緩存。
- 包含 exclude 屬性,添加黑名單,表示那些組件不用緩存
- max = x 最多緩存幾個組件, 如果超過最大限制 需要刪除第一個, 在增加最新的 LRU
- created 鉤子:創造一個對象 cache 來緩存組件,key[],表示緩存的是誰
- render():渲染
- mounted():掛載,通過 watch Api 監控 include 和 exclude 做緩存處理,pruneCache
render
獲取 keep-alive 中的所有子組件,獲取插槽中的第一個,根據組件的名稱, 判斷 include 和 exclude, 拿到後把組件的實例緩存起來
拿到組件的 key 用來做緩存,如果有緩存 獲取緩存的實例,ABA,=>shift 以後再 push
緩存組件 會緩存子組件,緩存的是父節點的 el, 其中包含著所有子組件渲染後完整的結果。
第一次渲染完畢後,會把虛擬節點進行標記直接返回一個組件,keep-alive 最終渲染的結果就是第一個子組件
mounted
緩存中存放了
{組件的 key : 組件的實例}
,復用的時候,直接使用緩存中,組件的實例
如果超過最大限制 需要刪除第一個,在增加最新的,遵循 LRU 原則(Least Recently Used 即最近最久未使用的)
組件更新
每次切換組件,都會進行組件的初始化流程 init 方法,第一次組件渲染時,會在組件虛擬節點上掛載 componentIntance 屬性和 keepalive 標記
更新時會再次調用 init 方法,此時會判斷虛擬節點的屬性和 keepalive 標記,進行 prepatch 方法,對會組件插槽中的內容進行比較。
會判斷組件是否需要進行強制更新,會比較新老節點,去執行當前實例的強制更新方法,vm.$forceUpdate ,實際走的就是 keep-alive 的 render()