Vue源碼分析—組件化(二)

patch

當(dāng)我們通過createComponent創(chuàng)建了組件VNode浑槽,接下來會走到vm._update窍箍,執(zhí)行vm.__patch__去把VNode轉(zhuǎn)換成真正的DOM節(jié)點土全。但是針對一個普通的VNode節(jié)點报嵌,接下來我們來看看組件的VNode會有哪些不一樣的地方旷祸。
patch的過程會調(diào)用createElm創(chuàng)建元素節(jié)點糜颠,回顧一下createElm的實現(xiàn)汹族,它的定義在src/core/vdom/patch.js中:

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

createComponent

我們刪掉多余的代碼,只保留關(guān)鍵的邏輯其兴,這里會判斷createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值顶瞒,如果為true則直接結(jié)束,那么接下來看一下createComponent方法的實現(xiàn):

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
    }
  }
}

createComponent 函數(shù)中元旬,首先對 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鉤子函數(shù),我們在創(chuàng)建組件VNode的時候合并鉤子函數(shù)中就包含init鉤子函數(shù)坑资,定義在src/core/vdom/create-component.js中:

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鉤子函數(shù)執(zhí)行也很簡單耗帕,我們先不考慮keepAlive的情況,它是通過createComponentInstanceForVnode創(chuàng)建一個Vue的實例袱贮,然后調(diào)用$mount方法掛載子組件仿便, 先來看一下createComponentInstanceForVnode的實現(xiàn):

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode函數(shù)構(gòu)造的一個內(nèi)部組件的參數(shù),然后執(zhí)行new vnode.componentOptions.Ctor(options)字柠。這里的vnode.componentOptions.Ctor對應(yīng)的就是子組件的構(gòu)造函數(shù)探越,它實際上是繼承于Vue的一個構(gòu)造器Sub,相當(dāng)于new Sub(options)這里有幾個關(guān)鍵參數(shù)要注意幾個點窑业,_isComponenttrue表示它是一個組件钦幔,parent表示當(dāng)前激活的組件實例。

所以子組件的實例化實際上就是在這個時機執(zhí)行的常柄,并且它會執(zhí)行實例的_init方法鲤氢,這個過程有一些和之前不同的地方需要挑出來說,代碼在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)
  } 
}

這里首先是合并options的過程有變化西潘,_isComponenttrue卷玉,所以走到了initInternalComponent過程,這個函數(shù)的實現(xiàn)也簡單看一下:

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.parent喷市、opts._parentVnode = parentVnode相种,它們是把之前我們通過createComponentInstanceForVnode函數(shù)傳入的幾個參數(shù)合并到內(nèi)部的選項$options里了。
再來看一下_init函數(shù)最后執(zhí)行的代碼:

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

由于組件初始化的時候是不傳el的品姓,因此組件是自己接管了$mount的過程寝并,回到組件init的過程,componentVNodeHooksinit鉤子函數(shù)腹备,在完成實例化的_init后衬潦,接著會執(zhí)行child.$mount(hydrating ? vnode.elm : undefined, hydrating) 。這里hydratingtrue一般是服務(wù)端渲染的情況植酥,我們只考慮客戶端渲染镀岛,所以這里$mount相當(dāng)于執(zhí)行child.$mount(undefined, false),它最終會調(diào)用mountComponent方法友驮,進而執(zhí)行vm._render()方法:

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

我們只保留關(guān)鍵部分的代碼漂羊,這里的_parentVnode就是當(dāng)前組件的父VNode,而render函數(shù)生成的vnode當(dāng)前組件的渲染vnode喊儡,vnodeparent指向了_parentVnode拨与,也就是vm.$vnode,它們是一種父子的關(guān)系艾猜。

我們知道在執(zhí)行完vm._render生成VNode后买喧,接下來就要執(zhí)行vm._update去渲染VNode了。來看一下組件渲染的過程中有哪些需要注意的匆赃,vm._update的定義在src/core/instance/lifecycle.js中:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

_update過程中有幾個關(guān)鍵的代碼淤毛,首先vm._vnode = vnode的邏輯,這個vnode是通過vm._render()返回的組件渲染VNode算柳,vm._vnodevm.$vnode的關(guān)系就是一種父子關(guān)系低淡,用代碼表達(dá)就是vm._vnode.parent === vm.$vnode。還有一段比較有意思的代碼:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...
    const prevActiveInstance = activeInstance
    activeInstance = vm
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // ...
}

