細談 vue - component 篇

本篇文章是細談 vue 系列的第六篇。看過我這個系列文章的小夥伴都知道:文章賊長,看不下去的建議先點個贊當收藏,而後等有時間靜下心來慢慢看,前端交流羣:731175396。之前的文章傳送門以下javascript

用過 vue 的小夥伴確定知道,在 vue 的開發中,component 可謂是隨處可見,項目中的那一個個 .vue (SFC) 文件,可不就是一個個的組件麼。html

那麼,既然 component 這麼核心,這麼重要,爲什麼很差好來研究一波呢?前端

why not ?vue

​ — 魯迅java

1、組件建立

以前咱們分析 vdom 的時候分析過一個函數 createElement,與它相同的是 createComponent,二者都是用來建立 vnode 節點的,若是是普通的 html 標籤,則直接實例化一個普通的 vnode 節點,不然經過 createComponent 來建立一個 Component 類型的 vnode 節點node

一、createElement

這裏僅列出不一樣狀況下 vnode 節點建立的代碼webpack

if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    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)
}
複製代碼

二、createComponent

接下來,咱們先看 createComponent() 的定義,具體以下git

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 賦值給變量 baseCtor ,並經過 extend 將參數 Ctor 進行擴展
const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
複製代碼

這裏咱們看到 $options._base ,其實就是構造函數 Vuegithub

// src/core/global-api/index.js
Vue.options._base = Vue

// src/core/instance/init.js
// 1. initMixin()
if (options && options._isComponent) {
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
// 2. initInternalComponent()
const opts = vm.$options = Object.create(vm.constructor.options)
複製代碼
  • 其次,緊接着,斷定組件是否爲異步組件、函數式組件或者抽象組件。具體每種狀況的處理後面我再詳細分析
// 異步組件
let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}

// 函數式組件
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// 抽象組件
if (isTrue(Ctor.options.abstract)) {
  const slot = data.slot
  data = {}
  if (slot) {
    data.slot = slot
  }
}
複製代碼
  • 對於組件上的事件也有相關處理,它會提取組件上的事件監聽器。它須要做爲子組件的監聽器,而並不是DOM監聽器。因此須要將其替換爲擁有 .native 修飾符的偵聽器,讓其能在父組件 patch 階段可以獲得處理
const listeners = data.on
data.on = data.nativeOn
複製代碼
  • 而後,安裝組件的鉤子函數。它將 componentVNodeHooks 的鉤子函數合併到 data.hook 中,而後 Component 類型的 vnode 節點在 patch 過程當中會執行相關的鉤子函數,若是某個時機的鉤子函數已經存在,則經過 mergeHook 將函數合併,即依次執行同一時機的這兩個函數
installComponentHooks(data)

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

const hooksToMerge = Object.keys(componentVNodeHooks)

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
複製代碼

三、componentVNodeHooks

上面的 componentVNodeHooks 則是組件初始化的時候實現的幾個鉤子函數,分別有 initprepatchinsertdestroyweb

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      const mountedNode: any = vnode
      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) {
        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 */)
      }
    }
  }
}
複製代碼

接下來咱們來仔細看看 componentVNodeHooks 裏面的四個鉤子函數都作了些什麼

  1. init :當 vnodekeep-alive 組件時、存在實例且沒被銷燬,爲了防止組件流動,直接執行了 prepatch。不然直接經過執行 createComponentInstanceForVnode 建立一個 Component 類型的 vnode 實例,並進行 $mount 操做
  2. prepatch:將已有組件更新成最新的 vnode 上的數據,這裏沒啥好說的
  3. insertinsert 鉤子函數
    • 首先會斷定組件實例是否已經被 mounted,若沒被渲染,則直接將 componentInstance 做爲參數執行 mounted 鉤子函數。
    • 其次,則是組件爲 keep-alive 內置組件的狀況。這裏有個操做有點騷,就是當它已經 mounted 了的時候,進入 insert 階段的時候,爲了防止 keep-alive 子組件更新觸發 activated 鉤子函數,直接就放棄了 walking tree 的更新機制,而是直接將組件實例 componentInstance 丟到 activatedChildren 這個數組中。固然沒有 mounted 的狀況則直接觸發 activated 鉤子函數進行 mounted 便可
  4. destroy:組件銷燬操做,這裏一樣對 keep-alive 組件作了兼容。若是不是 keep-alive 組件,直接執行 $destory 銷燬組件實例,不然觸發 deactivated 鉤子函數進行銷燬。

