Location>code7788 >text

How does vue3's defineAsyncComponent implement asynchronous components?

Popularity:91 ℃/2024-08-13 09:21:31

preamble

In the previous postGive me 5 minutes and I promise to teach you to dynamically load remote components in vue3!In the article, we went through thedefineAsyncComponentImplemented dynamic loading of remote components. In this article we will take you through the debug source code to figure out thedefineAsyncComponentis how to implement asynchronous components. Note: The version of vue used in this article is3.4.19

Ouyang wrote an open source ebookvue3 Compilation Principles Revealed, this book is accessible to middle school level front end. Totally free, just asking for a STAR.

Look at the demo.

It's the same routine. Let's take a look at adefineAsyncComponentDemos for asynchronous components.

local subassemblyThe code is as follows:

<template>
  <p> I am a local component</p>
</template>

asynchronous subassemblyThe code is as follows:

<template>
  <p> I'm an asynchronous component </p>
</template>

parent componentThe code is as follows:

<template>
  <LocalChild />
  <button @click="showAsyncChild = true">load async child</button>
  <AsyncChild v-if="showAsyncChild" />
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from "vue";
import LocalChild from "./";

const AsyncChild = defineAsyncComponent(() => import("./"));
const showAsyncChild = ref(false);
</script>

We have two subcomponents here, the first, he's the same as the components we normally use, nothing more to say.

The second subcomponent is theIn the parent component, we don't have the sameInstead of importing at the top like that, you can import in thedefineAsyncComponentDynamically imported in the received callback function.file, defined in this wayAsyncChildA component is an asynchronous component.

As you can see in the template, only when clicking on theload async childThe asynchronous component is loaded only after the buttonAsyncChild

Let's take a look at the execution as shown in the following gif:
demo

As you can see from the gif above, when we click on theload async childbutton in the network panel to load the asynchronous component.

defineAsyncComponentIn addition to receiving a callback function that returns a Promise as above, you can also receive an object as a parameter. demo code is as follows:

const AsyncComp = defineAsyncComponent({
  // loader function
  loader: () => import('. /'),

  // Component to use when loading asynchronous components
  loadingComponent: LoadingComponent, // the component to use when loading asynchronous components.
  // Show the delay before loading the component, defaults to 200ms.
  delay: 200, // The delay before displaying the loaded component, defaults to 200ms.

  // The component to display when loading fails
  errorComponent: ErrorComponent, // the component to display if loading fails.
  // If a timeout is provided and is exceeded, the error component configured here // will also be displayed.
  // The error component configured here will also be displayed, default: Infinity
  timeout: 3000
})

where the object parameter has several fields:

  • loaderfield actually corresponds to the callback function in the previous kind of writeup.

  • loadingComponentfor the loading component to be displayed during the loading of the asynchronous component.

  • delayThe delay time for displaying the loading component, default is 200ms, this is because when the network condition is good, the loading is completed very fast, the replacement between the loading component and the final component is too fast may produce flickering, instead of affecting the user's feeling.

  • errorComponentis the component that is displayed after a load failure.

  • timeoutis the timeout period.

In the next source code analysis, we still use the previous example of receiving a callback function that returns a Promise to debug the source code.

point of interruption (math.)

Let's move on to the parent component in the browserAfter compiling the code, it is very simple, in the browser you can use command (control in windows) + p like vscode to evoke an input box, and then in the input box, enterClicking enter opens the source panel with the compiledfile now. The figure below:
command

We see that the compiledThe file code is as follows:

import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/?v=868545d8";
import {
  defineAsyncComponent,
  ref,
} from "/node_modules/.vite/deps/?v=868545d8";
import LocalChild from "/src/components/defineAsyncComponentDemo/?t=1723193310324";
const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    __expose();
    const showAsyncChild = ref(false);
    const AsyncChild = defineAsyncComponent(() =>
      import("/src/components/defineAsyncComponentDemo/")
    );
    const __returned__ = { showAsyncChild, AsyncChild, LocalChild };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  // ...an omission
}

export default _export_sfc(_sfc_main, [["render", _sfc_render]]);

From the code above you can see that the compiledIt is divided into two main sections, the first of which is_sfc_mainin an objectsetupmethod, which corresponds to ourscriptModule. The second block is the_sfc_render, which is often referred to as the render function, corresponds to the contents of the template.

We're trying to figure it out.defineAsyncComponentmethod, then of course it is a good idea to give the setup method thedefineAsyncComponentmethod breaks the point. Refresh the page, at which point the code will stay at the breakpointdefineAsyncComponentMethodology Division.

defineAsyncComponentmethodologies

Then walk the breakpoint into thedefineAsyncComponentfunction internally, in our scenario the simplifieddefineAsyncComponentThe function code is as follows:

