浅曦Vue源码-35-挂载阶段-$mount-(24)渲染函数帮助函数_c(1)

161 阅读4分钟

这是我参与2022首次更文挑战的第39天,活动详情查看:2022首次更文挑战

一、前情回顾 & 背景

这是更文活动的最后一天了,但是却不是这个专栏的最后一篇,从心里感谢本次更文活动,让我有勇气坚持下来。把一件巨大无比的工作拆成细碎的一小点,每天完成就一点点,最后就是一个大成就,这不是鸡汤,是心得。为了避免烂尾,我会坚持着把这个专栏写完,欢迎大家监督!加油吧摸鱼专家~

上一篇小作文介绍了常见的渲染函数的帮助函数的功能:

  1. _c:创建元素的科里化方法
  2. _l:处理列表渲染
  3. _s:转成字符串
  4. _t:渲染插槽 slot 标签
  5. _ilooseIndexOf,判断某个值在数组中的位置,判断字面量
  6. _m:渲染静态树,缓存、静态标记

此外还讨论两种不同的初始化的场景:

  1. vm._c 的初始化是在执行 Vue 的初始化逻辑的方法 _init 方法中完成的创建,是 vm 实例的私有方法,再次期间还初始了 vm.$createElement 也就是大家熟知的 h
  2. 其余的帮助函数都是在 renderMixin 中,挂载到 Vue.prototype 对象的公有方法;

上一篇因为篇幅所限制,没有展开讲 _c,所以本篇小作文的重点就是讲透 _c 方法;

二、vm._c 方法

和其他的渲染函数帮助函数如 _l/_s... 等不同,vm._cVue 实例的私有方法,这个方法的初始化是伴随着 Vue 的实例初始化完成的,其顺序为:

new Vue() 
    -> this._init() 
          -> initRender 
                -> vm._c = (a, b, c, d) => createElement(vm , a, b, c, d, false) // vm._c 代码

2.1 vm._c 调用示例

  • 以下问渲染 <div class="static-div">静态节点</div> 元素对应的渲染函数
_c( 'div',{ staticClass: "static-div" }, [_v("静态节点")] )

方法位置:src/core/instance/render.js -> function initRender -> vm._c

方法参数:(你会发现大佬也会用 abcd 做参数😂😂)

  1. a, tag,标签名
  2. b, data,是渲染标签所需要用到数据,例如上面的 staticClass
  3. c, children,子节点数组
  4. dnormalizationType,节点规范化类型

方法作用:vm._c 作用是个科里化的函数,其目的在于自动绑定 vm 实例;

2.2 createElement 方法

方法位置:src/core/vdom/create-element.js -> function createElement

方法参数:

  1. context: Vue 实例
  2. tag:标签名
  3. data:渲染标签对应的数据,如上面的 staticClass
  4. normalizationType:节点的规范化类型
  5. alwaysNormalize: 是否一直 normalize

方法作用:它是 _createElement 的包装函数,生成组件或者普通元素的 VNode

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }

  // context 就是 vm
  // 执行 _createElement 方法创建组件 VNode
  return _createElement(context, tag, data, children, normalizationType)
}

2.3 _createElement

方法位置:src/core/vdom/create-element.js -> function _createElement

方法参数:

  1. context: Vue 实例
  2. tag:标签名
  3. data:渲染标签对应的数据,如上面的 staticClass
  4. normalizationType:节点的规范化类型
  5. alwaysNormaliza: 是否一直 normalize

