1. 程式人生 > 程式設計 >詳解vue 元件的實現原理

詳解vue 元件的實現原理

元件機制的設計,可以讓開發者把一個複雜的應用分割成一個個功能獨立元件,降低開發的難度的同時,也提供了極好的複用性和可維護性。本文我們一起從原始碼的角度,瞭解一下元件的底層實現原理。

元件註冊時做了什麼?

在Vue中使用元件,要做的第一步就是註冊。Vue提供了全域性註冊和區域性註冊兩種方式。

全域性註冊方式如下:

Vue.component('my-component-name',{ /* ... */ })

區域性註冊方式如下:

var ComponentA = { /* ... */ }

new Vue({
 el: '#app',components: {
  'component-a': ComponentA
 }
})

全域性註冊的元件,會在任何Vue例項中使用。區域性註冊的元件,只能在該元件的註冊地,也就是註冊該元件的Vue例項中使用,甚至Vue例項的子元件中也不能使用。

有一定Vue使用經驗的小夥伴都瞭解上面的差異,但是為啥會有這樣的差異呢?我們從元件註冊的程式碼實現上進行解釋。

// Vue.component的核心程式碼
// ASSET_TYPES = ['component','directive','filter']
ASSET_TYPES.forEach(type => {
  Vue[type] = function (id,definition
  ){
   if (!definition) {
    return this.options[type + 's'][id]
   } else {
    // 元件註冊
    if (type === 'component' && isPlainObject(definition)) {
     definition.name = definition.name || id
     // 如果definition是一個物件,需要呼叫Vue.extend()轉換成函式。Vue.extend會建立一個Vue的子類(元件類),並返回子類的建構函式。
     definition = this.options._base.extend(definition)
    }
    
    // ...省略其他程式碼
    // 這裡很關鍵,將元件新增到建構函式的選項物件中Vue.options上。
    this.options[type + 's'][id] = definition
    return definition
   }
  }
 })
// Vue的建構函式
function Vue(options){
 if (process.env.NODE_ENV !== 'production' &&
  !(this instanceof Vue)
 ) {
  warn('Vue is a constructor and should be called with the `new` keyword')
 }
 this._init(options)
  
}

// Vue的初始化中進行選項物件的合併
Vue.prototype._init = function (options) {
  const vm = this
  vm._uid = uid++
  vm._isVue = true
  // ...省略其他程式碼
  if (options && options._isComponent) {
   initInternalComponent(vm,options)
  } else {
   // 合併vue選項物件,合併建構函式的選項物件和例項中的選項物件
   vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),options || {},vm
   )
  }
  // ...省略其他程式碼
 }

以上摘取了元件註冊的主要程式碼。可以看到Vue例項的選項物件由Vue的建構函式選項物件和Vue例項的選項物件兩部分組成。

全域性註冊的元件,實際上通過Vue.component新增到了Vue建構函式的選項物件 Vue.options.components 上了。

Vue 在例項化時(new Vue(options))所指定的選項物件會與建構函式的選項物件合併作為Vue例項最終的選項物件。因此,全域性註冊的元件在所有的Vue例項中都可以使用,而在Vue例項中區域性註冊的元件只會影響Vue例項本身。

為啥在HTML模板中可以正常使用元件標籤?

我們知道元件可以跟普通的HTML一樣在模板中直接使用。例如:

<div id="app">
 <!--使用元件button-counter-->
 <button-counter></button-counter>
</div>
// 全域性註冊一個名為 button-counter 的元件
Vue.component('button-counter',{
 data: function () {
  return {
   count: 0
  }
 },template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

// 建立Vue例項
new Vue({
  el: '#app'
})

那麼,當Vue解析到自定義的元件標籤時是如何處理的呢?

Vue 對元件標籤的解析與普通HTML標籤的解析一樣,不會因為是非 HTML標準的標籤而特殊處理。處理過程中第一個不同的地方出現在vnode節點建立時。vue 內部通過_createElement函式實現vnode的建立。

export function _createElement (
 context: Component,tag?: string | Class<Component> | Function | Object,data?: VNodeData,children?: any,normalizationType?: number
): VNode | Array<VNode> {

 //...省略其他程式碼
 
 let vnode,ns
 if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  // 如果是普通的HTML標籤
  if (config.isReservedTag(tag)) {
   vnode = new VNode(
    config.parsePlatformTagName(tag),data,children,undefined,context
   )
  } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options,'components',tag))) {
   // 如果是元件標籤,e.g. my-custom-tag
   vnode = createComponent(Ctor,context,tag)
  } else {
   vnode = new VNode(
    tag,context
   )
  }
 } else {
  // direct component options / constructor
  vnode = createComponent(tag,children)
 }
 
 if (Array.isArray(vnode)) {
  return vnode
 } else if (isDef(vnode)) {
  if (isDef(ns)) applyNS(vnode,ns)
  if (isDef(data)) registerDeepBindings(data)
  return vnode
 } else {
  return createEmptyVNode()
 }
}

