While reviewing a background administration project I wrote, I found that I used Vue’s asynchronous component. And before just in the study of vUE source code, incidentally analysis of the asynchronous component loading and execution process.

What are asynchronous components?

Asynchronous, as opposed to synchronous. When we use Vue, most of the components we use are synchronous components. During the first rendering of the VUE instance, the component constructor has been generated. Asynchronous components, on the other hand, asynchronously pull the JS of the corresponding component (combined with the import of webpack) when the component is used. When the corresponding JS is loaded, the configuration items of the asynchronous component are obtained to create the component constructor. Through forceRender, it forces dependent VM instances to retrigger the rendering function.

Why did the project use asynchronous components?

Because for background management systems, there is usually a series of operation tabs on the left. Each TAB corresponds to a component. If all components are loaded when a user logs in, the user experience deteriorates and the waiting time is long. So you need to use asynchronous components.Copy the code

When the user enters the page, make only the components it uses at the beginning synchronized. This not only does not affect the user experience, but also optimizes the page loading speed.

In Vue, if multiple VM instances use asynchronous components, they are loaded only once and cached in memory.

How are asynchronous components used?

Registering asynchronous components is not much different from registering normal components. You can register both globally and locally. The difference, however, is that asynchronous components need to be imported via webPack’s import function. As follows:

// Local registration
newVue({ ... rest,components: {
        a: (a)= > import('./components/a.vue')}})// Global registration
Vue.component('async-comp', (resolve, reject) => ({
    component: (a)= > imort('./components/a.vue'),
    loading: loadingComp,
    error: errorComp,
    delay: 200.timeout: 3000
}));
Copy the code

There are also advanced asynchronous components. Advanced asynchronous components include loading components (used to display asynchronous loading components), error components (used to display error components), delay components (used to delay loading asynchronous components), and timeout components (used to set the maximum loading time of asynchronous components. If the loading is not complete within the specified time, The Error component is displayed.

The Error and loading components are synchronized. When an asynchronous component is loading or a loading error occurs, the corresponding state component needs to be displayed immediately.

For asynchronous components, by using the import function provided by WebPack, you can have WebPack package the component files passed in the import separately at packaging time. In this way, when the component is used, the JS of the component is loaded asynchronously by sending a request, and when the resource is loaded, it is handed over to the VUE to handle the loading.

How do asynchronous components perform?

First of all, we need to make a note of webpack import:

According to the WebPack website, import is used for dynamic code splitting. It passes in a module path and returns a Promise instance. When WebPack is packaged, the modules imported by import are packaged separately into a code package. And load the corresponding resource when the code executes on the line.Copy the code

For an exploration of the Import of Webpack, see the module approach.

Let’s look at how asynchronous components are loaded. Take the global asynchronous component in the usage above:

  1. Vue.component adds {‘async-comp’: fn} to Vue.options.components. To find the corresponding component definition in createElement when the vm._render method is executed at component rendering time.
const ASSET_TYPES = ['component'.'directive'.'filter'];
ASSET_TYPES.forEach(type= > {
    Vue[type] = function (id: string, definition: Function | Object) :Function | Object | void {
        / /... Omit parts that are not relevant to the process
        this.options[type + 's'][id] = definition
        Vue.options.components['async-comp'] = fn
        return definition
      }
    }
  })
}
Copy the code
  1. Call the createComponent function in createElement to enter the creation of the component vNode. In createComponent, asynchronous components are handled primarily by resolveAsyncComponent functions.