這個activeInstance作用就是保持當(dāng)前上下文的Vue實例瞬项,它是在lifecycle模塊的全局變量蔗蹋,定義是export let activeInstance: any = null,并且在之前我們調(diào)用createComponentInstanceForVnode方法的時候從lifecycle模塊獲取囱淋,并且作為參數(shù)傳入的猪杭。因為實際上JavaScript是一個單線程,Vue整個初始化是一個深度遍歷的過程妥衣,在實例化子組件的過程中皂吮,它需要知道當(dāng)前上下文的Vue實例是什么,并把它作為子組件的父Vue實例税手。之前我們提到過對子組件的實例化過程先會調(diào)用initInternalComponent(vm, options) 合并options蜂筹,把parent存儲在vm.$options中,在$mount之前會調(diào)用initLifecycle(vm)方法:

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

可以看到vm.$parent就是用來保留當(dāng)前vm的父實例芦倒,并且通過parent.$children.push(vm)來把當(dāng)前的vm存儲到父實例的$children中艺挪。

vm._update的過程中,把當(dāng)前的vm賦值給activeInstance兵扬,同時通過const prevActiveInstance = activeInstanceprevActiveInstance保留上一次的activeInstance麻裳。實際上,prevActiveInstance和當(dāng)前的vm是一個父子關(guān)系周霉,當(dāng)一個vm實例完成它的所有子樹的patch或者update過程后掂器,activeInstance會回到它的父實例,這樣就完美地保證了createComponentInstanceForVnode整個深度遍歷過程中俱箱,我們在實例化子組件的時候能傳入當(dāng)前子組件的父Vue實例国瓮,并在_init的過程中,通過vm.$parent把這個父子關(guān)系保留狞谱。

那么回到_update乃摹,最后就是調(diào)用__patch__ 渲染VNode了。

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // ...
  }
  // ...
}

之前分析過負(fù)責(zé)渲染成DOM的函數(shù)是createElm跟衅,注意這里我們只傳了2個參數(shù)孵睬,所以對應(yīng)的parentElmundefined。我們再來看看它的定義:

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

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // ...

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    // ...
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

注意伶跷,這里我們傳入的vnode是組件渲染的vnode掰读,也就是我們之前說的vm._vnode秘狞,如果組件的根節(jié)點是個普通元素,那么vm._vnode也是普通的vnode蹈集,這里createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值是false烁试。接下來的過程就是,先創(chuàng)建一個父節(jié)點占位符拢肆,然后再遍歷所有子VNode遞歸調(diào)用createElm减响,在遍歷的過程中,如果遇到子VNode是一個組件的VNode郭怪,則重復(fù)本節(jié)開始的過程支示,這樣通過一個遞歸的方式就可以完整地構(gòu)建了整個組件樹。

由于我們這個時候傳入的parentElm是空鄙才,所以對組件的插入颂鸿,在createComponent有這么一段邏輯:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

在完成組件的整個patch過程后,最后執(zhí)行insert(parentElm, vnode.elm, refElm)完成組件的DOM插入咒循,如果組件patch過程中又創(chuàng)建了子組件据途,那么DOM的插入順序是先子后父。

總結(jié)

那么到此叙甸,一個組件的VNode是如何創(chuàng)建颖医、初始化、渲染的過程也就介紹完畢了裆蒸。我們知道編寫一個組件實際上是編寫一個JavaScript對象熔萧,對象的描述就是各種配置,之前我們提到在_init的最初階段執(zhí)行的就是merge options的邏輯僚祷,那么我們從源碼角度來分析合并配置的過程佛致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辙谜,隨后出現(xiàn)的幾起案子俺榆,更是在濱河造成了極大的恐慌,老刑警劉巖装哆,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罐脊,死亡現(xiàn)場離奇詭異,居然都是意外死亡蜕琴,警方通過查閱死者的電腦和手機萍桌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凌简,“玉大人上炎,你說我怎么就攤上這事〕В” “怎么了藕施?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵寇损,是天一觀的道長。 經(jīng)常有香客問我铅碍,道長润绵,這世上最難降的妖魔是什么线椰? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任胞谈,我火速辦了婚禮,結(jié)果婚禮上憨愉,老公的妹妹穿的比我還像新娘烦绳。我一直安慰自己,他們只是感情好配紫,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布径密。 她就那樣靜靜地躺著,像睡著了一般躺孝。 火紅的嫁衣襯著肌膚如雪享扔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天植袍,我揣著相機與錄音惧眠,去河邊找鬼。 笑死于个,一個胖子當(dāng)著我的面吹牛氛魁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播厅篓,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼秀存,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了羽氮?” 一聲冷哼從身側(cè)響起或链,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎档押,沒想到半個月后澳盐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡汇荐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年洞就,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掀淘。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡旬蟋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出革娄,到底是詐尸還是另有隱情倾贰,我是刑警寧澤冕碟,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站匆浙,受9級特大地震影響安寺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜首尼,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一挑庶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧软能,春花似錦迎捺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跋核,卻和暖如春岖瑰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背砂代。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工蹋订, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泊藕。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓辅辩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親娃圆。 傳聞我的和親對象是個殘疾皇子玫锋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容