Question of the day please describe the rendering process of Vue components

Time:2022-10-18

Componentization isVue, ReactOne of the core ideas of these frameworks, by splitting pages into components with high cohesion and low coupling, can greatly improve our code reuse and make the project easier to maintain. Therefore, this article will analyze the rendering process of the component. We analyze it through the following example:

<div id="demo">
  <comp></comp>
</div>
<script>
  Vue.component('comp', {    template: '<div>I am comp</div>',  })  const app = new Vue({    el: '#demo',  })
</script>

Here we analyze it in two steps: component declaration, component creation and rendering

component declaration

First, let’s look atVue.componentwhat is it declared incore/global-api/assets.js

export function initAssetRegisters(Vue: GlobalAPI) {
  // ASSET_TYPES is an array: ['component','directive','filter']
  ASSET_TYPES.forEach((type) => {
    Vue[type] = function (
      id: string,      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // component declaration related code
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // _base is Vue
          // Vue.extend({}) returns the component constructor
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = {bind: definition, update: definition}
        }
        // register to the components option
        // Add component configuration to Vue's original options, other components will inherit in the future, and they will all have these component registrations
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

herethis.options._base.extend(definition)In fact, the call isVue.extend(definition)

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  const Sub = function VueComponent(options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(Super.options, extendOptions)
  Sub['super'] = Super

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

Here we can understand that it returns a namedVueComponentconstructor and inheritsVue. So, after the component definition here is completeVuewill become this:

{
  ...
  options: {
    components: {
      comp: function VueComponent() {}
    }
  }
  ..
}

Component creation and mounting

we knowVueThe template in will eventually become compiled intorenderfunction, such as the final example aboverenderThe function will look like this:

render() {
  with (this) {return _c('div',{attrs:{"id":"demo"}},[_c('comp')],1)}
}

here_ccan be defined incore/instance/render.jsfound in:

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

so_c('comp')Finally calledcreateElement(core/vdom/create-element.js) this method:

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  ...
  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> {
  ...
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // custom 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
      )
    }
  ...
}

Here we only look at the relevant logic of the custom component, and found that the last callcreateComponent (core/vdom/create-component.js):

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  ...

  // install component management hooks onto the placeholder node
  // Install component management hooks: component initialization will be done in the future (instance creation, mounting)
  installComponentHooks(data)

  // return a placeholder vnode
  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
  )

  return vnode
}

Here we skip other code, let’s take a look firstinstallComponentHooks

function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

here will bedata.hookmount somehooks, if the user also uploads the samehookswill be merged. thishookswhat is it: referenceFront-end vue interview questions answered in detail

const componentVNodeHooks = {
  // instantiate and mount
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance && // instance already exists
      !vnode.componentInstance._isDestroyed && // not destroyed
      vnode.data.keepAlive // ​​marked as keepAlive
    ) {
      // kept-alive components, treat as a patch
      // For cached components, just patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // create component instance
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ))
      // child component mount
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = (vnode.componentInstance = oldVnode.componentInstance)
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert(vnode: MountedComponentVNode) {
    const {context, componentInstance} = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    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(vnode: MountedComponentVNode) {
    const {componentInstance} = vnode
    if (!componentInstance._isDestroyed) {
      // Not directly destroy the cache component
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
}

here are fourhooks, look at their names to know that they will be executed in the corresponding operation. for exampleinitIt will be executed when the component is initialized, which will be discussed later. we continue to seecreateComponent

// return a placeholder vnode
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
)

return vnode
export default class VNode {
  ...
  constructor(
    tag?: string,    data?: VNodeData,    children?: ?Array<VNode>,    text?: string,    elm?: Node,    context?: Component,    componentOptions?: VNodeComponentOptions,    asyncFactory?: Function
  ) {
    ...
    this.componentOptions = componentOptions
  }

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

Here is initialized aVNodeand made a return, here_c('comp')task is completed. You can see that the constructor of our custom component is not executed in this step, it is just mounted tocomponentOptionsproperties. When will he execute it? Don’t worry, let’s go down.

when the root componentrenderAfter execution, it will executevm._updateUpdate the component, and then call__patch__, we followed the vine and finally came tocore/vdom/patch.js

return function patch(oldVnode, vnode, hydrating, removeOnly) {
      ...
      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )
      ...
  return vnode.elm
}

then go tocreateElm

function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
  ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
          // Initialize events, properties, etc.
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        // insert node
        insert(parentElm, vnode.elm, refElm);
      }
  ...

note herevnodeYes<div id="demo"></div>of this element, so it will go tocreateChildren

function createChildren(vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(
        children[i],
        insertedVnodeQueue,
        vnode.elm,
        null,
        true,
        children,
        i
      )
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

Here finally comes backcreateElm, but at this timevnodeIt’s a custom component, and it will go here:

function createElm(
    vnode,    insertedVnodeQueue,    parentElm,    refElm,    nested,    ownerArray,    index  ) {
    ...
    // create custom component
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return;
    }
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // cache case
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // The hooks installed earlier are used here, init is executed, and the custom component is instantiated
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    // If the above creation process has been completed, the component instance already exists
    if (isDef(vnode.componentInstance)) {
      // Initialize the component: events, properties, etc. on the component
      initComponent(vnode, insertedVnodeQueue)
      // insert dom
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

Note that this will executei.initmethod, which has been mentioned above, instantiates the component object, and then executes the$mount. while executing$mountwill eventually comepatchmethod, and finally executecreateElm

function patch(oldVnode, vnode, hydrating, removeOnly) {
  ...
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  }
  ...
}

Executing this method will recursively convert thevnoderender as realdom, finally throughinsertThe method inserts the entire dom tree into the parent element. At this point, the rendering process of the custom component is over.