關於vue的問題「乾貨」

關於vue的閱讀與總結,這是一份深刻思考後的關於vue的理解。舉一反三,多多學習。javascript

背景(爲何要學習開源項目的源碼)

舉一個最近本身看到的例子: vue-router插件中,借用vue.min能夠混入生命週期,在這裏混入的生命週期在每一個組件的這個生命週期的這個階段都會調用:css

Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
複製代碼

看到這個實現,對於之後想要實現vue插件而且綁定生命週期,提供了一種很好的思路和方法,每每能夠舉一反三,有意想不到的收穫。html

說說你對MVVM的理解

MVVM 由如下三個內容組成前端

  • View:界面
  • Model:數據模型
  • ViewModel:做爲橋樑負責溝通 View 和 Model

在 JQuery 時期,若是須要刷新 UI 時,須要先取到對應的 DOM 再更新 UI,這樣數據和業務的邏輯就和頁面有強耦合。vue

在 MVVM 中,最核心的也就是數據雙向綁定,例如 Angluar 的髒數據檢測,Vue 中的數據劫持。java

MVVM 究竟是什麼?與其專一於說明 MVVM 的來歷,不如讓咱們看一個典型的應用是如何構建的,並從那裏瞭解 MVVM:node

這是一個典型的 MVC 設置。Model 呈現數據,View 呈現用戶界面,而 View Controller 調節它二者之間的交互。react

雖然 View 和 View Controller 是技術上不一樣的組件,但它們幾乎老是手牽手在一塊兒,成對的。算法

能夠嘗試將它們聯繫: vue-router

在典型的 MVC 應用裏,許多邏輯被放在 View Controller 裏。它們中的一些確實屬於 View Controller,但更多的是所謂的「表示邏輯(presentation logic)」

以 MVVM 屬術語來講,就是那些將 Model 數據轉換爲 View 能夠呈現的東西的事情,例如將一個 NSDate 轉換爲一個格式化過的 NSString

咱們的圖解裏缺乏某些東西,那些使咱們能夠把全部表示邏輯放進去的東西。咱們打算將其稱爲 「View Model」 —— 它位於 View/Controller 與 Model 之間:

這個圖解準確地描述了什麼是 MVVM:一個 MVC 的加強版,咱們正式鏈接了視圖和控制器,並將表示邏輯從 Controller 移出放到一個新的對象裏,即 View Model。

說說v-if和v-show

v-show和v-if

  1. v-if: 真正的條件渲染。false,不在dom中。
  2. v-show: 一直在dom中,只是用css的display屬性進行切換(存在於html結構中,可是未用css進行渲染)。存在dom結構中
  3. display:none時,不在render(渲染樹)樹中。

visibility:hidden和display:none

display: none: 標籤不會出如今頁面上(儘管你仍然能夠經過dom與它進行交互)。其它標籤不會爲它分配空間。 visibility:hidden: 標籤會出如今頁面上,只是看不見而已。其它標籤會爲它分配空間。

組件裏的 data 必須是一個函數返回的對象,而不能就只是一個對象

若是須要,能夠經過將 vm.$data 傳入 JSON.parse(JSON.stringify(...)) 獲得深拷貝的原始數據對象。

說說組件通訊經常使用的幾種

props emit

父傳子:props 子傳父:emit

問題:多級嵌套組件

provide / inject

這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在其上下游關係成立的時間裏始終生效。

element-ui的button組件, 部分源碼

// Button 組件核心源碼
export default {
    name: 'ElButton',
    // 經過 inject 獲取 elForm 以及 elFormItem 這兩個組件
    inject: {
        elForm: {
            default: ''
        },
        elFormItem: {
            default: ''
        }
    },
    // ...
    computed: {
        _elFormItemSize() {
            return (this.elFormItem || {}).elFormItemSize;
        },
        buttonSize() {
            return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        },
        //...
    },
    // ...
};
複製代碼

問題:不可以實現子組件向祖先組件傳遞數據

