Interpretation of Vue source code (11) -- render helper

Posted by mac.php on Tue, 08 Mar 2022 06:14:40 +0100

preface

Last article Interpretation of Vue source code (10) -- generating rendering function of compiler Finally, when it comes to component update, you need to execute the rendering function generated by the compiler to get the vnode of the component.

The reason why the rendering function can generate vnode is through it_ c,_ l,,_ v,_ s and other methods. For example:

  • Ordinary nodes are compiled into executables_ c function

  • The v-for node is compiled to be executable_ l function

  • ...

But so far, we don't know the principle of these methods and how they generate vnode s? All we know is that they are Vue instance methods. Today we will find the answer from the source code.

target

On the basis of Vue compiler, further understand how a component generates VNode through these runtime tool methods (render helper)

Source code interpretation

entrance

We know that these methods are Vue instance methods. According to the previous understanding of the source code, the instance methods are generally placed in the / src/core/instance directory. Actually, before Interpretation of Vue source code (6) -- example method I saw render helper at the end of the article.

/src/core/instance/render.js

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  // Mount some tools and methods needed for running on the component instance
  installRenderHelpers(Vue.prototype)
  
  // ...
}

installRenderHelpers

/src/core/instance/render-helpers/index.js

/**
 * Mount the simplified rendering tool functions on the instance. These are runtime code
 * These tool functions are used in the rendering functions generated by the compiler
 * @param {*} target Vue example
 */
export function installRenderHelpers(target: any) {
  /**
   * v-once The runtime helper of the instruction, which marks VNode statically
   * It's a little redundant, because nodes with v-once instructions are treated as static nodes, so they won't go here
   */
  target._o = markOnce
  // Convert values to numbers
  target._n = toNumber
  /**
   * Convert the value to string form, ordinary value = > string (VAL), object = > JSON stringify(val)
   */
  target._s = toString
  /**
   * Render the help function of the v-for list at runtime, loop through the val value, execute the render method for each item in turn to generate VNode, and finally return a VNode array
   */
  target._l = renderList
  target._t = renderSlot
  /**
   * Judge whether the two values are equal
   */
  target._q = looseEqual
  /**
   * Equivalent to indexOf method
   */
  target._i = looseIndexOf
  /**
   * The help program of VNode responsible for generating static tree at runtime completes the following two things
   *   1,Execute the rendering function with the specified subscript in the staticRenderFns array, generate the VNode of the static tree and cache it, and read it directly from the cache during the next rendering (isInFor must be true)
   *   2,Mark the VNode of the static tree statically
   */
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  /**
   * Create VNode for text node
   */
  target._v = createTextVNode
  /**
   * Create VNode for empty node
   */
  target._e = createEmptyVNode
}

_o = markOnce

/src/core/instance/render-helpers/render-static.js

/**
 * Runtime helper for v-once.
 * Effectively it means marking the node as static with a unique key.
 * v-once The runtime helper of the instruction, which marks VNode statically
 * It's a little redundant, because nodes with v-once instructions are treated as static nodes, so they won't go here
 */
export function markOnce (
  tree: VNode | Array<VNode>,
  index: number,
  key: string
) {
  markStatic(tree, `__once__${index}${key ? `_${key}` : ``}`, true)
  return tree
}

markStatic

/src/core/instance/render-helpers/render-static.js

/**
 * Mark VNode statically and add three attributes to VNode:
 * { isStatick: true, key: xx, isOnce: true or false } 
 */
function markStatic (
  tree: VNode | Array<VNode>,
  key: string,
  isOnce: boolean
) {
  if (Array.isArray(tree)) {
    // The tree is an array of vnodes. It loops through each VNode and makes static marks for each VNode
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== 'string') {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    markStaticNode(tree, key, isOnce)
  }
}

markStaticNode

/src/core/instance/render-helpers/render-static.js

/**
 * Mark static VNode
 */
function markStaticNode (node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}

_l = renderList

/src/core/instance/render-helpers/render-list.js

