Super large cup with rich American ☕️, chat about VNode generation details.

Before we begin, let’s review the execution diagram:

  1. Vue executes engineering simply to understand the entire process;
  2. The template compiler details the process of generating the render function.
  3. Reactive talked in detail about the intermediate process, the data dependent collection and distribution update process;

As the process of _render and _update is quite complicated, it is divided into two periods. Let’s start with the _render process in this installment.

VNode

If not for the virtual DOM, jump to knowing the virtual DOM. This section goes into more detail about render’s VNode generation process. They’re going to be reactive. They’re going to be reactive.

export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q  = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners }Copy the code

Check out a few of them at 🌰 :

<div id="app">
  <Child a="hello vue" @click="handleClick"></Child>
  <ul>
    <li v-for="item of list" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</div>
<script>
  let Child = Vue.extend({
    name: 'Child'.props: {
      a: String
    },

    template: `
       
{{ a }}
`
}) new Vue({ el: '#app'.components: { Child }, data() { return { list: [{ name: 'A'.id: 'A' }, { name: 'B'.id: 'B' }, { name: 'C'.id: 'C' }, { name: 'D'.id: 'D'}}; },methods: { handleClick () { console.log('click event'); }}})
</script> Copy the code

🌰 generates the following render function:

with (this) {
  return _c('div', {
    attrs: {
      "id": "app"
    }
  }, [_c('child', {
    attrs: {
      "a": "hello vue"
    },
    on: {
      "click": handleClick
    }
  }), _v(""), _c('ul', _l((list), function(item) {
    return _c('li', {
      key: item.id
    }, [_v("\n " + _s(item.name) + "\n ")])}))],1)}Copy the code

RenderList is defined in SRC /core/instance/render-helpers/render-list.js:

export function renderList (val: any, render: ( val: any, keyOrIndex: string | number, index? : number ) => VNode): ?Array<VNode> {
  let ret: ?Array<VNode>, i, l, keys, key

  / / array
  if (Array.isArray(val) || typeof val === 'string') {
    ret = new Array(val.length)
    // Iterate through the group to generate VNode
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    } 
  // A single number can be traversed
  } else if (typeof val === 'number') {
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  // Iterate over the object
  } else if (isObject(val)) {
    keys = Object.keys(val)
    ret = new Array(keys.length)
    for (i = 0, l = keys.length; i < l; i++) {
      key = keys[i]
      // The first argument is the value, the second argument is the object key, and the third argument is the array position
      ret[i] = render(val[key], key, i)
    }
  }
  if (isDef(ret)) {
    (ret: any)._isVList = true
  }
  return ret
}
Copy the code

Render (renderList) {render (renderList);

function (item) {
   return _c('li', { key: item.id }, [
     _v("\n " + _s(item.name) + "\n ")])}Copy the code

If you look at it from the inside out, first of all, _s is simpler, which is toString function:

export function toString (val: any) :string {
  return val == null
    ? ' '
    : typeof val === 'object'
      ? JSON.stringify(val, null.2)
      : String(val)
}
Copy the code

And then _v, which corresponds to createTextVNode, which is defined in core/vdom/vnode:

/ * * *@file Virtual node definition */
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 cachingfnScopeId: ? string;// functional scope id support

  constructor (tag? : string,/ / tag namedata? : VNodeData,/ / data
    children?: ?Array<VNode>, / / child nodestext? : string,/ / textelm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function
  ) {
    / / tag name
    this.tag = tag
    // The corresponding object contains specific data information
    this.data = data
    / / child nodes
    this.children = children
    this.text = text
    // The actual DOM
    this.elm = elm
    // Namespace
    this.ns = undefined
    this.context = context
    // Function component scope
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    // Whether the tag is native HTML or plain text
    this.raw = false
    // Mark the static node
    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 instance */
  get child (): Component | void {
    return this.componentInstance
  }
}

/** * Create text VNode */
export function createTextVNode (val: string | number) {
  return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code

Then we can see more complex _c createElement method function, defined in SRC/core/instance/render. Js:

// Write the template call
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false);

// Hand-write the render function call
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
Copy the code

CreateElement is located in SRC /core/vdom/create-element.js:

export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {

  // If data is an array or a base type, the children parameter is passed, and it needs to be shifted back one bit
  // The number of parameters is uniform processing
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // Render: true; // render: true
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
Copy the code

The function determines whether data is an array or a primitive type, and moves the argument back one bit if it is. When we write our own render function, we can write children as the second parameter if there is no propertyrelated configuration, for example 🌰 :

/ * * *@demo Write 🌰 */ for the render function
new Vue({
  el: '#app',

  render (h) {
    return h('div', [
      h('span'.'hello vue! ')]); }})Copy the code

H (‘span’, ‘hello vue! ‘) the second argument is neither an array nor a base type, and the argument is moved back one bit, so _createElement(context, ‘span’, undefined, ‘Hello vue! ‘, 2).

After the arguments are processed, _createElement is called:

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {

  /** * if the data parameter is passed and the __ob__ of the data has been defined (meaning that the data has been bound to the Oberver object), * https://cn.vuejs.org/v2/guide/render-function.html# constraint * that * / create an empty node
  if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
      context
    )
    return createEmptyVNode()
  }

  // object syntax in v-bind
  // Use 
      
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }

  // If there is no tag name, create an empty node
  if(! tag) {// in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive 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
      )
    }
  }
  // support single function children as default scoped slot
  // Default scope slot
  if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }

  // To normalize the subcomponent parameters, the hand-written render function will enter the first branch
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    // Get the namespace
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // Check whether the tag is reserved
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      // If yes, create the corresponding node
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // Look for the tag in the COMPONENTS of the VM instance's Option. If it exists, it is a component and the corresponding node is created. Ctor is the component's constructor class
      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
      /* Unknown elements, checked at run time, because the parent component may allocate a namespace */ when the child component is sequenced
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    }
  } else {
    // direct component options / constructor
    // Create the component
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    // If there is a namespace, the namespace is recursively applied to all child nodes
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    // If the vnode is not created successfully, create an empty vnode
    return createEmptyVNode()
  }
}
Copy the code