$attrs $listeners

上述的provideinject實現了多層級組件數據的傳輸,可是不可以實現子組件向祖先組件傳遞數據,若是要實現子傳祖,可使用$ attrs和$ listeners

EventBus

對於一些沒有必要引進vuex的項目,可考慮

事件總線:EventBus能夠用來很方便的實現兄弟組件和跨級組件的通訊,可是使用不當時也會帶來不少問題(vue是單頁應用,若是你在某一個頁面刷新了以後,與之相關的EventBus會被移除,這樣就致使業務走不下去);因此適合邏輯並不複雜的小頁面,邏輯複雜時仍是建議使用vuex

class EventBus{
    constructor(){
        // 一個map,用於存儲事件與回調之間的對應關係
        this.event=Object.create(null);
    };
    //註冊事件
    on(name,fn){
        if(!this.event[name]){
            //一個事件可能有多個監聽者
            this.event[name]=[];
        };
        this.event[name].push(fn);
    };
    //觸發事件
    emit(name,...args){
        //給回調函數傳參
        this.event[name]&&this.event[name].forEach(fn => {
            fn(...args)
        });
    };
    //只被觸發一次的事件
    once(name,fn){
        //在這裏同時完成了對該事件的註冊、對該事件的觸發,並在最後取消該事件。
        const cb=(...args)=>{
            //觸發
            fn(...args);
            //取消
            this.off(name,fn);
        };
        //監聽
        this.on(name,cb);
    };
    //取消事件
    off(name,offcb){
        if(this.event[name]){
            let index=this.event[name].findIndex((fn)=>{
                return offcb===fn;
            })
            this.event[name].splice(index,1);
            if(!this.event[name].length){
                delete this.event[name];
            }
        }
    }
}
複製代碼

Vuex

狀態管理,邏輯複雜時仍是建議使用vuex

Vue生命週期,各階段都作了什麼

beforeCreatecreated

beforeCreatecreated生命週期是在初始化的時候,在_init中執行

具體代碼在vue/src/core/instance/init.js

Vue.prototype._init = function() {
      // expose real self
    //...
    vm._self = vm
    initLifecycle(vm) // 初始化生命週期
    initEvents(vm) // 初始化事件
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化props,methods,data,computed等
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // ...
}
複製代碼
  1. beforeCreate. 不能用props,methods,data,computed等。
  2. initState. 初始化props,methods,data,computed等。
  3. created. 此時已經有,props,methods,data,computed等,要用data屬性則能夠在這裏調用。

beforeCreatecreated這倆個鉤子函數執行的時候,並無渲染 DOM,因此咱們也不可以訪問 DOM,通常來講,若是組件在加載的時候須要和後端有交互,放在這倆個鉤子函數執行均可以,若是是須要訪問 props、data 等數據的話,就須要使用 created 鉤子函數。

beforeMountmounted

掛載是指將編譯完成的HTML模板掛載到對應虛擬dom

在掛載開始以前被調用:相關的 render 函數首次被調用。

該鉤子在服務器端渲染期間不被調用。

export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      // ...
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // ...
      vm._update(vnode, hydrating)
      // ...
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製代碼

在執行 vm._render() 函數渲染 VNode 以前,執行了 beforeMount 鉤子函數,在執行完 vm._update() 把 VNode patch 到真實 DOM 後,執行 mounted 鉤子。

beforeUpdateupdated

beforeUpdateupdated 的鉤子函數執行時機都應該是在數據更新的時候

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // ...
  }
複製代碼

這裏有個細節是_isMounted, 表示要在mounted以後才執行beforeUpdate

至於updated則表示,當這個鉤子被調用時 組件 DOM 已經更新,因此你如今能夠執行依賴於 DOM 的操做

beforeDestroydestroyed

beforeDestroydestroyed 鉤子函數的執行時機在組件銷燬的階段

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
複製代碼