/**
 * Runtime helper for rendering v-for lists.
 * Render the help function of the v-for list at runtime, loop through the val value, execute the render method for each item in turn to generate VNode, and finally return a VNode array
 */
export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode> {
  let ret: ?Array<VNode>, i, l, keys, key
  if (Array.isArray(val) || typeof val === 'string') {
    // val is an array or string
    ret = new Array(val.length)
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
  } else if (typeof val === 'number') {
    // If val is a numeric value, all numbers of 0 - val will be traversed
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    // val is an object and traverses the object
    if (hasSymbol && val[Symbol.iterator]) {
      // val is an iteratable object
      ret = []
      const iterator: Iterator<any> = val[Symbol.iterator]()
      let result = iterator.next()
      while (!result.done) {
        ret.push(render(result.value, ret.length))
        result = iterator.next()
      }
    } else {
      // val is a normal object
      keys = Object.keys(val)
      ret = new Array(keys.length)
      for (i = 0, l = keys.length; i < l; i++) {
        key = keys[i]
        ret[i] = render(val[key], key, i)
      }
    }
  }
  if (!isDef(ret)) {
    ret = []
  }
  // Return VNode array
  (ret: any)._isVList = true
  return ret
}

_m = renderStatic

/src/core/instance/render-helpers/render-static.js

/**
 * Runtime helper for rendering static trees.
 * The help program of VNode responsible for generating static tree at runtime completes the following two things
 *   1,Execute the rendering function with the specified subscript in the staticRenderFns array, generate the VNode of the static tree and cache it, and read it directly from the cache during the next rendering (isInFor must be true)
 *   2,Mark the VNode of the static tree statically
 * @param { number} index Indicates the index of the rendering function of the current static node in the staticRenderFns array
 * @param { boolean} isInFor Indicates whether the current static node is wrapped inside the node containing the v-for instruction
 */
 export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  // Cache: when the static node is rendered for the second time, the cached VNode is directly obtained from the cache
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  // If the current static tree has been rendered once (i.e. with cache) and is not wrapped inside the node where the v-for instruction is located, the cached VNode is returned directly
  if (tree && !isInFor) {
    return tree
  }
  // Execute the specified elements in the staticRenderFns array (rendering function of the static tree) to generate the VNode of the static tree and cache it
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  // Static flag: mark the VNode of the static tree, that is, add {isstatic: true, key: ` _static _ ${index} `, isonce: false}
  markStatic(tree, `__static__${index}`, false)
  return tree
}

_c

/src/core/instance/render.js

/**
 * Definition_ c. It is a Coriolis method of createElement
 * @param {*} a Tag name
 * @param {*} b Property
 * @param {*} c Child node array
 * @param {*} d Normalized type of node
 * @returns VNode or Array<VNode>
 */
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

createElement

/src/core/vdom/create-element.js

