Analysis of Vue Source Code for Asynchronous Component Registration

Keywords: Javascript Vue Webpack Attribute

Vue Asynchronous Component Registration

There are three ways for Vue official documents to provide registration of asynchronous components:

  1. Factory functions perform resolve callbacks
  2. Return Promise from the factory function
  3. Factory function returns a configurable component object

Factory functions perform resolve callbacks

Let's look at the example provided by the official Vue document:

Vue.component('async-webpack-example', function (resolve) {
  // This particular `require'grammar will tell webpack
  // Automatically cut your build code into multiple packages
  // Load through Ajax request
  require(['./my-async-component'], resolve)
})

To illustrate briefly, this example calls Vue's static method component to implement component registration. We need to understand the general implementation of Vue.component.

// At this point type is component
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)
    }
    // Is it an object?
    if (type === 'component' && isPlainObject(definition)) {
      definition.name = definition.name || id
      definition = this.options._base.extend(definition)
    }
    if (type === 'directive' && typeof definition === 'function') {
      definition = { bind: definition, update: definition }
    }
    // Record the declaration mapping corresponding to the global components, filters, directives of the current Vue
    this.options[type + 's'][id] = definition
    return definition
  }
}

To determine whether the incoming definition is our factory function, or not, is a factory function, which is definitely not an object, so instead of calling this.options._base.extend(definition) to get the component's constructor, we directly save the current definition (factory function) into the'async-webpack-example'attribute value of this.options.components, and Return definition.

What happens next?
In fact, we just called Vue.component to register an asynchronous component, but we finally rendered the page through a new Vue instance. Here's a brief overview of the rendering process:

Run:

  • new Vue executes the constructor
  • The constructor executes this._init, defining Vue.prototype._init when initMixin executes
  • Mount execution, Vue.prototype.$mount has been defined in web/runtime/index.js
  • Execute mountComponent in core/instance/lifecycle.js
  • Instantiate rendering of Watcher and pass in updateComponent (vm._update is triggered by getter of Watcher instance object, but how to trigger ignored first will be explained separately)
  • vm._update triggers vm. _render (when renderMixin is defined in Vue.prototype._render) execution
  • Get the render function in vm.$options and execute it so that the incoming vm.$createElement (defined in initRender in vm) executes. The vm.$createElement is the H function written in peacetime, h => h (App).
  • vm.$createElement = createElement
  • createComponent queries the current component for normal registration through resolveAsset

So let's go to the createComponent function and look at the implementation logic of the asynchronous component here.

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

  // Assign Vue to init initialization
  const baseCtor = context.$options._base

  // Ctor is currently a factory function for asynchronous components, so this step is not performed
  if (isObject(Ctor)) {
    // Get the constructor for components that are not globally registered
    Ctor = baseCtor.extend(Ctor)
  }

  // async component
  let asyncFactory
  // If Ctro.cid is undefined, then h will be registered as an asynchronous component
  // The reason is that Vue.extend has not been invoked for component constructor conversion acquisition.
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    // Parsing asynchronous components
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // If Ctor is undefined, the placeholder component Vnode of the asynchronous component is created and returned directly.
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  ...Code that is not analyzed is omitted here

  // Hook for mounting components
  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 }, // Component Object Component Options
    asyncFactory
  )

  return vnode
}

From the source code, we can see that asynchronous components do not perform component constructor conversion acquisition, but perform resolveAsyncComponent to obtain the returned component constructor. Since this process is an asynchronous request component, let's look at the implementation of resolveAsyncComponent

// Global variables defined in render.js to record the vm instance currently being rendered
import { currentRenderingInstance } from 'core/instance/render'

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  // Advanced asynchronous component usage
  if (isTrue(factory.error) && isDef(factory.errorComp)) {...First ellipsis}

  if (isDef(factory.resolved)) {
    return factory.resolved
  }
  // Get the vm instance currently being rendered
  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {...ellipsis}

  // Execute this logic
  if (owner && !isDef(factory.owners)) {
    const owners = factory.owners = [owner]
    // Used to mark whether or not
    let sync = true

    ...ellipsis
    const forceRender = (renderCompleted: boolean) => { ...ellipsis }
    
    // Once lets one of any functions wrapped by Once execute only once
    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)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => { ...ellipsis })

    // Execute factory functions, such as webpack, to retrieve asynchronous component resources
    const res = factory(resolve, reject)
    
    ...ellipsis
    
    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

resolveAsyncComponent passes in asynchronous component factory function and baseCtor (Vue.extend). First, we get the current rendered vm instance and then mark sync as true, indicating that resolve and reject functions are defined for the execution of synchronous code stage (ignoring no analysis). At this time, we can find that both resolve and reject are encapsulated by the on function, in order to make any function wrapped by the on function encapsulate. One of the numbers is executed only once, guaranteeing that both resolve and reject can only be executed once at a time. OK, then comes the execution of factory, which is actually the execution of the factory function passed in from the official example, at which time the request for an asynchronous component is initiated. Synchronization code continues to execute, sync sets false, indicating that the current synchronization code has been executed, and then returns undefined

Here you might ask how undefined can be returned, because the factory function we passed in does not have the loading attribute, and then the current factory does not have the resolved attribute.

Then go back to the createComponent code:

if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    // Parsing asynchronous components
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // If Ctor is undefined, the placeholder component Vnode of the asynchronous component is created and returned directly.
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

