前言
本文分享 12 道 vue 高頻原理面試題,覆寫了 vue 核心實作原理,其實一個架構的實作原理一篇文章是不可能說完的,希望通過這 12 道問題,讓讀者對自己的 Vue 掌握程度有一定的認識(B 數),進而彌補自己的不足,更好的掌握 Vue ❤️
1. Vue 響應式原理
核心實作類:
Observer : 它的作用是給對象的屬性添加 getter 和 setter,用于依賴收集和派發更新
Dep : 用于收集目前響應式對象的依賴關系,每個響應式對象包括子對象都擁有一個 Dep 執行個體(裡面 subs 是 Watcher 執行個體數組),當資料有變更時,會通過 dep.notify()通知各個 watcher。
Watcher : 觀察者對象 , 執行個體分為渲染 watcher (render watcher),計算屬性 watcher (computed watcher),偵聽器 watcher(user watcher)三種
Watcher 和 Dep 的關系
watcher 中執行個體化了 dep 并向 dep.subs 中添加了訂閱者,dep 通過 notify 周遊了 dep.subs 通知每個 watcher 更新。
依賴收集
- initState 時,對 computed 屬性初始化時,觸發 computed watcher 依賴收集
- initState 時,對偵聽屬性初始化時,觸發 user watcher 依賴收集
- render()的過程,觸發 render watcher 依賴收集
- re-render 時,vm.render()再次執行,會移除所有 subs 中的 watcer 的訂閱,重新指派。
派發更新
- 元件中對響應的資料進行了修改,觸發 setter 的邏輯
- 調用 dep.notify()
- 周遊所有的 subs(Watcher 執行個體),調用每一個 watcher 的 update 方法。
原理
當建立 Vue 執行個體時,vue 會周遊 data 選項的屬性,利用 Object.defineProperty 為屬性添加 getter 和 setter 對資料的讀取進行劫持(getter 用來依賴收集,setter 用來派發更新),并且在内部追蹤依賴,在屬性被通路和修改時通知變化。
每個元件執行個體會有相應的 watcher 執行個體,會在元件渲染的過程中記錄依賴的所有資料屬性(進行依賴收集,還有 computed watcher,user watcher 執行個體),之後依賴項被改動時,setter 方法會通知依賴與此 data 的 watcher 執行個體重新計算(派發更新),進而使它關聯的元件重新渲染。
一句話總結:
vue.js 采用資料劫持結合釋出-訂閱模式,通過 Object.defineproperty 來劫持各個屬性的 setter,getter,在資料變動時釋出消息給訂閱者,觸發響應的監聽回調
2. computed 的實作原理
computed 本質是一個惰性求值的觀察者。
computed 内部實作了一個惰性的 watcher,也就是 computed watcher,computed watcher 不會立刻求值,同時持有一個 dep 執行個體。
其内部通過 this.dirty 屬性标記計算屬性是否需要重新求值。
當 computed 的依賴狀态發生改變時,就會通知這個惰性的 watcher,
computed watcher 通過 this.dep.subs.length 判斷有沒有訂閱者,
有的話,會重新計算,然後對比新舊值,如果變化了,會重新渲染。 (
Vue 想確定不僅僅是計算屬性依賴的值發生變化,而是當計算屬性最終計算的值發生變化時才會觸發渲染 watcher 重新渲染,本質上是一種優化。)
沒有的話,僅僅把 this.dirty = true。 (
當計算屬性依賴于其他資料時,屬性并不會立即重新計算,隻有之後其他地方需要讀取屬性的時候,它才會真正計算,即具備 lazy(懶計算)特性。)
3. computed 和 watch 有什麼差別及運用場景?
差別
computed 計算屬性 : 依賴其它屬性值,并且 computed 的值有緩存,隻有它依賴的屬性值發生改變,下一次擷取 computed 的值時才會重新計算 computed 的值。
watch 偵聽器 : 更多的是「觀察」的作用,
無緩存性,類似于某些資料的監聽回調,每當監聽的資料變化時都會執行回調進行後續操作。
運用場景
運用場景:
當我們需要進行數值計算,并且依賴于其它資料時,應該使用 computed,因為可以利用 computed 的緩存特性,避免每次擷取值時,都要重新計算。
當我們需要在資料變化時執行異步或開銷較大的操作時,應該使用 watch,使用 watch 選項允許我們執行異步操作 ( 通路一個 API ),限制我們執行該操作的頻率,并在我們得到最終結果前,設定中間狀态。這些都是計算屬性無法做到的。
4. 為什麼在 Vue3.0 采用了 Proxy,抛棄了 Object.defineProperty?
Object.defineProperty 本身有一定的監控到數組下标變化的能力,但是在 Vue 中,從性能/體驗的成本效益考慮,尤大大就棄用了這個特性(Vue 為什麼不能檢測數組變動 )。為了解決這個問題,經過 vue 内部處理後可以使用以下幾種方法來監聽數組
push
由于隻針對了以上 7 種方法進行了 hack 處理,是以其他數組的屬性也是檢測不到的,還是具有一定的局限性。
Object.defineProperty 隻能劫持對象的屬性,是以我們需要對每個對象的每個屬性進行周遊。Vue 2.x 裡,是通過 遞歸 + 周遊 data 對象來實作對資料的監控的,如果屬性值也是對象那麼需要深度周遊,顯然如果能劫持一個完整的對象是才是更好的選擇。
Proxy 可以劫持整個對象,并傳回一個新的對象。Proxy 不僅可以代理對象,還可以代理數組。還可以代理動态增加的屬性。
5. Vue 中的 key 到底有什麼用?
key 是給每一個 vnode 的唯一 id,依靠 key,我們的 diff 操作可以更準确、更快速 (對于簡單清單頁渲染來說 diff 節點也更快,但會産生一些隐藏的副作用,比如可能不會産生過渡效果,或者在某些節點有綁定資料(表單)狀态,會出現狀态錯位。)
diff 算法的過程中,先會進行新舊節點的首尾交叉對比,當無法比對的時候會用新節點的 key 與舊節點進行比對,進而找到相應舊節點.
更準确 : 因為帶 key 就不是就地複用了,在 sameNode 函數 a.key === b.key 對比中可以避免就地複用的情況。是以會更加準确,如果不加 key,會導緻之前節點的狀态被保留下來,會産生一系列的 bug。
更快速 : key 的唯一性可以被 Map 資料結構充分利用,相比于周遊查找的時間複雜度 O(n),Map 的時間複雜度僅僅為 O(1),源碼如下:
function
6. 談一談 nextTick 的原理
JS 運作機制
JS 執行是單線程的,它是基于事件循環的。事件循環大緻分為以下幾個步驟:
- 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
- 主線程之外,還存在一個"任務隊列"(task queue)。隻要異步任務有了運作結果,就在"任務隊列"之中放置一個事件。
- 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的異步任務,于是結束等待狀态,進入執行棧,開始執行。
- 主線程不斷重複上面的第三步。
主線程的執行過程就是一個 tick,而所有的異步結果都是通過 “任務隊列” 來排程。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分為兩大類,分别是 macro task 和 micro task,并且每個 macro task 結束後,都要清空所有的 micro task。
for
在浏覽器環境中 :
常見的 macro task 有
setTimeout、MessageChannel、postMessage、setImmediate常見的 micro task 有
MutationObsever 和 Promise.then異步更新隊列
可能你還沒有注意到,Vue 在更新 DOM 時是
異步執行的。隻要偵聽到資料變化,Vue 将開啟一個隊列,并緩沖在同一事件循環中發生的所有資料變更。
如果同一個 watcher 被多次觸發,隻會被推入到隊列中一次。這種在緩沖時去除重複資料對于避免不必要的計算和 DOM 操作是非常重要的。
然後,在下一個的事件循環“tick”中,Vue 重新整理隊列并執行實際 (已去重的) 工作。
Vue 在内部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執行環境不支援,則會采用 setTimeout(fn, 0) 代替。
在 vue2.5 的源碼中,macrotask 降級的方案依次是:setImmediate、MessageChannel、setTimeoutvue 的 nextTick 方法的實作原理:
- vue 用異步隊列的方式來控制 DOM 更新和 nextTick 回調先後執行
- microtask 因為其高優先級特性,能確定隊列中的微任務在一次事件循環前被執行完畢
- 考慮相容問題,vue 做了 microtask 向 macrotask 的降級方案
7. vue 是如何對數組方法進行變異的 ?
我們先來看看源碼
const
簡單來說,Vue 通過原型攔截的方式重寫了數組的 7 個方法,首先擷取到這個數組的
ob,也就是它的 Observer 對象,如果有新的值,就調用 observeArray 對新的值進行監聽,然後手動調用 notify,通知 render watcher,執行 update
8. Vue 元件 data 為什麼必須是函數 ?
new Vue()執行個體中,data 可以直接是一個對象,為什麼在 vue 元件中,data 必須是一個函數呢?
因為元件是可以複用的,JS 裡對象是引用關系,如果元件 data 是一個對象,那麼子元件中的 data 屬性值會互相污染,産生副作用。
是以一個元件的 data 選項必須是一個函數,是以每個執行個體可以維護一份被傳回對象的獨立的拷貝。new Vue 的執行個體是不會被複用的,是以不存在以上問題。
9. 談談 Vue 事件機制,手寫$on,$off,$emit,$once
Vue 事件機制 本質上就是 一個 釋出-訂閱 模式的實作。
class
10. 說說 Vue 的渲染過程
- 調用 compile 函數,生成 render 函數字元串 ,編譯過程如下:
- parse 函數解析 template,生成 ast(抽象文法樹)
- optimize 函數優化靜态節點 (标記不需要每次都更新的内容,diff 算法會直接跳過靜态節點,進而減少比較的過程,優化了 patch 的性能)
- generate 函數生成 render 函數字元串
- 調用 new Watcher 函數,監聽資料的變化,當資料發生變化時,Render 函數執行生成 vnode 對象
- 調用 patch 方法,對比新舊 vnode 對象,通過 DOM diff 算法,添加、修改、删除真正的 DOM 元素
11. 聊聊 keep-alive 的實作原理和緩存政策
export
原理
- 擷取 keep-alive 包裹着的第一個子元件對象及其元件名
- 根據設定的 include/exclude(如果有)進行條件比對,決定是否緩存。不比對,直接傳回元件執行個體
- 根據元件 ID 和 tag 生成緩存 Key,并在緩存對象中查找是否已緩存過該元件執行個體。如果存在,直接取出緩存值并更新該 key 在 this.keys 中的位置( 更新 key 的位置是實作 LRU 置換政策的關鍵 )
- 在 this.cache 對象中存儲該元件執行個體并儲存 key 值,之後檢查緩存的執行個體數量是否超過 max 的設定值,超過則根據 LRU 置換政策 删除最近最久未使用的執行個體 (即是下标為 0 的那個 key)
- 最後元件執行個體的 keepAlive 屬性設定為 true,這個在渲染和執行被包裹元件的鈎子函數會用到,這裡不細說
LRU 緩存淘汰算法
LRU(Least recently used)算法根據資料的曆史通路記錄來進行淘汰資料,其核心思想是“如果資料最近被通路過,那麼将來被通路的幾率也更高”。
keep-alive 的實作正是用到了 LRU 政策,将最近通路的元件 push 到 this.keys 最後面,this.keys[0]也就是最久沒被通路的元件,當緩存執行個體超過 max 設定值,删除 this.keys[0]12. vm.$set()實作原理是什麼?
受現代 JavaScript 的限制 (而且 Object.observe 也已經被廢棄),Vue 無法檢測到對象屬性的添加或删除。
由于 Vue 會在初始化執行個體時對屬性執行 getter/setter 轉化,是以屬性必須在 data 對象上存在才能讓 Vue 将它轉換為響應式的。
對于已經建立的執行個體,Vue 不允許動态添加根級别的響應式屬性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套對象添加響應式屬性。
那麼 Vue 内部是如何解決對象新增屬性不能響應的問題的呢?
export
- 如果目标是數組,使用 vue 實作的變異方法 splice 實作響應式
- 如果目标是對象,判斷屬性存在,即為響應式,直接指派
- 如果 target 本身就不是響應式,直接指派
- 如果屬性不是響應式,則調用 defineReactive 方法進行響應式處理
後記
如果你和我一樣喜歡前端,也愛動手折騰,歡迎關注我一起玩耍啊~ ❤️
部落格
我的部落格github.com
公衆号
前端時刻
http://weixin.qq.com/r/qyi2rrXEOuhFrfHN9325 (二維碼自動識别)