Vue 2.0源碼學習

Vue2.0介紹

從去年9月份瞭解到Vue後,就被他簡潔的API所吸引。1.0版本正式發佈後,就在業務中開始使用,將原先jQuery的功能逐步的進行遷移。
今年的10月1日,Vue的2.0版本正式發佈了,其中核心代碼都進行了重寫,因而就專門花時間,對Vue 2.0的源碼進行了學習。本篇文章就是2.0源碼學習的總結。 javascript

先對Vue 2.0的新特性作一個簡單的介紹:html

  • 大小 & 性能。Vue 2.0的線上包gzip後只有12Kb,而1.0須要22Kb,react須要44Kb。並且,Vue 2.0的性能在react等幾個框架中,性能是最快的。vue

  • VDOM。實現了Virtual DOM, 而且將靜態子樹進行了提取,減小界面重繪時的對比。與1.0對比性能有明顯提高。java

  • template & JSX。衆所周知,Vue 1.0使用的是template來實現模板,而React使用了JSX實現模板。關於template和JSX的爭論也不少,不少人不使用React就是由於沒有支持template寫法。Vue 2.0對template和JSX寫法都作了支持。使用時,能夠根據具體業務細節進行選擇,能夠很好的發揮二者的優點。就這一點,Vue已經超過React了。node

  • Server Render。2.0還對了Server Render作了支持。這一點並無在業務中使用,不作評價。react

Vue的最新源碼能夠去 https://github.com/vuejs/vue 得到。本文講的是 2.0.3版本,2.0.3能夠去 https://github.com/vuejs/vue/... 這裏得到。 git

下面開始進入正題。首先從生命週期開始。github

生命週期

圖片描述

上圖就是官方給出的Vue 2.0的生命週期圖,其中包含了Vue對象生命週期過程當中的幾個核心步驟。瞭解了這幾個過程,能夠很好的幫助咱們理解Vue的建立與銷燬過程。
從圖中咱們能夠看出,生命週期主要分爲4個過程:web

  • createnew Vue時,會先進行create,建立出Vue對象。weex

  • mount。根據el, template, render方法等屬性,會生成DOM,並添加到對應位置。

  • update。當數據發生變化後,會從新渲染DOM,並進行替換。

  • destory。銷燬時運行。

那麼這4個過程在源碼中是怎麼實現的呢?咱們從new Vue開始。

new Vue

爲了更好的理解new的過程,我整理了一個序列圖:

new Vue序列圖

new Vue的過程主要涉及到三個對象:vm、compiler、watcher。其中,vm表示Vue的具體對象;compiler負責將template解析爲AST render方法;watcher用於觀察數據變化,以實現數據變化後進行re-render。

下面來分析下具體的過程和代碼:
首先,運行new Vue()的時候,會進入代碼src/core/instance/index.js的Vue構造方法中,並執行this._init()方法。在_init中,會對各個功能進行初始化,並執行beforeCreatecreated兩個生命週期方法。核心代碼以下:

initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)

這個過程有一點須要注意:
beforeCreate和created之間只有initState,和官方給出的生命週期圖並不徹底同樣。這裏的initState是用於初始化data,props等的監聽的。

_init的最後,會運行initRender方法。在該方法中,會運行vm.$mount方法,代碼以下:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

這裏的vm.$mount能夠在業務代碼中調用,這樣,new 過程和 mount過程就能夠根據業務狀況進行分離。

這裏的$mountsrc/entries/web-runtime-with-compiler.js中,主要邏輯是根據el, template, render三個屬性來得到AST render方法。代碼以下:

if (!options.render) {   // 若是有render方法,直接運行mount
  let template = options.template
  if (template) {  // 若是有template, 獲取template參數對於的HTML做爲模板
    if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
        template = idToTemplate(template)
      }
    } else if (template.nodeType) {
      template = template.innerHTML
    } else {
      if (process.env.NODE_ENV !== 'production') {
        warn('invalid template option:' + template, this)
      }
      return this
    }
  } else if (el) {  // 若是沒有template, 且存在el,則獲取el的outerHTML做爲模板
    template = getOuterHTML(el)
  }
  if (template) { // 若是獲取到了模板,則將模板轉化爲render方法
    const { render, staticRenderFns } = compileToFunctions(template, {
      warn,
      shouldDecodeNewlines,
      delimiters: options.delimiters
    }, this)
    options.render = render
    options.staticRenderFns = staticRenderFns
  }
}
return mount.call(this, el, hydrating)

