createComponent

上一章我们在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。

  1. if (typeof tag === 'string') {
  2. let Ctor
  3. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  4. if (config.isReservedTag(tag)) {
  5. // platform built-in elements
  6. vnode = new VNode(
  7. config.parsePlatformTagName(tag), data, children,
  8. undefined, undefined, context
  9. )
  10. } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  11. // component
  12. vnode = createComponent(Ctor, data, context, children, tag)
  13. } else {
  14. // unknown or unlisted namespaced elements
  15. // check at runtime because it may get assigned a namespace when its
  16. // parent normalizes children
  17. vnode = new VNode(
  18. tag, data, children,
  19. undefined, undefined, context
  20. )
  21. }
  22. } else {
  23. // direct component options / constructor
  24. vnode = createComponent(tag, data, context, children)
  25. }

在我们这一章传入的是一个 App 对象,它本质上是一个 Component 类型,那么它会走到上述代码的 else 逻辑,直接通过 createComponent 方法来创建 vnode。所以接下来我们来看一下 createComponent 方法的实现,它定义在 src/core/vdom/create-component.js 文件中:

  1. export function createComponent (
  2. Ctor: Class<Component> | Function | Object | void,
  3. data: ?VNodeData,
  4. context: Component,
  5. children: ?Array<VNode>,
  6. tag?: string
  7. ): VNode | Array<VNode> | void {
  8. if (isUndef(Ctor)) {
  9. return
  10. }
  11. const baseCtor = context.$options._base
  12. // plain options object: turn it into a constructor
  13. if (isObject(Ctor)) {
  14. Ctor = baseCtor.extend(Ctor)
  15. }
  16. // if at this stage it's not a constructor or an async component factory,
  17. // reject.
  18. if (typeof Ctor !== 'function') {
  19. if (process.env.NODE_ENV !== 'production') {
  20. warn(`Invalid Component definition: ${String(Ctor)}`, context)
  21. }
  22. return
  23. }
  24. // async component
  25. let asyncFactory
  26. if (isUndef(Ctor.cid)) {
  27. asyncFactory = Ctor
  28. Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
  29. if (Ctor === undefined) {
  30. // return a placeholder node for async component, which is rendered
  31. // as a comment node but preserves all the raw information for the node.
  32. // the information will be used for async server-rendering and hydration.
  33. return createAsyncPlaceholder(
  34. asyncFactory,
  35. data,
  36. context,
  37. children,
  38. tag
  39. )
  40. }
  41. }
  42. data = data || {}
  43. // resolve constructor options in case global mixins are applied after
  44. // component constructor creation
  45. resolveConstructorOptions(Ctor)
  46. // transform component v-model data into props & events
  47. if (isDef(data.model)) {
  48. transformModel(Ctor.options, data)
  49. }
  50. // extract props
  51. const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  52. // functional component
  53. if (isTrue(Ctor.options.functional)) {
  54. return createFunctionalComponent(Ctor, propsData, data, context, children)
  55. }
  56. // extract listeners, since these needs to be treated as
  57. // child component listeners instead of DOM listeners
  58. const listeners = data.on
  59. // replace with listeners with .native modifier
  60. // so it gets processed during parent component patch.
  61. data.on = data.nativeOn
  62. if (isTrue(Ctor.options.abstract)) {
  63. // abstract components do not keep anything
  64. // other than props & listeners & slot
  65. // work around flow
  66. const slot = data.slot
  67. data = {}
  68. if (slot) {
  69. data.slot = slot
  70. }
  71. }
  72. // install component management hooks onto the placeholder node
  73. installComponentHooks(data)
  74. // return a placeholder vnode
  75. const name = Ctor.options.name || tag
  76. const vnode = new VNode(
  77. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  78. data, undefined, undefined, undefined, context,
  79. { Ctor, propsData, listeners, tag, children },
  80. asyncFactory
  81. )
  82. // Weex specific: invoke recycle-list optimized @render function for
  83. // extracting cell-slot template.
  84. // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  85. /* istanbul ignore if */
  86. if (__WEEX__ && isRecyclableComponent(vnode)) {
  87. return renderRecyclableComponentTemplate(vnode)
  88. }
  89. return vnode
  90. }

可以看到,createComponent 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就 3 个关键步骤:

构造子类构造函数,安装组件钩子函数和实例化 vnode

构造子类构造函数

  1. const baseCtor = context.$options._base
  2. // plain options object: turn it into a constructor
  3. if (isObject(Ctor)) {
  4. Ctor = baseCtor.extend(Ctor)
  5. }

我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的 App.vue 为例,代码如下:

  1. import HelloWorld from './components/HelloWorld'
  2. export default {
  3. name: 'app',
  4. components: {
  5. HelloWorld
  6. }
  7. }

这里 export 的是一个对象,所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor),在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:

  1. // this is used to identify the "base" constructor to extend all plain-object
  2. // components with in Weex's multi-instance scenarios.
  3. Vue.options._base = Vue

细心的同学会发现,这里定义的是 Vue.option,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.js 里 Vue 原型上的 _init 函数中有这么一段逻辑:

  1. vm.$options = mergeOptions(
  2. resolveConstructorOptions(vm.constructor),
  3. options || {},
  4. vm
  5. )

这样就把 Vue 上的一些 option 扩展到了 vm.$option 上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。mergeOptions 的实现我们会在后续章节中具体分析,现在只需要理解它的功能是把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm.$options 上。