上面用的一些輔助函數以下

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)
}
複製代碼
  • 最後實例化 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
複製代碼

createComponent 的所有過程就是:首先先構建 Vue 的子類構造函數,而後安裝組件的鉤子函數,最後實例化 VNode,而後返回。裏面的不少操做都對 keep-alive 內置組件作了不少兼容。因此假如你用過 keep-alive 組件,而且恰巧看到這,相信你會有不少感悟。

2、配置合併

一般來講,設計一款插件或者組件,爲了保證其可定製化、可擴展性,通常會在自身定義一些默認配置,而後在內部作好 merge 配置項的操做,讓你能在其初始化階段進行自定義的配置。

固然,Vue 在這塊設計也是如此。vue 中對於 options 合併策略其實我上面也列出過代碼,具體在 src/core/instance/init.js 中(這裏我只保留相關代碼)。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // ...
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // ...
  }
}
複製代碼

能看出來,合併策略有兩個。一種是爲 Component 組件的狀況下,執行 initInternalComponent 進行內部組件配置合併,一種是非組件的狀況,直接經過 mergeOptions 作配置合併。

一、normal merge

這裏直接將 resolveConstructorOptions(vm.constructor) 的返回值和 options 進行合併

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
複製代碼

咱們先來看下 Vue.options 的定義

// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
	// ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // ...
}

// src/shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
複製代碼

接着,咱們再來看看 mergeOptions 的邏輯:它是 vue 核心合併策略之一,它主要功能就是將 parantchild 進行策略合併,而後返回一個新的對象,代碼在 src/core/util/options.js 中。

  1. 首先會先對 child 上面的 propsinjectdirectives 進行 object format 操做(具體邏輯可自行研究,主要就是對其進行 object 轉換操做)
  2. child._base 不存在,遍歷 child.extendschild.mixins ,將其合併到 parent
  3. 遍歷 parent,調用 mergeField 合併到變量 options
  4. 遍歷 child,若 childparent 不存在的屬性,則調用 mergeField 將該屬性合併到 options
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
複製代碼

vue 中除了對 options 的合併外,還有不少合併策略,感興趣的能夠本身去 src/core/util/options.js 中查閱研究

二、component merge

在分析 createComponent 的時候咱們瞭解到組件的構造函數是經過 Vue.extendVue 進行繼承的,代碼以下

// src/core/global-api/index.js
Vue.options._base = Vue
// src/core/vdom/create-component.js
const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
複製代碼

咱們再來 Vue.extend, 它的定義在 src/core/global-api/extend.js 中(僅保留關鍵邏輯),它經過執行 mergeOptions()Super.options,即 Vue.options 合併到 Sub.options

export function initExtend (Vue: GlobalAPI) {
	// ...
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    // ...
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // ...
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // ...
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // ...
    return Sub
  }
}
複製代碼

而後在 componentVNodeHooksinit 鉤子函數中,即子組件的初始化階段,會執行 createComponentInstanceForVnode 進行組件實例的初始化。createComponentInstanceForVnode 函數中的 vnode.componentOptions.Ctor 指向的其實就是上面 Vue.extend 中返回的 Sub,因此執行 new 操做的時候會執行到 this._init(options),即 Vue._init(options) 操做,又由於 options._isComponent 的定義是 true,因此直接進入了 initInternalComponent 操做

// componentVNodeHooks init()
const child = vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
)
// createComponentInstanceForVnode()
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
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}
複製代碼

initInternalComponent 只是作了一些簡單的對象賦值,具體我就不分析了,代碼以下:

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
  }
}
複製代碼

講到這,可能有些小夥伴會有些懵,我舉個例子來講明下

<template>
  <div class="hello">
    {{ msg }}
  </div>
</template>

<script> export default { name: 'HelloWorld', props: { msg: String }, created () { console.log('this is child') } } </script>
複製代碼

而後在父組件進行調用

<template>
  <div class="home">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script> import HelloWorld from '@/components/HelloWorld.vue' export default { name: 'home', components: { HelloWorld }, created () { console.log('this is parent') } } </script>
複製代碼

走完上面的合併策略後,vm.$options 的值大體以下