Because the resolveAsyncComponent execution just said returns undefined, execute createAsyncPlaceholder to create the annotation vnode

Here you may also ask why you want to create an annotated vnode to reveal the answer in advance:

Because we first return a placeholder vnode, wait for the asynchronous request to load, then perform forceUpdate Rendering, and the node will be updated to render the component node.

That goes on, as the answer just said, when the asynchronous component request is completed, resolve is executed and the corresponding asynchronous component is passed in. At this time factory.resolved is assigned the value of the return result of ensureCtor execution, which is a component constructor. Then sync is false, so forceRender is executed, and forceRender actually calls vm.$forceUpdate to implement as follows:

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

The $forceUpdate executes the update method for rendering watcher, and then we execute the createComponent method and resolveAsyncComponent, when factory.resolved has been defined, and we return directly to the factory.resolved component constructor. The rendering and patch logic of the subsequent components of createComponent is then executed. Component rendering and patch are not expanded here.

So the whole asynchronous component process is over.

Return Promise from the factory function

Take a look at the examples provided by the official website documentation:

Vue.component(
  'async-webpack-example',
  // This `import'function returns a `Promise' object.
  () => import('./my-async-component')
)

From the above example, you can see that when Vue.component is called, definition is a function that returns Promise, unlike factory function that performs a resolve callback, which is:

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {

    ...ellipsis

    // Execute factory functions, such as webpack, to retrieve asynchronous component resources
    const res = factory(resolve, reject)
    if (isObject(res)) {
      // For Promise object, import('./async-component')
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            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
  }
}

The main difference is that after the factory factory function is executed, our factory function will return a Promise, so res. then (resolution, reject) will execute. The next step is to wait for the asynchronous component request to complete, then execute the resolve function, then execute forceRender, and then return to the component constructor.

There is not much difference between the asynchronous component registration process written in Promise and the execution of callback functions.

Factory function returns a configurable component object

Similarly, look at the official website example:

const AsyncComponent = () => ({
  // Components that need to be loaded (should be a `Promise'object)
  component: import('./MyComponent.vue'),
  // Components used when loading asynchronous components
  loading: LoadingComponent,
  // Components used when loading fails
  error: ErrorComponent,
  // Show the delay time of the component when loading. The default value is 200 (milliseconds)
  delay: 200,
  // If a timeout is provided and component loading is timed out,
  // The components used when the load fails are used. The default value is: `Infinity'.`
  timeout: 3000
})

As can be seen from the example above, the factory function returns a configuration object after successful execution, and the five attributes of this object can be understood from the annotations in the official documents. Let's see the difference between this method and the two methods mentioned earlier.

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  // Advanced asynchronous component usage
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  ...Knowing, omitting

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

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

    ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

    const forceRender = (renderCompleted: boolean) => {...ellipsis}
    // Once lets one of any functions wrapped by Once execute only once
    const resolve = once((res: Object | Class<Component>) => {
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })

    // Execute factory functions, such as webpack, to retrieve asynchronous component resources
    const res = factory(resolve, reject)
    if (isObject(res)) {
      // For Promise object, import('./async-component')
      if (isPromise(res)) {
        ...ellipsis
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            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
  }
}

The rendering process also comes to resolveAsyncComponent. At first, we judge whether factory.error is true or not. Of course, it must be false at first, but not enter the logic. Then we also execute const res = factory (resolution, reject) because we just said that our factory function returned an asynchronous component configuration object, so res is the factory letter we defined. Number returned object, isObject(res) is true, isPromise(res) is false, isPromise(res.component) is true, and then determine whether res.error is defined, so in fact definition extended errorComp, errorComp tor is to ensure Ctor to define components into components constructors, loading is the same logic, in fact extended loadingC. OMP component constructor.

Next, we need to pay special attention to the fact that when we define res.delay as 0, factory.loading is set to true directly, because this affects the return value of resolveAsyncComponent.

return factory.loading
      ? factory.loadingComp
      : factory.resolved

When factory.loading is true, it returns loadingComp so that when creating teComponet, instead of creating a comment vnode, it directly performs the rendering of loadingComp.

If our res.delay is not zero, a timer will be enabled. First, undefined is returned synchronously to trigger annotation node creation, then factory. load = true and forceRender(false) are executed after a certain time, provided that the component is not loaded and reject ed without error, and then the rendering of the annotation vnode is replaced by the loading process component loadingComp.

res.timeout is mainly used for timing. If the current factory.resolved is undefined in the time of res.timeout, the asynchronous component loading has been timed out, and reject method will be called. In fact, reject is to call forceRender to perform the rendering of errorComp.

OK, when our component is loaded, the resolve method is executed, factory.resloved is set to true, and forceRender is called to replace the annotation node or the node of loadingComp with the component rendered as loaded.

So far, we've learned about the registration process of three asynchronous components.

To sum up

The rendering of asynchronous components is essentially to perform rendering twice or more. First, the current component is rendered as a comment node. When the component is loaded successfully, the rendering is performed through forceRender. Or render it as a comment node and then as a loading node, rendering it as a request-completed component.

Here we need to pay attention to the implementation of forceRender, forceRender is used to enforce the current node re-rendering, as to the whole rendering process is how follow-up articles have the opportunity.... Explain it again.

I have limited ability to express in Chinese, but I just have a sudden fantasy in order to express the process I understand in my own words, if there are any mistakes, I hope to be more inclusive.

Posted by excessnet on Fri, 14 Jun 2019 12:33:07 -0700