在了解了 baseCtor 指向了 Vue 之后,我们来看一下 Vue.extend 函数的定义,在 src/core/global-api/extend.js 中。

  1. /**
  2. * Class inheritance
  3. */
  4. Vue.extend = function (extendOptions: Object): Function {
  5. extendOptions = extendOptions || {}
  6. const Super = this
  7. const SuperId = Super.cid
  8. const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  9. if (cachedCtors[SuperId]) {
  10. return cachedCtors[SuperId]
  11. }
  12. const name = extendOptions.name || Super.options.name
  13. if (process.env.NODE_ENV !== 'production' && name) {
  14. validateComponentName(name)
  15. }
  16. const Sub = function VueComponent (options) {
  17. this._init(options)
  18. }
  19. Sub.prototype = Object.create(Super.prototype)
  20. Sub.prototype.constructor = Sub
  21. Sub.cid = cid++
  22. Sub.options = mergeOptions(
  23. Super.options,
  24. extendOptions
  25. )
  26. Sub['super'] = Super
  27. // For props and computed properties, we define the proxy getters on
  28. // the Vue instances at extension time, on the extended prototype. This
  29. // avoids Object.defineProperty calls for each instance created.
  30. if (Sub.options.props) {
  31. initProps(Sub)
  32. }
  33. if (Sub.options.computed) {
  34. initComputed(Sub)
  35. }
  36. // allow further extension/mixin/plugin usage
  37. Sub.extend = Super.extend
  38. Sub.mixin = Super.mixin
  39. Sub.use = Super.use
  40. // create asset registers, so extended classes
  41. // can have their private assets too.
  42. ASSET_TYPES.forEach(function (type) {
  43. Sub[type] = Super[type]
  44. })
  45. // enable recursive self-lookup
  46. if (name) {
  47. Sub.options.components[name] = Sub
  48. }
  49. // keep a reference to the super options at extension time.
  50. // later at instantiation we can check if Super's options have
  51. // been updated.
  52. Sub.superOptions = Super.options
  53. Sub.extendOptions = extendOptions
  54. Sub.sealedOptions = extend({}, Sub.options)
  55. // cache constructor
  56. cachedCtors[SuperId] = Sub
  57. return Sub
  58. }

Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 propscomputed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。

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

安装组件钩子函数

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

我们之前提到 Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:

  1. const componentVNodeHooks = {
  2. init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  3. if (
  4. vnode.componentInstance &&
  5. !vnode.componentInstance._isDestroyed &&
  6. vnode.data.keepAlive
  7. ) {
  8. // kept-alive components, treat as a patch
  9. const mountedNode: any = vnode // work around flow
  10. componentVNodeHooks.prepatch(mountedNode, mountedNode)
  11. } else {
  12. const child = vnode.componentInstance = createComponentInstanceForVnode(
  13. vnode,
  14. activeInstance
  15. )
  16. child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  17. }
  18. },
  19. prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  20. const options = vnode.componentOptions
  21. const child = vnode.componentInstance = oldVnode.componentInstance
  22. updateChildComponent(
  23. child,
  24. options.propsData, // updated props
  25. options.listeners, // updated listeners
  26. vnode, // new parent vnode
  27. options.children // new children
  28. )
  29. },
  30. insert (vnode: MountedComponentVNode) {
  31. const { context, componentInstance } = vnode
  32. if (!componentInstance._isMounted) {
  33. componentInstance._isMounted = true
  34. callHook(componentInstance, 'mounted')
  35. }
  36. if (vnode.data.keepAlive) {
  37. if (context._isMounted) {
  38. // vue-router#1212
  39. // During updates, a kept-alive component's child components may
  40. // change, so directly walking the tree here may call activated hooks
  41. // on incorrect children. Instead we push them into a queue which will
  42. // be processed after the whole patch process ended.
  43. queueActivatedComponent(componentInstance)
  44. } else {
  45. activateChildComponent(componentInstance, true /* direct */)
  46. }
  47. }
  48. },
  49. destroy (vnode: MountedComponentVNode) {
  50. const { componentInstance } = vnode
  51. if (!componentInstance._isDestroyed) {
  52. if (!vnode.data.keepAlive) {
  53. componentInstance.$destroy()
  54. } else {
  55. deactivateChildComponent(componentInstance, true /* direct */)
  56. }
  57. }
  58. }
  59. }
  60. const hooksToMerge = Object.keys(componentVNodeHooks)
  61. function installComponentHooks (data: VNodeData) {
  62. const hooks = data.hook || (data.hook = {})
  63. for (let i = 0; i < hooksToMerge.length; i++) {
  64. const key = hooksToMerge[i]
  65. const existing = hooks[key]
  66. const toMerge = componentVNodeHooks[key]
  67. if (existing !== toMerge && !(existing && existing._merged)) {
  68. hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
  69. }
  70. }
  71. }
  72. function mergeHook (f1: any, f2: any): Function {
  73. const merged = (a, b) => {
  74. // flow complains about extra args which is why we use any
  75. f1(a, b)
  76. f2(a, b)
  77. }
  78. merged._merged = true
  79. return merged
  80. }

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数,具体的执行我们稍后在介绍 patch 过程中会详细介绍。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。

实例化 VNode

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

最后一步非常简单,通过 new VNode 实例化一个 vnode 并返回。需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的,这点很关键,在之后的 patch 过程中我们会再提。

总结

这一节我们分析了 createComponent 的实现,了解到它在渲染一个组件的时候的 3 个关键逻辑:构造子类构造函数,安装组件钩子函数和实例化 vnodecreateComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数,我们在上一章对 patch 函数做了简单的分析,那么下一节我们会对它做进一步的分析。

原文: https://ustbhuangyi.github.io/vue-analysis/components/create-component.html