Vue高級指南-02 Vue.js源碼深刻解析

目前社區有不少 Vue.js 的源碼解析文章,不少大牛寫的都很是詳細,但說到底。光看文章本身不去研究源碼和總結筆記,終究不會深刻了解和記憶。前端

本篇文章將本身研究 Vue.js源碼的一些內容作成筆記而且記錄下來。加深印象和理解,俗話說讀書百遍不如手寫一遍。vue

理解什麼是MVVM模式?

MVC模式是指用戶操做會請求服務端路由,路由會調用對應的控制器來處理,控制器會獲取數據。將結果返回給前端,頁面從新渲染。而且前端會將數據手動的操做DOM渲染到頁面上,很是消耗性能。node

雖然沒有徹底遵循 MVVM 模型,可是 Vue 的設計也受到了它的啓發。Vue中則再也不須要用戶手動操做DOM元素,而是將數據綁定到viewModel層上,數據會自動渲染到頁面上,視圖變化會通知viewModel層更新數據。ViewModel就是咱們MVVM模式中的橋樑.react


Vue中響應式數據的原理是什麼(2.x版本)?

Vue2.x版本響應式數據的原理是 Object.defineProperty(Es6筆記中有詳細介紹)web

Vue在初始化的時候,也就是new Vue()的時候,會調用底層的一個initData()方法,方法中有一個observe()會將初始化傳入的data進行數據響應式控制,其中會對data進行一系列操做,判斷是否已經被觀測過。判斷觀測的數據是對象仍是數組。面試

觀測的數據是對象

假若觀測的是一個對象,會調用一個walk()方法其內部內就是調用Object.defineProperty進行觀測,假若對象內部的屬性仍是一個對象的話,就會進行遞歸觀測。ajax

這時當對當前對象取值的時候就會調用get方法,get方法中就進行依賴收集(watcher),若是對當前對象進行賦值操做,就會調用set方法,set方法中會判斷新舊值是否不同,不同就會調用一個notify方法去觸發數據對應的依賴收集進行更新。算法

觀測的數據是一個數組

假若觀測的是一個數組,數組不會走上面的方法進行依賴收集,Vue底層重寫了數組的原型方法,當前觀測的是數組時,Vue將數組的原型指向了本身定義的原型方法。而且只攔截瞭如下7個數組的方法。express

// 由於只有如下7中數組方法纔會去改變原數組。
push, pop, shift, unshift, splice, sort, reverse
複製代碼

原型方法內部採用的是函數劫持的方式,若是用戶操做的是以上7中數組方法,就會走Vue重寫的數組方法。這時候就能夠在數組發生變化時候,去手動調用notify方法去更新試圖。數組

固然在對數據進行數據更新的時候,也會對新增的數據進行依賴收集觀測。

若是數組中的數據也是對象,它會繼續調用Object.defineProperty對其進行觀測。

知道以上內容,你就能夠理解爲什麼數組經過下標修改數據,數據變化了可是視圖沒有更新的緣由。

觀測對象核心代碼

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // ** 收集依賴 ** /
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify() /** 通知相關依賴進行更新 **/
    }
  })
複製代碼

觀測數組核心代碼

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (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
  })
})

this.observeArray(value) // 進行深度監控
複製代碼

以上內容最好下載Github上Vue源碼一塊兒看。


Vue採用異步渲染的緣由是什麼?

首先咱們要知道Vue是組件級更新。

當咱們操做某個組件中的方法進行數據更新的時候,例如

data() {
    return {
        msg: 'hello word',
        name: '只會番茄炒蛋'
    }
}
methods:{
    add() {
        this.msg = '我更改了 => hello word'
        this.name = '我更改了 => 只會番茄炒蛋'
    }
}
複製代碼