$mount -> mountComponent -> vm._update(vm._render(), hydrating) -> vm._update(vm._render(), hydrating) ->
// -> vm._render -> vm._c -> createElement -> _createElement -> createComponent
// The createComponent function is used to create the createComponent function.

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

  // plain options object: turn it into a constructor
  // Ctor is the asynchronous loading function defined earlier, not object
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  
  / / a cut

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    // It will end up here.
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // CTOR defaults to undefined when the render function is executed for the first time if it is not an advanced asynchronous component or delay is not 0 for advanced asynchronous components. Returns an empty placeholder node
    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
      )
    }
  }

  / /... Leave out the rest
  const vnode = newVNode(... opts);return vnode;
}
Copy the code
  1. In the resolveAsyncComponent function, we do the following:

    • Determine the error and Resolved status of the asynchronous component. If Error or ISloading is true or Resolved is not undefined, the component in the corresponding state is returned.
    • All VM instances that reference the asynchronous component are collected and stored in factory.owners.
    • ForceRender functions are defined (to call the owners’ render on a successful or unsuccessful load), resolve(to handle asynchronous components after a successful load), reject(to handle asynchronous components after a failed load)
    • Call the corresponding function of the asynchronous component and do further processing on the advanced asynchronous component.
export function resolveAsyncComponent (factory: Function, baseCtor: Class
       ) :Class<Component> | void {
  /** Hits start */ on second entry
  // If the asynchronous component fails to load or has timed out, it will enter here
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  // After the component is loaded normally, it will enter here
  if (isDef(factory.resolved)) {
    return factory.resolved
  }
  
  // Collect vm instances that reference the asynchronous component so that the rendering function of the corresponding VM instance is triggered at forceRender
  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === - 1) {
    // already pending
    factory.owners.push(owner)
  }

  // If loading is enabled, loadingComp is returned.
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }
  /** Hits end */ on second entry

  if(owner && ! isDef(factory.owners)) {const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    // When the VM instance is destroyed, remove the owner currently listening in factory.owners.
    ;(owner: any).$on('hook:destroyed', () = > remove(owners, owner))

    const forceRender = (renderCompleted: boolean) = > {
      for (let i = 0, l = owners.length; i < l; i++) {
        Vm._update (vm._render(), hytrating);
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if(timerLoading ! = =null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if(timerTimeout ! = =null) {
          clearTimeout(timerTimeout)
          timerTimeout = null}}}const resolve = once((res: Object | Class<Component>) = > {
      // Cache and generate the loaded asynchronous component constructor
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      // Sync is false when the asynchronous component is loaded. Execute forceRender to trigger rerendering and hit the Factory.resolved branch after re-entering the function.
      if(! sync) { forceRender(true)}else {
        owners.length = 0}})const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
        `Failed to resolve async component: The ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : ' '))// Failed to load the asynchronous component
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)}})// For advanced asynchronous components, an Object is returned, as in the example usage for globally registered components.
    const res = factory(resolve, reject)

    if (isObject(res)) {
      // As with the locally registered component in the above usage, import returns a Promise instance.
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        // Advanced asynchronous components. Example: Component configured: () => import('./ a.ue ')
        res.component.then(resolve, reject)
        
        // Cache error component constructor
        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }
        
        // Cache loading component constructor
        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            // Only when delay is set to 0 will the loading component constructor be returned for the first rendering
            factory.loading = true
          } else {
            // If delay is set,
            timerLoading = setTimeout((a)= > {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)}}if (isDef(res.timeout)) {
          // If the asynchronous component load time exceeds timeout, the component load fails
          timerTimeout = setTimeout((a)= > {
            timerTimeout = null
            if(isUndef(factory.resolved)) { reject( process.env.NODE_ENV ! = ='production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
Copy the code

So asynchronous components are actually loaded and executed by executing the render function twice.

The first time you execute the async component, you define the loading/error state component, and pass resolve/reject to factory. After the asynchronous component file is loaded, use resolve or Reject to process it. In this case, the Error or Resolved status of the Factory will change. When factory.error or factory.resolved is set, forceRender will be called to trigger the update to the rendering watcher for the vm instance and re-execute the rendering and then perform the second rendering.

The second rendering will also enter the resolveAsyncComponent method. At this point, the loading result of the asynchronous component is determined, and the corresponding successful state component (that is, the asynchronous component we defined) or failed state component is returned. The subsequent process is then followed.


So much for the exploration of asynchronous components…