目錄
- 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()
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/509637.html
標籤:其他
上一篇:axios學習筆記
