preface

When using Vue to develop single-page applications, it often reduces the amount of code on the first screen by routing lazy loading, so as to realize the function of visiting other pages and loading corresponding components

For the current page, sometimes asynchronous loading of components will further reduce the amount of code on the current page

  components: {
    Imgshow: (a)= > import('.. /.. /.. /components/Imgshow'),
    Audioplay: (a)= > import('.. /.. /.. /components/Audioplay'),
    Videoplay: (a)= > import('.. /.. /.. /components/Videoplay')}Copy the code

These components may be displayed only when the user opens a Dialog. Conversely, it is unnecessary to load these components when the user does not open a Dialog. Through asynchronous components, the code of the component can be loaded asynchronously when the user opens a Dialog, thus achieving a faster response

Routing lazy loading and asynchronous components are actually the same principle, this article I will analyze the implementation principle of asynchronous components from the source point of view

The source code only retains the core logic complete source code address

Vue version: 2.5.21 (slightly different from the latest version, but with the same core principles)

Principle of Vue loading components

Before explaining how asynchronous components work, let’s start with how Vue loads components

In development using Vue single-file components, the DOM structure is often described by the template template string

<template>
  <div>
    <HelloWorld />
  </div>
</template>

<script>
export default {
  name: "home",
  components: {
    HelloWorld: () => import("@/components/HelloWorld.vue")}}; </script>Copy the code
<script>
export default {
  name: "home",
  components: {
    HelloWorld: () => import("@/components/HelloWorld.vue")
  },
  render(h) {
    return h("div", [h("HelloWorld")]); }}; </script>Copy the code

In the first case, vue-Loader parses the template string in the template tag and converts it to the render function, so the effect is essentially the same

The h argument of the Render method is an alias of the createElement function (the first letter of the HTML word hyper), which converts the argument to a vNode (virtual DOM)

export function _createElement(context: Component, tag? : string | Class
       
         | Function | Object, data? : VNodeData, children? : any, normalizationType? : number
       ) :VNode | Array<VNode> {
  let vnode
  if (typeof tag === 'string') {
    let Ctor
    if (config.isReservedTag(tag)) { // Native HTML tags
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined.undefined,
        context
      )
    } else if((! data || ! data.pre) &&// Convert the label string to a component function
      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)
  }
  return vnode
}
Copy the code

The first argument to createElement, context, refers to the parent Vue instance object, which is passed to Vue by default as the first argument, starting with the second tag argument passed to createElement in the render function

When you pass createElement a non-HTML default tag name (corresponding to ‘HelloWorld’ in the example), Vue assumes that it is a component tag, Perform resolveAsset from $options.com ponents finds the corresponding component function is () = > import (” @ / components/HelloWorld. Vue “), CreateComponent is then executed to generate the component VNode

Create components

CreateComponent is a component used to create the vnode function, after the previous step resolveAsset will eventually () = > import (” @ / components/HelloWorld. Vue “) as the first parameter Ctor incoming,

export function createComponent(Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component,//Vm instance children:? Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  const baseCtor = context.$options._base
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  // async component
  let asyncFactory
  // Async component if CID cannot be found
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
  }
 
  / /...
  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
}
Copy the code

For asynchronous components, we only need to care about the resolveAsyncComponent function. Ctor is a function that returns dynamically loaded components. Ctor does not have cid. And execute the intermediate resolveAsyncComponent to try to resolve the asynchronous component. What does the function that actually resolves the asynchronous component do

Asynchronous components

export function resolveAsyncComponent(factory: Function, baseCtor: Class
       
        , context: Component
       ) :Class<Component> | void {
  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context)
  } else {
    const contexts = (factory.contexts = [context])
    let sync = true
    
    // Part 3
    const forceRender = (renderCompleted: boolean) = > {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }

      if (renderCompleted) {
        contexts.length = 0}}// Part 2
    const resolve = once((res: Object | Class<Component>) = > {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if(! sync) { forceRender(true)}})const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' &&
        warn(
          `Failed to resolve async component: The ${String(factory)}` +
            (reason ? `\nReason: ${reason}` : ' '))})// The first part
    // Old WebPack factory function syntax
    const res = factory(resolve, reject)

    // New ES6 dynamic import syntax
    if (isObject(res)) {
      if (typeof res.then === 'function') {
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      }
    } else {
      // Special asynchronous components are not discussed in this article
      // https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81
      / /...
    }

    sync = false
    // return in case resolved synchronously
    return factory.resolved
  }
}
Copy the code

The source code is still long, even if stripped down, but the implementation is not complicated. The core is to take the asynchronous component code from the server, turn it into a component builder, and update the view

But since it is an asynchronous component, two things need to be considered

  • How should the view be presented when the component loads
  • How do I update the view after the component is loaded successfully