這個過程有三點須要注意:
compile時,將最大靜態子樹提取出來做爲單獨的AST渲染方法,以提高後面vNode對比時的性能。因此,當存在多個連續的靜態標籤時,能夠在外邊添加一個靜態父節點,這樣,staticRenderFns數目能夠減小,從而提高性能。
Vue 2.0中的模板有三種引用寫法:el, template, render(JSX)。其中的優先級是render > template > el。
el, template兩種寫法,最後都會經過compiler轉化爲render(JSX)來運行,也就是說,直接寫成render(JSX)是性能最優的。固然,若是使用了構建工具,最終生成的包就是使用的render(JSX)。這樣子,在源碼上就能夠不用過多考慮這一塊的性能了,直接用可維護性最好的方式就行。

將模板轉化爲render,用到了compileToFunctions方法,該方法最後會經過src/compiler/index.js文件中的compile方法,將模板轉化爲AST語法結構的render方法,並對靜態子樹進行分離。

完成render方法的生成後,會進入_mount(src/core/instance.lifecycle.js)中進行DOM更新。該方法的核心邏輯以下:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

首先會new一個watcher對象,在watcher對象建立後,會運行傳入的方法vm._update(vm._render(), hydrating)(watcher的邏輯在下面的watcher小節中細講)。其中的vm._render()主要做用就是運行前面compiler生成的render方法,並返回一個vNode對象。這裏的vNode就是一個虛擬的DOM節點。

拿到vNode後,傳入vm._update()方法,進行DOM更新。

VDOM

上面已經講完了new Vue過程當中的主要步驟,其中涉及到template如何轉化爲DOM的過程,這裏單獨拿出來說下。先上序列圖:

virtual DOM

從圖中能夠看出,從template到DOM,有三個過程:

  • template -> AST render (compiler解析template)

  • AST render -> vNode (render方法運行)

  • vNode -> DOM (vdom.patch)

首先是template在compiler中解析爲AST render方法的過程。上一節中有說到,initRender後,會調用到src/entries/web-runtime-with-compiler.js中的Vue.prototype.$mount方法。在$mount中,會獲取template,而後調用src/platforms/web/compiler/index.jscompileToFunctions方法。在該方法中,會運行compile將template解析爲多個render方法,也就是AST render。這裏的compile在文件src/compiler/index.js中,代碼以下:

const ast = parse(template.trim(), options)   // 解析template爲AST
optimize(ast, options)  // 提取static tree
const code = generate(ast, options)  // 生成render 方法
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

能夠看出,compile方法就是將template以AST的方式進行解析,並轉化爲render方法進行返回。

再看第二個過程:AST render -> vNode。這個過程很簡單,就是將AST render方法進行運行,得到返回的vNode對象。

最後一步,vNode -> DOM。該過程當中,存在vNode的對比以及DOM的添加修改操做。
在上一節中,有講到vm._update()方法中對DOM進行更新。_update的主要代碼以下:

// src/core/instance/lifecycle.js
if (!prevVnode) {
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating)  // 首次添加
} else {
  vm.$el = vm.__patch__(prevVnode, vnode)  // 數據變化後觸發的DOM更新
}

能夠看出,不管是首次添加仍是後期的update,都是經過__patch__來更新的。這裏的__patch__核心步驟是在src/core/vdom/patch.js中的patch方法進行實現,源碼以下:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (!oldVnode) {
      ...
    } else {
      ...
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)  // diff並更新DOM。
      } else {
        elm = oldVnode.elm
        parent = nodeOps.parentNode(elm)
        ...
        if (parent !== null) {
          nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm))  // 添加element到DOM。
          removeVnodes(parent, [oldVnode], 0, 0)
        }
        ...
      }
    }
    ...
  }

首次添加很簡單,就是經過insertBefore將轉化好的element添加到DOM中。若是是update,則會調動patchVnode()。最後來看下patchVnode的代碼:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ...
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  ...
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {  // 當都存在時,更新Children
      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(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {  // 刪除了textContent
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) { // 修改了textContent
    nodeOps.setTextContent(elm, vnode.text)
  }
}

其中有調用了updateChildren來更新子節點,代碼以下:

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  ...
  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)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      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)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      ...
    }
  }
  ...
}