Next look at the handwritten render 🌰, which will execute to normalizeChildren:

if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children)
}

export function normalizeChildren (children: any): ?Array<VNode> {
  // Create a text VNode if it is a basic child, otherwise call normalizeArrayChildren processing
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]

    // Node c is an array, called recursively
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    // If node C is the base type, convert it to VNode using the createTextVNode method
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if(c ! = =' ') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__ `
        }
        res.push(c)
      }
    }
  }
  return res
}
Copy the code

The normalizeArrayChildren function handles three cases:

  1. The childcIt’s an array. It’s a recursive callnormalizeArrayChildren
  2. The childcIs a normal type and creates the textVNodeProcessing;
  3. The childcIs alreadyVNodeType: Here are two more cases if the childchildrenIf it’s a nested array, it will be defined automaticallykeyOtherwise, create the text VNode.

In all three cases, the isTextNode is used to determine that if the two nodes are text vnodes, the two nodes will be merged. And it returns an array of VNodes.

Go back to the _c function in the first 🌰 to analyze the component and retain the label VNode generated. Look at the component first:

_c('child', {
  attrs: {
    "a": "hello vue"
  },
  on: {
    "click": handleClick
  }
})
Copy the code

IsDef (Ctor = resolveAsset(context.$options, ‘components’, tag)) The value of context.$options in 🌰 will print:

! [] (/ Users/apple/Documents/vue source series / / cafe chat patch with diff/context. The $options. JPG)

Is this the options in the app when performing a _init assignment, this part of logic is located in the SRC/core/instance/init. Js:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
Copy the code

Then look at the resolveAsset function, which is located in SRC /core/util/options.js:

export function resolveAsset (
  options: Object, type: string, id: string, warnMissing? : boolean) :any {
  /* istanbul ignore if */
  if (typeofid ! = ='string') {
    return
  }
  
  const assets = options[type]
  // check local registration variations first
  // Start with id
  if (hasOwn(assets, id)) return assets[id]

  // Change the id to a hump
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]

  // Change the first letter to a capital form on the basis of the hump
  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
}
Copy the code

ResolveAsset goes through various transformations (hump, capital hump) and finds the definition of the component. Then create the component VNode through createComponent:

export function createComponent (

  // The component constructor
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  // Construct the subcomponent constructor, defined at initGlobalAPI time
  const baseCtor = context.$options._base // => Vue

  // plain options object: turn it into a constructor
  // Export default {... Export default vue.extend ({}) and export default {} are both valid
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  // If Ctor is not a constructor at this stage or an asynchronous component factory returns directly
  if (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
    }
    return
  }

  / /... Omit asynchronous component factories

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  // Extract the props attribute from VNodeData based on the options definition of the component
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component

  // Functional component, stateless, no instance
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // Install the component hook function
  // Add componentVNodeHooks to data.hook, and execute the hook functions when VNode executes patch
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag

  // Instantiate vNode
  // The VNode structure of a component is not passed to children
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Omit the logic related to WEEX

  return vnode
}
Copy the code

For the Child component in 🌰, get and process the constructor, process the VNode properties, install the component hook, and finally generate the component VNode:

After looking at the component VNode, take a look at HTML and SVG (including SVG here at ⚠️!). .

In the case of retaining the tag, this article uses the Li tag as 🌰. Child-like procedures are not stated, but the following branch will be executed at _createElement:

if (config.isReservedTag(tag)) {
  // platform built-in elements
  vnode = new VNode(
    config.parsePlatformTagName(tag), data, children,
    undefined.undefined, context
  );
}
Copy the code

VNode generated by li in 🌰 :

conclusion

When _render is executed, various dwarf functions are pulled up to generate vNodes. We focused on _l and _c. _c is divided into two cases: template and render. After processing the arguments, call _createElement. This function does two things: regulate the child elements and generate VNode. Vnodes can be generated in two different ways: built-in labels and components. The component VNode is generated by createComponent, which does three things:

  1. Ctor-> create constructor;
  2. installComponentHooks-> install component hooks;
  3. new VNode-> Instantiate the componentVNode;

There are also many branch processes in the code, such as asynchronous factory functions, processing of dynamic components, and so on. These are not the main processes, you can run into problems and look back (lazy 😄). With VNode in hand, the next section details the update process — VNode generates the DOM and the interview required question Diff.