浅曦Vue源码-36-挂载阶段-$mount-(25)渲染函数帮助函数_c(2)

184 阅读9分钟

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

一、前情回顾 & 背景

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

另外还介绍了 _createElement、resolveAssets 等方法;

这篇小作文就 Vue 在渲染过程中创建自定义组件 VNodecreateComponent 方法展开讨论;

二、createComponent

方法位置:src/core/vdom/create-component.js -> function createComponent

方法参数:

  1. CtorVue 子类,还可以是构造函数或者选项对象;
  2. data,组件的 data 对象,包含 props 等数据;
  3. children,子节点列表;
  4. tag,标签名;

方法作用:

  1. 和并组件 options 和根实例 optionsVue.options),基于 options 扩展出用于创建组件的 Vue 子类构造函数;
  2. 处理异步组件,这里先忽略吧;
  3. 处理 v-model,将 v-model 绑定的值添加到 data.attrs,把实现双向绑定的运行时事件添加到 data.ondata.attrs 就是将来真实 DOM 上的行内属性,例如在 <input v-model="inputValue" /> 会在 data.attrs['value'] = inputVlue,而 data.on 上的事件就是编译时生成的运行时事件代码,监听 inputoninput 事件获取新值并更新数据;
  4. 提取 propsData,数据源有两个地方,data.attrsdata.props,其中 props 就是声明组件时 props 选项;
  5. installComponentHooks,为组件的 data 增加 hook 对象,包含 init、prepatch、insert、destory 四个钩子方法
  6. 创建标签名字为 vue-component-${cid}-${name}VNode,并返回
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // 组件构造函数不存在,结束
  if (isUndef(Ctor)) {
    return
  }

  // baseCtor 就是 Vue 构造函数本身
  const baseCtor = context.$options._base

  // 当 Ctor 为配置对象时,通过 Vue.extend 将其转换为构造函数
  // 所以能看出来,子组件的构造函数是 Vue 的子类
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor) // Vue.extend() 
  }

  // 到此为止,如果 Ctor 仍不是一个函数,则表示这是个无效的组件定义
  if (typeof Ctor !== 'function') {
    return
  }

  // 异步组件
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // 异步组件返回一个占位符节点,组件被渲染为注释节点,原始信息,
      // 这些信息将用于异步渲染和合成
      // 先管同步的吧,异步的先放放
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  // 节点属性 JSON 字符串
  data = data || {}

  // 这里其实就是组件做选项合并的地方,即编译器将组件编译为渲染函数,
  // 渲染时执行 render 函数如果遇到自定义组件,然后执行其中的 _c,就会走到这里了
  // 解析构造函数选项,合并基类选项,合并过程中会比对两次基类的 options 和 缓存的 options 比较,
  // 以防止在组件构造函数创建后引用全局混入丢失掉新混入的信息
  resolveConstructorOptions(Ctor)

  // 将组件的 v-model 的信息(值和回调)转换为 data.attrs 对象的属性
  // data.on 对象上的事件、回调
 
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 提取 props 数据,得到 propsData 对象,propsData[key] = val
  // 声明组件时 props 配置中的属性为 key,父组件对应的数据为 value
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // 函数式组件
 
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // 获取事件监听器 data.on,因为这些监听器需要作为子组件的自定义事件监听器处理,
  // 而不是 DOM 监听器
  const listeners = data.on

  // 将带有 .native 修饰符的事件对象赋值给 data.on,这样就可以再父组件 patch 的时候被处理
  data.on = data.nativeOn

  // 如果是抽象组件,则保留 props、listeners 和 slot
  if (isTrue(Ctor.options.abstract)) {
  
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 在组件的 data 对象上设置组件管理的 hook 对象
  // hook 对象增加四个属性:
  // init: 组件创建
  // prepatch:组件更新
  // insert: 组件被挂载到父组件
  // destroy: 组件销毁
  // 这些方法在组件 patch 阶段会被调用
  
  installComponentHooks(data)

  // 返回占位符节点
  // return a placeholder vnode
  const name = Ctor.options.name || tag

  // 实例化组件的 VNode,
  // 对于普通组件标签名: vue-component-${cid}-${name}
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

2.1 baseCtor.extend

baseCtor 就是 Vue 构造函数自己;

方法位置:src/core/global-api/extend.js -> function initExtend -> Vue.extend

方法参数:Vue.extend 的参数,extendOptions,需要扩展的选项对象;

方法作用:基于 Vue 自身根据 extendOptions 选项生成 Vue 的一个子类,当然子类还可以继续 extend 出孙子类。

子类的 optionsVue.optionsextendOptions 通过 mergeOptions 合并后的结果;所以,这样一来子类就复用了父类的能力,最简单的就是全局的组件、指令、过滤器就是通过这种方式实现每个子组件都能够访问到的。

export function initExtend (Vue: GlobalAPI) {

  Vue.cid = 0
  let cid = 1

  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid

    // 利用缓存,如果存在则直接返回缓存中的构造函数
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    // extendOptions 就是我们声明组件时创建的选项对象
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    // 定义 Sub 构造函数,和 Vue 构造函数一样
    const Sub = function VueComponent (options) {
      this._init(options)
    }

    // 通过原型继承的方式继承 Vue,所以 this._init 就有了
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++

    // 选项合并,合并 Vue 的配置项到自己的配置项,实现复用 Vue 类的能力
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )

    // 记录自己的基类
    Sub['super'] = Super

    // 下面的将 props、computed 属性代理到扩展原型对象上,
    // 这能避免在实例创建时 Object.defineProperty 调用
    // 初始化 props, 将 props 配置代理到 Sub.prototype._props 对象上
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 初始化 computed,将 computed 配置代理到 Sub.prototype 对象上
    if (Sub.options.computed) {
      initComputed(Sub)
    }

   
    // 定义 extend、mixin、use 这三个静态方法,允许在 Sub 基础上进一步构造子类
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // 定义 component/filter/directive 三个静态方法
    // 子类就可以有自己的 component、filter、directive 方法了;
    // 给子类加三个声明全局组件、过滤器;、指令的方法 
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    
    // enable recursive self-lookup
    // 递归组件的原理,如果组件设置了 name 属性,则将自己注册到自己的 components 选项中
    if (name) {
      Sub.options.components[name] = Sub
    }

    // 在扩展时保留对基类选项的引用
    // 稍后在实例化时,我们可以检查 Super 的选项是否具有更新,如果有更新要取用最新的选项
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // 缓存
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

我们的每一个子组件都会有自己的子类,当他被渲染的时候,就会初始这个子类的实例来完成各种初始化、数据响应式、编译模板并挂载。如果子组件还有子组件,那么这个过程是个递归的,直到最深的一级完成挂载;

2.2 transformModel

方法位置:src/core/vdom/create-component.js -> function transformModel

方法参数:

  1. options, Ctor.options 是经过合并后的选项对象,如果是子组件,这个 options 还包含了父类的 options
  2. data, 使用 v-model 指令的元素的 data 对象;

方法作用:将组件的 v-model 绑定的属性信息变成 data.attrs 上的属性;运行时的事件监听器变成 data.on 上的属性;v-model 默认绑定的是 value 属性,事件默认是 input 事件;

function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    on[event] = callback
  }
}