beforeDestroy 鉤子函數的執行時機是在 $destroy 函數執行最開始的地方,接着執行了一系列的銷燬動做,包括從 parent$children 中刪掉自身,刪除 watcher,當前渲染的 VNode 執行銷燬鉤子函數等,執行完畢後再調用 destroy 鉤子函數。

$destroy 的執行過程當中,它又會執行 vm.__patch__(vm._vnode, null) 觸發它子組件的銷燬鉤子函數,這樣一層層的遞歸調用,因此 destroy 鉤子函數執行順序是先子後父,和 mounted 過程同樣。

activeddeactivated

activateddeactivated 鉤子函數是專門爲 keep-alive 組件定製的鉤子

  1. activatedkeep-alive 組件激活時調用。
  2. deactivatedkeep-alive 組件銷燬時調用。

errorCaptured

當捕獲一個來自子孫組件的錯誤時被調用。此鉤子會收到三個參數:錯誤對象、發生錯誤的組件實例以及一個包含錯誤來源信息的字符串。此鉤子能夠返回 false 以阻止該錯誤繼續向上傳播。

new Vue時發生了什麼

  1. 調用_init合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
  2. 經過 Object.defineProperty 設置 settergetter 函數,用來實現響應式以及依賴收集

說說響應式原理的過程

當建立 Vue 實例時,vue 會遍歷 data 選項的屬性,利用 Object.defineProperty 爲屬性添加 getter 和 setter 對數據的讀取進行劫持(getter 用來依賴收集,setter 用來派發更新),而且在內部追蹤依賴,在屬性被訪問和修改時通知變化。

每一個組件實例會有相應的 watcher 實例,會在組件渲染的過程當中記錄依賴的全部數據屬性(進行依賴收集,還有 computed watcher,user watcher 實例),以後依賴項被改動時,setter 邏輯會通知依賴與此 data 的 watcher 實例從新計算(派發更新),從而使它關聯的組件從新渲染。

總結就是: vue.js 採用數據劫持結合發佈-訂閱模式,經過 Object.defineproperty 來劫持各個屬性的 setter,getter,在數據變更時發佈消息給訂閱者,觸發響應的監聽回調

核心角色

  • Observer(監聽器):給對象添加getter和setter,用於依賴蒐集和派發更新。不只是一個數據監聽器,也是發佈者;
  • Watcher(訂閱者):observer 把數據轉發給了真正的訂閱者——watcher對象。watcher 接收到新的數據後,會去更新視圖。watcher實例分爲渲染watcher(render watcher), computed watcher, 偵聽器user watcher。維護了一個deps(用於收集依賴)的實例數組。二次依賴收集時,cleanupDeps 在每次添加完新的訂閱,會移除掉舊的訂閱的deps;
  • compile(編譯器):MVVM 框架特有的角色,負責對每一個節點元素指令進行掃描和解析,指令的數據初始化、訂閱者的建立這些「雜活」也歸它管;
  • Dep:用於收集當前響應式對象的依賴關係,每一個響應式對象都有一個Dep實例(裏邊subs是Watcher實例數組),數據變動觸發setter邏輯時,經過dep.notify()(遍歷subs,調用每一個Watcher的update()方法)通知各個Watcher

核心角色的關係以下:

核心代碼

實現observer

// 遍歷對象
function observer(target) {
  // target是對象,則遍歷
  if (target && typeof target === 'object') {
    Object.keys(target).forEach(key => {
      defineReactive(target, key, target[key])
    })
  }
}

// 用defineProperty監聽當前屬性
function defineReactive(target, key, val) {
  const dep = new Dep()
  // 遞歸
  observer(val)
  Object.defineProperty(target, key, {
    // 可枚舉
    enumerable: true,
    // 不可配置
    configurable: false,
    get: function() {
      return val
    },
    set: function(value) {
      console.log(val, value)
      val = value
    }
  })
}
複製代碼

實現dep訂閱者

