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