能夠看到updateChildren中,又經過patchVnode來更新當前節點。梳理一下,patch經過patchVnode來更新根節點,而後經過updateChildren來更新子節點,具體子節點,又經過patchVnode來更新,經過一個相似於遞歸的方式逐個節點的完成對比和更新。

Vue 2.0中對如何去實現VDOM的思路是否清晰,經過4層結構,很好的實現了可維護性,也爲實現server render, weex等功能提供了可能。拿server render舉例,只須要將最後的vNode -> DOM 改爲 vNode -> String 或者 vNode -> Stream, 就能夠實現server render。剩下的compiler和Vue的核心邏輯都不須要改。

Watcher

咱們都知道MVVM框架的特徵就是當數據發生變化後,會自動更新對應的DOM節點。使用MVVM以後,業務代碼中就能夠徹底不寫DOM操做代碼,不只能夠將業務代碼聚焦在業務邏輯上,還能夠提升業務代碼的可維護性和可測試性。那麼Vue 2.0中是怎麼實現對數據變化的監聽的呢?照例,先看序列圖:
watcher

能夠看出,整個Watcher的過程能夠分爲三個過程。

  • 對state設置setter/getter

  • 對vm設置好Watcher,添加好state 觸發 setter時的執行方法

  • state變化觸發執行

前面有說過,在生命週期函數beforeCreatecreated直接,會運行方法initState()。在initState中,會對Props, Data, Computed等屬性添加Setter/Getter。拿Data舉例,設置setter/getter的代碼以下:

function initData (vm: Component) {
  let data = vm.$options.data
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    ...
    proxy(vm, keys[i])   // 設置vm._data爲代理
  }
  // observe data
  observe(data)
}

經過調用observe方法,會對data添加好觀察者,核心代碼爲:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()  // 處理好依賴watcher
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    childOb = observe(newVal)  // 對新數據從新observe
    dep.notify()  // 通知到dep進行數據更新
  }
})

這個時候,對data的監聽已經完成。能夠看到,當data發生變化的時候,會運行dep.notify()。在notify方法中,會去運行watcher的update方法,內容以下:

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
run () {
  if (this.active) {
    const value = this.get()
  }
  ...
}

update方法中,queueWatcher方法的目的是經過nextTicker來執行run方法,屬於支線邏輯,就不分析了,這裏直接看run的實現。run方法其實很簡單,就是調用get方法,而get方法會經過執行this.getter()來更新DOM。

那麼this.getter是什麼呢?本文最開始分析new Vue過程時,有講到運行_mount方法時,會運行以下代碼:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

那麼this.getter就是這裏Watcher方法的第二個參數。來看下new Watcher的代碼:

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object = {}
  ) {
    ...
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    ...
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

能夠看出,在new Vue過程當中,Watcher會在構造完成後主動調用this.get()來觸發this.getter()方法的運行,以達到更新DOM節點。

總結一下這個過程:首先_init時,會對Data設置好setter方法,setter方法中會調用dep.notify(),以便數據變化時通知DOM進行更新。而後new Watcher時,會將更新DOM的方法進行設置,也就是Watcher.getter方法。最後,當Data發生變化的時候,dep.notify()運行,運行到watcher.getter()時,就會去運行render和update邏輯,最終達到DOM更新的目的。

總結與收穫

剛開始以爲看源碼,是由於但願能瞭解下Vue 2.0的實現,看看能不能獲得一些從文檔中沒法知道的細節,用於提高運行效率。把主要流程理清楚後,的確瞭解到一些,這裏作個整理:

  • el屬性傳入的若是不是element,最後會經過document.querySelector來獲取的,這個接口性能較差,因此,el傳入一個element性能會更好。

  • $mount方法中對htmlbody標籤作了過濾,這兩個不能用來做爲渲染的根節點。

  • 每個組件都會從_init開始從新運行,因此,當存在一個長列表時,將子節點做爲一個組件,性能會較差。

  • *.vue文件會在構建時轉化爲render方法,而render方法的性能比指定template更好。因此,源碼使用*.vue的方式,性能更好。

  • 若是須要自定義delimiters,每個組件都須要單獨指定。

  • 若是是*.vue文件,指定delimiters是失效的,由於vue-loader*.vue文件進行解析時,並無將delimiters傳遞到compiler.compile()中。(這一點不肯定是bug仍是故意這樣設計的)。

相關文章
相關標籤/搜索