Vue2.0源码理解(6) - 组件注册

组件注册

前言

在 Vue.js 中,除了它内置的组件如 keep-alive、component、transition、transition-group 等,其它用户自定义组件在使用前必须注册。在开发过程中可能会遇到如下报错信息:

Unknown custom element: <app> - did you register the component correctly? 
For recursive components, make sure to provide the "name" option.

一般报这个错的原因都是我们使用了未注册的组件。Vue.js 提供了 2 种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。

全局注册

在初始化加载阶段会调用initAssetRegisters函数把需要注册的组件挂载到Vue.options上。

// src\core\global-api\index.js
initAssetRegisters(Vue)
// src\core\global-api\assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
  // 标注①
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          // 优先拿name,没有则取id
          definition.name = definition.name || id
          // 标注②
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

标注①:
对ASSET_TYPES进行遍历,我们先看看遍历对象ASSET_TYPES是什么?

// src\shared\constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

其实就是存放着插件、指令、过滤器这三个分类名称的数组,这里我们只单独针对component进行分析。
标注②:
this.options._base其实是Vue,具体原因请查看之前的文章《组件的创建和patch过程》
通过Vue.extend把对象转换成构造器。
最后把definition放到this.options即Vue.options上,然后return definition。
虽然挂载到Vue.options上,但是又是什么时候会被拿去注册成真正的组件呢?
我们回顾_createElement函数:

// src\core\vdom\create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    ...
    if (typeof tag === 'string') {
        //是否HTML原生标签
        if (config.isReservedTag(tag)) {
            ...
        // 标注①:resolveAsset
        } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
            // component
            vnode = createComponent(Ctor, data, context, children, tag)
        } else {
            ...
        }
    }
}

标注①:resolveAsset函数做了什么?

// src\core\util\options.js
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  //判断配置中是否存在该组件
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  //id转换成驼峰型判断
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  //id转换成首字母大写判断
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
//   原型上面找
  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
}

其实就是经过各种情况判断识别Vue.options是否有定义该组件,有的话则返回,然后最后经过createComponent函数进行了组件的注册。关于createComponent函数的不了解的请查阅之前文章《组件的创建和patch过程》
局部注册其实和全局注册的几乎一样,只是它需要在此前做一个option合并:

// src\core\global-api\extend.js
Sub.options = mergeOptions(
  Super.options,
  extendOptions
)

关于合并的详细分析请查阅之前文章《合并配置》
由于合并配置是挂载于Sub上的,也就是说它只是一个在当前Sub作用域下的,一次这种创建方式的组件只能局部使用。

异步组件注册

工程模式

平时开发,特别涉及到首屏加载的时候,一次性加载完成会对首屏体验变成,所以往往加了各种异步加载,组件注册当然也支持异步注册,下面我们看看源码是怎么实现。
我们先在main.js写一个注册一个异步组件

Vue.component('HelloWorld',function(resolve,reject){
  require(['./components/HelloWorld'],function(res){
    resolve(res);
  })
})

还是回到最初的方法initAssetRegisters(Vue)

// src\core\global-api\assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          // 优先拿name,没有则取id
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

其实在这个函数里只是把方法挂载到options上,那么在哪里处理异步呢?我们回看创建组件方法

// 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 {
  ...
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {  //因为是方法,所以没有id
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    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
      )
    }
  }

因为我们异步注册传进的是一个方法,所以是没有cid的,所以就到了判断里面逻辑。这里面我们分两步分析,分别是resolveAsyncComponent方法和createAsyncPlaceholder方法。

①、resolveAsyncComponent方法
// src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  ...
    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)
      }
    })

    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)
    // 异步,因此res为undefined
    if (isObject(res)) {
      ...
    }
  ...
    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

第一次进来主要处理了两件事:
1、定义了resolve和reject方法用于回调。
2、执行了组件注册时参入的方法。
最后return时候factory.loading是undefined的,因此回到createComponent时进入createAsyncPlaceholder方法

②、createAsyncPlaceholder方法
// src\core\vdom\helpers\resolve-async-component.js
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
}

createAsyncPlaceholder方法其实就是创建一个空的注释节点。
至此同步线程结束,回到了组件注册时,我们定义的方法。

Vue.component('HelloWorld',function(resolve,reject){
  require(['./components/HelloWorld'],function(res){
    //同步线程结束,调用resolve方法。
    resolve(res);
  })
})

res参数指向的就是组件的本身。
然后我们回看resolve方法:

// src\core\vdom\helpers\resolve-async-component.js
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)
  }
})

其中ensureCtor方法主要是利用Vue.extend做构造器转换,执行后挂载到我们定义的方法下,factory.resolved在后面重新强制渲染会用来做判断,然后执行下一步,进入forceRender方法。

// src\core\vdom\helpers\resolve-async-component.js
const forceRender = (renderCompleted: boolean) => {
  for (let i = 0, l = contexts.length; i < l; i++) {
    contexts[i].$forceUpdate()
  }

  if (renderCompleted) {
    contexts.length = 0
  }
}

forceRender方法主要是调用$forceUpdate()触发一次强制渲染。
这次我们又回到最初的起点,呆呆的。。。。
来,我们继续看resolveAsyncComponent老友:

// src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  ...
  if (isDef(factory.resolved)) {
    return factory.resolved
  }
})

这次factory.resolved是有的了,然后就返回factory.resolved构造器。

// 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 {
  ...
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {  //因为是方法,所以没有id
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    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
      )
    }
  }

返回构造器后createAsyncPlaceholder方法就不再去跑了,然后会正常进入patch阶段把组件注册。

promise模式

// import会返回一个promise对象
Vue.component('HelloWorld',
    () => import('./components/HelloWorld')
)

和工厂模式殊途同归,还是去到了resolveAsyncComponent方法

// src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  ...
  //这时候返回的是promise对象
  const res = factory(resolve, reject)

  if (isObject(res)) {
    // 此处调用promise对象的then方法
    if (typeof res.then === 'function') {
      // () => Promise
      if (isUndef(factory.resolved)) {
        res.then(resolve, reject)
      }
    } 
    ...
  }
})

和工厂模式不一样的是factory执行后会返回一个promise对象,从而进入到判断逻辑,然后使用promise的then方法从而进行到异步调用注册组件。

高级模式

const LoadingComp = {
  template:"<div>LoadingComp</div>"
}

const ErrorComp = {
  template:"<div>ErrorComp</div>"
}

const AsyncComp = {
  // 需要加载的组件,应当是一个promise对象
  component: import('./components/HelloWorld.vue'),
  //加载中应当渲染的组件
  loading:LoadingComp,
  // 出错时渲染的组件
  error:ErrorComp,
  // 渲染加载中组件前的等待时长
  delay:200,
  // 最长等待时长,超时渲染报错组件
  timeout:3000,
}

Vue.component('HelloWorld',AsyncComp);

高级模式就不再分析了,走的还是resolveAsyncComponent方法,有兴趣的可以自行研究一下,总体流程差不多.

组件注册就说到这了,后面我们会探讨响应式的原理。

posted @ 2022-02-22 19:09  Inès  阅读(474)  评论(0编辑  收藏  举报