[Vue源码学习] _render(上)

751 阅读5分钟

系列文章

前言

从上一章节中我们知道,在调用$mount进行挂载的过程中,会执行updateComponent方法:

/* core/instance/lifecycle.js */
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

可以看到,在updateComponent方法中,首先会调用_render方法,生成组件对应的VNode,然后调用_update方法,根据VNode渲染成真实的DOM。那么接下来,我们就来看看_render方法是如何生成VNode的。

_render

_render是在引入Vue时添加到Vue.prototype上的,代码如下所示:

/* core/instance/render.js */
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
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    // 根据渲染函数,生成VNode
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    // ...
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

可以看到,在_render方法中,首先从$options上取出渲染函数render、父占位符节点_parentVnode,对于子组件来说,此时还会调用normalizeScopedSlots,用于解析作用域插槽,然后将_parentVnode赋值给实例的$vnode,从这里可以看出,由于根组件不存在父占位符节点,所以它的$vnodeundefined,当根组件完成挂载后,在$mount的最后会调用mounted钩子函数,而子组件则会跳过这段逻辑:

/* core/instance/lifecycle.js */
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
}

接着就会调用render渲染函数,它是_render的核心逻辑,除了生成VNode外,Vue还在此过程中完成了对依赖的收集。这里的render函数可以是经过vue-loader处理过的template模板,也可以是用户手写的render函数,总之,render的返回结果就是当前组件的渲染根VNode,然后通过parent属性,构建渲染VNode与占位符VNode之间的父子关系。

那么接下来,我们就来看看对于单个节点,Vue是如何生成VNode的。

createElement

Vue提供了两个方法生成VNode,一个是_c,一个是$createElement,通过vue-loader生成的渲染函数,其内部使用的是vm._c,手写渲染函数时,传入的第一个参数是vm.$createElement

/* core/instance/render.js */
export function initRender(vm: Component) {
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

可以看到,这两个方法内部都是调用createElement方法,只是最后一个参数不同。

其实简单点来说,createElement就是用来创建VNode,而对于每个VNode节点,最关键的无非是三样东西:

  1. tag:标签名,表示当前VNode是何种标签元素,比如divp,也可以表示组件。

  2. data:节点数据,表示当前VNode上所有与之相关的数据,比如attrson等。

  3. children:子节点,表示当前VNode的所有子节点。

那么接下来,我们就详细看看createElement方法的实现:

/* core/vdom/create-element.js */
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
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // ...
  // 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
  }
  // 规范化子节点
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 根据tag,生成普通节点或组件节点
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && 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 {
    // 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)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

可以看到,createElement就是对_createElement方法的包装,在_createElement方法中,首先根据normalizationType,调用normalizeChildrensimpleNormalizeChildren方法,规范化子节点,其实内部就是用来合并相邻的文本节点,将嵌套子节点展平的逻辑,处理完成后,children是一个VNode类型的一维数组。

接下来,就是根据tag的类型生成不同种类的VNode节点,我们首先来看看对于普通元素节点,Vue是如何创建VNode的。对于普通元素节点来说,其tag肯定是一个字符串,所以typeof tag === 'string'true,接着通过isReservedTag判断tag是否是平台内置的标签元素,如果是的话,则说明该节点是普通元素节点,就直接调用VNode构造函数,创建元素节点对应的VNodeisReservedTag方法的代码如下所示:

/* platforms/web/util/element.js */
export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}

export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)

最后,就可以通过tagdatachildren,创建一个VNode,它所包含的信息就会告诉Vue页面上需要渲染什么样的节点以及其子节点。

通过上面的分析,我们已经知道tagchildren分别代表标签名和子节点,那么data中又应该包含哪些信息呢?我们可以从VNodeData中找到:

export interface VNodeData {
  key?: string | number;
  // 普通命名插槽
  slot?: string;
  // 作用域插槽(在父组件占位符VNode中存在)
  scopedSlots?: { [key: string]: ScopedSlot | undefined };
  ref?: string;
  refInFor?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | object[] | object;
  // 组件的prop
  props?: { [key: string]: any };
  // html attribute 通过el.setAttribute设置
  attrs?: { [key: string]: any };
  // DOM property 通过el[prop]=xxx设置
  domProps?: { [key: string]: any };
  // VNode的hook
  hook?: { [key: string]: Function };
  // html原生事件 + 组件自定义事件
  on?: { [key: string]: Function | Function[] };
  // 定义在组件上的原生事件
  nativeOn?: { [key: string]: Function | Function[] };
  transition?: object;
  show?: boolean;
  inlineTemplate?: {
    render: Function;
    staticRenderFns: Function[];
  };
  // 自定义指令
  directives?: VNodeDirective[];
  keepAlive?: boolean;
}

总结

通过createElement方法,可以生成对应的VNode节点,对于一个普通的元素节点来说,只需要给定tagdatachildren,就可以完整的描述对应的真实节点。