/**
 * vnode that generates components or ordinary labels is a wrapper function, regardless
 * 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
  }
  // Execute_ The createElement method creates the VNode of the component
  return _createElement(context, tag, data, children, normalizationType)
}

_createElement

/src/core/vdom/create-element.js

/**
 * Generate vnode,
 *   1,The platform retains labels and unknown elements and executes new Vnode() to generate vnodes
 *   2,Component executes createComponent to generate vnode
 *     2.1 The functional component executes its own render function to generate VNode
 *     2.2 Ordinary components instantiate a VNode and display it in its data Four methods are set on the hook object and will be called in the patch phase of the component,
 *         Thus, it enters the instantiation and mounting stage of sub components until rendering is completed
 * @param {*} context context
 * @param {*} tag label
 * @param {*} data Property JSON string
 * @param {*} children Child node array
 * @param {*} normalizationType Node normalization type
 * @returns VNode or Array<VNode>
 */
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__)) {
    // Property cannot be a responsive object
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    // If the property is a responsive object, the VNode of an empty node is returned
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // If the is attribute of a dynamic component is a false value and the tag is false, the VNode of an empty node is returned
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // The only key detected can only be string or number
  // 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
      )
    }
  }

  // When there is only one function in the child node array, treat it as the default slot, and then clear the child node list
  // 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
  }
  // Normalize child elements
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

   /**
   * Here is the key point at the beginning. The previous ones don't need to be paid attention to. They are basically some exception handling or optimization
   */

  let vnode, ns
  if (typeof tag === 'string') {
    // When the label is a string, there are three possibilities for the label:
    //   1. Platform retention label
    //   2. Custom components
    //   3. Unknown label
    let Ctor
    // Namespace
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // Tag is a platform native tag
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        // Of v-on instruction native only works on components
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // Instantiate a VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // tag is a custom component
      // In this$ options. The component constructor for the specified label name was found in the components object
      // Create the VNode of the component. The functional component directly executes its render function to generate VNode,
      // Ordinary components instantiate a VNode and display it in its data Four methods are set on the hook object and will be called in the patch phase of the component,
      // Thus, it enters the instantiation and mounting stage of sub components until rendering is completed
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // An unknown tag, but VNode is also generated because it may be given an appropriate namespace at runtime
      // 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 {
     // tag is a non string, such as the configuration object of a component or the constructor of a component
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  // Returns the VNode of the component
  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()
  }
}

createComponent

/src/core/vdom/create-component.js

/**
 * Create the VNode of the component,
 *   1,The functional component generates the VNode of the component by executing its render method
 *   2,Ordinary components generate their VNode through new VNode(), but an important operation of ordinary components is in data Four hook functions are set on the hook object,
 *      init, prepatch, insert and destroy are called in the patch phase of the component,
 *      For example, when init method is called, it will enter the creation and mounting stage of sub component instances until rendering is completed
 * @param {*} Ctor Component constructor
 * @param {*} data Property
 * @param {*} context context
 * @param {*} children Child node array
 * @param {*} tag Tag name
 * @returns VNode or Array<VNode>
 */
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // Component constructor does not exist, end directly
  if (isUndef(Ctor)) {
    return
  }

  // Vue.extend
  const baseCtor = context.$options._base

  // When Ctor is a configuration object, through Vue Extend turns it into a constructor
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // If Ctor is still not a function up to this point, it indicates that this is an invalid component definition
  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // Asynchronous component
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // Returns a placeholder node for the asynchronous component, which is rendered as an annotation node, but retains all the original information of the node, which will be used for Asynchronous Server rendering and hydration
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  // Property JSON string of node
  data = data || {}

  // In fact, this is where the options of components are merged, that is, the compiler compiles the components into rendering functions, executes the render function during rendering, and then executes the_ c. Will come here
  // After the component constructor is created, the options are prevented from being mixed after the global constructor is parsed
  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // Convert the information (value and callback) of the v-model of the component into data The attributes, values, and data of the attrs object Events and callbacks on the on object
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // Extract props data to get propsData object, propsData[key] = val
  // Take the attribute in the component props configuration as the key and the corresponding data in the parent component as the value
  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // Functional component
  // functional component
  if (isTrue(Ctor.options.functional)) {
    /**
     * Execute the render function of the functional component to generate the VNode of the component, and do the following three things:
     *   1,Sets the props object of the component
     *   2,Set the rendering context of functional components and pass it to the render function of functional components
     *   3,Call render function of functional component to generate vnode
     */
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // Get event listener object data On, because these listeners need to be handled as subcomponent listeners, not DOM listeners
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // It will be with The event object of the native modifier is assigned to 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)) {
    // In the case of abstract components, the values are props, listeners, and slot
    // abstract components do not keep anything
    // other than props & listeners & slot

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

  /**
   * Set the hook object on the data object of the component,
   * hook Add four attributes to the object, init, prepatch, insert, destroy,
   * It is responsible for the creation, update and destruction of components. These methods will be called in the patch phase of components
   * install component management hooks onto the placeholder node
   */
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  // The VNode of the instantiated component has a special tag name for ordinary components, 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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

resolveConstructorOptions

/src/core/instance/init.js

/**
 * Parse configuration item from constructor
 */
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // Get options from instance constructor
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    // cache
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // Description the configuration item of the base class has changed
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      // Found changed options
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        // Merge the changed option with the extend option
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // Assign the new option to options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

resolveModifiedOptions

/src/core/instance/init.js

/**
 * Resolve the options that are subsequently modified or added in the constructor options
 */
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified
  // Constructor options
  const latest = Ctor.options
  // Sealed constructor option, backup
  const sealed = Ctor.sealedOptions
  // Compare the two options and record the inconsistent options
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

transformModel

src/core/vdom/create-component.js

/**
 * Convert the information (value and callback) of the v-model of the component into data The attributes, values, and data of the attrs object Events and callbacks on the on object
 * transform component v-model info (value and callback) into
 * prop and event handler respectively.
 */
function transformModel(options, data: any) {
  // Properties and events of model. The default values are value and input
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
    // In data Store the value of v-model on attrs object
    ; (data.attrs || (data.attrs = {}))[prop] = data.model.value
  // In data Events that store v-model on the on object
  const on = data.on || (data.on = {})
  // Existing event callback function
  const existing = on[event]
  // Callback function corresponding to events in v-model
  const callback = data.model.callback
  // Merge callback function
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    on[event] = callback
  }
}