vm.$options = {
  parent: VueComponent, // 父組件實例
  propsData: {
    msg: 'Welcome to Your Vue.js App'
  },
  _componentTag: 'HelloWorld',
  _parentListeners: undefined,
  _parentVnode: VNode, // 父節點 vnode 實例
  _propKeys: ['msg'],
  _renderChildren: undefined,
  __proto__: {
    components: {
      HelloWorld: function VueComponent(options) {}
    },
    directives: {},
    filters: {},
    _base: function Vue(options) {},
    _Ctor: {},
    created: [
      function created() {
        console.log('this is parent')
      },
      function created() {
        console.log('this is child')
      }
    ]
  }
}
複製代碼

3、異步組件

在上面分析 createComponent 的時候,咱們留下幾種特殊狀況沒有分析,其中一種就是異步組件的狀況。它的場景是,當 Ctor.cid 未定義的狀況下,則直接走異步組件建立的流程,具體代碼以下

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
    )
  }
}
複製代碼

在作具體分析前,咱們先經過官方示例來看下異步組件的用法和普通組件的用法有何不一樣

// 普通組件
Vue.component('my-component-name', {
  // ... options ...
})

// 異步組件
Vue.component('async-webpack-example', function (resolve, reject) {
  // 這個特殊的 require 語法
  // 將指示 webpack 自動將構建後的代碼,
  // 拆分到不一樣的 bundle 中,而後經過 Ajax 請求加載。
  require(['./my-async-component'], resolve)
})
複製代碼

在例子中,Vue 普通組件是一個對象,而異步組件則是一個工廠函數,它接收2個參數,一個 resolve 回調函數用來從服務器獲取到組件定義的對象,另一個 reject 回調函數來代表加載失敗。除了上面的寫法外,異步組件還支持如下兩種寫法

// Promise 異步組件
Vue.component(
  'async-webpack-example',
  // `import` 函數返回一個 Promise.
  () => import('./my-async-component')
)

// 高級異步組件
const AsyncComponent = () => ({
  // 加載組件(最終應該返回一個 Promise)
  component: import('./MyComponent.vue'),
  // 異步組件加載中(loading),展現爲此組件
  loading: LoadingComponent,
  // 加載失敗,展現爲此組件
  error: ErrorComponent,
  // 展現 loading 組件以前的延遲時間。默認:200ms。
  delay: 200,
  // 若是提供 timeout,而且加載用時超過此 timeout,
  // 則展現錯誤組件。默認:Infinity。
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
複製代碼

一、resolveAsyncComponent

resolveAsyncComponent 主要功能就是對上面說起的 3 種異步組件建立方式進行支持,具體代碼以下

export function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if (timerLoading !== null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout)
          timerTimeout = null
        }
      }
    }

    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })

    const res = factory(resolve, reject)

    if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
複製代碼

首先咱們先來看看對於異步組件是如何加載的。這裏咱們先跳過 resolveAsyncComponent 一開始就對咱們前面說起的高級異步組件作的處理。

在分析 resolveAsyncComponent 異步組件建立邏輯前,咱們先過看看其中會用到的一些核心的方法

  • forceRender:對組件強制進行從新渲染,而後在 render 完成的時候清掉工廠函數中當前的渲染實例 owners,順帶把 timerLoadingtimerTimeout 清除掉。

    $forceUpdate:調用 watcherupdate 方法,即組件的從新渲染。對 vue 中通常只有數據變動纔會觸發視圖的從新渲染,而異步組件在加載過程當中數據是不會發生變化的,那麼這個時候是不會觸發組件從新渲染的,因此須要經過執行 $forceUpdate 強制對組件進行從新渲染

const forceRender = (renderCompleted: boolean) => {
  for (let i = 0, l = owners.length; i < l; i++) {
    (owners[i]: any).$forceUpdate()
  }

  if (renderCompleted) {
    owners.length = 0
    if (timerLoading !== null) {
      clearTimeout(timerLoading)
      timerLoading = null
    }
    if (timerTimeout !== null) {
      clearTimeout(timerTimeout)
      timerTimeout = null
    }
  }
}

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}
複製代碼
  • once:利用閉包以及一個標識變量 called 保證其包裝的函數只會執行一次
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
複製代碼
  • resolve:內部 resolve 函數,首先會執行 ensureCtor 並將其返回值做爲 factoryresolved 值。緊接着若 sync 異步變量爲 false ,則直接執行 forceRender 強制讓組件從新渲染,不然則清空 owners

    ensureCtor 則是爲了保證能找到異步組件上定義的組件對象而定義的函數。若是發現它是普通對象,則直接經過 Vue.extend 將其轉換成組件的構造函數