Let’s look at how resolveAsyncComponent solves these two problems, starting with the first part of the comment. It first executes the factory function passed in, That is in the example () = > import (” @ / components/HelloWorld. Vue)”

We know that import, when executed as a function, returns a promise and assigns that promise to the res variable, but resolve and reject are also passed in when factory is executed. What does that do?

This is essentially a syntax for introducing asynchronous components into older versions and does not work

// The old webpack factory function method
const Foo = resolve= > {
  require.ensure(['./Foo.vue'], () => {
    resolve(require('./Foo.vue'))})}Copy the code

It is still recommended to use ES6’s import() syntax, which is more standards-compliant

Vue puts the parsing of import() syntax behind, calling the THEN methods of RES and passing resolve and reject, which means that resolve is executed when the asynchronous component is successfully loaded and reject is executed when the asynchronous component is successfully loaded. We’ll leave these two functions to the next paragraph, but continue with the synchronization logic

When asynchronous components load

Resolve and REJECT are registered with the RES then method, and the Factory. resolved value is returned as resolveAsyncComponent, but the Resolved property is not defined. So it returns undefined and then returns to the outer createComponent function

When resolveAsyncComponent returns undefined, it first renders a comment node as a placeholder

It then waits for the asynchronous component to load successfully and executes the subsequent logic

After the asynchronous component is loaded successfully

Once the synchronization logic is complete, let’s go back to the second part of the comment. What exactly do resolve and Reject do when the asynchronous component is successfully loaded to execute the resolve function registered with the previous THEN method

As you can see, both resolve and reject are wrapped in the helper function once, so that resolve/ Reject is always executed one and only once, but with new versions, import() returns a promise, Promise already has a built-in implementation of once, so once exists to accommodate older versions of the syntax

When the asynchronous component is loaded successfully, the component configuration item of the asynchronous component is passed into the ensureCtor function as the RES parameter

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
Copy the code

It then determines that comp (the value of the default property in the figure), if an object, is converted to a component constructor and returns (Ctor stands for constructor), assigning it to factory.resolve. This assignment is very important. Remember that factory.resolve was an undefined state, and now its value is the component constructor

As an aside, most single-file components in Vue are option-based, that is, exported objects are generally an object

<template>
  <div class="hello-world">hello world</div>
</template>

<script>
export default {
  name: "HelloWorld".data() {
    return {
      a: 1
    };
  },
  methods: {
    handleClick() {}}}; </script>Copy the code

The downside of this approach is its heavy reliance on this, which makes TypeScript hard to type down

There is also a function-based component that exports a function (or component class)

import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  template: ''
})
export default class MyComponent extends Vue {
  message: string = 'Hello! '
  onClick (): void {
    window.alert(this.message)
  }
}
Copy the code

This is exactly how TypeScript is implemented for Vue access

React demonstrates that functions can perfectly render components. In fact, Vue3 ditches option-based syntax in favor of functions in favor of better TypeScript types

How to convert option-base to funtion-base components can be seen in Vue. Extend, which is the base. Extend in the above code, does exactly what is done, which is beyond the scope of this article

Sync is false because it is asynchronous, and forceRender is executed

Refresh the view

When the asynchronous component is loaded successfully, you need to let the existing component know that the asynchronous component is loaded and add the asynchronous component to the current view. This is also what the third forceRender comment does. You can see that this line of code is executed when the resolveAsyncComponent is initially executed

const contexts = factory.contexts = [context]

It adds a context property to the Factory function that holds an array of contexts. A context is a component instance, or more specifically the parent of the current asynchronous component. Context holds all references to this asynchronous component in Contexts to its parent. So how do you tell the parent that the asynchronous component has been loaded successfully?

$forceUpdate ($forceUpdate, $forceUpdate, $forceUpdate, $forceUpdate);

Again to refresh the parent component will perform the initial createComponent method, again the Ctor is still not cid attribute, is the beginning of the function () = > import (” @ / components/HelloWorld. Vue)”

// createComponent
 if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
Copy the code

ResolveAsyncComponent is executed the second time, but the first time the factory.resolve property is undefined, and now it passes through the resolve function of the first time. Assigned to the component constructor of the asynchronous component, resolveAsyncComponent no longer returns undefined. Instead, resolveAsyncComponent returns the component constructor of the asynchronous component

By taking the component constructor, you can generate the component normally, and then the logic is the same as that of the synchronized component

conclusion

When Vue encounters an asynchronous component, it renders a comment node, and when the asynchronous component is loaded, it refreshes the view via the $forceUpdate API

There are also some advanced uses for asynchronous components, such as customizable placeholders to render when loading or when loading fails, or custom delays and timeouts, which are essentially extensions to resolveAsyncComponent. See the full source code for more details

The resources

Vue. Js technology revealed