extractPropsFromVNodeData

/src/core/vdom/helpers/extract-props.js

/**
 * <comp :msg="hello vue"></comp>
 * 
 * Extract props and get res[key] = val 
 * 
 * Take the attribute in props configuration as the key and the corresponding data in the parent component as value
 * When the data in the parent component is updated, the responsive update is triggered, the render is executed again, and a new vnode is generated. Here we go again
 * In this way, the corresponding data in the sub component will be updated 
 */
export function extractPropsFromVNodeData (
  data: VNodeData, // { msg: 'hello vue' }
  Ctor: Class<Component>, // Component constructor
  tag?: string // Component tag name
): ?Object {
  // Props option of component, {props: {MSG: {type: string, default: XX}}}
  
  // Here, only the original values are extracted, and the validation and default values are processed in the sub components
  // we are only extracting raw values here.
  // validation and default values are handled in the child
  // component itself.
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    // Undefined props direct return
    return
  }
  // Take the attribute in the component props configuration as the key, and the value passed down by the parent component is value
  // When the data in the parent component is updated, the responsive update is triggered, the render is executed again, and a new vnode is generated. Here we go again
  // In this way, the corresponding data in the sub component will be updated
  const res = {}
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    // Traverse propsOptions
    for (const key in propOptions) {
      // Convert the key in the form of small hump to hyphen
      const altKey = hyphenate(key)
      // Tip: if the declared props is in the form of small hump (testProps), but html is not case sensitive, test props should be used instead of testProps in the HTML template
      if (process.env.NODE_ENV !== 'production') {
        const keyInLowerCase = key.toLowerCase()
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          tip(
            `Prop "${keyInLowerCase}" is passed to component ` +
            `${formatComponentName(tag || Ctor)}, but the declared prop name is` +
            ` "${key}". ` +
            `Note that HTML attributes are case-insensitive and camelCased ` +
            `props need to use their kebab-case equivalents when using in-DOM ` +
            `templates. You should probably use "${altKey}" instead of "${key}".`
          )
        }
      }
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}

checkProp

/src/core/vdom/helpers/extract-props.js

/**
 * Key = res [Val]
 */
function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    // Judge whether there is a key or altKey in the hash (props/attrs) object
    // If it exists, set it to 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
}

createFunctionalComponent

/src/core/vdom/create-functional-component.js

installRenderHelpers(FunctionalRenderContext.prototype)