const resolve = once((res: Object | Class<Component>) => {
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    forceRender(true)
  } else {
    owners.length = 0
  }
})
function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
複製代碼
  • reject:內部 reject 函數,異步組件加載失敗時執行
const reject = once(reason => {
  process.env.NODE_ENV !== 'production' && warn(
    `Failed to resolve async component: ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : '')
  )
  if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender(true)
  }
})
複製代碼

看完其中的核心方法後,接下來咱們具體異步組件是如何建立的。

  1. 咱們從 resolveAsyncComponent 的定義中知道該方法接收 2 個參數,一個是 factory 工廠函數,一個是 baseCtor ,即 Vue
  2. 而後在當前渲染實例存在、且在 factory.owners 中存在的狀況下,即組件進入 pending 階段,則直接將當前實例丟到 factory.owners 中。
  3. 然而,初始化異步組件的時候 factory 是不會有 owners 滴,那這個時候又該怎麼辦呢?很簡單唄,直接執行 factory 工廠函數,並把內部定義的 resolvereject 函數做爲其參數,這樣咱們就能直接經過 resolvereject 作點事了,這些邏輯也正是對普通異步組件支持的邏輯,相關代碼以下
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
  // already pending
  factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
  const owners = factory.owners = [owner]
  let sync = true
  ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
  const forceRender = (renderCompleted: boolean) => {
    // ...
  }
  const resolve = once((res: Object | Class<Component>) => {
    // ...
  })
  const reject = once(reason => {
    // ...
  })
  const res = factory(resolve, reject)
  // ...
}
複製代碼
  • Promise 異步組件

vue 中,你可使用 webpack2+ + ES6 的方式來異步加載組件,以下

Vue.component(
  'async-webpack-example',
  // `import` 函數返回一個 Promise.
  () => import('./my-async-component')
)
複製代碼

即執行完 res = factory(resolve, reject) 時,res 的值爲 import('./my-async-component') 的返回值,是一個 Promise 對象。以後進入 Promise 異步組件的處理邏輯,異步組件加載成功後執行 resolve,加載失敗則執行 reject

const res = factory(resolve, reject)
if (isPromise(res)) {
  // () => Promise
  if (isUndef(factory.resolved)) {
    res.then(resolve, reject)
  }
}
複製代碼
  • 高級異步組件

其實這裏所謂的高級,其實就是 vue 在 2.3.0+ 版本新增了加載狀態處理的功能,即拋出了一些可配置的字段給用戶,其中有 componentloadingerrordelaytimeout,其中 component 支持 Promise 異步組件加載的形式,具體案例代碼以下

const AsyncComponent = () => ({
  // 加載組件(最終應該返回一個 Promise)
  component: import('./MyComponent.vue'),
  // 異步組件加載中(loading),展現爲此組件
  loading: LoadingComponent,
  // 加載失敗,展現爲此組件
  error: ErrorComponent,
  // 展現 loading 組件以前的延遲時間。默認:200ms。
  delay: 200,
  // 若是提供 timeout,而且加載用時超過此 timeout,
  // 則展現錯誤組件。默認:Infinity。
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
複製代碼

和剛分析 Promise 異步組件加載邏輯同樣,若執行完 res = factory(resolve, reject)res.component 的返回值是 Promise 的話,直接執行 then 方法,代碼以下

else if (isPromise(res.component)) {
  res.component.then(resolve, reject)
}
複製代碼

緊接着就是對其它 4 個可配置字段的處理

  1. 首先斷定是否自定義了 error 組件,若是有,執行 ensureCtor(res.error, baseCtor) 並將返回值直接賦值給 factory.errorComp
  2. 同理若傳入了 loading 組件,則執行 ensureCtor(res.loading, baseCtor) 並將返回值直接賦值給 factory.loadingComp
  3. 緊接着,在定義了 loading 組件的邏輯中,若設置了 delay 值爲 0,則直接將 factory.loading 值設爲 true,不然延時 delay 執行,delay 未設置,延時默認爲 200ms
  4. 最後,若設置了組件加載的 timeout 加載時長的話,若組件在 res.timeout 時間後還未加載成功,則直接執行 reject 進行拋錯
if (isDef(res.error)) {
  factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
  factory.loadingComp = ensureCtor(res.loading, baseCtor)
  if (res.delay === 0) {
    factory.loading = true
  } else {
    timerLoading = setTimeout(() => {
      timerLoading = null
      if (isUndef(factory.resolved) && isUndef(factory.error)) {
        factory.loading = true
        forceRender(false)
      }
    }, res.delay || 200)
  }
}

if (isDef(res.timeout)) {
  timerTimeout = setTimeout(() => {
    timerTimeout = null
    if (isUndef(factory.resolved)) {
      reject(
        process.env.NODE_ENV !== 'production'
        ? `timeout (${res.timeout}ms)`
        : null
      )
    }
  }, res.timeout)
}
複製代碼

而後最後經過斷定 factory.loading 進行不一樣值的返回,從上面自定義字段 loading 的處理咱們得知,若自定義字段 delay 設爲 0,則說明此次直接渲染 loading 組件,不然會直接延時並執行到 forceRender 方法,這樣就會觸發組件的從新渲染,從而再次執行 resolveAsyncComponent

sync = false
return factory.loading
  ? factory.loadingComp
	: factory.resolved
複製代碼

而後咱們再次回到 resolveAsyncComponent 開篇被咱們跳過的一些操做

if (isTrue(factory.error) && isDef(factory.errorComp)) {
  return factory.errorComp
}

if (isDef(factory.resolved)) {
  return factory.resolved
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  return factory.loadingComp
}
複製代碼

二、createAsyncPlaceholder

從上面的對 resolveAsyncComponent 的分析中,咱們得知,若是是第一次執行 resolveAsyncComponent ,返回值會是 undefined,固然,你將 delay 值設置爲 0 的時候除外。爲了不 Ctorundefined 時,致使節點信息沒法捕獲的狀況,會直接經過 createAsyncPlaceholder 建立一個註釋的 vnode 節點,做爲異步組件的佔位符,同時用來保留該 vnode 節點全部的原始信息。具體代碼以下

export function createAsyncPlaceholder ( factory: Function, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag: ?string ): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
複製代碼

4、函數式組件

分析 createComponent 組件建立的時候咱們還留下一個問題沒講,那就是 functional component(函數式組件),具體場景以下

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

分析前,防止有小夥伴不是很瞭解函數式組件是啥,我先列舉兩個官方支持的函數式組件寫法

// 方式一 render function
Vue.component('my-component', {
  functional: true,
  // Props 是可選項
  props: {
    // ...
  },
  // 爲了彌補缺乏的實例
  // 咱們提供了第二個參數 context 做爲上下文
  render: function (createElement, context) {
    // ...
  }
})

// 方式二 template functional
<template functional></template>
複製代碼

想了解更多的,能夠直接去官方文檔先仔細閱讀。

函數式組件官方定義:組件被標記成 functional,它無狀態,無響應式 data,無實例,即沒有 this 上下文。

下面咱們就來揭開函數式組件的面紗。

一、createFunctionalComponent

createFunctionalComponent 主要核心分爲三步

  1. Ctor.options 中的 props 合併到新對象 props 中。若 Ctor.options 存在 props,直接遍歷其 props,執行 validatePropCtor.options.props 當前屬性進行校驗並將當前屬性複製給 props[key]。若 Ctor.options.props 未定義,則將 data 上定義好的 attrsprops 經過執行 mergeProps 函數合併到新對象 props 上。
  2. 執行 new FunctionalRenderContext 實例化 functional 組件的上下文,並執行 options 上的 render 函數實例化 vnode 節點
  3. 對實例化的 vnode 進行特殊的克隆操做並進行返回
export function createFunctionalComponent ( Ctor: Class<Component>, propsData: ?Object, data: VNodeData, contextVm: Component, children: ?Array<VNode> ): VNode | Array<VNode> | void {
  const options = Ctor.options
  const props = {}
  const propOptions = options.props
  if (isDef(propOptions)) {
    for (const key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject)
    }
  } else {
    if (isDef(data.attrs)) mergeProps(props, data.attrs)
    if (isDef(data.props)) mergeProps(props, data.props)
  }

  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )

  const vnode = options.render.call(null, renderContext._c, renderContext)

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    const vnodes = normalizeChildren(vnode) || []
    const res = new Array(vnodes.length)
    for (let i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
    }
    return res
  }
}
複製代碼

上面說起的兩個輔助函數以下

  1. cloneAndMarkFunctionalResult :爲了不復用節點,fnContext 致使命名槽點不匹配的狀況,直接在設置 fnContext 以前克隆節點,最後返回克隆好的 vnode
  2. mergePropsprops 合併策略
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, renderContext) {
  const clone = cloneVNode(vnode)
  clone.fnContext = contextVm
  clone.fnOptions = options
  if (process.env.NODE_ENV !== 'production') {
    (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext
  }
  if (data.slot) {
    (clone.data || (clone.data = {})).slot = data.slot
  }
  return clone
}

function mergeProps (to, from) {
  for (const key in from) {
    to[camelize(key)] = from[key]
  }
}
複製代碼

二、FunctionalRenderContext

從文檔中咱們知道函數式組件支持兩種書寫方式,第一種是 render function 的方式,另一種則是 <template functional> 單文件組件的方式。render function 的方式在 createFunctionalComponent 的處理中已經作了支持,它會直接執行 Ctor.options 上的 render 方法。在函數式組件渲染上下文構造函數 FunctionalRenderContext 中則是對 <template functional> 單文件組件的方式也進行了支持。

  1. 首先,它爲了確保函數式組件的 createElement 函數可以得到惟一的上下文,將克隆的 parent 對象賦值給上下文 vm 變量 contextVmcontextVm._original 則賦值爲 parent,當作其上下文來源的標記。其中有種比較臨界的狀況就是,若傳入的上下文 vm 也是函數式上下文,這該怎麼辦呢?其實只要按照 _uid 存在的狀況來逆向推進下邏輯便可,contextVm 接收 parentparent 接收 parent._original 便可,由於往上繼續找,總能找着存在 _uidparent 不是。
  2. 接下來就是對函數式組件中 datapropslistenersinjections 等進行支持處理,這裏對於 slots 作了一層轉換處理,即將 normal slots 對象轉換成 scoped slots
  3. 最後對 options._scopeId 存在與否的場景進行不一樣的 createElement 節點建立操做
export function FunctionalRenderContext ( data: VNodeData, props: Object, children: ?Array<VNode>, parent: Component, Ctor: Class<Component> ) {
  const options = Ctor.options
  let contextVm
  if (hasOwn(parent, '_uid')) {
    contextVm = Object.create(parent)
    contextVm._original = parent
  } else {
    contextVm = parent
    parent = parent._original
  }
  const isCompiled = isTrue(options._compiled)
  const needNormalization = !isCompiled

  this.data = data
  this.props = props
  this.children = children
  this.parent = parent
  this.listeners = data.on || emptyObject
  this.injections = resolveInject(options.inject, parent)
  this.slots = () => {
    if (!this.$slots) {
      normalizeScopedSlots(
        data.scopedSlots,
        this.$slots = resolveSlots(children, parent)
      )
    }
    return this.$slots
  }

  Object.defineProperty(this, 'scopedSlots', ({
    enumerable: true,
    get () {
      return normalizeScopedSlots(data.scopedSlots, this.slots())
    }
  }: any))

  // support for compiled functional template
  if (isCompiled) {
    // exposing $options for renderStatic()
    this.$options = options
    // pre-resolve slots for renderSlot()
    this.$slots = this.slots()
    this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots)
  }

  if (options._scopeId) {
    this._c = (a, b, c, d) => {
      const vnode = createElement(contextVm, a, b, c, d, needNormalization)
      if (vnode && !Array.isArray(vnode)) {
        vnode.fnScopeId = options._scopeId
        vnode.fnContext = parent
      }
      return vnode
    }
  } else {
    this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
  }
}
複製代碼

5、抽象組件

對於抽象組件,我曾經寫過幾篇文章對其進行分析,因此這裏就再也不贅述了,想看的能夠點擊傳送門自行去閱讀。

總結

文章到這,通過了大篇幅的文字分析,咱們對 vue 中組件的建立(其中包括異步組件的建立、函數式組件的建立以及抽象組件的建立)、組件的鉤子函數、組件配置合併等都有了一個較爲全面的瞭解。

這裏我也但願各位小夥伴能在瞭解組件的這些原理之後,在自身業務開發中,能夠結合業務進行最佳的組件開發實踐,好比我我的曾因業務中的權限操做的統一管理而採用了我的認爲的最佳方案 - 抽象組件,它很好的解決權限管理這一業務痛點

前端交流羣:731175396,歡迎你們加入

我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。

相關文章
相關標籤/搜索