假若一旦更改數據就進行視圖的渲染(以上更改了兩次數據),必然會影響性能,所以Vue採用異步渲染方式,也就是多個數據在一個事件中同時被更改了,同一個watcher被屢次觸發,只會被推入到隊列中一次。當最後數據被更改完畢之後調用nexttick方法去異步更新視圖。

內部還有一些其餘的操做,例如添加 watcher 的時候給一個惟一的id, 更新的時候根據 id 進行一個排序,更新完畢還會調用對應的生命週期也就是 beforeUpdate 和 updated 方法等。

以上內容最好下載Github上Vue源碼一塊兒看。


Vue中nextTick實現原理是什麼?

在瞭解nextTick實現原理以前,你須要掌握什麼Event Loop,而且瞭解微任務和宏任務,這裏我簡單介紹一下。

Event Loop

你們也知道了當咱們執行 JS 代碼的時候其實就是往執行棧中放入函數,那麼遇到異步代碼的時候該怎麼辦?其實當遇到異步的代碼時,會被掛起並在須要執行的時候加入到 Task(有多種 Task) 隊列中。一旦執行棧爲空,Event Loop 就會從 Task 隊列中拿出須要執行的代碼並放入執行棧中執行,因此本質上來講 JS 中的異步仍是同步行爲。

不一樣的任務源會被分配到不一樣的 Task 隊列中,任務源能夠分爲 微任務(microtask) 和 宏任務(macrotask)。在 ES6 規範中,microtask 稱爲 jobs,macrotask 稱爲 task。

微任務包括 process.nextTick ,promise.then ,MutationObserver,其中 process.nextTick 爲 Node 獨有。

宏任務包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

簡單瞭解 Event Loop 以後繼續學習 Vue 中 nextTick 實現原理

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

官方原話

當你設置vm.someData = 'new value',該組件不會當即從新渲染。當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)。這樣回調函數將在 DOM 更新完成後被調用。

總結:nextTick方法主要是使用了宏任務和微任務,定義了一個異步方法,屢次調用nextTick會將方法存入隊列中,經過這個異步方法清空當前隊列。 因此這個nextTick方法就是異步方法

nextTick原理核心代碼

let timerFunc  // 會定義一個異步方法
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' ) { // setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {   // setTimeout
    setTimeout(flushCallbacks, 0)
  }
}
// nextTick實現
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}
複製代碼

以上內容最好下載Github上Vue源碼一塊兒看。


Vue中 computed 的原理

通常面試題中都問會問到 computed 和 watch 的區別,實際上 computed 和 watch 的原理都是使用watcher實現的,而他倆的區別就是 computed 具備緩存的功能。

computed 緩存功能

當咱們默認初始化建立計算屬性的時候,它會建立一個watcher, 而且這個watcher有兩個個屬性lazy:true,dirty: true, 也就是說當建立一個計算屬性的時候,默認是不執行的,只有當用戶取值的時候(也就是在組件上使用的時候),它會判斷若是dirty: true的話就會讓這個watcher執行去取值,而且在求值結束後,更改dirty: false,這樣當你再次使用這個計算屬性的時候,判斷條件走到dirty: false的時候,就不在執行watcher求值操做,而是直接返回上次求值的結果。

那麼何時會從新計算求職呢?

只有當計算屬性的值發生變化的時候,它會調用對應的update方法,而後更改dirty: true,而後執行的時候根據條件從新執行watcher求值。

computed原理核心代碼

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // 若是依賴的值沒發生變化,就不會從新求值
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
複製代碼

以上內容最好下載Github上Vue源碼一塊兒看。


Watch中的 deep : true 是如何實現的?

Vue官方關於watch的介紹

  • 類型:{ [key: string]: string | Function | Object | Array }

  • 詳細:一個對象,鍵是須要觀察的表達式,值是對應回調函數。值也能夠是方法名,或者包含選項的對象。Vue 實例將會在實例化時調用 $watch(),遍歷 watch 對象的每個屬性。

