Vue源碼剖析

来源:https://www.cnblogs.com/echoyya/archive/2022/09/26/16726541.html
-Advertisement-
Play Games

Vue 響應式數據 什麼是響應式數據:數據變了,視圖能更新,反之視圖更新,數據要不要更新,不歸響應式數據管。 Vue 在內部實現了一個最核心的defineReactive方法,藉助了Object.defineProperty,核心就是劫持屬性(只會劫持已經存在的屬性),把所有的屬性,重新的添加了 g ...


目錄

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 函數,大致可分為以下三步:

  1. 將 template 模板轉換成 ast 語法樹 - parserHTML
  • 定義一個 stack 棧,存放標簽的父子關係
  • 通過正則匹配模板字元串,不停的解析,不停的刪除,直至字元串解析完成,
  • 得到 ast 樹,(存放標簽名,子節點,及屬性列表)
  1. 對靜態語法做靜態標記 static,會遞歸遍歷子節點進行標記,組件和插槽不屬於靜態語法 - markUp
  • 只有在第一次編譯時,會進行靜態標記,不是每次渲染都標記
  • 靜態標記主要是用來做 diff 優化的,靜態節點跳過 diff 操作
  • 子節點有一個變化,父節點都不是靜態的
  1. 生成代碼,核心就是拼接字元串(_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;
}
  1. beforeCreate 在實例初始化 init 之後,數據初始化(data observer)之前調用,拿不到響應式的狀態,可以拿到$on、$events 以及一些父子關係。在當前階段 data、methods、computed 以及 watch 上的數據和方法都不能被訪問。
  2. created 數據初始化完畢後調用,實例已經創建完成。完成數據觀測(data observer),屬性和方法的運算,可以直接用響應式數據。但是沒有$el,不能進行 dom 操作。
  3. beforeMount 在掛載開始之前被調用(在 mountComponent 方法中被調用):之後相關的 render 函數首次被調用。
  4. mounted el 被新創建的真實的 vm.$el 替換,並掛載到實例上後調用該鉤子。此階段可以獲取渲染後的節點。
  5. beforeUpdate 數據更新前調用,在創建 Watcher 時會傳一個 before 方法,它裡面會調用 beforeUpdate 鉤子,每次頁面更新都會去調用當前的渲染 watcher,會判斷有沒有 before 方法,有的話就會調用 beforeUpdate, 發生在虛擬 DOM 重新渲染和打補丁 patch 之前。然後再去執行 watcer.run()真實的更新方法。
  6. updated 執行完 watcer.run()之後,調用 updated 鉤子,表示 dom 已完成更新。 (執行數據更改導致的虛擬 DOM 重新渲染和打補丁)。註意避免在此期間更新數據,因為可能會導致為無限迴圈的更新。
  7. beforeDestroy 實例銷毀之前調用。僅作為實例即將的信號,實例仍然完全可用。之後會進行一系列的卸載操作。執行真正的卸載(從父節點中移除、清空自己的 watcher、卸載所有的屬性、標記當前組件銷毀狀態、把虛擬節點也銷毀掉、然後調 destroyed)。可以在這時進行一些收尾工作如清除定時器等。
  8. 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 方法實現原理

  1. 如果目標不存在或者是原始類型,直接報錯,cannot set reactive property on undefined,null,or primitive value
  2. 如果是數組,Vue.set(arr,1,100),調用重寫的target.splice(key,1,val)方法,可以更新視圖
  3. 如果是對象,看這個對象本身有沒有這個值,如果有就直接更新就好,因為他本身就是響應式的,
  4. 如果是根實例,或者根數據 data 時,會報錯提示 應該在初始化時聲明該數據
  5. 如果不是響應式數據,也不需要將其定義成響應式屬性 Vue.set({},'age',18),相當於這個對象本身就不是響應式的,就直接賦值,也不需要更新視圖
  6. 最後就把調用的屬性定義成響應式的即可。調用defineReactive(ob.value,key,val)
  7. 通知視圖更新 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 演算法是平級比較,不考慮跨級比較的情況。內部採用深度遞歸的方式 + 雙指針的方式進行比較(雙指針分別指向新舊的結尾)。

  1. 先比較是否是相同節點,判斷屬性 key + tag
  2. 相同節點比較屬性,並復用老節點
  3. 比較兒子節點,考慮老節點和新節點兒子的情況
  4. 優化比較:頭頭、尾尾、頭尾、尾頭
  5. 比對查找進行復用

diff 的複雜度 是 O(n),當一方子元素的頭尾相等時,結束迴圈,(因為同層比較,內部只有一層迴圈).子元素嵌套時,遞歸同層比較
如果不能匹配到的話,就會根據當前的老的索引 key 創建一個映射表,拿新的去裡面找,如果能找到就復用,找不到就創建新的,最終把老的多餘的刪掉,

