本文分享 12 道 vue 高頻原理面試題,覆蓋了 vue 核心實現原理,其實一個框架的實現原理一篇文章是不可能說完的,但願經過這 12 道問題,讓讀者對本身的 Vue 掌握程度有必定的認識(B 數),從而彌補本身的不足,更好的掌握 Vue ❤️
Observer : 它的做用是給對象的屬性添加 getter 和 setter,用於依賴收集和派發更新前端
Dep : 用於收集當前響應式對象的依賴關係,每一個響應式對象包括子對象都擁有一個 Dep 實例(裏面 subs 是 Watcher 實例數組),當數據有變動時,會經過 dep.notify()通知各個 watcher。vue
Watcher : 觀察者對象 , 實例分爲渲染 watcher (render watcher),計算屬性 watcher (computed watcher),偵聽器 watcher(user watcher)三種node
watcher 中實例化了 dep 並向 dep.subs 中添加了訂閱者,dep 經過 notify 遍歷了 dep.subs 通知每一個 watcher 更新。git
當建立 Vue 實例時,vue 會遍歷 data 選項的屬性,利用 Object.defineProperty 爲屬性添加 getter 和 setter 對數據的讀取進行劫持(getter 用來依賴收集,setter 用來派發更新),而且在內部追蹤依賴,在屬性被訪問和修改時通知變化。github
每一個組件實例會有相應的 watcher 實例,會在組件渲染的過程當中記錄依賴的全部數據屬性(進行依賴收集,還有 computed watcher,user watcher 實例),以後依賴項被改動時,setter 方法會通知依賴與此 data 的 watcher 實例從新計算(派發更新),從而使它關聯的組件從新渲染。web
一句話總結:面試
vue.js 採用數據劫持結合發佈-訂閱模式,經過 Object.defineproperty 來劫持各個屬性的 setter,getter,在數據變更時發佈消息給訂閱者,觸發響應的監聽回調算法
computed 本質是一個惰性求值的觀察者。segmentfault
computed 內部實現了一個惰性的 watcher,也就是 computed watcher,computed watcher 不會馬上求值,同時持有一個 dep 實例。數組
其內部經過 this.dirty 屬性標記計算屬性是否須要從新求值。
當 computed 的依賴狀態發生改變時,就會通知這個惰性的 watcher,
computed watcher 經過 this.dep.subs.length 判斷有沒有訂閱者,
有的話,會從新計算,而後對比新舊值,若是變化了,會從新渲染。 (Vue 想確保不只僅是計算屬性依賴的值發生變化,而是當計算屬性最終計算的值發生變化時纔會觸發渲染 watcher 從新渲染,本質上是一種優化。)
沒有的話,僅僅把 this.dirty = true。 (當計算屬性依賴於其餘數據時,屬性並不會當即從新計算,只有以後其餘地方須要讀取屬性的時候,它纔會真正計算,即具有 lazy(懶計算)特性。)
computed 計算屬性 : 依賴其它屬性值,而且 computed 的值有緩存,只有它依賴的屬性值發生改變,下一次獲取 computed 的值時纔會從新計算 computed 的值。
watch 偵聽器 : 更多的是「觀察」的做用,無緩存性,相似於某些數據的監聽回調,每當監聽的數據變化時都會執行回調進行後續操做。
運用場景:
當咱們須要進行數值計算,而且依賴於其它數據時,應該使用 computed,由於能夠利用 computed 的緩存特性,避免每次獲取值時,都要從新計算。
當咱們須要在數據變化時執行異步或開銷較大的操做時,應該使用 watch,使用 watch 選項容許咱們執行異步操做 ( 訪問一個 API ),限制咱們執行該操做的頻率,並在咱們獲得最終結果前,設置中間狀態。這些都是計算屬性沒法作到的。
Object.defineProperty 自己有必定的監控到數組下標變化的能力,可是在 Vue 中,從性能/體驗的性價比考慮,尤大大就棄用了這個特性( Vue 爲何不能檢測數組變更 )。爲了解決這個問題,通過 vue 內部處理後可使用如下幾種方法來監聽數組
push(); pop(); shift(); unshift(); splice(); sort(); reverse();
因爲只針對了以上 7 種方法進行了 hack 處理,因此其餘數組的屬性也是檢測不到的,仍是具備必定的侷限性。
Object.defineProperty 只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷。Vue 2.x 裏,是經過 遞歸 + 遍歷 data 對象來實現對數據的監控的,若是屬性值也是對象那麼須要深度遍歷,顯然若是能劫持一個完整的對象是纔是更好的選擇。Proxy 能夠劫持整個對象,並返回一個新的對象。Proxy 不只能夠代理對象,還能夠代理數組。還能夠代理動態增長的屬性。
key 是給每個 vnode 的惟一 id,依靠 key,咱們的 diff 操做能夠更準確、更快速 (對於簡單列表頁渲染來講 diff 節點也更快,但會產生一些隱藏的反作用,好比可能不會產生過渡效果,或者在某些節點有綁定數據(表單)狀態,會出現狀態錯位。)
diff 算法的過程當中,先會進行新舊節點的首尾交叉對比,當沒法匹配的時候會用新節點的 key 與舊節點進行比對,從而找到相應舊節點.
更準確 : 由於帶 key 就不是就地複用了,在 sameNode 函數 a.key === b.key 對比中能夠避免就地複用的狀況。因此會更加準確,若是不加 key,會致使以前節點的狀態被保留下來,會產生一系列的 bug。
更快速 : key 的惟一性能夠被 Map 數據結構充分利用,相比於遍歷查找的時間複雜度 O(n),Map 的時間複雜度僅僅爲 O(1),源碼以下:
function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key; const map = {}; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (isDef(key)) map[key] = i; } return map; }
JS 執行是單線程的,它是基於事件循環的。事件循環大體分爲如下幾個步驟:
主線程的執行過程就是一個 tick,而全部的異步結果都是經過 「任務隊列」 來調度。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分爲兩大類,分別是 macro task 和 micro task,而且每一個 macro task 結束後,都要清空全部的 micro task。
for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } }
在瀏覽器環境中 :
常見的 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、setTimeout
vue 的 nextTick 方法的實現原理:
咱們先來看看源碼
const arrayProto = Array.prototype; export const arrayMethods = Object.create(arrayProto); const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function(method) { // cache original method const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { const result = original.apply(this, args); const ob = this.__ob__; let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); break; } if (inserted) ob.observeArray(inserted); // notify change ob.dep.notify(); return result; }); }); /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
簡單來講,Vue 經過原型攔截的方式重寫了數組的 7 個方法,首先獲取到這個數組的ob,也就是它的 Observer 對象,若是有新的值,就調用 observeArray 對新的值進行監聽,而後手動調用 notify,通知 render watcher,執行 update
new Vue()實例中,data 能夠直接是一個對象,爲何在 vue 組件中,data 必須是一個函數呢?
由於組件是能夠複用的,JS 裏對象是引用關係,若是組件 data 是一個對象,那麼子組件中的 data 屬性值會互相污染,產生反作用。
因此一個組件的 data 選項必須是一個函數,所以每一個實例能夠維護一份被返回對象的獨立的拷貝。new Vue 的實例是不會被複用的,所以不存在以上問題。
Vue 事件機制 本質上就是 一個 發佈-訂閱 模式的實現。
class Vue { constructor() { // 事件通道調度中心 this._events = Object.create(null); } $on(event, fn) { if (Array.isArray(event)) { event.map(item => { this.$on(item, fn); }); } else { (this._events[event] || (this._events[event] = [])).push(fn); } return this; } $once(event, fn) { function on() { this.$off(event, on); fn.apply(this, arguments); } on.fn = fn; this.$on(event, on); return this; } $off(event, fn) { if (!arguments.length) { this._events = Object.create(null); return this; } if (Array.isArray(event)) { event.map(item => { this.$off(item, fn); }); return this; } const cbs = this._events[event]; if (!cbs) { return this; } if (!fn) { this._events[event] = null; return this; } let cb; let i = cbs.length; while (i--) { cb = cbs[i]; if (cb === fn || cb.fn === fn) { cbs.splice(i, 1); break; } } return this; } $emit(event) { let cbs = this._events[event]; if (cbs) { const args = [].slice.call(arguments, 1); cbs.map(item => { args ? item.apply(this, args) : item.call(this); }); } return this; } }
export default { name: "keep-alive", abstract: true, // 抽象組件屬性 ,它在組件實例創建父子關係的時候會被忽略,發生在 initLifecycle 的過程當中 props: { include: patternTypes, // 被緩存組件 exclude: patternTypes, // 不被緩存組件 max: [String, Number] // 指定緩存大小 }, created() { this.cache = Object.create(null); // 緩存 this.keys = []; // 緩存的VNode的鍵 }, destroyed() { for (const key in this.cache) { // 刪除全部緩存 pruneCacheEntry(this.cache, key, this.keys); } }, mounted() { // 監聽緩存/不緩存組件 this.$watch("include", val => { pruneCache(this, name => matches(val, name)); }); this.$watch("exclude", val => { pruneCache(this, name => !matches(val, name)); }); }, render() { // 獲取第一個子元素的 vnode const slot = this.$slots.default; const vnode: VNode = getFirstComponentChild(slot); const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // name不在inlcude中或者在exlude中 直接返回vnode // check pattern const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode; } const { cache, keys } = this; // 獲取鍵,優先獲取組件的name字段,不然是組件的tag const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; // 命中緩存,直接從緩存拿vnode 的組件實例,而且從新調整了 key 的順序放在了最後一個 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); keys.push(key); } // 不命中緩存,把 vnode 設置進緩存 else { cache[key] = vnode; keys.push(key); // prune oldest entry // 若是配置了 max 而且緩存的長度超過了 this.max,還要從緩存中刪除第一個 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } } // keepAlive標記位 vnode.data.keepAlive = true; } return vnode || (slot && slot[0]); } };
LRU(Least recently used)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高」。
keep-alive 的實現正是用到了 LRU 策略,將最近訪問的組件 push 到 this.keys 最後面,this.keys[0]也就是最久沒被訪問的組件,當緩存實例超過 max 設置值,刪除 this.keys[0]
受現代 JavaScript 的限制 (並且 Object.observe 也已經被廢棄),Vue 沒法檢測到對象屬性的添加或刪除。
因爲 Vue 會在初始化實例時對屬性執行 getter/setter 轉化,因此屬性必須在 data 對象上存在才能讓 Vue 將它轉換爲響應式的。
對於已經建立的實例,Vue 不容許動態添加根級別的響應式屬性。可是,可使用 Vue.set(object, propertyName, value) 方法向嵌套對象添加響應式屬性。
那麼 Vue 內部是如何解決對象新增屬性不能響應的問題的呢?
export function set(target: Array<any> | Object, key: any, val: any): any { // target 爲數組 if (Array.isArray(target) && isValidArrayIndex(key)) { // 修改數組的長度, 避免索引>數組長度致使splcie()執行有誤 target.length = Math.max(target.length, key); // 利用數組的splice變異方法觸發響應式 target.splice(key, 1, val); return val; } // target爲對象, key在target或者target.prototype上 且必須不能在 Object.prototype 上,直接賦值 if (key in target && !(key in Object.prototype)) { target[key] = val; return val; } // 以上都不成立, 即開始給target建立一個全新的屬性 // 獲取Observer實例 const ob = (target: any).__ob__; // target 自己就不是響應式數據, 直接賦值 if (!ob) { target[key] = val; return val; } // 進行響應式處理 defineReactive(ob.value, key, val); ob.dep.notify(); return val; }
若是你和我同樣喜歡前端,也愛動手摺騰,歡迎關注我一塊兒玩耍啊~ ❤️
前端時刻