以文中的button-counter元件為例,由於button-counter標籤不是合法的HTML標籤,不能直接new VNode()建立vnode。Vue 會通過resolveAsset函式檢查該標籤是否為自定義元件的標籤。

export function resolveAsset (
 options: Object,type: string,id: string,warnMissing?: boolean
): any {
 /* istanbul ignore if */
 if (typeof id !== 'string') {
  return
 }
 const assets = options[type]

 // 首先檢查vue例項本身有無該元件
 if (hasOwn(assets,id)) return assets[id]
 const camelizedId = camelize(id)
 if (hasOwn(assets,camelizedId)) return assets[camelizedId]
 const PascalCaseId = capitalize(camelizedId)
 if (hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]

 // 如果例項上沒有找到,去查詢原型鏈
 const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
 if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
  warn(
   'Failed to resolve ' + type.slice(0,-1) + ': ' + id,options
  )
 }
 return res
}

button-counter是我們全域性註冊的元件,顯然可以在this.$options.components找到其定義。因此,Vue會執行createComponent函式來生成元件的vnode。

// createComponent
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
 }
 
 // 獲取Vue的建構函式
 const baseCtor = context.$options._base

 // 如果Ctor是一個選項物件,需要使用Vue.extend使用選項物件,建立將元件選項物件轉換成一個Vue的子類
 if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
 }

 // 如果Ctor還不是一個建構函式或者非同步元件工廠函式,不再往下執行。
 if (typeof Ctor !== 'function') {
  if (process.env.NODE_ENV !== 'production') {
   warn(`Invalid Component definition: ${String(Ctor)}`,context)
  }
  return
 }

 // 非同步元件
 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,tag
   )
  }
 }

 data = data || {}

 // 重新解析建構函式的選項物件,在元件建構函式建立後,Vue可能會使用全域性混入造成建構函式選項物件改變。
 resolveConstructorOptions(Ctor)

 // 處理元件的v-model
 if (isDef(data.model)) {
  transformModel(Ctor.options,data)
 }

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

 // 函式式元件
 if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor,propsData,children)
 }

 const listeners = data.on
 data.on = data.nativeOn

 if (isTrue(Ctor.options.abstract)) {
  const slot = data.slot
  data = {}
  if (slot) {
   data.slot = slot
  }
 }

 // 安裝元件hooks
 installComponentHooks(data)

 // 建立 vnode
 const name = Ctor.options.name || tag
 const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,{ Ctor,listeners,tag,children },asyncFactory
 )

 return vnode
}
 

由於Vue允許通過一個選項物件定義元件,Vue需要使用Vue.extend將元件的選項物件轉換成一個建構函式。

/**
  * Vue類繼承,以Vue的原型為原型建立Vue元件子類。繼承實現方式是採用Object.create(),在內部實現中,加入了快取的機制,避免重複建立子類。
  */
 Vue.extend = function (extendOptions: Object): Function {
  // extendOptions 是元件的選項物件,與vue所接收的一樣
  extendOptions = extendOptions || {}
  // Super變數儲存對父類Vue的引用
  const Super = this
  // SuperId 儲存父類的cid
  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)
  }

  // 元件的原型物件指向Vue的選項物件
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  // 為元件分配一個cid
  Sub.cid = cid++

  // 將元件的選項物件與Vue的選項合併
  Sub.options = mergeOptions(
   Super.options,extendOptions
  )
  // 通過super屬性指向父類
  Sub['super'] = Super
  
  // 將元件例項的props和computed屬代理到元件原型物件上,避免每個例項建立的時候重複呼叫Object.defineProperty。
  if (Sub.options.props) {
   initProps(Sub)
  }

  if (Sub.options.computed) {
   initComputed(Sub)
  }

  // 複製父類Vue上的extend/mixin/use等全域性方法
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // 複製父類Vue上的component、directive、filter等資源註冊方法
  ASSET_TYPES.forEach(function (type) {
   Sub[type] = Super[type]
  })

  // enable recursive self-lookup
  if (name) {
   Sub.options.components[name] = Sub
  }

  // 儲存父類Vue的選項物件
  Sub.superOptions = Super.options
  // 儲存元件的選項物件
  Sub.extendOptions = extendOptions
  // 儲存最終的選項物件
  Sub.sealedOptions = extend({},Sub.options)

  // 快取元件的建構函式
  cachedCtors[SuperId] = Sub
  return Sub
 }
}