Vue 中 key 的作用和原理

  1. Vue 在 patch 過程中通過 key 可以判斷兩個虛擬節點是否是相同節點。 (可以復用老節點)
  2. 無 key 會導致更新的時候出問題,比如 unshift 變成 push 效果,並更新所有節點,有 key 時,就可以節點復用,僅做節點的移動即可。
  3. 儘量不要採用索引作為 key,而是使用數據的唯一標識

vue 初渲染流程

  • vue 初始化流程 _init:
  1. 預設會調用 vue._init 方法將用戶的參數掛在到$options 選項上,vm.$options。(vue 調用的方法使用原型擴展的形式)
  2. vue 會根據用戶的參數進行數據的初始化,data props computed watch 等 ,在外界是無法訪問的,可以通過 vm._data 訪問到用戶的數據。
  3. 對數據進行觀測,對象(遞歸使用 Object.defineProperty),數組(方法重寫,切片編程),劫持到用戶的操作,觀測的目的是用戶修改數據時 -> 更新視圖
  4. 將數據代理到 vm 對象上,vm.xxx => vm._data.xxx
  • vue 掛載流程 $mount:
  1. 判斷用戶是否傳入了 el 屬性, 內部會調用$mount 方法,用戶也可以自行調用該方法
  2. 處理模板優先順序 render / template / outerHTML
  3. 將模板編譯成函數, 步驟: parseHTML 解析模板 -> ast 語法樹, generate 解析語法樹生成 code -> new Function 生成 render 函數
  4. 通過 render 方法,生成虛擬 dom + 真實的數據 => 真實的 dom
  5. 根據虛擬節點渲染真實的節點

vue 更新流程 依賴收集實現過程

  1. vue 中使用了觀察者模式,預設組件渲染的時候,會創建一個 watcher,並且會渲染視圖
  2. 當渲染視圖的時候,會取 data 中的數據,會走每個屬性的 get 方法,就讓這個屬性的 dep 記錄 watcher
  3. 同時讓 watcher 也記住 dep,dep 和 watcher 是多對多的關係,因為一個屬性可以對應多個視圖,一個視圖對應多個數據
  4. 如果數據發生變化,會通知對應屬性的 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

組件的初始化流程

  1. 第一步:創造組件的虛擬節點,創建虛擬節點的時候,內部會去調用 Vue.extend 方法,產生組件的構造函數 Ctor
  2. 第二步:給組件添加鉤子函數,data.hook = {init},合併 mergeOptions (自己的組件.proto = 全局的組件),最終返回了一個虛擬節點
  3. 第三步:頁面開始渲染,渲染的時候,會去調用 patch 方法,並且根據當前的虛擬節點,轉換成真實節點,這時會去調用 createElm,創造真實節點。
  4. 第四步:創造真實節點的時候發現,如果這個節點是組件,就會調用組件的 createCompontent => 調用 hook.init 方法,
  5. 第五步: 此時 init 方法,會 new Ctor(),之後會進行子組件的初始化操作 this._init
  6. 第六步:最終再去調用組件的掛載操作$mount,產生一個$el 真實節點,對應組件模板渲染後的結果。
  7. 第七步:將組件的 vnode.componentInstance.$el 插入到父標簽中

keep-alive 實現原理