/**
 * Execute the render function of the functional component to generate the VNode of the component, and do the following three things:
 *   1,Sets the props object of the component
 *   2,Set the rendering context of functional components and pass it to the render function of functional components
 *   3,Call render function of functional component to generate vnode
 * 
 * @param {*} Ctor Component constructor 
 * @param {*} propsData Extra props object
 * @param {*} data JSON string composed of node attributes
 * @param {*} contextVm context
 * @param {*} children Child node array
 * @returns Vnode or Array<VNode>
 */
export function createFunctionalComponent (
  Ctor: Class<Component>,
  propsData: ?Object,
  data: VNodeData,
  contextVm: Component,
  children: ?Array<VNode>
): VNode | Array<VNode> | void {
  // Component configuration item
  const options = Ctor.options
  // Get props object
  const props = {}
  // props option of the component itself
  const propOptions = options.props
  // Set props object of functional component
  if (isDef(propOptions)) {
    // If props option is provided for the functional component itself, props The value of the key is set to the value of the corresponding key passed down from the component
    for (const key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject)
    }
  } else {
    // If the current functional component does not provide props option, the attribute on the component will be automatically resolved to props
    if (isDef(data.attrs)) mergeProps(props, data.attrs)
    if (isDef(data.props)) mergeProps(props, data.props)
  }

  // Render context for instantiating functional components
  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )

  // Call the render function to generate vnode and pass it to the render function_ c and rendering context
  const vnode = options.render.call(null, renderContext._c, renderContext)

  // Add some marks on the last generated VNode object to indicate that the VNode is generated by a functional component, and finally return VNode
  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    const vnodes = normalizeChildren(vnode) || []
    const res = new Array(vnodes.length)
    for (let i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
    }
    return res
  }
}

installComponentHooks

/src/core/vdom/create-component.js

