vue@2.0原始碼學習---元件究竟是什麼

菜菜_張發表於2017-12-25

本篇文章從最簡單的情況入手,不考慮prop和元件間通訊。

Vue.component

vue文件告訴我們可以使用Vue.component(tagName, options)註冊一個元件

Vue.component('my-component', {
  // 選項
})
複製程式碼

毫無疑問這是一個全域性API,我們順著程式碼最終可以找到Vue.component是這樣的

Vue.component = function(id, definition) {
	definition.name = definition.name || id
	definition = Vue.extend(definition)
    this.options[type + 's'][id] = definition
    return definition	
}
複製程式碼

Vue.component實際上是Vue.extend的封裝,Vue.extend如下:

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const isFirstExtend = Super.cid === 0
  if (isFirstExtend && extendOptions._Ctor) {
    return extendOptions._Ctor
  }
  let name = extendOptions.name || Super.options.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
  // allow further extension
  Sub.extend = Super.extend
  // create asset registers, so extended classes
  // can have their private assets too.
  config._assetTypes.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
  // cache constructor
  if (isFirstExtend) {
    extendOptions._Ctor = Sub
  }
  return Sub
}
複製程式碼

可以看到Vue.extend返回的實際上是一個建構函式Sub,並且此建構函式繼承自Vue。裡面有這麼幾行程式碼

  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
複製程式碼

那麼Super.options(即Vue.options)是什麼呢?

  Vue.options = Object.create(null)
  //  包含components directives filters
  config._assetTypes.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  util.extend(Vue.options.components, builtInComponents)
複製程式碼

Vue.options事實上存放了系統以及使用者定義的component、directive、filter,builtInComponents為Vue內建的元件(如keep-alive),列印看下:

image

所以Sub建構函式的options不僅包含components、directives、filters,還包含預先定義的例項化時所需的選項。定義一個元件如下:

let MyComponent = Vue.extend({
    data() {
        return {
            msg: "this's compoennt"
        }
    },
    render(h) {
        return h('p', this.msg)
    }
})
複製程式碼

列印MyComponent.options如下:

image

再回過頭看Vue.component,可以發現他做的工作就是擴充套件一個Vue建構函式(VueComponent),並將這個建構函式(VueComponent)新增到Vue.options.components

現在我們已經可以回答最開始的問題---vue的元件是什麼?vue的元件其實就是擴充套件的Vue建構函式,並且在適當的時候例項化為Vue例項。

元件對應的vnode

元件對應的vnode是什麼樣子?從一個簡單的例子入手:

let MyComponent = Vue.component('my-component', {
    data() {
        return {
            msg: "this's component"
        }
    },
    render(h) {
        return h('p', this.msg)
    }
})

window.app = new Vue({
    render(h) {
        return h('my-component')
    }
}).$mount('#root')
複製程式碼

上篇文章已經說道在initRender的時候會初始一個系統watcher,如下:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)
複製程式碼

上篇文章提到vm._render()返回的是一個虛擬dom(vnode),具體到本篇,那麼元件標籤會被解析成什麼樣的虛擬節點呢?

事實上render的時候會首先呼叫createElement,根據傳入的tag(html標籤或者元件標籤)不同,vnode可以分為以下兩種:

  • platform built-in elements

    這種就是普通的html標籤(p、div、span等)對應的vnode

  • component

    當tag是元件標籤的時候,會呼叫createComponent,如下:

    else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      
      return createComponent(Ctor, data, context, children, tag)
	}
複製程式碼

這裡的Ctor就是我們擴充套件的元件建構函式,createComponent最終返回的vnode如下:

  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }
  )
複製程式碼

image

需要注意的是data有一個操作:

  // merge component management hooks onto the placeholder node
  mergeHooks(data)
複製程式碼

merge之後data.hook會新增四個方法:

  • init 例項化元件時呼叫
  • prepatch patch之前呼叫
  • insert 真實節點插入時呼叫
  • destory 元件例項銷燬時呼叫

例項化元件

前文看到元件建構函式實際上是存在元件對應vnode的componentOptions中,那麼究竟是什麼時候例項化元件呢?

順著vm._update(vm._render(), hydrating)往下看發現最終呼叫的是patch操作,而對於元件例項化而言並不存在與之對應的oldVnode(因為oldVnode是在元件更新後產生的),所以最終的邏輯歸到根據元件對應的vnode建立真實dom節點,即

createElm(vnode, insertedVnodeQueue)
複製程式碼

我們還記得元件的建構函式是vnode.componentOptions.Ctor,其實最終呼叫的也是這個建構函式。

createElm函式中與元件初始化相關的關鍵程式碼如下:

    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode)

      if (isDef(i = vnode.child)) {
        initComponent(vnode, insertedVnodeQueue)
        return vnode.elm
      }
    }
複製程式碼

init的程式碼如下:

function init (vnode: VNodeWithData, hydrating: boolean) {
  if (!vnode.child || vnode.child._isDestroyed) {
    const child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance)
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}


export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any // activeInstance in lifecycle state
): Component {
  const vnodeComponentOptions = vnode.componentOptions
  const options: InternalComponentOptions = {
    _isComponent: true,
    parent,
    propsData: vnodeComponentOptions.propsData,
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    _parentListeners: vnodeComponentOptions.listeners,
    _renderChildren: vnodeComponentOptions.children
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (inlineTemplate) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }

  return new vnodeComponentOptions.Ctor(options)
}
複製程式碼

經過init之後可以看到元件vnode.child對應的就是元件的例項,且child.$el即為元件對應的真實dom,但是實際上createElm返回的是vnode.elm,怎麼回事?事實上initComponent 中做了處理

vnode.elm = vnode.child.$el
複製程式碼

綜上,元件例項化是在由虛擬dom對映為真實dom時完成的。


寫到這裡已經對元件機制有了初步的認識,資料的傳遞、父子元件通訊本文並沒有涉及,留到以後再看。

相關文章