I’ve had a lot of interviews lately, and in almost every interview I’ve had a question about Vue source code. Here to open a series of columns, to summarize the experience of this aspect, if you feel helpful, might as well click a like support.
preface
In the previous 🚩Vue source code – component is how to register and use, a detailed description of the component registration and use of the internal logic flow, which is introduced are synchronized component registration and use. But in real development, asynchronous components are often used. Take a look at the official documentation on how to register asynchronous components in three ways.
- Ordinary functions asynchronous components
Vue.component('aa', function(resolve, reject) { setTimeout(function() { resolve({ template: '< div > < p > < span > {{aa}} {{bb}} < / span > < / p > < / div >', the data () {return {aa: 'welcome', bb: 'Vue'}}})}, 1000)})Copy the code
- Promise asynchronous components
Vue.component('aa', () => import('./aa.js') )
Copy the code
- Advanced asynchronous component
Const aa = () => ({// Component to load. It should be a Promise component: import('./aa.vue'), // Component loading: LoadingComp, 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}) Vue.component('aa', aa)Copy the code
As you can see from the example above, the component registered with Vue.com Ponent is no longer an object, but a function, and this function is not a component constructor, but a factory function. The factory function has two arguments, resolve and reject, which are defined internally by Vue. In the factory function, there is an asynchronous function. When the asynchronous function is successfully executed, resolve is called, and its arguments are the object of the asynchronous component.
Vnode = createComponent(Ctor, data, context, children, tag); vnode = createComponent; Where the parameter Ctor can be a function or an object, we start with the createComponent method to show how asynchronous components are used.
function createComponent(Ctor, data, context, children, tag) { if (isUndef(Ctor)) { return } var baseCtor = context.$options._base; if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } if (typeof Ctor ! == 'function') { return } // async component var asyncFactory; if (isUndef(Ctor.cid)) { asyncFactory = Ctor; Ctor = resolveAsyncComponent(asyncFactory, baseCtor); if (Ctor === undefined) { return createAsyncPlaceholder(asyncFactory, data, context, children, tag) } } data = data || {}; resolveConstructorOptions(Ctor); installComponentHooks(data); var name = Ctor.options.name || tag; var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), data, undefined, undefined, undefined, context, { Ctor: Ctor, tag: tag, children: children }, asyncFactory ); return vnode }Copy the code
Ctor = baseCtor. Extend (Ctor) is not executed when the Ctor value is of type function.
In Vue, the vue.extend method is called to create component constructors that inherit from Vue. Sub.cid = cid++ is assigned to the cid property of the component constructor in vue.extend.
var cid = 1;
Vue.extend = function(extendOptions) {
var Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
return Sub
}
Copy the code
Ctor = resolveAsyncComponent(asyncFactory, baseCtor); Ctor = resolveAsyncComponent(asyncFactory, baseCtor) Take a look at the resolveAsyncComponent function.
function resolveAsyncComponent(factory, baseCtor) {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance;
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner);
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
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(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;
}
}
};
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
});
var reject = once(function(reason) {
warn(
"Failed to resolve async component: " + (String(factory)) +
(reason ? ("\nReason: " + reason) : '')
);
if (isDef(factory.errorComp)) {
factory.error = true;
forceRender(true);
}
});
var res = factory(resolve, reject);
if (isObject(res)) {
if (isPromise(res)) {
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(function() {
timerLoading = null;
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender(false);
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(function() {
timerTimeout = null;
if (isUndef(factory.resolved)) {
reject(
"timeout (" + (res.timeout) + "ms)"
);
}
}, res.timeout);
}
}
}
sync = false;
return factory.loading ? factory.loadingComp : factory.resolved
}
}
Copy the code
ResolveAsyncComponent (resolve, reject, reject); resolve (reject, reject); resolveAsyncComponent (resolve, reject, reject); Reject fails, and returns the component constructor or undefined.
Asynchronous components are registered in the same way as synchronous components, but they are used differently. The following three methods of registering asynchronous components are used to analyze and introduce the principles of using asynchronous components.
Common function asynchronous components
Vue.component('aa', function(resolve, reject) { setTimeout(function() { resolve({ template: '< div > < p > < span > {{aa}} {{bb}} < / span > < / p > < / div >', the data () {return {aa: 'welcome', bb: 'Vue'}}})}, 1000)})Copy the code
ResolveAsyncComponent (factory, baseCtor), the factory argument is the second argument to vue.com.component. baseCtor is the Vue constructor. Clean up the code and remove any code that is not relevant to the scenario.
function resolveAsyncComponent(factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance;
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner);
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner];
var sync = true;
(owner).$on('hook:destroyed', function() {
return remove(owners, owner);
});
var forceRender = function(renderCompleted) {
for (var i = 0, l = owners.length; i < l; i++) {
(owners[i]).$forceUpdate();
}
if (renderCompleted) {
owners.length = 0;
}
};
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
});
var reject = once(function(reason) {
warn(
"Failed to resolve async component: " + (String(factory)) +
(reason ? ("\nReason: " + reason) : '')
);
});
var res = factory(resolve, reject);
sync = false;
return factory.loading ? factory.loadingComp : factory.resolved
}
}
Copy the code
The resolveAsyncComponent function internally defines three functions forceRender, resolve, and Reject. Resolve and reject are wrapped in once.
function once(fn) { var called = false; return function() { if (! called) { called = true; fn.apply(this, arguments); }}}Copy the code
The once function is a higher-order function that makes clever use of closures and the called variable to ensure that the wrapped function is executed only once. That is, ensure that resolve and reject are executed only once.
Because the resolveAsyncComponent function ends with return factory.loading? Factory. loadingComp: factory.resolved, return factory.resolved.
So take a look at factory.resolved, which is defined in the resolve function.
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
})
Copy the code
After ensureCtor(res, baseCtor) is assigned to factory.resolved, take a look at the ensureCtor method.
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
If base is the Vue constructor, then return isObject(comp)? Extend (comp) : comp, if the argument comp is an object, base. Extend (comp) is executed, that is, Vue. Extend (comp) generates a constructor that extends from Vue.
The comp argument is passed as the resolve argument res. Var res = Factory (resolve, reject) var res = Factory (resolve, reject) var res = Factory (resolve, Reject) var res = Factory (resolve, reject) var res = Factory
function(resolve, reject) { setTimeout(function() { resolve({ template: '< div > < p > < span > {{aa}} {{bb}} < / span > < / p > < / div >', the data () {return {aa: 'welcome', bb: 'Vue'}}})}, 1000)}Copy the code
Resolve is the resolve function defined inside the resolveAsyncComponent function. The value of the parameter comp, shown below, is an option object for the component.
{template: '< div > < p > < span > {{aa}} {{bb}} < / span > < / p > < / div >', the data () {return {aa: 'welcome', bb: 'Vue'}}}Copy the code
This ensureCtor(res, baseCtor) results in a component constructor, and the factory.resolved value is a component constructor.
Back in the createComponent method, look at this code
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
Copy the code
The resolveAsyncComponent method returns a component constructor assigned to Ctor. That’s the end of it. Vue.component: Resolve (//…) There is also a setTimeout timer, which is an asynchronous task. JavaScript is single-threaded, and asynchronous tasks wait until all synchronous tasks have finished executing. Resolve in the resolveAsyncComponent method will not be executed and factory. Resolved should be undefined. So Ctor is undefined, return createAsyncPlaceholder(asyncFactory, data, context, children, tag). The createAsyncPlaceholder method is used to create an annotation node, vNode, as a placeholder.
function createAsyncPlaceholder(factory, data, context, children, tag) {
var node = createEmptyVNode();
node.asyncFactory = factory;
node.asyncMeta = {
data: data,
context: context,
children: children,
tag: tag
};
return node
}
Copy the code
The createComponent method generates a comment node vNode instead of a component vNode. How to render that component? Resolve (true) is executed after 1000ms. Take a look at forceRender.
var resolve = once(function(res) { factory.resolved = ensureCtor(res, baseCtor); if (! sync) { forceRender(true); } else { owners.length = 0; } }) var owner = currentRenderingInstance; if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { factory.owners.push(owner); } if (owner && ! isDef(factory.owners)) { var owners = factory.owners = [owner]; var forceRender = function(renderCompleted) { for (var i = 0, l = owners.length; i < l; i++) { (owners[i]).$forceUpdate(); } if (renderCompleted) { owners.length = 0; }}; }Copy the code
CurrentRenderingInstance is the current Vue instance that uses the asynchronous component and is assigned to the owner.
If the same asynchronous component is locally registered in many places. This will repeat the same resolve function many times. So I made an optimization here.
Asynchronous components are defined with a factory function factory, where the owners property is defined to store the current Vue instance that uses the asynchronous component, which is the context in which the Factory function is called.
If owner has a value and factory.owners are not present, the factory function is executed for the first time. If owner has a value and factory.owners has a value, factory has already been executed. Factory.owner.indexof (owner) === -1 Check whether factory.owners has the current Vue instance. If not, add the current Vue instance to factory.owners.
Back in the forceRender function, executing (owners[I]).$forceUpdate() is equivalent to executing the vm.$forceUpdate() instance method. This is because no data changes during an asynchronous component load, so the Vue instance is forced to re-render by executing vm.$forceUpdate().
Vue.prototype.$forceUpdate = function() { var vm = this; if (vm._watcher) { vm._watcher.update(); }}Copy the code
Executing vm._watcher.update() is equivalent to executing vm._update(vm._render(), hydrating) in the mountComponent method, Calling the createComponent method during vm._render() executes the following logic.
// async component
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
When resolveAsyncComponent(asyncFactory, baseCtor) is executed again, 1000ms has passed, so resolve in the factory function registered by the async component has been executed, so factory. Resolved Return directly to factory.resolved.
function resolveAsyncComponent(factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
}
Copy the code
The return value factory.resolved is an asynchronous component constructor assigned to Ctor. When the Ctor value is obtained, vNode will be generated and vm._update will be executed to enter the patch process to replace the placeholder annotation node with the real COMPONENT DOM content. The patch process of component update is not introduced here, and a single chapter will be introduced later. The principle of using the asynchronous component of ordinary functions is introduced here, and the principle of using the Promise asynchronous component is introduced below.
The Promise asynchronous component
Vue.component('aa', () => import('./aa.js') )
Copy the code
ResolveAsyncComponent (factory, baseCtor), whose argument baseCtor is the Vue constructor. The value of the factory argument is the second argument to Vue.component above and the return value is import(‘./aa.js’), which is a Promise object. Clean up the code and remove any code that is not relevant to the scenario.
function resolveAsyncComponent(factory, baseCtor) { if (isDef(factory.resolved)) { return factory.resolved } var owner = currentRenderingInstance; if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { factory.owners.push(owner); } if (owner && ! isDef(factory.owners)) { var owners = factory.owners = [owner]; var sync = true; (owner).$on('hook:destroyed', function() { return remove(owners, owner); }); var forceRender = function(renderCompleted) { for (var i = 0, l = owners.length; i < l; i++) { (owners[i]).$forceUpdate(); } if (renderCompleted) { owners.length = 0; }}; var resolve = once(function(res) { factory.resolved = ensureCtor(res, baseCtor); if (! sync) { forceRender(true); } else { owners.length = 0; }}); var reject = once(function(reason) { warn( "Failed to resolve async component: " + (String(factory)) + (reason ? ("\nReason: " + reason) : '') ); if (isDef(factory.errorComp)) { factory.error = true; forceRender(true); }}); var res = factory(resolve, reject); if (isObject(res)) { if (isPromise(res)) { if (isUndef(factory.resolved)) { res.then(resolve, reject); } } } sync = false; return factory.loading ? factory.loadingComp : factory.resolved } }Copy the code
Because the return value of the factory function factory in this scenario is a Promise object, satisfy the conditions for isObject(res) and isPromise(res) to perform the following logic
if (isUndef(factory.resolved)) {
res.then(resolve, reject);
}
Copy the code
Because we return a Promise object, the example method then takes two functions resolve and reject. Resolve is executed on success, reject is called on failure.
Res. then(resolve, reject) is tactically executed, and resolve, which is custom defined in resolveAsyncComponent, is dropped on success. The logic that follows is exactly the same as for a normal function asynchronous component.
Advanced asynchronous components
In advanced asynchronous components, you can define components that are displayed during an asynchronous component load and components that are displayed during a load failure, making it more user-friendly.
Const aa = () => ({// Component to load. It should be a Promise component: import('./aa.vue'), // Loading: LoadingComp, // Loading failed to show 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}) Vue.component('aa', aa)Copy the code
ResolveAsyncComponent (factory, baseCtor), whose argument baseCtor is the Vue constructor. The value of the factory argument is the second argument to Vue.component above and the return value is an object. Clean up the code and remove any code that is not relevant to the scenario.
function resolveAsyncComponent(factory, baseCtor) {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance;
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner);
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
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(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;
}
}
};
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
});
var reject = once(function(reason) {
warn(
"Failed to resolve async component: " + (String(factory)) +
(reason ? ("\nReason: " + reason) : '')
);
if (isDef(factory.errorComp)) {
factory.error = true;
forceRender(true);
}
});
var res = factory(resolve, reject);
if (isObject(res)) {
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(function() {
timerLoading = null;
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender(false);
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(function() {
timerTimeout = null;
if (isUndef(factory.resolved)) {
reject(
"timeout (" + (res.timeout) + "ms)"
);
}
}, res.timeout);
}
}
}
sync = false;
return factory.loading ? factory.loadingComp : factory.resolved
}
}
Copy the code
Because in this scenario the return value of the factory function factory is an object res,
If the res.component property is a Promise object, execute res.component.then(resolve, reject).
ErrorComp = ensureCtor(res.error, baseCtor) if res.error has a value, convert the component shown as a load failure into a component constructor and assign the value to factory.errorcomp.
If res.loading has a value, factory.loadingComp = ensureCtor(res.loading, baseCtor) to convert the component shown in loading into a component constructor and assign the value to factory.loadingComp.
If res.delay is 0, the loading component is displayed directly. Set factory.loading to true.
If res.delay is not 0, it indicates that the loading component will be displayed after a delay. Use setTimeout timer after a delay, Setting factory.loading to true and executing forceRender(false) if the asynchronous component does not load successfully or fails, triggers the patch process of component update to render the component shown in loading.
If res.timeout has a value, use the setTimeout timer to report a timeout error if the asynchronous component has not finished loading after res.timeout is exceeded.
Finally, if the loading component constructor factory.loading has a value that returns factory.loading, instead of calling the createAsyncPlaceholder method to create annotation nodes as placeholders, use DOM nodes generated by the component shown in the loading as placeholders.
Asynchronous components are rendered during the patch process of component updates, and the resolveAsyncComponent method is called.
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
Copy the code
ErrorComp if factory.error is true and the component constructor factory.errorComp exists, return factory.errorComp.
If factory.loading is true and the loading component constructor factory.loadingComp exists, return factory.loadingComp.
The asynchronous component loads successfully and returns factory.Resolved, and the logic will be the same as for the normal function asynchronous component.
Finally, when an asynchronous component fails to load, it calls the custom reject function, which sets factory.error to true if factory.errorComp. Then execute forceRender(true), which takes true to clear the load and load timeout timers during forced rerendering.
Four,
Vue asynchronous components can be implemented in three modes. Advanced asynchronous components can be loaded, resolve, Reject, and timeout. The essence of asynchronous component implementation is two-time rendering. Except for the advanced asynchronous component whose delay is 0, it is the first time to render directly into loading component, and all other components generate a comment node for the first time. When the asynchronous component is successfully loaded, the resolve function is executed. Call forceRender to force rerendering, and then call resolveAsyncComponent a second time to return the factory.resolved constructor. Patch as the component updates.