Vue2.x源码学习笔记(十二)——vm._render生成虚拟DOM

400 阅读3分钟

vm._render

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
    vm.$vnode = _parentVnode
    let vnode
    try {
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    vnode.parent = _parentVnode
    return vnode
  }

_render方法实际调用的是options中的render函数。如果用户没有提供render函数,则会通过编译模板生成。通过调用render函数生成一个vnode,然后返回这个vnode。render函数的参数是$createElement也就是我们平常使用的createElement函数。调用render函数实际就是获取createElement函数的返回值。

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

$createElement

  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

vm.$createElement函数是用户手写提供的render函数,而vm._c是编译后生成的render函数。它们都调用createElement函数。a代表标签,b代表数据对象,c是子节点数组。

createElement

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
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
  }
  return _createElement(context, tag, data, children, normalizationType)
}

当用户在render函数中使用createElement($createElement)时,因为数据对象参数可选,所以用户可能直接传标签和子节点。因此需要判断参数b的类型是不是对象,如果不是对象则需要对参数进行处理:没传数据对象所以data是undefined,这里传进来的data其实是子节点children。用户的createElement情况下规范子节点的类型是ALWAYS_NORMALIZE即2。处理完参数后会调用_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__)) {
    //warn
    return createEmptyVNode()
  }
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    return createEmptyVNode()
  }
  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)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    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)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

如果data数据对象是一个响应式对象则会抛出警告,并且返回一个空的注释vnode节点。如果data数据对象中有is属性,则说明它是一个动态组件,则将is的值作为tag。如果动态组件的is属性是一个假值时,tag为false,则返回一个空的注释vnode节点。如果data数据对象中有key属性,会检测key是否是string或者number类型,否则抛出警告。如果子节点数组中只有一个函数时,将它当作默认插槽,然后清空子节点列表。根据不同的类型将子元素进行标准化处理。

如果标签tag是平台保留tag,类似div,span....。如果平台保留标签上的事件v-on有.native指令会抛出错误,因为.native只能在组件上。最后会通过new VNode创建虚拟DOM。

如果tag是一个自定义组件,在this.$options.components对象中找到指定标签名称的组件构造函数通过createComponent创建组件的VNode。

对于不知名的一个标签,也生成 VNode,因为考虑到在运行时可能会给一个合适的名字空间。

如果tag为非字符串,比如可能是一个组件的配置对象或者是一个组件的构造函数,通过createComponent创建组件的VNode。

createComponent

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
  }
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
  data = data || {}
  resolveConstructorOptions(Ctor)
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
  const listeners = data.on
  data.on = data.nativeOn
  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }
  installComponentHooks(data)
  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
  )
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }
  return vnode
}

如果组件的构造函数不存在则直接return。如果Ctor是一个对象,则说明Ctor是一个配置对象,context.$options._base就是Vue,通过Vue.extend将Ctor转为构造函数。经过操作后如果Ctor依然不是一个函数则表示这是一个无效的组件定义。

对于异步组件的处理:为异步组件返回一个占位符节点,组件被渲染为注释节点,但保留了节点的所有原始信息,这些信息将用于异步服务器渲染。

然后通过resolveConstructorOptions对组件做选项合并。当编译器将组件编译为渲染函数,渲染时执行render函数,然后执行其中的 _c时,这里会解析构造函数选项,并合基类选项,以防止在组件构造函数创建后应用全局混入。

通过transformModel将组件的v-model的信息(值和回调)转换为 data.attrs 对象的属性、值和data.on对象上的事件、回调。

通过extractPropsFromVNodeData提取props数据,得到propsData对象,propsData[key] = val,以组件props配置中的属性为key,父组件中对应的数据为 value。

处理函数式组件:执行函数式组件的render函数生成组件的 VNode,做了以下 3 件事:1、设置组件的 props 对象,2、设置函数式组件的渲染上下文,传递给函数式组件的render函数,3、调用函数式组件的render函数生成vnode。

获取事件监听器对象 data.on,因为这些监听器需要作为子组件监听器处理,而不是 DOM 监听器。

将带有.native修饰符的事件对象赋值给data.on。

如果是抽象组件,则值保留 props、listeners 和 slot。

通过installComponentHooks在组件的data对象上设置hook对象,hook 对象增加四个属性:init、prepatch、insert、destroy,负责组件的创建、更新、销毁,这些方法在组件的 patch 阶段会被调用。最后创建组件的Vnode。

resolveConstructorOptions

function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      Ctor.superOptions = superOptions
      const modifiedOptions = resolveModifiedOptions(Ctor)
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

这个方法在Vue初始化时也会用到,当Vue初始化时,不存在父类,所以直接返回Vue构造函数的options。 如果存在父类,则递归调用resolveConstructorOptions去解析获得父类构造函数的options。从子类的superOptions属性上获取缓存的父类构造函数的options,与解析到的真正的父类构造函数的options相比较。如果不相同,则说明父类的配置项发生了更改,更新子类的superOptions属性。通过resolveModifiedOptions找到更改的选项,将更改的选项和extend选项合并,然后将新的选项赋值给options,并更新子类构造函数的options。

resolveModifiedOptions

function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified
  const latest = Ctor.options
  const sealed = Ctor.sealedOptions
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

将之前的optins,也就是之密封(浅拷贝)的options,与最新的options进行比较找出不同点。

transformModel

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
  }
}

在data的attrs对象上存放v-model的值,在data的on对象上存放v-model事件。获取v-model中事件对应的回调函数,然后看原来的on中是否存放了这个事件名,如果没存放则添加,如果已经存放了则合并成数组。

extractPropsFromVNodeData

function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }
  const res = {}
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    for (const key in propOptions) {
      const altKey = hyphenate(key)
      if (process.env.NODE_ENV !== 'production') {
        const keyInLowerCase = key.toLowerCase()
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          //tip
        }
      }
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}

如果未定义props直接返回。否则便利porps,将小驼峰形式的key转换为连字符形式。

checkProp

function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    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
}

判断 hash(props/attrs)对象中是否存在key或altKey,存在则设置给 res => res[key] = hash[key]