一般咱們在項目中通常使用watch來監聽路由或者data中的屬性發生變化時做出對應的處理方式。

那麼deep : true的使用場景就是當咱們監測的屬性是一個對象的時候,咱們會發現watch中監測的方法並無執行,緣由是受現代 JavaScript 的限制 (以及廢棄 Object.observe),Vue 不能檢測到對象屬性的添加或刪除。因爲 Vue 會在初始化實例時對屬性執行 getter/setter 轉化過程,因此屬性必須在 data 對象上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。

deep的意思就是深刻觀察,監聽器會一層層的往下遍歷,給對象的全部屬性都加上這個監聽器,可是這樣性能開銷就會很是大了,任何修改obj裏面任何一個屬性都會觸發這個監聽器裏的 handler。

這時候咱們能夠優化這個問題,經過如下方式

// 使用字符串形式監聽具體對象中的某個值。
watch: {
  'obj.a': {
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    immediate: true, // 當即執行一次handler方法
    deep: true // 深度監測
  }
} 
複製代碼

須要注意的是,當咱們經過下標去修改數組中某個值的時候,也不會引發watch的變化,原理請看上面Vue中響應式數據的原理是什麼?

固然除了改變數組的方法能夠進行監測數組變化,Vue也提供來Vue.set()方法。

Watch中的 deep : true 核心代碼

get () {
    pushTarget(this) // 先將當前依賴放到 Dep.target上
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) { // 若是須要深度監控
        traverse(value) // 會對對象中的每一項取值,取值時會執行對應的get方法
      }
      popTarget()
    }
    return value
}
function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
複製代碼

以上內容最好下載Github上Vue源碼一塊兒看。


Vue中的生命週期

附上Vue官方關於生命週期的介紹圖

每一個生命週期函數在何時調用?

  • beforeCreate(){}
在實例初始化之後,數據觀測(data observe)以前進行調用,此時獲取不到data中的數據。
複製代碼
  • created(){}
在實例建立完成以後調用,這時候實例已經完成了數據觀測(data observe),屬性和方法的運算,watch/event 事件回調。
注意:這裏沒有$el
複製代碼
  • beforeMount(){}
在掛載以前調用,相關的render函數首次被調用。
複製代碼
  • mounted(){}
el綁定的元素被內部新建立的$el替換掉,而且掛載到實例上去以後調用。
複製代碼
  • beforeUpdate(){}
數據更新時調用,發生在虛擬DOM從新渲染和打補丁以前。
複製代碼
  • updated(){}
因爲數據更改致使的虛擬 DOM 從新渲染和打補丁,在這以後會調用該鉤子。
複製代碼
  • beforeDestroy(){}
實例銷燬以前調用。在這一步,實例仍然徹底可用
複製代碼
  • destroyed(){}
Vue實例銷燬後調用。調用後,Vue實例指示的全部東西都會解綁定,全部的事件監聽器會被移除。
全部的子實例也會被銷燬,該鉤子在服務器端渲染期間不被調用。
複製代碼

經常使用生命週期內部能夠作的事情。

  • created(){}
一般咱們在項目中會在created(){}生命週期中去調用ajax進行一些數據資源的請求,可是因爲當前生命週期沒法操做DOM
因此通常在項目中,全部的請求我都會統一放到mounted(){}生命週期中。
複製代碼
  • mounted(){}
在當前生命週期中,實例已經掛載完成,一般我會將ajax請求放到這個生命週期函數中。
若是有一些須要根據獲取的數據並去初始化DOM操做,在這裏是最佳方案。
複製代碼
  • beforeUpdate(){}
能夠在這個生命週期函數中進一步地更改狀態,這不會觸發附加的重渲染過程
複製代碼
  • updated(){}
能夠執行依賴於 DOM 的操做。然而在大多數狀況下,你應該避免在此期間更改狀態,由於這可能會致使更新無限循環。 
該鉤子在服務器端渲染期間不被調用。
複製代碼
  • destroyed(){}
