This article will analyze how vNodes are created in VUE by interpreting the render function source code. In ve2. X, whether you write the render function directly, use the template or el attribute, or use a.vue single file, you need to compile the render function to create a VNode and render it into the DOM. If you are not familiar with the vUE source directory, it is recommended to read vUE – source directory and compilation process.

Render function

Render method defined in the file SRC/core/instance/render. Js

  Vue.prototype._render = function () :VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // ... 
    // 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 {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      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
      }
    }
    // 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(! (vnodeinstanceof 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()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}
Copy the code

_render is defined on the prototype of vue and returns vNode, which is created with render. Call (vm._renderProxy, vm.$createElement). During vNode creation, if an error occurs, the code in catch is executed to degrade the vNode. The core code in _render is

vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code

Render vm._renderProxy vm.$createElement vm. renderProxy vm.$createElement

Render function

Const {render, _parentVnode} = vm.$options render method is extracted from $options. The Render method comes in two ways

  1. The developer hand-writes the render function directly in the component
  2. Generated by compiling the template property

Parameters of the vm. _renderProxy

Vm. _renderProxy defined in SRC/core/instance/init. Js, is the call of the first parameter, specify the render function execution context.

   /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
Copy the code
The production environment

Vm. _renderProxy = VM, that is, in production, the context in which the render function is executed is the current Vue instance, that is, this of the current component.

The development environment

Development environment will perform initProxy (vm), initProxy defined in the file SRC/core/instance/proxy. Js.

  let initProxy
  // ...
  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}
Copy the code

HasProxy is defined as follows

 const hasProxy =
    typeof Proxy! = ='undefined' && isNative(Proxy)
Copy the code

Proxy used to determine whether the browser supports ES6.

The function of Proxy is to intercept an object when accessing it. The first parameter of new Proxy indicates the object to be intercepted, and the second parameter is the object used to customize the interception behavior.

The development environment intercepts vm instances if Proxy is supported, otherwise it assigns VMS directly to vm._renderProxy as in production. The specific interception behavior is specified by the Handlers object. Handlers = hasHandler when we write the render function by hand, and handlers = getHandler when we generate the render function by template. HasHandler code:

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) = = ='_' && !(key in target.$data))
      if(! has && ! isAllowed) {if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      returnhas || ! isAllowed } }Copy the code

GetHandler code

  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }
Copy the code

HasHandler and getHandler intercepts the read of attributes of VM objects and the operation of propKey in proxy respectively, and verifies the parameters of VM. Call warnNonPresent and warnReservedPrefix to Warn. It can be seen that the initProxy method is mainly used to intercept VM instances during development, discover problems, and throw errors so that developers can rectify problems in a timely manner.

Parameters of the vm. $createElement method

vm.CreateElement is the createElement function passed in when render is used. It is defined in the initRender method, which is executed when new Vue is initialized with the instance VM as an argument.

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)
  // ...
}
Copy the code

Vm. $createElement is the method for the developer’s hand-written render function, vm._c is the method for the render function generated by compiling the template. They both call the createElement method.

CreateElement method method

The createElement method is defined in the SRC /core/vdom/create-element.js file

  const SIMPLE_NORMALIZE = 1
  const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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)
}
Copy the code

