組件化之創(chuàng)建組件Vnode

Vue.js 另一個核心思想是組件化广鳍。所謂組件化逆趣,就是把頁面拆分成多個組件 (component)蝶溶,每個組件依賴的 CSS、JavaScript宣渗、模板抖所、圖片等資源放在一起開發(fā)和維護(hù)。組件是資源獨(dú)立的痕囱,組件在系統(tǒng)內(nèi)部可復(fù)用田轧,組件和組件之間可以嵌套

我們將從源碼的角度來分析 Vue 的組件內(nèi)部是如何工作的,只有了解了內(nèi)部的工作原理鞍恢,才能讓我們使用它的時候更加得心應(yīng)手傻粘。
繼續(xù)使用vue-cli初始化的代碼來研究和調(diào)試

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  // 這里的 h 是 createElement 方法
  render: h => h(App)
})

這也是通過 render 函數(shù)去渲染的,通過 createElement 傳的參數(shù)是一個組件而不是一個原生的標(biāo)簽帮掉,那么接下來我們就開始分析這一過程弦悉。
new Vue的時候也像之間分析的一樣通過init函數(shù)的 通過轉(zhuǎn)化成Vnode 然后去轉(zhuǎn)化成patch渲染
createElement 的實(shí)現(xiàn)的時候,它最終會調(diào)用 _createElement 方法蟆炊,其中有一段邏輯是對參數(shù) tag 的判斷稽莉,如果是一個普通的 html 標(biāo)簽,像上一章的例子那樣是一個普通的 div涩搓,則會實(shí)例化一個普通 VNode 節(jié)點(diǎn)污秆,否則通過 createComponent 方法創(chuàng)建一個組件 VNode。
src/core.vdoo/create-element.js

let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }

在我們這一章傳入的是一個 App 對象昧甘,它本質(zhì)上是一個 Component 類型良拼,那么它會走到上述代碼的 else 邏輯,直接通過 createComponent 方法來創(chuàng)建 vnode疾层。所以接下來我們來看一下 createComponent 方法的實(shí)現(xiàn)将饺,它定義在 src/core/vdom/create-component.js 文件中:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

這個主要就是建立一個繼承于VUE的一個子類贡避,安裝一些鉤子函數(shù)和新建一個組件化的Vnode數(shù)據(jù)
構(gòu)造子類構(gòu)造函數(shù)

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

我們在編寫一個組件的時候痛黎,通常都是創(chuàng)建一個普通對象予弧,還是以我們的 App.vue 為例,代碼如下:

import HelloWorld from './components/HelloWorld'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}

這里 export 的是一個對象湖饱,所以 createComponent 里的代碼邏輯會執(zhí)行到 baseCtor.extend(Ctor)掖蛤,在這里 baseCtor 實(shí)際上就是 Vue,這個的定義是在最開始初始化 Vue 的階段井厌,在 src/core/global-api/index.js 中的 initGlobalAPI 函數(shù)有這么一段邏輯:

Vue.options._base = Vue

這里定義的是 Vue.option蚓庭,而我們的 createComponent 取的是 context.$options,實(shí)際上在 src/core/instance/init.js 里 Vue 原型上的 _init 函數(shù)中有這么一段邏輯:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

這樣就把 Vue 上的一些 option 擴(kuò)展到了 vm.$option 上仅仆,所以我們也就能通過 vm.$options._base 拿到 Vue 這個構(gòu)造函數(shù)了器赞。mergeOptions 的實(shí)現(xiàn)我們會在后續(xù)章節(jié)中具體分析,現(xiàn)在只需要理解它的功能是把 Vue 構(gòu)造函數(shù)的 options 和用戶傳入的 options 做一層合并墓拜,到 vm.$options 上港柜。
在了解了 baseCtor 指向了 Vue 之后,我們來看一下 Vue.extend 函數(shù)的定義咳榜,在src/core/global-api/extend.js中夏醉。

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

Vue.extend 的作用就是構(gòu)造一個 Vue 的子類,它使用一種非常經(jīng)典的原型繼承的方式把一個純對象轉(zhuǎn)換一個繼承于 Vue 的構(gòu)造器 Sub 并返回涌韩,然后對 Sub 這個對象本身擴(kuò)展了一些屬性畔柔,如擴(kuò)展 options、添加全局 API 等臣樱;并且對配置中的 props 和 computed 做了初始化工作靶擦;最后對于這個 Sub 構(gòu)造函數(shù)做了緩存,避免多次執(zhí)行 Vue.extend 的時候?qū)ν粋€子組件重復(fù)構(gòu)造雇毫。
這樣當(dāng)我們?nèi)?shí)例化 Sub 的時候奢啥,就會執(zhí)行 this._init 邏輯再次走到了 Vue 實(shí)例的初始化邏輯,實(shí)例化子組件的邏輯在之后的章節(jié)會介紹。

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

