Asynchronous components

Before viewing this article, it is a good idea to be familiar with component instantiation and mounting. You can read my last post if you don’t

Vue component analysis

With components out of the way, it’s time to look at asynchronous components. Asynchronous components are important in project development. In combination with code-spliting of Webpack, the js packed by components can be loaded asynchronously to optimize browser loading and improve page rendering speed. Since it works with WebPack, we need to create a basic project using vue-CLI, so let’s look at an example first

Vue.component('HelloWorld'.function (resolve) {
  // This particular 'require' syntax will be told to Webpack
  // Automatically splits your build code into multiple packages
  // Will be loaded via promise
  require(['./components/HelloWorld.vue'], resolve)
})

new Vue({
  render: h= > h(App)
}).$mount('#app')
Copy the code

We remove the local registration of the App component in the demo, and then global registration. Here is the example code.

With the example ready, we need to think about where the entrance is. When we open package-json, we see that it is started by vue-cli-service, so obviously we need to find the startup file. In node_modules, we look at the.bin directory, where we can find the vue-cli-service script file. Here we can see that its execution directory is @vue/cli-service/bin/vue-cli-service.js. The process is not described in detail, let’s directly look at the code in config/base.js

webpackConfig.resolve
  .alias
    .set(
      'vue$',
      options.runtimeCompiler
        ? 'vue/dist/vue.esm.js'
        : 'vue/dist/vue.runtime.esm.js'
    )
Copy the code

It’s very simple if we turn on the runtimeCompiler option it brings in vue.esm.js, otherwise it brings in vue.Runtime.esm.js. The difference is the presence or absence of compiler.

Now that the imported VUE source code is there, let’s think about where the entry for the asynchronous component is. As we learned in the previous article, creating asynchronous components uses the createComponent method, which is in the vDOM/create-Component file, Obviously now we can just write debugger at the beginning of createComponent and go into debug mode in development state

Initialize the

In this case, when we register globally, the second argument is a method, not an object, so we execute initAssetRegisters when type=component

if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id
  definition = this.options._base.extend(definition)
}
Copy the code

If isPlainObject is false, vue will not execute the code inside. Then we look at the createComponent method.

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

  // Asynchronous components
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    // The component constructs the factory function Vue
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
  return vnode
}
Copy the code

You can see that Ctor we passed in function, and we didn’t extend. So it doesn’t have CID, which is the statement that goes into it. With the entry parsed, let’s start with resolveAsyncComponent.

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

  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) = > {}

    const resolve = once((res: Object | Class<Component>) = > {})
    // use a callback function
    const reject = once(reason= >{})const res = factory(resolve, reject)

    sync = false

    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
Copy the code

CurrentRenderingInstance Gets the current render instance, which is the App instance. This method is a long one, so let’s delete this example and just look at the core. The first time you use factory.owners will not exist. So it goes into if.

We first declare a number of attributes, mainly resolve and reject, and they are executed only once. When we execute factory(resolve, reject), we’re actually executing the definition function, which means we execute it

require(['./components/HelloWorld.vue'], resolve)
Copy the code

Require is webpack’s method, so we’ll go into Webpack and do it. By using the resolve and reject functions above, we can guess that require must be a new Promise.

Exactly. The code in the Webpack, which is new Promise, then creates a script on the fly. Then go back to the process to continue. In a long if statement, it’s actually not executing now because there’s no return value, and the current RES is empty. So the last two pieces of code are now executed. Sync = false and return factory.resolved

While the current factory.resolved is undefined, look at the createComponent method

if (Ctor === undefined) {
  return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
Copy the code

So you go into createAsyncPlaceholder, and the name is creating an asynchronous Placeholder. Let’s look at the code

export function createAsyncPlaceholder (
  factory: Function, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag: ? string) :VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
Copy the code

Very simply, vue creates an empty Vnode and assigns parameters to vNode. After executing the INSERT node we can see that the element page of the console is

<div id="app">
  <img alt="Vue logo" src="/img/logo.82b9c7a5.png">
  <! ---->
  </div>
Copy the code

Look at the presence of a comment node in the component area. And in network we can see an empty 0.js. This completes the initialization process

Perform the resolve

Here we make a breakpoint inside the resolve function and look at the call stack. There’s a big Promise. Then async, and then resolve is executed.

const resolve = once((res: Object | Class<Component>) = > {
  factory.resolved = ensureCtor(res, baseCtor)
  if(! sync) { forceRender(true)}else {
    owners.length = 0}})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

The method is simple

  1. performensureCtorThe method is executed by taking the component objectVue.extendInitialize the child component constructor.
  2. The currentsyncisfalse. So we will executeforceRender(true)

ForceRender is very simple. Let’s look at the code

var forceRender = function (renderCompleted) {
   for (var i = 0, l = owners.length; i < l; i++) {
     owners[i].$forceUpdate()
   }
}
Copy the code

Here we have the owners[I] saved by the closure, which currently has only one App instance. This.$forceUpdate is executed. Re-run watcher.update to update the page. At this point we can get the child component instance, which goes through the normal createComponent process and renders it to the page

Other examples

Generally we do not use the above method, it has a better writing method based on ES2015

Vue.component('HelloWorld'.// The import function returns a Promise object
  () = > import(/* webpackChunkName: "HelloWorld" */'./components/HelloWorld.vue'))Copy the code

The important thing to note in this example is that we have a return value, which is returned by WebPack, which is a Promise. Since there is a return value, in the following code

 const res = factory(resolve, reject)
 // If it's a promise
 if (isObject(res)) {
   if (isPromise(res)) {
     // () => Promise
     if (isUndef(factory.resolved)) {
       res.then(resolve, reject)
     }
   }
 }

Copy the code

Obviously we run res.then, which executes to resolve, and the code is the same after that.

There is another example in the official documentation

// The third way is advanced asynchronous components
const AsyncComp = () = > ({
  // Components to load. It should be a Promise
  component: import('./components/HelloWorld.vue'),
  // Load the component that should be rendered
  loading: LoadingComp,
  // Render component when error occurs
  error: ErrorComp,
  // Render the wait time before loading the component. Default value: 200ms.
  delay: 200.// Maximum waiting time. Beyond this time the error component is rendered. Default: Infinity
  timeout: 3000
})

const LoadingComp = {
  template: '<div>loading</div>'
}
const ErrorComp = {
  template: '<div>error</div>'
}
Vue.component('HelloWorld', AsyncComp)
Copy the code

This syntax is new, and the execution of the code is really just a matter of determining how well the promise performs. In the source code is also very simple, I believe that can see.

conclusion

If an interviewer asks: How does vUE’s asynchronous component work?

An asynchronous component, as its name implies, initializes the child component constructor when a Promise created by Webpack is executed to THEN, and then re-renders it by executing $foreUpdate of the current instance, the parent instance.