1. 程式人生 > >vue的原始碼學習之六——3.patch

vue的原始碼學習之六——3.patch

1. 介紹

       版本:2.5.17。 

       我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。

       學習文件:https://ustbhuangyi.github.io/vue-analysis/components/patch.html

 2.patch

通過上一節的分析我們知道,當我們通過 createComponent

 建立了元件 VNode,接下來會走到 vm._update,執行 vm.__patch__ 去把 VNode 轉換成真正的 DOM 節點。這個過程我們在前一章已經分析過了,但是針對一個普通的 VNode 節點,接下來我們來看看元件的 VNode 會有哪些不一樣的地方。

patch 的過程會呼叫 createElm 建立元素節點,回顧一下 createElm 的實現,它的定義在 src/core/vdom/patch.js 中:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // ...
}

3.createComponent

我們刪掉多餘的程式碼,只保留關鍵的邏輯,上面的程式碼會判斷 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果為 true 則直接結束。

createComponent其實呼叫的就是給逐漸VNode新增的init方法

那麼接下來看一下 createComponent 方法的實現:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

3.1 對vnode.data 做了一些判斷

let i = vnode.data
if (isDef(i)) {
  // ...
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false /* hydrating */)
    // ...
  }
  // ..
}
  • vnode 是一個元件 VNode,那麼條件會滿足,並且得到 i 就是 init 鉤子函式
  • 判斷vnode.data中是否有hook,並且有init方法(因為上一節講了會給元件VNode merage一些鉤子,其中就有init,所以這裡是true),就會呼叫init方法

3.2 init方法

init方法定義在 src/core/vdom/create-component.js 中:

其實就是和元件的data.hook鉤子合併的 componentVNodeHooks 鉤子物件的init方法

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

init 鉤子函式執行也很簡單,我們先不考慮 keepAlive 的情況,它是通過 createComponentInstanceForVnode 建立一個 Vue 的例項,然後呼叫 $mount 方法掛載子元件

3.3 createComponentInstanceForVnode 

建立一個 vnode 的例項 
src/core/vdom/create-component.js

export function createComponentInstanceForVnode (
  vnode: any, // 元件VNode
  parent: any, // 當前vue例項vm
): Component { 
    // 定義引數
  const options: InternalComponentOptions = {
    _isComponent: true,
    // 父VNode,是一個佔位節點,Vue例項A呼叫B元件,B呼叫C元件,_parentVnode就是C元件佔位符
    _parentVnode: vnode, 
   //表示當前啟用的子元件的父級例項,例如 app =new Vue,並呼叫子元件,parent就是app
    parent 
  }
     ...

  // 由上一節我們知道元件會生成子構造器,vnode.componentOptions.Ctor 對應的就是子元件的建構函式
  return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode 函式構造的一個內部元件的引數,然後執行 new 
vnode.componentOptions.Ctor(options)。

上一節我們知道元件會生成子構造器,vnode.componentOptions.Ctor 對應的就是子元件的建構函式,

我們上一節分析了子構造器實際上是繼承於 Vue 的一個構造器 Sub,相當於 new Sub(options)

這裡有幾個關鍵引數要注意幾個點,_isComponent 為 true 表示它是一個元件,parent 表示當前啟用的元件例項。

3.4 Sub定義

sub定義在 src/core/global-api/exten.js 

const Sub = function VueComponent (options) {
    this._init(options)cd
   }

所以子元件的例項化實際上就是在這個時機執行的,並且它會執行例項的 _init 方法

3.5 普通VNode 節點和元件的 VNode 初始化的不同。

他們都是在src/core/instance/init.js中初始化的,只是有一些不同,我們主要來看不同的

程式碼在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}

3.5.1. 合併 options 的過程有變化

合併 options 的過程有變化,_isComponent 為 true,所以走到了 initInternalComponent 過程,,而上一章普通的 VNode 節點則會走else。這個函式的實現也在當前頁面 src/core/instance/init.js :

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

這個過程我們重點記住以下幾個點即可:opts.parent = options.parentopts._parentVnode = parentVnode,它們是把之前我們通過 createComponentInstanceForVnode 函式傳入的幾個引數合併到內部的選項 $options 裡了。

_init 函式最後執行的程式碼

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

$mount 相當於執行 child.$mount(undefined, false),它最終會呼叫 mountComponent 方法,進而執行 vm._render() 方法