function defineAsyncComponent(source) {
  if (isFunction(source)) {
    source = { loader: source };
  }
  const { loader, loadingComponent, errorComponent, delay = 200 } = source;
  let resolvedComp;

  const load = () => {
    return loader()
      .catch(() => {
        // ...an omission
      })
      .then((comp) => {
        if (
          comp &&
          (comp.__esModule || comp[] === "Module")
        ) {
          comp = ;
        }
        resolvedComp = comp;
        return comp;
      });
  };

  return defineComponent({
    name: "AsyncComponentWrapper",
    setup() {
      const instance = currentInstance;
      const loaded = ref(false);
      const error = ref();
      const delayed = ref(!!delay);
      if (delay) {
        setTimeout(() => {
           = false;
        }, delay);
      }
      load()
        .then(() => {
           = true;
        })
        .catch((err) => {
          onError(err);
           = err;
        });
      return () => {
        if ( && resolvedComp) {
          return createInnerComp(resolvedComp, instance);
        } else if ( && errorComponent) {
          return createVNode(errorComponent, {
            error: ,
          });
        } else if (loadingComponent && !) {
          return createVNode(loadingComponent);
        }
      };
    },
  });
}

As you can see from the code abovedefineAsyncComponentIt is divided into three parts.

  • Part 1: Processing incoming parameters.

  • The second part is:loadfunction is used to load asynchronous components.

  • Part III reads: ReturndefineComponentDefined components.

Part I: Handling incoming parameters

Let's look at the first part: handling incoming parameters. The code is as follows:

function defineAsyncComponent(source) {
  if (isFunction(source)) {
    source = { loader: source };
  }
  const { loader, loadingComponent, errorComponent, delay = 200 } = source;
  let resolvedComp;
  // ...an omission
}

first usingisFunction(source)Determine the incomingsourceis not a function, and if it is, then thesourceRewrite to includeloaderThe object of the field:source = { loader: source }. Then use theconst { loader, loadingComponent, errorComponent, delay = 200 } = sourceDeconstruct the corresponding loading component, loading failure component, and delay time.

I think you can see why.defineAsyncComponentThe parameters received by the function can be either a callback function or a callback function containing theloaderloadingComponenterrorComponentobjects with fields like. This is because if we pass in a callback function, internally we will assign the incoming callback function to theloaderfields. However, parameters such as the loading component, load failure component, etc. will not have values, only thedelayThe delay time is given 200 by default.

Then comes the definition ofloadfunction is used to load asynchronous components, this function is in the third part of thedefineComponentso let's start with thedefineComponentFunction section.

Part 3: Returns the component defined by defineComponent

Let's see.defineAsyncComponentThe return value of thedefineComponentdefined component with the following code:

function defineAsyncComponent(source) {
  // ...an omission

  return defineComponent({
    name: "AsyncComponentWrapper",
    setup() {
      const instance = currentInstance;
      const loaded = ref(false);
      const error = ref();
      const delayed = ref(!!delay);
      if (delay) {
        setTimeout(() => {
           = false;
        }, delay);
      }
      load()
        .then(() => {
           = true;
        })
        .catch((err) => {
          onError(err);
           = err;
        });
      return () => {
        if ( && resolvedComp) {
          return createInnerComp(resolvedComp, instance);
        } else if ( && errorComponent) {
          return createVNode(errorComponent, {
            error: ,
          });
        } else if (loadingComponent && !) {
          return createVNode(loadingComponent);
        }
      };
    },
  });
}

defineComponentThe function's received argument is a vue component object and the return value is also a vue component object. He doesn't really do much, simply just providing type derivation for ts.

Let's move on to the vue component object, which has only two fields in it:nameattributes andsetupfunction.

nameattribute is familiar and indicates the name of the current vue component.

people usually<script setup>Syntactic sugar is used more often, this syntactic sugar is compiled to be thesetupfunction, and of course vue supports letting us write our own handwrittensetupfunction.

Ask a question:setupfunction corresponds to the<script setup>We usually write code have template module corresponds to the view part, that is, familiar with the render function. Why is there no render function here?

do sth (for sb)setupfunction to make a breakpoint so that when the asynchronous component is rendered, it will execute thissetupfunction. The code will stop at thesetupfunction at the breakpoint.

existsetupfunction that first uses therefThree responsive variables are defined:loadederrordelayed

  • loadedis a boolean value that serves to record whether the asynchronous component has finished loading.

  • errorrecords the error message logged when the load fails, if it is also passed in theerrorComponentcomponent, which is displayed when loading an asynchronous component failserrorComponentComponent.

  • delayedis also a boolean value, since the loading component is not displayed immediately, but after a delay. ThisdelayedThe boolean value records whether or not it is still in the delay phase, if it is then the loading component will not be shown.