能夠執行一些優化操做,清空定時器,解除綁定事件
複製代碼

經過以上描述咱們能夠總結出如下結論

  1. ajax請求放在一般放在created(){}或者mounted(){}生命週期中。 而且在created的時候,視圖中的dom並無渲染出來,因此此時若是直接去操dom節點,沒法找到相關的元素,在mounted中,因爲此時dom已經渲染出來了,因此能夠直接操做dom節點 ,通常狀況下都放到mounted中,保證邏輯的統一性,由於生命週期是同步執行的,ajax是異步執行的。

    注意:服務端渲染不支持mounted方法,因此在服務端渲染的狀況下統一放到created中

  2. 假若當前組件中有定時器,使用了$on方法,綁定 scroll mousemove等事件,須要在beforeDestroy鉤子中去清除。


Vue中模板編譯原理

查看源碼後發現,Vue在底層會調用一個parseHTML方法將模版轉爲AST語法樹(內部經過正則走一些方法),最後將AST語法樹轉爲render函數(渲染函數),渲染函數結合數據生成Virtual DOM樹,Diff和Patch後生成新的UI。

關於虛擬DOM

Vue的編譯器在編譯模板以後,會把這些模板編譯成一個渲染函數。而函數被調用的時候就會渲染而且返回一個虛擬DOM的樹。當咱們有了這個虛擬的樹以後,再交給一個Patch函數,負責把這些虛擬DOM真正施加到真實的DOM上。

在這個過程當中,Vue有自身的響應式系統來偵測在渲染過程當中所依賴到的數據來源。在渲染過程當中,偵測到數據來源以後就能夠精確感知數據源的變更。到時候就能夠根據須要從新進行渲染。當從新進行渲染以後,會生成一個新的樹,將新的樹與舊的樹進行對比,就能夠最終得出應施加到真實DOM上的改動。

最後再經過Patch函數施加改動。簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在應該狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。

實際上這一部分的源碼仍是比較多的。這裏我簡單的理解了一些。


關於v-if和v-show的區別以及底層是如何實現的。

這裏我簡單描述一下二者的區別

  • v-if
若是當前條件判斷不成立,那麼當前指令所在節點的DOM元素不會渲染
複製代碼
  • v-show
當前指令所在節點的DOM元素始終會被渲染,只是根據當前條件去動態改變 display: none || block
從而達到DOM元素的顯示和隱藏。
複製代碼

底層實現方式

  • v-if

Vue底層封裝了一些特殊的方法,代碼位於此處。 vue/packages/weex-vue-framework/factory.js

VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`);

with(this) {
    return (true) ? _c('div', _l((3), function (i) {
        return _c('span', [_v("hello")])
    }), 0) : _e() // _e()方法建立一個空的虛擬dom等等。
}
複製代碼

經過上述代碼,能夠得知若是當前條件判斷不成立,那麼當前指令所在節點的DOM元素不會渲染

  • v-show

v-show編譯出來裏面沒有任何東西,只有一個directives,它裏面有一個指令叫作v-show

VueTemplateCompiler.compile(`<div v-show="true"></div>`);
/**
with(this) {
    return _c('div', {
        directives: [{
            name: "show",
            rawName: "v-show",
            value: (true),
            expression: "true"
        }]
    })
}
複製代碼

只有在運行的時候它會去處理這個指令,代碼以下:

// v-show 操做的是樣式  定義在platforms/web/runtime/directives/show.js
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    } else {
      el.style.display = value ? originalDisplay : 'none'
    }
}
複製代碼

經過源碼咱們能夠清晰的看到它是在操做DOM的display屬性。


在項目中 v-for 爲何不能和 v-if 連用

一樣能夠經過觀看源碼就知道緣由

VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`);

with(this) {
    return _l((3), function (i) {
        return (false) ? _c('div', [_v("hello")]) : _e()
    })
}
複製代碼