方法作用:根据 data、和 tag 处理不同场景下创建 VNode,具体工作如下:

  1. 如果 data 是个响应式的数据,返回空节点
  2. 处理动态组件,将 tag 参数改成 is 属性绑定的真实标签名;
  3. 如果此时 tag 为假值,返回空节点;
  4. 检查组件上的 key 属性只能为数字或者字符串;
  5. 处理只有一个子节点的情况,将其当做插槽处理并情况子节点列表;
  6. 开始处理普通的元素、组件的 VNode 生成逻辑;
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
    )
    // 如果属性是一个响应式对象,则返回一个空节点的 VNode
    return createEmptyVNode()
  }

  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    // 这里是处理动态组件 component,data.is 就是 <component is="someComp" /> 的 is 属性
    tag = data.is
  }

  if (!tag) {
    // 动态组件的 is 属性是一个假值时 tag 为 false,则返回一个空节点 VNode
    return createEmptyVNode()
  }

  // 检测唯一键 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
      )
    }
  }

  // 子节点数组中只有一个函数时,将他当做默认插槽处理并清空子节点列表
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }

  // 将子元素进行标准化处理
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

  // 从这里开始才是重点,前面都不需要关注,以下是需要创建 VNode 的过程
  let vnode, ns
  if (typeof tag === 'string') {
    // 标签是字符串时,该标签有三种可能:
    // 1. 平台保留标签
    // 2. 自定义组件
    // 3. 不知名标签
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
       // tag 原生 HTML 标签
       // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        // v-on 的 native 只在组件上生效
        // ... 抛出警告
      }

      // 实例化一个 VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
   
      // resolveAssets 是个值得一看的方法
      // 普通组件,例如我们的例子中的 <some-com /> 的渲染就会走这里
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 未知标签,也生成 VNode,
      // 运行时检查会检查,因为父元素进行规范化子节点时有可能获取一个命名空间

      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // tag 不是字符串,可能是一个组件的配置对象或构造函数
    vnode = createComponent(tag, data, context, children)
  }

  // 返回组件的 VNode
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

2.4 resolveAssets

方法位置:src/core/util/options.js -> function resolveAssets

方法参数:

  1. options,是 Vue 实例的 $options
  2. typeassets_type 类型,指的是 options 上的 components/directives/filters 中的一个,Vue 中导出了一个数组常量,名为 ASSET_TYPES = ['component', 'directive', 'filter'] 这个东西在讲初始化的时候有提到过;
  3. id,要获取的资源名字,比如上面的例子中就是从 vm.$options['components'][id = "some-com"]id 就是组件名;

方法作用:该方法用于从 vm.$options 中的指定资源类型 type 中对应 id 的资源。说人话就是从 vm.$options 即选项获取注册的组件、指令、过滤器等资源;

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }

  // 这个 options 就是合并过选项后的 vm.$options 从这个上面拿 vm.components[key] ,
  // 对于组件来说,要么就是全局组件存在 Vue.components 中,
  // 要么就是自父组件的 options 中注册的子组件,即 { components: { someComp: Obj }}
  const assets = options[type]

  // 首先查找本地注册的
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id) // some-comp 变成 someComp 这种驼峰命名的
  if (hasOwn(assets, camelizedId)) return assets[camelizedId] // 这里就命中了我们的组件 some-com
  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
}

下面是在渲染组件 some-com 是调用 resolveAssets 得到的结果:

image.png

这个东西就是我们注册的子组件:

const sub = {
  template: `
    <div style="color: red;background: #5cb85c;display: inline-block">
  <slot name="namedSlot"></slot>
 {{ someKey + foo }}</div>`,
  props: {
    someKey: {
      type: Object,
      default: () => 'hhhhhhh'
    }
  },
  inject: ['foo']
}
debugger
new Vue({
  el: '#app',
  data: {},

  components: {
    someCom: sub // 子组件选项对象
  }
})

当然,要从选项对象变成组件,还需要下面的 createComponent 方法

2.5 createComponent

createComponent 是处理自定义组件渲染的方法,该方法合并基类 Vue 的选项: Vue.options 和 组件选项 options, 然后基于合并后的选项以 Vue 为基类扩展出用于创建子组件的子类;

然后处理 v-model 指令,提取 propsData、installComponentHooks,最后得到组件的 VNode 并返回,这个方法的篇幅也很长,在下一篇深入讨论;

三、总结

本篇小作文的主题有以下几方面:

  1. Vue 中最复杂的渲染函数的运行时帮助函数 vm._c,我们强调了这个方法与其他的不同,其他的帮助函数如 _l/_t/_s... 都是挂在 Vue.prototype 对象上的公有方法,而 vm._cvm 实例私有的方法;

  2. 此外还解释了 vm._c 科里化 createElement 的原因——绑定 vm 实例;

  3. 接着讨论了 _createElement、resolveAssets 方法细节,处理自定义组件 VNode 的方法 createElement 将会再下一篇继续深入讨论;