This article is for VUE 2.6.x only.
This question is a big proposition, we can start from the principle of componentization, advantages, and then fall into the principle of components in VUE, implementation.
componentization
As a front-end worker, YOU must be familiar with components. Vue is all componentized framework, react is all componentized framework.
So what exactly is a componentized framework?
In fact, with the rise of mobile Internet a few years ago, the business logic of many programs has become increasingly complex, and the simple architecture in the early days has been unable to adapt to such complex programs. (Like this)
In fact, the actual project is far more complex than the picture above.
The core idea of componentization is to break the view into independent modules and assemble them into the final rendered page. (Like this)
Does that make the logic a lot clearer?
In fact, componentization has more advantages than that:
- Components are independent, reusable units of code organization. It allows developers to build large, complex applications from small, independent, reusable components
- Component-based development can greatly improve the application development efficiency, test coverage, code reuse rate
Vue componentization principle
As we know, there are two ways to define components in VUE:
/ / 1
Vue.component('comp', {
template: '<div>this is a component</div>'
})
/ / 2
<template>
<div>this is a component</div>
</template>
Copy the code
The second single-file component definition is used more often in normal development, but it’s important to remember that when writing a VUE component, we’re not writing the component directly, we’re writing the configuration of the component.
The above two definitions are not the same in the source code implementation position, here we introduce one by one.
Vue.component
/** * filepath: src/core/global-api/assets.js */
export function initAssetRegisters (Vue: GlobalAPI) {
/** * Create asset registration methods. */
ASSET_TYPES.forEach(type= > {
Vue[type] = function (
id: string,
definition: Function | Object
) :Function | Object | void {
if(! definition) {return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Copy the code
As you can see, component is processed first, and finally hangs on options.
Here’s how it works:
/** * filepath: src/core/global-api/extend.js */
Vue.extend = function (extendOptions: Object) :Function {
extendOptions = extendOptions || {};
const Super = this;
const SuperId = Super.cid;
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId];
}
const name = extendOptions.name || Super.options.name;
if(process.env.NODE_ENV ! = ="production" && name) {
validateComponentName(name);
}
const Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(Super.options, extendOptions);
Sub["super"] = Super;
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub);
}
if (Sub.options.computed) {
initComputed(Sub);
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// cache constructor
cachedCtors[SuperId] = Sub;
return Sub;
};
Copy the code
As you can see, the core of the code above is:
- A VueComponent class is created
- Established an inheritance relationship with VUE
- Option to merge
- Finally, some initialization operations
Single file component
The first thing to know is that for a single-file component, vue-loader compiles the template into the render function, which ultimately exports the component’s configuration object.
The single file component is not registered with Vue.com Ponent, so where is it implemented?
This brings us to the mountComponent:
/** * filepath: src/core/instance/lifecycle.js */
export function mountComponent(vm: Component, el: ? Element, hydrating? : boolean) :Component {
vm.$el = el;
if(! vm.$options.render) { vm.$options.render = createEmptyVNode;if(process.env.NODE_ENV ! = ="production") {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0)! = ="#") ||
vm.$options.el ||
el
) {
warn(
"You are using the runtime-only build of Vue where the template " +
"compiler is not available. Either pre-compile the templates into " +
"render functions, or use the compiler-included build.",
vm
);
} else {
warn(
"Failed to mount component: template or render function not defined.",
vm
);
}
}
}
callHook(vm, "beforeMount");
let updateComponent;
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ="production" && config.performance && mark) {
updateComponent = () = > {
const name = vm._name;
const id = vm._uid;
const startTag = `vue-perf-start:${id}`;
const endTag = `vue-perf-end:${id}`;
mark(startTag);
const vnode = vm._render();
mark(endTag);
measure(`vue ${name} render`, startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(`vue ${name} patch`, startTag, endTag);
};
} else {
updateComponent = () = > {
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// Each component has its own Watcher
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,"beforeUpdate"); }}},true /* isRenderWatcher */
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
Copy the code
The patch operation is performed, and within patch, createElm is called to create the node:
/** * filepath: src/core/vdom/patch.js */
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.vnode = ownerArray[index] = cloneVNode(vnode); } vnode.isRootInsert = ! nested;// for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
if (isDef(tag)) {
if(process.env.NODE_ENV ! = ="production") {
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
"Unknown custom element: <" +
tag +
"> - did you " +
"register the component correctly? For recursive components, " +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree);
if(! appendAsTree) {if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
createChildren(vnode, children, insertedVnodeQueue);
if (appendAsTree) {
if(isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); }}else {
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if(process.env.NODE_ENV ! = ="production"&& data && data.pre) { creatingElmInVPre--; }}else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else{ vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); }}Copy the code
CreateComponent is called to create a custom component:
/** * filepath: src/core/vdom/patch.js */
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true; }}}Copy the code
Here the component lifecycle hook function will be called. Combining the previous study of vue source code, we already know the component lifecycle as follows:
/** * filepath: src/core/vdom/create-component.js */
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ? boolean {if(vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = (vnode.componentInstance = oldVnode.componentInstance)
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children)},insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if(! componentInstance._isMounted) { componentInstance._isMounted =true
callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)}}},destroy(vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
deactivateChildComponent(componentInstance, true /* direct */)}}},}Copy the code
While in the init, will create a component instance through createComponentInstanceForVnode.
conclusion
With that out of the way, let’s go back to the business layer.
As mentioned above, components are independent from each other, so they must need some way to communicate with each other, so that they can communicate with each other and build a complete application. In VUE, the three core methods of data transfer between components are:
- Properties props
- Custom events
- slot
In fact, from a more abstract point of view, you can even think of all three as parameters args.
Beyond that, how should components be classified?
Generally we classify components into three categories:
- Page component: Usually each route corresponds to a page component, which is a collection of several components
- Business components: Such as form components, table components, calendar components, and so on, usually components that have actual business significance
- Common components: Buttons, input fields, etc. A popular library of common components such as Element UI, ANTD, etc
conclusion
008 – Talk about your understanding of vUE componentization