const hooksToMerge = Object.keys(componentVNodeHooks)
/**
 * Set the hook object on the data object of the component,
 * hook Add four attributes to the object, init, prepatch, insert, destroy,
 * Responsible for creating, updating and destroying components
 */
 function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  // Traverse the hooksToMerge array, hooksToMerge = ['init', 'Prepatch', 'Insert', 'destroy']
  for (let i = 0; i < hooksToMerge.length; i++) {
    // For example, key = init
    const key = hooksToMerge[i]
    // From data Get the method corresponding to the key in the hook object
    const existing = hooks[key]
    // Method of key object in componentVNodeHooks object
    const toMerge = componentVNodeHooks[key]
    // Merging the hook method passed by the user and the hook method provided by the framework is actually to execute the two methods respectively
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook(f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

componentVNodeHooks

/src/core/vdom/create-component.js

// Call inline hook on component vnode during patch
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  // initialization
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // Components wrapped by keep alive
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // Create a component instance, namely new vnode componentOptions. Ctor (options) = > get Vue component instance
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // Execute the $mount method of the component and enter the mount phase. Next, get the render function through the compiler, and then go through the path of mount and patch until the component is rendered to the page
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  // Update VNode and update various attributes on the old VNode with the new VNode configuration
  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // Component configuration item of new VNode
    const options = vnode.componentOptions
    // Component instance of old VNode
    const child = vnode.componentInstance = oldVnode.componentInstance
    // Update various attributes on child with attributes on vnode
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  // Execute the mounted declaration cycle hook of the component
  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    // If the component is not mounted, the mounted declaration cycle hook is called
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // Handle exceptions of keep alive components
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  /**
   * Destroy components
   *   1,If a component is wrapped by a keep alive component, the component will be inactivated and the component instance will not be destroyed, so as to cache the state of the component
   *   2,If the component is not wrapped by keep alive, call the $destroy method of the instance directly to destroy the component
   */
  destroy (vnode: MountedComponentVNode) {
    // Get component instance from vnode
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      // If the component instance is not destroyed
      if (!vnode.data.keepAlive) {
        // If the component is not wrapped by the keep alive component, the $destroy method is called directly to destroy the component
        componentInstance.$destroy()
      } else {
        // It is responsible for deactivating the component without destroying the component instance, so as to cache the state of the component
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

createComponentInstanceForVnode

/src/core/vdom/create-component.js

/**
 * new vnode.componentOptions.Ctor(options) => Get Vue component instance 
 */
export function createComponentInstanceForVnode(
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // Check inline template rendering functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // New Vue component (options) = > Vue instance
  return new vnode.componentOptions.Ctor(options)
}

summary

The interviewer asked: how does a component become VNode?

Answer:

  • Initialize the component instance, and finally execute $mount to enter the mount phase

  • If it is a Vue that contains only the runtime JS, only directly enter the mount stage, because at this time, the component has become a rendering function, and the compilation process is completed through module packer + Vue loader + Vue template compiler

  • If precompiling is not used, you must use the full amount of Vue js

  • If it is found that there is no render option on the component configuration item during mounting, it will enter the compilation phase

  • Compiling the template string into an AST syntax tree is actually an ordinary JS object

  • Then optimize the AST, traverse the AST object, and mark whether each node is static; Then further mark the static root nodes, and the updates of these static nodes will be skipped during subsequent updates of components to improve performance

  • Next, a rendering function is generated from AST. The generated rendering function consists of two parts:

    • Responsible for generating the render function of the dynamic node VNode

    • There is also a staticRenderFns array, in which each element is a function to generate the VNode of the static node. These functions will be used as a part of the render function to generate the VNode of the static node

  • Next, put the rendering function on the configuration object of the component and enter the mount phase, that is, execute the mountComponent method

  • The final responsible for rendering and updating components is a method called updateComponent, which needs to execute VM before each execution_ Render function, which is responsible for executing the render generated by the compiler to obtain the VNode of the component

  • The specific work of generating a component into VNode is performed by the_ c,_ o,_ l,_ m and other methods. These methods are mounted on the Vue instance and are responsible for generating the component VNode at run time

Tip: here, you must first understand what VNode is. In one sentence, it is the JS object representation of the component template. It is an ordinary JS object that describes the information of each node in the component in detail

There are a lot of things to say below. In fact, just remember one sentence. Set the component configuration information, and then generate the VNode of the component through new VNode

  • _ c. VNode responsible for generating components or HTML elements_ C is the most complex and core method among all render helper methods. Others_ xx is an integral part of it

    • Receive tag, attribute JSON string, child node array and node normalization type as parameters

    • If the tag is a platform reserved tag or an unknown element, VNode can be obtained directly from new VNode (tag information)

    • If the tag is a component, execute the createComponent method to generate VNode

      • The functional component executes its own render function to generate VNode

      • Ordinary components instantiate a VNode, and in data Set four methods on the hook object, which will be called in the patch phase of the component, so as to enter the instantiation and mounting phase of the sub component, and then compile to generate the rendering function until the rendering is completed

      • Of course, some configuration processing will be carried out before generating VNode, such as:

        • The sub component options are merged, and the global configuration items are merged to the component configuration items

        • v-model for handling custom components

        • Process the props of the component, extract the props data of the component, take the attribute in the props configuration of the component as the key, and the corresponding data in the parent component as the value to generate a propsData object; When the component is updated, a new VNode is generated, and this step will be carried out again. This is the principle of props response

        • Process other data, such as listeners

        • Install the built-in init, prepatch, insert and destroy hooks to data On the hooks object, these hook methods will be used in the patch phase of the component

  • _ l. Render the help function of the v-for list at runtime, loop through the val value, execute the render method for each item in turn to generate VNode, and finally return a VNode array

  • _ m. VNode responsible for generating static nodes, that is, the function that executes the subscript specified in the staticRenderFns array

Briefly summarize the function of render helper: mount some runtime tools and methods on Vue instances, which are used in the rendering function generated by the compiler to generate the VNode of components.

Well, here's how a component becomes a VNode from initialization to the end. Finally, the rest is the patch stage. The next article will talk about how to render the VNode of the component to the page.

link

Thank you for your attention, likes, collections and comments. See you next time.

Topics: Javascript Front-end TypeScript Vue.js source code