The createElement method does some processing on the parameters and then calls _createElement to create a VNode. Let’s take a look at the parameters that createElement can receive in the Vue document.

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // An HTML tag string, component option object, or
  // Parse an async function of any of the above. Required parameters.
  'div'.// {Object}
  // A data object containing attributes related to the template
  // You can use these features in template. This parameter is optional.{},// {String | Array}
  // Child virtual nodes (VNodes), built from 'createElement()',
  // You can also use strings to generate "text virtual nodes". This parameter is optional.
  [
    'Write some words first.',
    createElement('h1'.'A headline'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'}})])Copy the code

In this document, all parameters are optional except the first parameter. This means that when you use createElement, you can pass only the first and third parameters without passing the second. The argument processing we just talked about is dealing with this situation.

 if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
Copy the code

Check whether data is an array or an underlying type. If this condition is met, the argument passed at this position is children, and then reassign the parameters. This approach is called overloading.

Overloading: the function name is the same, the function argument list is different (including the number of arguments and the parameter type), and the return type can be the same or different.

After processing the parameters, call the _createElement method to create a VNode. Here is the core code for the _createElement method.

export function _createElement (context: Component, tag? : string | Class
       
         | Function | Object, data? : VNodeData, children? : any, normalizationType? : number
       ) :VNode | Array<VNode> {
  // ...
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    // ...
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      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()
  }
}
Copy the code

The createEmptyVNode method is invoked to create an empty vNode if data is responsive and the Component’s IS property is not true. Next, the children parameter is processed by calling the normalizeChildren or simpleNormalizeChildren methods based on the value of normalizationType. These two methods defined in SRC/core/vdom/helpers/normalize – children. Js file.

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
Copy the code

The purpose of both normalizeChildren and simpleNormalizeChildren is to flatten the Children array to return a one-dimensional array of VNodes. SimpleNormalizeChildren works with functional components, so you only need to consider children as a two-dimensional array. The normalizeChildren method takes into account the case that children are multiple nested arrays. NormalizeChildren will start by determining the type of children. If children is a basic type, create a text vnode directly. If it is an array, call normalizeArrayChildren. The normalizeArrayChildren method is called recursively to convert children into a one-dimensional array. Next, look at the _createElement method. If the tag parameter is a component and not a String, call createComponent to create a VNode. If the tag is a String, the VNode instance is created by calling new VNode() and passing in different parameters.

In either case, the VNode class is ultimately used to create the VNode. Here is the source code for the VNode class, defined in the file SRC /core/vdom/vnode.js

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 nodesfnOptions: ? ComponentOptions;// for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtoolsfnScopeId: ? 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 / / tag name
    this.data = data // Current node data
    this.children = children / / child nodes
    this.text = text / / text
    this.elm = elm // The corresponding real DOM node
    this.ns = undefined // Namespace
    this.context = context // The current node context
    this.fnContext = undefined // Functional component context
    this.fnOptions = undefined // Functional component configuration parameters
    this.fnScopeId = undefined // The functional component ScopeId
    this.key = data && data.key // Key property of the child node
    this.componentOptions = componentOptions // Component configuration items
    this.componentInstance = undefined // Component instance
    this.parent = undefined / / the parent node
    this.raw = false // Whether it is a native HTML fragment or just plain text
    this.isStatic = false // Static node tag
    this.isRootInsert = true // Whether to insert it as the root node
    this.isComment = false // Whether it is a comment node
    this.isCloned = false // Whether it is a clone node
    this.isOnce = false // Whether there is a V-once command
    this.asyncFactory = asyncFactory // Asynchronous factory method
    this.asyncMeta = undefined Asynchronous Meta / /
    this.isAsyncPlaceholder = false // Whether to use asynchronous placeholders
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
Copy the code

The VNode class defines data that describes a VNode. At this point, the render function to create vDOM source code analysis is finished, we simply summarize the comb.

_render is defined in vue. prototype, _render function execution will call the method render, in the development environment, will be on the VM instance proxy, verify vm instance data correctness. Within the render function, the createElement method of render is executed, and the createElement method processes the parameters. Call _createElement after processing the argument. The _createElement method eventually creates a VNode instance by calling new VNode() directly or indirectly.

vnode && vdom

The vNode returned by createElement is not a real DOM element. The full name of a VNode is “Virtual Node.” It contains information that tells the Vue what Node to render on the page and its children. We often refer to the “Virtual DOM” as the name for the entire VNode tree built from the Vue component tree.

tips

Read the source code do not only look at the source code, must be combined with the specific use of analysis, so as to better understand the intent of a section of code. Like the render function in this article, if you have never used the render function, directly read the source code may be more difficult, might as well take a look at the document, write a demo, see the specific use, and then compare the use to analyze the source code, so that many more confusing problems will be solved.

This article is some understanding of their own reading source code, if there are wrong places, thank you for pointing out.