咱們知道 v-for 的優先級比 v-if 高,那麼在編譯階段會發現他給內部的每個元素都加了 v-if,這樣在運行的階段會走驗證,這樣很是的消耗性能。所以在項目中咱們要避免這樣的操做。

固然若是咱們有這樣的需求的話也是能夠實現的。

咱們能夠經過計算屬性來達到目的

<div v-for="i in computedNumber">hello</div>

export default {
    data() {
        return {
            arr: [1, 2, 3]
        }
    },
    computed: {
        computedNumber() {
            return arr.filter(item => item > 1)
        }
    }
}
複製代碼

關於解析指令的源碼,建議你們也去看看源碼的實現過程


關於虛擬DOM的實現過程

在我理解來講,就是用一個對象來描述咱們的虛擬DOM結構,例如:

<div id="container">
    <p></p>
</div>

// 簡單用對象來描述的虛擬DOM結構
let obj = {
    tag: 'div',
    data: {
        id: "container"
    },
    children: [
        {
            tag: 'p',
            data: {},
            children: {}
        }
    ]
}
複製代碼

固然在Vue中的實現是比較複雜的,我這裏添加來一些注視方便理解

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {

    // 兼容不傳data的狀況
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
    }

    // 若是alwaysNormalize是true
    // 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值
    if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
        // 調用_createElement建立虛擬節點
        return _createElement(context, tag, data, children, normalizationType)
    }

    function _createElement (context, tag, data, children, normalizationType) {
        /**
        * 若是存在data.__ob__,說明data是被Observer觀察的數據
        * 不能用做虛擬節點的data
        * 須要拋出警告,並返回一個空節點
        * 
        * 被監控的data不能被用做vnode渲染的數據的緣由是:
        * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做
        */
        if (data && data.__ob__) {
            process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context
            )
            return createEmptyVNode()
        }

        // 當組件的is屬性被設置爲一個falsy的值
        // Vue將不會知道要把這個組件渲染成什麼
        // 因此渲染一個空節點
        if (!tag) {
            return createEmptyVNode()
        }

        // 做用域插槽
        if (Array.isArray(children) && typeof children[0] === 'function') {
            data = data || {}
            data.scopedSlots = { default: children[0] }
            children.length = 0
        }

        // 根據normalizationType的值,選擇不一樣的處理方法
        if (normalizationType === ALWAYS_NORMALIZE) {
            children = normalizeChildren(children)
        } else if (normalizationType === SIMPLE_NORMALIZE) {
            children = simpleNormalizeChildren(children)
        }
        let vnode, ns

        // 若是標籤名是字符串類型
        if (typeof tag === 'string') {
            let Ctor
            // 獲取標籤名的命名空間
            ns = config.getTagNamespace(tag)

            // 判斷是否爲保留標籤
            if (config.isReservedTag(tag)) {
                // 若是是保留標籤,就建立一個這樣的vnode
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                )

                // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
            } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
                // 若是找到了這個標籤的定義,就以此建立虛擬組件節點
                vnode = createComponent(Ctor, data, context, children, tag)
            } else {
                // 兜底方案,正常建立一個vnode
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                )
            }

        // 當tag不是字符串的時候,咱們認爲tag是組件的構造類
        // 因此直接建立
        } else {
            vnode = createComponent(tag, data, context, children)
        }

        // 若是有vnode
        if (vnode) {
            // 若是有namespace,就應用下namespace,而後返回vnode
            if (ns) applyNS(vnode, ns)
            return vnode
        // 不然,返回一個空節點
        } else {
            return createEmptyVNode()
        }
    }
}
複製代碼

以上內容最好下載Github上Vue源碼一塊兒看。


diff算法的時間複雜度,以及Vue中 diff 算法原理

算法方面不是很瞭解,這邊也只是簡單看視頻和文章介紹描述一下。