還有一處重要的程式碼是installComponentHooks(data)。該方法會給元件vnode的data新增元件鉤子,這些鉤子在元件的不同階段被呼叫,例如init鉤子在元件patch時會呼叫。

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]
  // 內建的元件vnode鉤子
  const toMerge = componentVNodeHooks[key]
  // 合併鉤子
  if (existing !== toMerge && !(existing && existing._merged)) {
   hooks[key] = existing ? mergeHook(toMerge,existing) : toMerge
  }
 }
}

// 元件vnode的鉤子。
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
   )
   // 掛載元件,與vue的$mount一樣
   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
   // 觸發元件的mounted鉤子
   callHook(componentInstance,'mounted')
  }
  if (vnode.data.keepAlive) {
   if (context._isMounted) {
    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)

最後,與普通HTML標籤一樣,為元件生成vnode節點:

// 建立 vnode
 const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,asyncFactory
 )

元件在patch時對vnode的處理與普通標籤有所不同。

Vue 如果發現正在patch的vnode是元件,那麼呼叫createComponent方法。

 function createComponent (vnode,insertedVnodeQueue,parentElm,refElm) {
  let i = vnode.data
  if (isDef(i)) {
   const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
   // 執行元件鉤子中的init鉤子,建立元件例項
   if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode,false /* hydrating */)
   }
   
   // init鉤子執行後,如果vnode是個子元件,該元件應該建立一個vue子例項,並掛載到DOM元素上。子元件的vnode.elm也設定完成。然後我們只需要返回該DOM元素。
   if (isDef(vnode.componentInstance)) {
    // 設定vnode.elm
    initComponent(vnode,insertedVnodeQueue)
    // 將元件的elm插入到父元件的dom節點上
    insert(parentElm,vnode.elm,refElm)
    if (isTrue(isReactivated)) {
     reactivateComponent(vnode,refElm)
    }
    return true
   }
  }
 }

createComponent會呼叫元件vnode的data物件上定義的init鉤子方法,建立元件例項。現在我們回過頭來看下init鉤子的程式碼:

// ... 省略其他程式碼
 init (vnode: VNodeWithData,hydrating)
  }
 }
 // ...省略其他程式碼

由於元件是初次建立,因此init鉤子會呼叫createComponentInstanceForVnode建立一個元件例項,並賦值給vnode.componentInstance。

export function createComponentInstanceForVnode (
 vnode: any,parent: any,): Component {
 // 內部元件選項
 const options: InternalComponentOptions = {
  // 標記是否是元件
  _isComponent: true,// 父Vnode
  _parentVnode: vnode,// 父Vue例項
  parent
 }
 // check inline-template render functions
 const inlineTemplate = vnode.data.inlineTemplate
 if (isDef(inlineTemplate)) {
  options.render = inlineTemplate.render
  options.staticRenderFns = inlineTemplate.staticRenderFns
 }
 // new 一個元件例項。元件例項化 與 new Vue() 執行的過程相同。
 return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode 中會執行 new vnode.componentOptions.Ctor(options)。由前面我們在建立元件vnode時可知,vnode.componentOptions的值是一個物件:{ Ctor,children },其中包含了元件的建構函式Ctor。因此 new vnode.componentOptions.Ctor(options)等價於new VueComponent(options)。

// 生成元件例項
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance)
// 掛載元件,與vue的$mount一樣
child.$mount(hydrating ? vnode.elm : undefined,hydrating)

等價於:

new VueComponent(options).$mount(hydrating ? vnode.elm : undefined,hydrating)

這段程式碼想必大家都很熟悉了,是元件初始化和掛載的過程。元件的初始化和掛載與在前文中所介紹Vue初始化和掛載過程相同,因此不再展開說明。大致的過程就是建立了一個元件例項並掛載後。使用initComponent將元件例項的$el設定為vnode.elm的值。最後,呼叫insert將元件例項的DOM根節點插入其父節點。然後就完成了元件的處理。

總結

通過對元件底層實現的分析,我們可以知道,每個元件都是一個VueComponent例項,而VueComponent又是繼承自Vue。每個元件例項獨立維護自己的狀態、模板的解析、DOM的建立和更新。篇幅有限,文中只分析了基本的元件的註冊解析過程,未對非同步元件、keep-alive等做分析。等後面再慢慢補上。

以上就是詳解vue 元件的實現原理的詳細內容,更多關於vue元件的資料請關注我們其它相關文章!