2.3 extractPropsFromVNodeData

方法位置:src/core/vdom/helpers/extract-props.js -> function extractPropsFromVNodeData

方法参数:

  1. data, 虚拟 DOMdata 对象,就是前面 generate 得到的 data,在运行时里就是对象了;
  2. Ctor,构造函数;
  3. tag:标签名;

方法作用:从 dataattrsprops 中提取出最终的 props 对象,keyprops 中的 key,值有可能来自 attrs 也可能来自 props

export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  // 组件的 props 选项,{ props: { someKey: { type: Object, default () { return { a: 'ddddd'} } } }
  // 这里只提取原始值,验证默认值和子组件中处理
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }

  // 以 props 配置中的属性为 key,父组件传递下来的值为 value
  // 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里
  // 这样子子组件中相应的数据就会被更新
  const res = {}
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    // 遍历 propsOptions
    for (const key in propOptions) {
      // 将小驼峰形式 key 转成 连字符
      const altKey = hyphenate(key)

      if (process.env.NODE_ENV !== 'production') {
         // 提示,props 为小驼峰,html 不区分大小写,要用连字符替换驼峰  
      }
      
      // 从 props 和 attrs 中取值,props 要保留,attrs 不用保留
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}
  • checkProp 方法,尝试从 attrs 或者 props 中取值,如果 preserve 为 false 的时候从对象中删除掉;