安裝組件鉤子函數(shù)

installComponentHooks(data)

我們之前提到 Vue.js 使用的 Virtual DOM 參考的是開源庫 snabbdom嘴拢,它的一個特點(diǎn)是在 VNode 的 patch 流程中對外暴露了各種時機(jī)的鉤子函數(shù)桩盲,方便我們做一些額外的事情,Vue.js 也是充分利用這一點(diǎn)席吴,在初始化一個 Component 類型的 VNode 的過程中實(shí)現(xiàn)了幾個鉤子函數(shù):

const componentVNodeHooks = {
  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)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

整個 installComponentHooks 的過程就是把 componentVNodeHooks 的鉤子函數(shù)合并到 data.hook 中赌结,在 VNode 執(zhí)行 patch 的過程中執(zhí)行相關(guān)的鉤子函數(shù),具體的執(zhí)行我們稍后在介紹 patch 過程中會詳細(xì)介紹孝冒。這里要注意的是合并策略柬姚,在合并過程中,如果某個時機(jī)的鉤子已經(jīng)存在 data.hook 中庄涡,那么通過執(zhí)行 mergeHook 函數(shù)做合并量承,這個邏輯很簡單,就是在最終執(zhí)行的時候,依次執(zhí)行這兩個鉤子函數(shù)即可撕捍。

實(shí)例化 VNode

const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode

通過 new VNode 實(shí)例化一個 vnode 并返回拿穴。需要注意的是和普通元素節(jié)點(diǎn)的 vnode 不同,組件的 vnode 是沒有 children 的忧风,會增加一個包含標(biāo)簽和子節(jié)點(diǎn)等的對象傳入 這點(diǎn)很關(guān)鍵默色,在之后的 patch 過程中我們會再提。

總結(jié)狮腿、
這一節(jié)我們分析了 createComponent 的實(shí)現(xiàn)腿宰,了解到它在渲染一個組件的時候的 3 個關(guān)鍵邏輯:構(gòu)造子類構(gòu)造函數(shù),安裝組件鉤子函數(shù)和實(shí)例化 vnode缘厢。createComponent 后返回的是組件 vnode吃度,它也一樣走到 vm._update 方法,進(jìn)而執(zhí)行了 patch 函數(shù)贴硫,我們在上一章對 patch 函數(shù)做了簡單的分析规肴,那么下一節(jié)我們會對它做進(jìn)一步的分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夜畴,一起剝皮案震驚了整個濱河市拖刃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贪绘,老刑警劉巖兑牡,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異税灌,居然都是意外死亡均函,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門菱涤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苞也,“玉大人,你說我怎么就攤上這事粘秆∪绯伲” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵攻走,是天一觀的道長殷勘。 經(jīng)常有香客問我,道長昔搂,這世上最難降的妖魔是什么玲销? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮摘符,結(jié)果婚禮上贤斜,老公的妹妹穿的比我還像新娘策吠。我一直安慰自己,他們只是感情好瘩绒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布猴抹。 她就那樣靜靜地躺著,像睡著了一般草讶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炉菲,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天堕战,我揣著相機(jī)與錄音,去河邊找鬼拍霜。 笑死嘱丢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祠饺。 我是一名探鬼主播越驻,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼道偷!你這毒婦竟也來了缀旁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤勺鸦,失蹤者是張志新(化名)和其女友劉穎并巍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體换途,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡懊渡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了军拟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剃执。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖懈息,靈堂內(nèi)的尸體忽然破棺而出肾档,到底是詐尸還是另有隱情,我是刑警寧澤辫继,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布阁最,位于F島的核電站,受9級特大地震影響骇两,放射性物質(zhì)發(fā)生泄漏速种。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一低千、第九天 我趴在偏房一處隱蔽的房頂上張望配阵。 院中可真熱鬧馏颂,春花似錦、人聲如沸棋傍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘫拣。三九已至亿絮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間麸拄,已是汗流浹背派昧。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拢切,地道東北人蒂萎。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像淮椰,于是被迫代替她去往敵國和親五慈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

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