keep-alive 組件是一個抽象組件, 也是一個虛擬組件, 不會被記錄到父子組件關係當中,一般用在路由組件的外層, 主要為了緩存組件, 為頻繁掛載銷毀,提供緩存功能節約性能,

  • 包含 include 屬性,添加白名單,表示那些組件需要緩存,切換過後才會進行緩存,並不是將白名單中的 name 直接全部緩存。
  • 包含 exclude 屬性,添加黑名單,表示那些組件不用緩存
  • max = x 最多緩存幾個組件, 如果超過最大限制 需要刪除第一個, 在增加最新的 LRU
  1. created 鉤子:創造一個對象 cache 來緩存組件,key[],表示緩存的是誰
  2. render():渲染
  3. 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()


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前幾天裝了幾台伺服器測試,在使用的過程中發現,每次重啟系統,登錄界面會彈出網卡提示 “r8169 0000:02:00 eth0 Invalid ocp reg 17758!” 系統版本: 經過測試發現: 1、開機前將eth0/eth1網口插上網線,系統啟動後識別到eth0/eth1網口有網線連接, ...
  • 前言 什麼是Redis Redis是一個基於記憶體的key-value結構資料庫。Redis 是互聯網技術領域使用最為廣泛的存儲中間件,它是「Remote Dictionary Service」的首字母縮寫,也就是「遠程字典服務」。 [ ] 基於記憶體存儲,讀寫性能高 [ ] 適合存儲熱點數據(熱點商品 ...
  • 摘要:本文主要介紹 Presto 如何更好的利用 Hudi 的數據佈局、索引信息來加速點查性能。 本文分享自華為雲社區《華為雲基於 Apache Hudi 極致查詢優化的探索實踐!》,作者:FI_mengtao。 背景 湖倉一體(LakeHouse)是一種新的開放式架構,它結合了數據湖和數據倉庫的最 ...
  • 雲端分析是針對 CocoaPods 依賴管理雲端化的優化方案。對大量重覆的 iOS 工程構建任務進行了收斂和資源復用,在保證正確性的前提下達到了加速依賴管理速率的目的,實現了 Pod install 分析階段提速 60% 以上的能力。 ...
  • 華為帳號自擬形象上線啦!用戶只需一張照片,即可輕鬆創建屬於自己的華為帳號自擬形象,還能對形象進行個性化裝扮,DIY髮型、服裝、配飾等。點擊“手機設置 > 華為帳號 > 自擬形象 ”即刻擁有手機中的另一個你。 華為帳號自擬形象提供數個預置虛擬形象供用戶直接使用,用戶也能夠通過拍攝/上傳照片,一鍵生成照 ...
  • 模塊 HTML 網頁中,瀏覽器通過<script>標簽載入 JavaScript 腳本。 <!-- 頁面內嵌的腳本 --> <script type="application/javascript"> // module code </script> <!-- 外部腳本 --> <script ty ...
  • Vue2動態添加路由 點擊打開視頻講解更加詳細 場景: 一般結合VueX和localstorage一起使用 router.addRoutes vue-router4後 已廢棄:使用 router.addRoute() 代替。 vue-router4版本前也可用 函數簽名: router.addRou ...
  • #概述 webpack的使用中我們會遇到各種各樣的插件、loader。 webpack的功力主要體現在能理解各個插件、loader的數量上。理解的越多功力越深 loader是什麼呢? #背景 瞭解loader前,我們在來看個問題,有了前面的基礎我們還是用個簡單的樣例來說明 由於一切都是模塊,我們想用 ...
一周排行
    -Advertisement-
    Play Games
  • 1.部署歷史 猿友們好,作為初來實習的我,已經遭受社會的“毒打”,所以請容許我在下麵環節適當吐槽,3Q! 傳統部署 ​ 回顧以往在伺服器部署webapi項目(非獨立發佈),dotnet環境、守護進程兩個逃都逃不掉,正常情況下還得來個nginx代理。不僅僅這仨,可能牽扯到yum或npm。node等都要 ...
  • 隨著技術的進步,跨平臺開發已經成為了標配,在此大背景下,ASP.NET Core也應運而生。本文主要基於ASP.NET Core+Element+Sql Server開發一個校園圖書管理系統為例,簡述基於MVC三層架構開發的常見知識點,前一篇文章,已經簡單介紹瞭如何搭建開發框架,和登錄功能實現,本篇... ...
  • 這道題只要會自定義cmp恰當地進行排序,其他部分沒有什麼大問題。 上代碼: 1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,s,h1,h2,cnt; 4 struct apple{ 5 int height,ns;//height為蘋 ...
  • 這篇文章主要描述RPC的路由策略,包括為什麼需要請求隔離,為什麼不在註冊中心中實現請求隔離以及不同粒度的路由策略。 ...
  • 簡介: 中介者模式,屬於行為型的設計模式。用一個中介對象來封裝一系列的對象交互。中介者是各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變他們之間的交互。 適用場景: 如果平行對象間的依賴複雜,可以使用中介者解耦。 優點: 符合迪米特法則,減少成員間的依賴。 缺點: 不適用於系統出現對 ...
  • 【前置內容】Spring 學習筆記全系列傳送門: Spring學習筆記 - 第一章 - IoC(控制反轉)、IoC容器、Bean的實例化與生命周期、DI(依賴註入) Spring學習筆記 - 第二章 - 註解開發、配置管理第三方Bean、註解管理第三方Bean、Spring 整合 MyBatis 和 ...
  • 簡介: 享元模式,屬於結構型的設計模式。運用共用技術有效地支持大量細粒度的對象。 適用場景: 具有相同抽象但是細節不同的場景中。 優點: 把公共的部分分離為抽象,細節依賴於抽象,符合依賴倒轉原則。 缺點: 增加複雜性。 代碼: //用戶類 class User { private $name; fu ...
  • 這次設計一個通用的多位元組SPI介面模塊,特點如下: 可以設置為1-128位元組的SPI通信模塊 可以修改CPOL、CPHA來進行不同的通信模式 可以設置輸出的時鐘 狀態轉移圖和思路與多位元組串口發送模塊一樣,這裡就不給出了,具體可看該隨筆。 一、模塊代碼 1、需要的模塊 通用8位SPI介面模塊 `tim ...
  • AOP-03 7.AOP-切入表達式 7.1切入表達式的具體使用 1.切入表達式的作用: 通過表達式的方式定義一個或多個具體的連接點。 2.語法細節: (1)切入表達式的語法格式: execution([許可權修飾符] [返回值類型] [簡單類名/全類名] [方法名]([參數列表]) 若目標類、介面與 ...
  • 測試一、虛繼承與繼承的區別 1.1 單個繼承,不帶虛函數 1>class B size(8): 1> + 1> 0 | + (base class A) 1> 0 | | _ia //4B 1> | + 1> 4 | _ib //4B 有兩個int類型數據成員,占8B,基類邏輯存在前面 1.2、單個 ...