Introduction to the
In large applications, some components may not show at the beginning, only under certain conditions can apply colours to a drawing, in which case the component resources actually don’t need to load from the start, can completely when needed to request, it can also reduce page loading resource volume for the first time to use asynchronous components in Vue is simple:
// asyncComponent. vue <template> <div> I am the content of the asynchronous component </div> </template> <script> export default {name: 'AsyncComponent' } </script>Copy the code
// app. vue <template> <div id=" App ">< AsyncComponent v-if="show"></AsyncComponent @click="load"> </button> </div> </template> <script> export default { name: 'App', components: { AsyncComponent: () => import('./AsyncComponent'), }, data() { return { show: false, } }, methods: { load() { this.show = true }, }, } </script>Copy the code
We don’t register AsyncComponent directly. Instead, we load dynamically using the import() method, which is defined by the ES2015 Loader specification and supported by WebPack. The AsyncComponent content is split into a separate JS file. The page will not load initially, and the request will be made only after the load button is clicked. This method returns a promise.
In this article, you can see how Vue handles asynchronous components and how WebPack resources are loaded.
Compile the product
First we make a package, generate three JS files:
The first file is the entry file of our application, which contains the contents of main.js and app. vue, as well as some webpack injection methods. The second file is the contents of our asynchronous component AsyncComponent, and the third file is the contents of some other common libraries, such as vue.
Then let’s look at app. vue compiled:
Above is the option object for the App component. You can see how asynchronous components are registered, which is a function.
Show is true, _c(‘AsyncComponent’) is executed, otherwise _vm._e() is executed to create an empty VNode. _c is createElement:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
Copy the code
Let’s take a look at how this method works when we click the button.
CreateElement method method
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
Context is the App component instance, tag is the AsyncComponent argument of _c, and the other arguments are undefined or false, so the if branches of this method are not called, and the _createElement method is directly called:
function _createElement (context, tag, data, children, normalizationType) {
// If data is the observed data
if (isDef(data) && isDef((data).__ob__)) {
return createEmptyVNode()
}
// Object syntax in V-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
// Tag does not exist, probably component :is property not set
if(! tag) {return createEmptyVNode()
}
// Single function items are supported as default scope slots
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {};
data.scopedSlots = { default: children[0]}; children.length =0;
}
// Process the child nodes
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
// ...
}
Copy the code
None of the above logic will be entered in our example, so read on:
function _createElement (context, tag, data, children, normalizationType) {
// ...
var vnode, ns;
// Tag is a string
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// Whether the element is reserved, such as HTML element or SVG element
if (false) {}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
);
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
/ / component
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// Other unknown tags
vnode = new VNode(
tag, data, children,
undefined.undefined, context ); }}else {
// Tag is a component option or constructor
vnode = createComponent(tag, data, context, children);
}
// ...
}
Copy the code
For our asynchronous component, the tag is AsyncComponent, which is a string, and we can find our registered AsyncComponent using the resolveAsset method:
function resolveAsset (
options,// $options for App component instance
type,// components
id,
warnMissing
) {
if (typeofid ! = ='string') {
return
}
var assets = options[type];
// Check the local registry first
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
// If there is no local one, look it up on the prototype chain
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (false) {}
return res
}
Copy the code
Vue creates each of our components as a constructor and then instantiates them. During creation, options are merged, that is, the options of this component are merged with the options of the parent constructor:
In the figure above, the child option is the component option of App, and the parent option is the option object of the Vue constructor. For the Components option, an object is created based on the value of the parent option, and the option value of the subclass itself is added to the object as a property. Finally, this object is used as the subclass constructor for the options.components property value:
Then, when the component is instantiated, an object is created using the constructor’s options object as a prototype, as the instance’s $options:
So an App instance can find the AsyncComponent from the Options.com ponents object in its constructor via $options:
You can see that this is the compiled function we saw earlier.
The createComponent method is then executed:
function createComponent (Ctor, data, context, children, tag) {
// ...
// Asynchronous components
var asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// ...
}
Copy the code
We then execute the resolveAsyncComponent method:
function resolveAsyncComponent (factory, baseCtor) {
// ...
var owner = currentRenderingInstance;
if(owner && ! isDef(factory.owners)) {var owners = factory.owners = [owner];
var sync = true;
var timerLoading = null;
var timerTimeout = null; (owner).$on('hook:destroyed'.function () { return remove(owners, owner); });
var forceRender = function(){}
var resolve = once(function(){})
var reject = once(function(){})
// Execute the asynchronous component's function
var res = factory(resolve, reject);
}
// ...
}
Copy the code
Here we finally execute the asynchronous component’s function, which is the following:
function AsyncComponent() {
return __webpack_require__.e( / *! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null./ *! ./AsyncComponent */ "c61d"));
}
Copy the code
To see what res is, we need to look at what these Webpack functions do.
Loading component Resources
webpack_requireE. method
First look at the __webpack_require__.e method:
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// Chunk that has been loaded
var installedChunkData = installedChunks[chunkId];
if(installedChunkData ! = =0) { // 0 indicates that the file is loaded
// If the value is not 0, the component is loading. InstalledChunkData [2] is a Promise object
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// Create a Promise and cache the two callback parameters on the installedChunks object
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// Add the Promise object itself to the cache array
promises.push(installedChunkData[2] = promise);
// Start to initiate chunk requests
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
// Splice the chunk request URL
script.src = jsonpScriptSrc(chunkId);
var error = new Error(a);// Chunk load completed/failed return
onScriptComplete = function (event) {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk ! = =0) {
// If the value of this chunkId still exists on the installedChunks object, it indicates a loading error
if (chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ':' + realSrc + ') ';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined; }};// Set the timeout period
var timeout = setTimeout(function () {
onScriptComplete({
type: 'timeout'.target: script
});
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script); }}return Promise.all(promises);
};
Copy the code
This method is a bit long, but the logic is very simple. First, the function returns a promise. If the chunk to be loaded has not been loaded, create a promise, cache it in the installedChunks object, then create a script tag to load the chunk. The only thing that is hard to understand is the onScriptComplete function, because in this function, if the chunk’s cache information on installedChunks is not zero, it will be treated as a failure. The problem is that the promise information was just cached before, and there is no modification. To understand this, we need to look at the contents of the chunk we are loading:
You can see that the code executes directly and adds an entry to the webpackJsonp array:
window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-1f79b58b"] and {.. }])Copy the code
The window[“webpackJsonp”] push method has been modified:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] | | [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
var parentJsonpFunction = oldJsonpFunction;
Copy the code
Changed to the webpackJsonpCallback method:
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// Add the chunk's Promise's resolve callback to the converger array
resolves.push(installedChunks[chunkId][0]);
}
// Indicates that the chunk has been loaded
installedChunks[chunkId] = 0;
}
// Add the chunk's module data to modules objects
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; }}// Execute the original push method
if (parentJsonpFunction) parentJsonpFunction(data);
// Execute resolve
while(resolves.length) { resolves.shift()(); }}Copy the code
This function will fetch the resolve function of the chunk-loaded promise and mark its installedChunks message as 0 to indicate a successful load, so the onScriptComplete function that will be executed later will determine if the load has failed by being 0. The resolve function is executed so that the promise state returned by the previous __webpack_require__.e function becomes successful.
Let’s review the AsyncComponent functions again:
function AsyncComponent() {
return __webpack_require__.e( / *! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null./ *! ./AsyncComponent */ "c61d"));
}
Copy the code
After the chunk is loaded, the __webpack_require__ method is executed.
__webpack_require__
methods
This method is the most important method of Webpack and is used to load modules:
function __webpack_require__(moduleId) {
// Check whether the module has been loaded
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module and cache it
var module = installedModules[moduleId] = {
i: moduleId,
l: false.exports: {}};// Execute the module function
modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
// marks the module loading status
module.l = true;
// Returns the export of the module
return module.exports;
}
Copy the code
So __webpack_require__.bind(null, /*! /AsyncComponent */ “c61D “) loads the c61D module in the chunk we just requested back:
This module will load its dependent modules internally, and return the result:
This is the AsyncComponent option.
Go back to the createElement method
Back to the resolveAsyncComponent method:
var res = factory(resolve, reject);
Copy the code
Now we know that the RES is an unfinished promise, and instead of waiting for the asynchronous component to load, Vue continues to execute backwards:
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if(isUndef(factory.resolved)) { res.then(resolve, reject); }}}return factory.resolved
Copy the code
Resolve and reject are passed as arguments to the Promise RES, and factory.resolved is returned. This property has not been set to anything, so it is undefined.
Coming back to the createComponent method:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// Returns a placeholder node for the asynchronous component that is rendered as a comment node but retains all the original information for that node.
// This information will be used for asynchronous server rendering.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
Copy the code
Since Ctor is undefined, the createAsyncPlaceholder method is called to return a placeholder node:
function createAsyncPlaceholder (factory, data, context, children, tag) {
// Create an empty VNode, which is a comment node
var node = createEmptyVNode();
// Retain information about the component
node.asyncFactory = factory;
node.asyncMeta = { data: data, context: context, children: children, tag: tag };
return node
}
Copy the code
Finally, let’s return to the _createElement method:
// ...
vnode = createComponent(Ctor, data, context, children, tag);
// ...
return vnode
Copy the code
For asynchronous nodes, simply return the created comment node, and finally convert the virtual node to the real node, which actually creates a comment node:
Now let’s look at resolve, defined in the resolveAsyncComponent function, which executes when the chunk load is complete:
var resolve = once(function (res) {d
// Cache the results
factory.resolved = ensureCtor(res, baseCtor);
// called when parsing asynchronously
// (SSR will parse asynchronous to synchronous)
if(! sync) { forceRender(true);
} else {
owners.length = 0; }});Copy the code
Res is the AsyncComponent option and baseCtor is the Vue constructor that calls the ensureCtor method as arguments:
function ensureCtor (comp, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default;
}
return isObject(comp)
? base.extend(comp)
: comp
}
Copy the code
You can see that the extend method is actually called:
As mentioned earlier, Vue will create a constructor for each of our components using this method. This method will create a subclass of baseCtor, which will create an AsyncComponent subclass:
The forceRender method is executed when the subclass is successfully created:
var forceRender = function (renderCompleted) {
for (var i = 0, l = owners.length; i < l; i++) {
(owners[i]).$forceUpdate();
}
if (renderCompleted) {
owners.length = 0;
if(timerLoading ! = =null) {
clearTimeout(timerLoading);
timerLoading = null;
}
if(timerTimeout ! = =null) {
clearTimeout(timerTimeout);
timerTimeout = null; }}};Copy the code
The owners have the App component instance, so its $forceUpdate method is called, which forces the Vue instance to re-render, i.e. re-execute the render function for diff and path updates to the virtual DOM.
So the render function of the App component will be re-executed, and the createElement method will be executed, and we’ll do the same thing again, except the AsyncComponent has been loaded and the corresponding constructor has been created, so for the createComponent method, This time the result of the resolveAsyncComponent method is not undefined, but the constructor of the AsyncComponent:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
function resolveAsyncComponent (factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
}
Copy the code
The normal component rendering logic is then followed:
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ' ')),
data, undefined.undefined.undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
Copy the code
It can be seen that a VNode is actually created for the component. It is not the focus of this article to introduce how to render the VNode of the component into the real DOM. Basically, during the diff and patch of the virtual DOM, if the VNode encountered is a component type, Then a new instance of this component will be associated with VNode. Component instantiation is no different from our new Vue(), which will first merge options, initialize life cycle, initialize events, observe data and other operations, then execute the rendering function of this component, generate the VNode of this component, and finally perform patch operation. The actual DOM node is generated, and the diff and patch process of the parent component will be returned only after these operations of the child component are completed. Because the DOM of the child component has been created, it can be inserted. If you are interested in the more detailed process, you can learn about it by yourself.
That’s all for this article.