兩個樹的徹底的diff算法是一個時間複雜度爲 O(n3),Vue進行了優化·O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題(只比較同級不考慮跨級問題) 在前端當中, 你不多會跨越層級地移動Dom元素。 因此 Virtual Dom只會對同一個層級的元素進行對比。

Vue中 diff 算法原理 (B站公開課視頻及Vue源碼)

  • 1.先同級比較,在比較子節點
  • 2.先判斷一方有兒子一方沒兒子的狀況
  • 3.比較都有兒子的狀況
  • 4.遞歸比較子節點

自我理解

第一種狀況:同級比較
當新節點和舊節點不相同狀況,新節點直接替換舊節點。

第二種狀況:同級比較,節點一致,但一方有子節點,一方沒有
當新舊節點相同狀況下,若是新節點有子節點,但舊節點沒有,那麼舊會直接將新節點的子節點插入到舊節點中。
當新舊節點相同狀況下,若是新節點沒有子節點,但舊節點有子節點,那麼舊節點會直接刪除子節點。

第三種狀況:新舊節點相同,且都有子節點。(這時候舊用到了上圖雙指針比較方法。)
狀況一:
    舊:1234
    新:12345
    當前雙指針指向新舊1,1和4,5,判斷首部節點一致,指針向後移繼續判斷,直到最後一項不相同,將新5插入到舊4後面。
    
狀況二:
    舊:1234
    新:01234
    當前雙指針指向新舊1,0和4,4 發現不想等時會從最後的指針查看,這時候發現相同後,會從後面往前移動指針進行判斷。直到到達首部,將新0插入到舊1以前。

狀況三:
    舊:1234
    新:4123
    當前發現頭部和頭部不想等,而且尾部和尾部不想等的時候,就混進行頭尾/尾頭的互相比較。這時候發現舊的4在新的第一位,就會將本身的4調整到1的前面。
    
狀況四:
    舊:1234
    新:2341
    當前發現頭部和頭部不想等,而且尾部和尾部不想等的時候,就混進行頭尾/尾頭的互相比較。這時候發現舊的1在新的第四位,就會將本身的1調整到4的後面。
    
特殊狀況五:(也就是咱們循環數組時候須要加key值的緣由)
    舊:1234
    新:2456
    這時候遞歸遍歷會拿新的元素的Key去舊的比較而後移動位置,若是舊的沒有就直接將新的放進去,反之將舊的中有,新的沒有的元素刪除掉。
複製代碼

經過上述內容咱們大體舊瞭解diff算法的一部分了。

核心源碼

core/vdom/patch.js

const oldCh = oldVnode.children // 老的兒子 
const ch = vnode.children  // 新的兒子
if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
        // 比較孩子
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) { // 新的兒子有 老的沒有
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { // 若是老的有新的沒有 就刪除
        removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {  // 老的有文本 新的沒文本
        nodeOps.setTextContent(elm, '') // 將老的清空
    }
} else if (oldVnode.text !== vnode.text) { // 文本不相同替換
    nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
複製代碼

v-for中爲何要用key

經過上圖以及diff算法的第五種複雜狀況能夠得知緣由。

例如當咱們利用for循環出三個chenckbox, 當咱們經過一個按鈕將當前循環數組的第一項刪除的時候,會發現第一項依舊是選中狀態,而最後一項被刪除了,緣由就是diff的過程當中。當對比新舊虛擬dom的時候,發現DOM一致,這時候內部複用了當前要刪除的第一項DOM(內容會是要現實的內容,而不是刪除的內容),作完比對後,將舊dom最後一項刪除了。

1 (1是選中狀態)                1 (1是選中狀態)
2                              2
3                              3 (被刪除了)
複製代碼

描述的可能有些混亂,你們能夠本身在項目中實踐一下。(ps:v-for循環必定要加上key且key不能爲index下標)


持續總結中。。。

相關文章
相關標籤/搜索