Next, determine which of the incoming parameters sets the setting of thedelaydelay, if so use thesetTimeoutlatencydelaymilliseconds before thedelayedis set to false when the value ofdelayedvalue is false, the loading component will only be displayed during the loading phase. The code is as follows:

if (delay) {
  setTimeout(() => {
     = false;
  }, delay);
}

The next step is to executeloadfunction, thisloadfunction is what we talked about earlierdefineAsyncComponentThe second part of the code in the function. The code is as follows:

load()
  .then(() => {
     = true;
  })
  .catch((err) => {
    onError(err);
     = err;
  });

As you can see from the code aboveloadfunction obviously returns a Promise, which is why it can be used later with the.then()cap (a poem).catch(). And here in the.then()air marshalloadedvalue is set to true, the breakpoint walks into theloadfunction with the following code:

const load = () => {
  return loader()
    .catch(() => {
      // ...an omission
    })
    .then((comp) => {
      if (
        comp &&
        (comp.__esModule || comp[] === "Module")
      ) {
        comp = ;
      }
      resolvedComp = comp;
      return comp;
    });
};

Here.loadThe function code is also very simple to execute directly inside theloaderFunctions. Remember this.loaderIs the function what it is?

defineAsyncComponentfunction can receive an asynchronous load function that can go to import to import the component at runtime. This asynchronous load function is theloaderfunction that executes theloaderfunction then goes and loads the asynchronous component. In our case it's asynchronous loadingcomponent with the following code:

const AsyncChild = defineAsyncComponent(() => import("./"));

So here's how to executeloaderfunction is the function that executes the() => import("./")The execution of theimport()After that it can be used in thenetworkThe panel sees the loadingWeb requests for documents.import()The return is a Promise, which will be triggered when the imported file is loaded.then()So here'sthen()will not be triggered at this point.

Then take the breakpoint out of theloadThe function returns to thesetupThe last return part of the function has the following code:

setup() {
  // ...an omission
  return () => {
    if ( && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if ( && errorComponent) {
      return createVNode(errorComponent, {
        error: ,
      });
    } else if (loadingComponent && !) {
      return createVNode(loadingComponent);
    }
  };
},

Notice how thesetupThe return value is a function, not an object as we often see. Since the return here is a function, the code will not go to the returned function to go inside the function to return to the function to make a breakpoint. We do not look at the function for the time being, so that the breakpoint out of thesetupfunction. Discoversetupfunction is created from the vuesetupStatefulComponentfunction call, in our scenario the simplifiedsetupStatefulComponentThe function code is as follows:

function setupStatefulComponent(instance) {
  const Component = ;
  const { setup } = Component;
  const setupResult = callWithErrorHandling(setup, instance, 0, [
    ,
    setupContext,
  ]);
  handleSetupResult(instance, setupResult);
}

uppercallWithErrorHandlingA function, as you should be able to tell from the name, calls a function and does error handling. In this case it's calling thesetupfunction, which will then call thesetupThe return value of the function is thrown to thehandleSetupResultfunction processing.

Walk the breakpoint into thehandleSetupResultfunction, in our scenariohandleSetupResultThe simplified code for the function is as follows:

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
     = setupResult;
  }
}

Earlier we talked about our scenariosetupThe return value of a function is a function, soisFunction(setupResult)The code will go to the = setupResultHere.instanceis the current instance of the vue component, and executing this will set thesetupResultassign a value torenderfunction.

We know that the render function is generally compiled from the template module, the implementation of the render function will generate the virtual DOM, and finally generated by the virtual DOM corresponding to the real DOM.

(coll.) fail (a student)setupis a function, that function acts as the component's render function. This is why the previousdefineComponentOnlynamefamiliarity with andsetupfunction without therenderfunction.

When the render function is executed to generate the virtual DOM it goes to thesetupThe code will stop at the function returned by thesetupreturned in the function. Recall that thesetupThe returned function code is as follows:

setup() {
  // ...an omission
  return () => {
    if ( && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if ( && errorComponent) {
      return createVNode(errorComponent, {
        error: ,
      });
    } else if (loadingComponent && !) {
      return createVNode(loadingComponent);
    }
  };
},

Since the asynchronous component hasn't finished loading at this point, theloadedis also false, the code will not walk into the firstifCenter.

The same components are not loaded yet and there is no error, and the code does not go to the firstelse ifCenter.

If we had passed in the loading component, the code wouldn't have gone to the secondelse ifin. Because at this point in time thedelayedvalue is still true, which means it is still in the delay phase. You can only wait until the previoussetTimeoutThe callbacks are executed only after thedelayedvalue is set to false.

moreover, as a result ofdelayedis a ref-responsive variable, so in thesetTimeoutThe callbacks in thedelayedvalue will be re-rendered, i.e. the render function will be executed again. As mentioned earlier the render function here is thesetupThe code retraces its steps to the function returned in the secondelse ifCenter.

this timeelse if (loadingComponent && !)whichloadingComponentis the loading component, andvalue is also false now. The code then goes to thecreateVNode(loadingComponent)in the page, executing this function renders the loading component to the page.

Loading Asynchronous Components

Earlier we talked about how rendering an asynchronous component executes theloadfunction, in which it actually executes the() => import("./")Loading Asynchronous ComponentsWe can also see in the network panel an additionaldocument request.

We know.import()The return value of the Promise is a Promise, which is triggered when the file is loaded.then()The code will now go to the first step. At this point the code will go to the firstthen()in it, recall the code:

const load = () => {
  return loader()
    .catch(() => {
      // ...an omission
    })
    .then((comp) => {
      if (
        comp &&
        (comp.__esModule || comp[] === "Module")
      ) {
        comp = ;
      }
      resolvedComp = comp;
      return comp;
    });
};

existthen()to determine if the file loaded in is an es6 module, and if so, set the module'sdefaultExport rewrite tocompcomponent object. And assign the loaded-in vue component object to theresolvedCompVariables.

After executing the firstthen()After that the code will go to the secondthen()in it, recall the code:

load()
  .then(() => {
     = true;
  })

second reasonthen()The code is simple, set theloadedvariable is set to true, which indicates that the asynchronous component has been loaded. Since theloadedis a responsive variable, and changing its value will cause the page to be re-rendered, and the render function will be executed again. As we mentioned earlier, the render function here is thesetupThe code retraces its steps to the function returned in the secondelse ifCenter.

Recap.setupThe code for the function returned in is as follows:

setup() {
  // ...an omission
  return () => {
    if ( && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if ( && errorComponent) {
      return createVNode(errorComponent, {
        error: ,
      });
    } else if (loadingComponent && !) {
      return createVNode(loadingComponent);
    }
  };
},

as at this timeloadedis true, and theresolvedCompvalue for the asynchronously loaded vue component object, so the virtual DOM returned by the render function this time will be thecreateInnerComp(resolvedComp, instance)The results of the implementation of the

createInnerComp function

Then walk the breakpoint into thecreateInnerCompfunction, the simplified code in our scenario is as follows:

function createInnerComp(comp, parent) {
  const { ref: ref2, props, children } = ;
  const vnode = createVNode(comp, props, children);
   = ref2;
  return vnode;
}

createInnerCompThe function takes two arguments, the first is the vue component object to be loaded asynchronously. The second argument is the object that will be loaded asynchronously using thedefineAsyncComponentThe vue instance corresponding to the created vue component.

Then it's time to executecreateVNodefunction, which you may have heard of, vue provides theh()function is actually a call to thecreateVNodefunction.

It's here with us.createVNodeThe first argument the function receives is the subcomponent object, the second argument is the props to be passed to the subcomponent, and the third argument is the children to be passed to the subcomponent.createVNodeThe function will generate the virtual DOM of the corresponding asynchronous component according to these three parameters, return the generated virtual DOM of the asynchronous component, and finally generate the real DOM according to the virtual DOM to render the asynchronous component to the page. The following figure (there is a summary after the figure):
progress

summarize

This article is aboutdefineAsyncComponentis how asynchronous components are implemented:

  • existdefineAsyncComponentfunction returns a vue component object with only thenameattributes andsetupfunction.

  • When rendering an asynchronous component thesetupfunction in thesetupfunction executes one of the built-inloadMethods. In theloadmethod will go ahead and execute the method defined by thedefineAsyncComponentdefined asynchronous component loading function, the return value of this loading function is a Promise, the asynchronous component loading is completed will trigger the Promise'sthen()

  • existsetupfunction will return a function that will be the render function of the component.

  • When the asynchronous component has finished loading it will go to the previously mentioned Promise'sthen()method, in which theloadedThe value of the responsive variable is modified to true.

  • Changing the value of the responsive variable causes the page to be re-rendered, and then the render function is executed. As mentioned earlier the render function at this point is thesetupThe callback function that will be returned in the function. Executing this callback function calls thecreateInnerCompfunction generates the virtual DOM of the asynchronous component, and finally it is the generation of the real DOM based on the virtual DOM that renders the asynchronous subcomponent to the page.

Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue

Also Ouyang wrote an open source ebookvue3 Compilation Principles Revealed, this book is accessible to middle school level front end. Totally free, just asking for a STAR.