• createComponent
    • 构造子类构造函数
    • 安装组件钩子函数
    • 实例化 VNode
    • 总结

    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