function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    // 判断 hash (props、attrs)对象中是否存在 key 或者 altKey
    // 存在则设置给 res => res[key] = hash[key]
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}

2.4 installComponentHooks

方法位置:src/core/vdom/create-component.js -> function installComponent

方法参数:data, 组件的 data 对象

方法作用:为组件的 data 对象增加 hook 对象,hook 中有四方法:

  1. init,组件初始化时调用,这个方法将在 Vue.prototype._render 方法中调用,相当于创建 Vue 实例
  2. prepatch,更新 VNode 时调用
  3. insert,执行组件的 mounted 生命周期钩子
  4. destroy,销毁组件
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})

  // 遍历 hooksToMerge = ['init', 'prepatch', 'insert', 'destroy']
  for (let i = 0; i < hooksToMerge.length; i++) {
    // 比如 key = init
    const key = hooksToMerge[i]

    // 从 data.hook 对象中获取 key 对应的方法
    const existing = hooks[key]

    // 合并用户传递的 hook 方法和框架自带的 hook 方法,支持多个 hook 方法
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

2.5 componentVNodeHooks

在组件的 patch 期间会调用这些钩子,实现组件的初始化、更新、挂载和销毁,在稍晚的小作文中就会看到 init 的执行,init 执行就是让子组件重新走一遍 Vue 实例的初始化过程;

const componentVNodeHooks = {
  // 初始化
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // 被 keep-alive 包裹的组件
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // 执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数,接着走挂载、patch 这条路直到组件渲染到页面
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  // 更新 VNode,用新的 VNode 配置更新就的 VNode
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 新的 VNode,用新的 VNode 配置更新就的 VNode 上的各种属性
    const options = vnode.componentOptions
    // 老的 VNode 组件的组件实例
    const child = vnode.componentInstance = oldVnode.componentInstance

    // 用 vnode 上的属性更新 child 上的各种属性
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  // 执行组件的 mounted 生命周期钩子
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode

    // 如果组件未挂载,则调用组件的 mounted 生命周期钩子
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }

    // 处理 keep-alive 组件的异常情况
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  // 销毁组件:
  // 1. 如果组件被 keep-alive 组件包裹,则组件失活,并不销毁组件实例,从而缓存组件状态
  // 2. 如果组件未被 keep-alive 组件包裹,则直接调用实例的 $destroy 方法销毁组件实例
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

2.6 VNode

类的位置:src/core/vdom/vnode.js -> class VNode

构造函数参数:

  1. tag, 标签名,自定义组件的标签为 vue-component-${cid}-${name};
  2. data, data 对象;
  3. children,子节点列表;
  4. elm:元素节点;
  5. context:上下文,Vue 实例或者组件实例;
  6. componentOptions:组件选项;
  7. asyncFactory:异步组件工厂函数;

类的作用:创建 VNode 实例VNode 实例中包含了元素节点所需的全部信息,并绑定了对应的组件实例;

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
  get child (): Component | void {
    return this.componentInstance
  }
}

三、总结

本篇小作文讲述了 Vue 处理自定义组件产生 VNode 的方法 createComponent;这里最需要理解的就是子组件不是由 Vue 直接创建的,而是由 Vue 的子类创建的;

我们日常的开发,以写 .vue 文件为例,在 script 部分,我们导出的其实就是一个选项对象,但是即便写多个组件,他们之间互不影响,这就是因为多个组件是多个实例实例之间是天然隔离的。既然说是实例,那么就是由构造函数创建,这个构造函数就是通过咱们写的这个选项对象扩展得来的子类

还有一个重点就是每个组件都会有自己的 hook 对象,这个对象后四个方法 initprepatchinsertdestroy 四个钩子方法,其中 init 负责子组件实例的初始化,这个过程相当于 new Vue 的全流程,包含子组件的模板编译、渲染函数的创建过程;