class Dep {
  constructor() {
    // 初始化訂閱隊列
    this.subs = []
  }
  // 增長訂閱
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知訂閱者
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
複製代碼

訂閱者Dep裏的subs數組是Watcher實例

實現Watcher類

class Watcher {
  constructor() {}
  update() {
    // 更新視圖
  }
}
複製代碼

改寫 defineReactive 中的 setter 方法,在監聽器裏去通知訂閱者了:

// 用defineProperty監聽當前屬性
function defineReactive(target, key, val) {
  const dep = new Dep()
  // 遞歸
  observer(val)
  Object.defineProperty(target, key, {
    // 可枚舉
    enumerable: true,
    // 不可配置
    configurable: false,
    get: function() {
      return val
    },
    set: function(value) {
      console.log(val, value)
      dep.notify()
    }
  })
}
複製代碼

2. Watcher和Dep的關係

watcher 中實例化了 dep 並向 dep.subs 中添加了訂閱者, dep 經過 notify 遍歷了 dep.subs 通知每一個 watcher 更新。

3. computed 和 watch

computed 本質是一個惰性求值的觀察者。

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 偵聽器 : 更多的是「觀察」的做用,無緩存性,相似於某些數據的監聽回調,每當監聽的數據變化時都會執行回調進行後續操做。

4. 依賴收集

  1. initState時,對computed屬性初始化時,觸發computed Watcher依賴收集
  2. initState時,對watch屬性初始化時,觸發user Watcher依賴收集
  3. render()的過程,觸發render watcher依賴收集
  4. re-render時,vm.render()再次執行,會移除subs的訂閱,從新賦值

5. 派發更新

  1. 組件中,對響應式的數據進行了修改,觸發setter的邏輯
  2. 調用dep.notity()
  3. 遍歷dep.subs(Watcher 實例),調用每一個Watcehr 的 update()
  4. update()過程,又利用了隊列作了進一步優化,在 nextTick 後執行全部 watcher 的 run,最後執行它們的回調函數。

在一個for循環中改變當前組件依賴的數據,改變一萬次,會有什麼效果( nextTick 的原理)

假如我在一個for循環中改變當前組件依賴的數據,改變一萬次,會有什麼效果?(涉及批量更新和 nextTick 原理) 總體過程: 在這裏插入圖片描述

JS運行機制

JS 執行是單線程的,它是基於事件循環的。事件循環大體分爲如下幾個步驟:

