源码分析 vue 组件是如何渲染到页面(上)

1,011 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

组件是如何渲染到页面的

我之前的文章介绍了在Vue中普通的数据是如何渲染到页面的,关于这段逻辑可以点击这里,主要包括init,$mount,patch的过程:

graph TD
init --> $mount --> patch

那么组件与普通数据在执行整个流程中有什么不同呢,主要不同的点主要包括两个流程,一个是$mount过程中,创建vnode(虚拟节点)的时候,另一个过程是patch(vnode转化成真实DOM)过程,下面我们针对这两个过程进行分析:

在页面渲染过程中,如果遇到了组件节点,会进入组件的$mount(原因下文会分析),这个过程中需要生成组件vnode,会执行createElement函数,接下来我们来看看组件的createElement过程,上源码:

1.createElement(创建组件虚拟节点)

_createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  // 渲染组件节点的时候,tag存在且是string类型
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  // 插槽相关逻辑,跳过
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 这儿的逻辑是将children参数规范化,使其统一格式
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // tag是string类型,进入逻辑
  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
      )
    // 判断当前实例上的options.components中是否存在该标签,渲染组件节点的逻辑
    } else if (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 {
    // tag传入的不是string类型的时候直接创建组件节点
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    // 返回vnode
    return vnode
  } else {
    return createEmptyVNode()
  }
}

如果是组件节点的时候,有两种情况,第一种情况,传入的tag参数是字符串,首先会判断该字符串是否是保留标签,不是的话会执行resolveAsset(context.$options, 'components', tag)的方法,去查找该tag定义的对象:

resolveAsset

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  // 没有id直接返回
  if (typeof id !== 'string') {
    return
  }
  // 获取实例的options.components
  const assets = options[type]
  // check local registration variations first
  // 检查实例中是否存在这个构造器,有则直接返回
  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]
  // 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
}

查找到定义的components对象后,根据拿到查找到的对象执行createComponent函数,创建vnode节点;第二种情况传入的tag参数直接就是component对象或构造器,就直接去执行createComponent函数,我们继续看下这个函数:

createComponent

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // 如果Ctor未定义,直接返回
  if (isUndef(Ctor)) {
    return
  }

  // baseCtor 是Vue
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  // 创建组件构造器,将Vue上面的components也合并了,可以参考我的另一篇文章,extend
  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, 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
      )
    }
  }

  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
  // 创建占位符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)
  }

  // 返回vnode
  return vnode
}

createComponent函数,通过Vue.extend函数创建了一个子类构造器(关于Vue.extend的逻辑可以点击这里),这个创建的子类构造器会继承Vue构造函数上面的一些能力,同时也会使用mergeOptions函数将Vue上面的一些配置项进行并入(关于mergeOptions的逻辑可以点击这里),包括全局注册的组件(components)等,这也是全局注册的组件可以在各个组件内部使用的原因。

拥有了组件构造器之后,就可以创建组件vnode了

// return a placeholder vnode 
// 创建占位符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 
)

组件vnode与普通vnode有一点不同之处,名称会命名为以vue-component为开头,另外,它的第3、4、5(分别为children,text,elm)个参数传入的都是undefined,这对后面patch阶段的分析很有帮助,到了这里,我们就清楚了组件节点的vnode创建过程了,下一节,我们会继续分析,组件是如何从vnode转化成真实DOM元素的。

未完待续。。。