  1. 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
  2. 主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  3. 一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。

主線程的執行過程就是一個 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

異步更新隊列

例題解答:number會被不停地進行++操做,不斷地觸發它對應的Dep中的Watcher對象的update方法。而後最終queue中由於對相同idWatcher對象進行了篩選(過濾),從而queue中實際上只會存在一個number對應的Watcher對象。在下一個 tick 的時候(此時number已經變成了 1000),觸發Watcher對象的run方法來更新視圖,將視圖上的number` 從 0 直接變成 1000。

若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。

Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。

在 vue2.5 的源碼中,macrotask 降級的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的實現原理:

  1. vue 用異步隊列的方式來控制 DOM 更新和 nextTick 回調前後執行
  2. microtask 由於其高優先級特性,能確保隊列中的微任務在一次事件循環前被執行完畢
  3. 考慮兼容問題,vue 作了 microtask 向 macrotask 的降級方案

Vue 的響應式對數組是如何處理的

  1. 對數組中全部能改變數組自身的方法,如 push、pop 等這些方法進行重寫。
  2. 而後手動調用 notify,通知 render watcher,執行 update

computed 屬性爲何可以在依賴改變的時候,本身發生變化

computed 和 watch 公用一個 Watcher 類,在 computed 的狀況下有一個 deps 。 Vue 在二次收集依賴時用 cleanupDeps 在每次添加完新的訂閱,會移除掉舊的訂閱

爲何在 Vue3.0 採用了 Proxy,拋棄了 Object.defineProperty

  1. Object.defineProperty 自己有必定的監控到數組下標變化的能力, 可是在 Vue 中,從性能/體驗的性價比考慮,尤大大就棄用了這個特性(Vue 爲何不能檢測數組變更 )。

爲了解決這個問題, 對數組中全部能改變數組自身的方法,如 push、pop 等這些方法進行重寫。 而後手動調用 notify,通知 render watcher,執行 update

push();
pop();
shift();
unshift();
splice();
sort();
reverse();
複製代碼
  1. Object.defineProperty 只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷。若是屬性值也是對象那麼須要深度遍歷, 顯然若是能劫持一個完整的對象是纔是更好的選擇。

Proxy 能夠劫持整個對象,並返回一個新的對象。Proxy 不只能夠代理對象,還能夠代理數組。還能夠代理動態增長的屬性。

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)

說說Vue的渲染過程

  1. 調用 compile 函數,生成 render 函數字符串 ,編譯過程以下:

    • parse 函數解析 template,生成 ast(抽象語法樹)
    • optimize 函數優化靜態節點 (標記不須要每次都更新的內容,diff 算法會直接跳過靜態節點,從而減小比較的過程,優化了 patch 的性能)
    • generate 函數生成 render 函數字符串
  2. 調用 new Watcher 函數,監聽數據的變化,當數據發生變化時,Render 函數執行生成 vnode 對象

  3. 調用 patch 方法,對比新舊 vnode 對象,經過 DOM diff 算法,添加、修改、刪除真正的 DOM 元素。

說說keep-alive 的實現原理和緩存策略

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]);
  }
};
複製代碼

原理

  1. 獲取 keep-alive 包裹着的第一個子組件對象及其組件名

  2. 根據設定的 include/exclude(若是有)進行條件匹配,決定是否緩存。不匹配,直接返回組件實例

  3. 根據組件 ID 和 tag 生成緩存 Key,並在緩存對象中查找是否已緩存過該組件實例。若是存在,直接取出緩存值並更新該 key 在 this.keys 中的位置(更新 key 的位置是實現 LRU 置換策略的關鍵)

  4. 在 this.cache 對象中存儲該組件實例並保存 key 值,以後檢查緩存的實例數量是否超過 max 的設置值,超過則根據 LRU 置換策略刪除最近最久未使用的實例(便是下標爲 0 的那個 key)

  5. 最後組件實例的 keepAlive 屬性設置爲 true,這個在渲染和執行被包裹組件的鉤子函數會用到,這裏不細說

緩存策略

LRU (Least Recently Used)緩存策略:從內存中找出最久未使用的數據置換新的數據。

核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高。

最多見的實現是使用一個鏈表保存緩存數據,詳細算法實現以下:

  1. 新數據插入到鏈表頭部;
  2. 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
  3. 鏈表滿的時候,將鏈表尾部的數據丟棄;

Vue2.0引入虛擬 DOM 的目的是什麼

Vue 爲何要用虛擬 DOM (Virtual DOM)

  1. 能夠接受 Parser 解析轉化,抽象了本來的渲染過程。
  2. 跨平臺的能力。渲染到 DOM 之外的平臺, 實現 SSR、同構渲染這些高級特性。
  3. 關於虛擬dom比真實dom更快。虛擬DOM的優點體如今大量、頻繁更改dom的狀況。可是這樣的狀況並很少。

能談談vue實現時,有哪些性能優化方面的考慮嗎

翻開vue源代碼的過程,發現裏邊有寫多值得學習的優化過程,這裏記錄下來:

  1. cache函數,利用閉包實現緩存

  2. 二次依賴收集時,cleanupDeps, 剔除上一次存在但本次渲染不存在的依賴

  3. traverse,處理深度監聽數據,解除循環引用

  4. 編譯優化階段,optimize的主要做用是標記 static 靜態節點

  5. keep-alive組件利用lRU緩存淘汰算法

  6. 異步組件,分兩次渲染

Diff 本質的過程,簡單說說

過程

  1. 先同級比較再比較子節點

  2. 先判斷一方有子節點和一方沒有子節點的狀況。若是新的一方有子節點,舊的一方沒有,至關於新的子節點替代了原來沒有的節點;同理,若是新的一方沒有子節點,舊的一方有,至關於要把老的節點刪除。

  3. 再來比較都有子節點的狀況,這裏是diff的核心。首先會經過判斷兩個節點的key、tag、isComment、data同時定義或不定義以及當標籤類型爲input的時候type相不相同來肯定兩個節點是否是相同的節點,若是不是的話就將新節點替換舊節點。

  4. 若是是相同節點的話纔會進入到patchVNode階段。在這個階段核心是採用雙指針的算法,同時重新舊節點的兩端進行比較,在這個過程當中,會用到模版編譯時的靜態標記配合key來跳過對比靜態節點,若是不是的話再進行其它的比較。

舉例說明:

// old arr
["a", "b", "c", "d", "e", "f", "g", "h"]
// new arr
["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
複製代碼
  1. 從頭至尾開始比較,[a,b]是sameVnode,進入patch,到 [c] 中止;

  2. 從尾到頭開始比較,[h,g]是sameVnode,進入patch,到 [f] 中止;

  3. 判斷舊數據是否已經比較完畢,多餘的說明是新增的,須要mount(本例中沒有)

  4. 判斷新數據是否已經比較完畢,多餘的說明是刪除的,須要unmount(本例中沒有)

  5. patchVNode階段。在這個階段核心是採用雙指針的算法,同時重新舊節點的兩端進行比較,在這個過程當中,會用到模版編譯時的靜態標記配合key來跳過對比靜態節點,若是不是的話再進行其它的比較。

缺點:由於採用的是同級比較,因此若是發現本級的節點不一樣的話就會將新節點之間替換舊節點,不會再去比較其下的子節點是否有相同

vue二、vue3和react比較

Vue二、Vue3

Vue3.x借鑑了ivi算法和inferno算法。

它在建立VNode的時候就肯定了其類型,以及在mount/patch的過程當中採用位運算來判斷一個VNode的類型,在這個基礎之上再配合核心的Diff算法,使得性能上較Vue2.x有了提高

vue 和 react

相同是都是用同層比較,不一樣是 vue使用雙指針比較,react 是用 key 集合級比較

談談vue-router

VueRouter對不一樣模式的實現大體是這樣的:

  1. 首先根據mode來肯定所選的模式,若是當前環境不支持history模式,會強制切換到hash模式;

  2. 若是當前環境不是瀏覽器環境,會切換到abstract模式下。而後再根據不一樣模式來生成不一樣的history操做對象。

new Router過程

  1. init 方法內的 app變量即是存儲的當前的vue實例的this。
  2. 將 app 存入數組apps中。經過this.app判斷是實例否已經被初始化。
  3. 經過history來肯定不一樣路由的切換動做動做 history.transitionTo。
  4. 經過 history.listen來註冊路由變化的響應回調。

hash和history的區別

  1. 最明顯的是在顯示上,hash模式的URL中會夾雜着#號,而history沒有。
  2. Vue底層對它們的實現方式不一樣。hash模式是依靠onhashchange事件(監聽location.hash的改變),而history模式(popstate)是主要是依靠的HTML5 history中新增的兩個方法,pushState()能夠改變url地址且不會發送請求,replaceState()能夠讀取歷史記錄棧,還能夠對瀏覽器記錄進行修改。
  3. 當真正須要經過URL向後端發送HTTP請求的時候,好比常見的用戶手動輸入URL後回車,或者是刷新(重啓)瀏覽器,這時候history模式須要後端的支持。由於history模式下,前端的URL必須和實際向後端發送請求的URL一致,例若有一個URL是帶有路徑path的(例如www.lindaidai.wang/blogs/id),若是後端沒有對這個路徑作處理的話,就會返回404錯誤。因此須要後端增長一個覆蓋全部狀況的候選資源,通常會配合前端給出的一個404頁面。
做者:大俊_  
相關文章
相關標籤/搜索