This article is the sixth in the vUE series. I have seen this series of articles partners know: the article thief is long, can not read the suggestion of the first point when the collection, and then have time to calm down to slowly read, front-end communication group: 731175396. Previous article portals are as follows
- Vue Core – VDOM
- In Detail on Vue-Slot
- In Detail on Vue-Transition
- The Vue-Transition-Group
- Vue – Abstract Components In Action
If you have used VUE, you will know that components are everywhere in vue development. The.vue (SFC) files in a project are components.
So, if Component is so central and important, why not take a look at a wave?
According to the not?
Lu xun –
Component creation
CreateElement createComponent is used to create a vNode. If it is a normal HTML tag, it will instantiate a normal VNode. Otherwise, create a Component vNode using createComponent
1, the createElement method
Only the code for vNode creation in different cases is listed here
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
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 {
// 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)
}
Copy the code
2, the createComponent
Next, let’s look at createComponent() as defined below
export function createComponent (
Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeofCtor ! = ='function') {
if(process.env.NODE_ENV ! = ='production') {
warn(`Invalid Component definition: The ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
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
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
Copy the code
- Inside it, the first thing to do is to get the constructor
Vue
Assign to a variablebaseCtor
, and through theextend
The parameterCtor
extend
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
Copy the code
Here we see $options._base, which is the constructor Vue
// src/core/global-api/index.js
Vue.options._base = Vue
// src/core/instance/init.js
// 1. initMixin()
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2. initInternalComponent()
const opts = vm.$options = Object.create(vm.constructor.options)
Copy the code
- Next, determine whether the component is asynchronous, functional, or abstract. I’ll talk more about how to deal with each case later
// Asynchronous components
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// Functional components
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// Abstract the component
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
Copy the code
- There is also processing for events on the component, which extracts event listeners on the component. It needs to be a listener for a child component, not a DOM listener. So you need to replace it with owning
.native
Modifier so that it can be processed during the parent component patch phase
const listeners = data.on
data.on = data.nativeOn
Copy the code
- Then, install the component’s hook function. It will be
componentVNodeHooks
Merges the hook function todata.hook
And thenComponent
The type ofvnode
Nodes in thepatch
Procedure executes the associated hook function, and passes if the hook function already exists at some pointmergeHook
Merge functions, that is, execute the two functions in sequence at the same time
installComponentHooks(data)
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if(existing ! == toMerge && ! (existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }const hooksToMerge = Object.keys(componentVNodeHooks)
function mergeHook (f1: any, f2: any) :Function {
const merged = (a, b) = > {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
Copy the code
3, componentVNodeHooks
ComponentVNodeHooks init, PrePatch, Insert, destroy
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {const mountedNode: any = vnode
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) {
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
ComponentVNodeHooks What do the four hook functions in componentVNodeHooks do
init
: whenvnode
为keep-alive
Component, instance exists and has not been destroyed. To prevent component flow, execute directlyprepatch
. Otherwise, execute it directlycreateComponentInstanceForVnode
To create aComponent
The type ofvnode
Instance, and proceed$mount
operationprepatch
: Updates existing components to the latestvnode
There’s nothing to tell hereinsert
:insert
Hook function- It first determines whether the component instance has been
mounted
, if not rendered, will be directlycomponentInstance
Execute as parametermounted
Hook function. - Second, the component is
keep-alive
The case for built-in components. There’s an operation here that’s a little bit snappy, which is when it’s alreadymounted
When it is, enterinsert
In order to preventkeep-alive
Child component update triggeredactivated
The hook function is abandonedwalking tree
Update mechanism, but directly to the component instancecomponentInstance
Put it in theactivatedChildren
In this array. Of course notmounted
Is triggered directlyactivated
Hook function proceedmounted
Can be
- It first determines whether the component instance has been
destroy
The component destruction operation is also true herekeep-alive
Components are compatible. If it is notkeep-alive
Component, directly executed$destory
Destroys the component instance or firesdeactivated
The hook function is destroyed.
Some of the helper functions used above are as follows
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
) :Component {
const options: InternalComponentOptions = {
_isComponent: true._parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
Copy the code
- Final instantiation
VNode
And then return
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
The whole process of createComponent is to first build the Vue subclass constructor, then install the component’s hook function, finally instantiate VNode, and then return. Many of these operations are compatible with the Keep-Alive built-in components. So if you’ve ever used the Keep-alive component and happened to see this, you’ll have a lot to learn.
2. Merge configurations
In general, to ensure the customization and scalability of a plug-in or component, you define some default configurations for the plug-in or component, and then perform internal operations of the Merge configuration items, so that you can customize the configurations during its initialization.
Of course, Vue does the same here. For the options in the vue merge strategy: actually, I also list the code above, concrete in SRC/core/instance/init. Js in here I only keep related code).
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
// ...
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...}}Copy the code
As you can see, there are two strategies for merging. One is for Component components, execute initInternalComponent to merge internal Component configuration. The other is for non-component components, directly merge configuration through mergeOptions.
1, normal merge
Here will directly resolveConstructorOptions (vm) constructor) return values and the options to merge
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
Copy the code
Let’s take a look at the definition of vue. options
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type= > {
Vue.options[type + 's'] = Object.create(null)})// ...
}
// src/shared/constants.js
export const ASSET_TYPES = [
'component'.'directive'.'filter'
]
Copy the code
MergeOptions is one of vue’s core merge strategies. Its main function is to merge parant and child, and return a new object in SRC /core/util/options.js.
- The first one will be right
child
The aboveprops
,inject
,directives
forobject format
Operation (specific logic can be studied, mainly on itsobject
Conversion operation) - if
child._base
Does not exist, traversalchild.extends
和child.mixins
, merge it toparent
上 - traverse
parent
, the callmergeField
Merge into variablesoptions
上 - traverse
child
If,child
有parent
Property that does not existmergeField
Merge the property tooptions
上
export function mergeOptions (
parent: Object,
child: Object, vm? : Component) :Object {
if(process.env.NODE_ENV ! = ='production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if(! child._base) {if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if(! hasOwn(parent, key)) { mergeField(key) } }function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
Copy the code
In addition to merging options, there are also many merging strategies in VUE. If you are interested, you can refer to SRC /core/util/options.js for research
2, component merge
When analyzing createComponent, we learned that the component’s constructor extends Vue via vue.extend, as shown below
// src/core/global-api/index.js
Vue.options._base = Vue
// src/core/vdom/create-component.js
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
Copy the code
Vue.extend, defined in SRC /core/global-api/ exten.js (with only the key logic), extends super.options, Options merged into sub. options
export function initExtend (Vue: GlobalAPI) {
// ...
Vue.extend = function (extendOptions: Object) :Function {
extendOptions = extendOptions || {}
const Super = this
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...
return Sub
}
}
Copy the code
Then in componentVNodeHooks init hook function, namely the subcomponents initialization phase, executes createComponentInstanceForVnode component instance initialization. CreateComponentInstanceForVnode function the vnode.com ponentOptions. Ctor point is above the Vue. The extend returned in the Sub, _init(options), Vue._init(options), and since options._isComponent is defined as true, So we go directly to the initInternalComponent operation
// componentVNodeHooks init()
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// createComponentInstanceForVnode()
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
) :Component {
const options: InternalComponentOptions = {
_isComponent: true._parentVnode: vnode,
parent
}
// ...
return new vnode.componentOptions.Ctor(options)
}
Copy the code
InitInternalComponent does a few simple object assignments that I won’t go into. The code looks like this:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
Copy the code
Now, if you’re a little confused at this point, let me give you an example
<template>
<div class="hello">
{{ msg }}
</div>
</template>
<script>
export default {
name: 'HelloWorld'.props: {
msg: String
},
created () {
console.log('this is child')}}</script>
Copy the code
The call is then made in the parent component
<template>
<div class="home">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home'.components: {
HelloWorld
},
created () {
console.log('this is parent')}}</script>
Copy the code
After the merge strategy, the vm.$options values are roughly as follows
vm.$options = {
parent: VueComponent, // Parent component instance
propsData: {
msg: 'Welcome to Your Vue.js App'
},
_componentTag: 'HelloWorld'._parentListeners: undefined._parentVnode: VNode, // Parent vNode instance
_propKeys: ['msg']._renderChildren: undefined.__proto__: {
components: {
HelloWorld: function VueComponent(options) {}},directives: {},
filters: {},
_base: function Vue(options) {},
_Ctor: {},
created: [
function created() {
console.log('this is parent')},function created() {
console.log('this is child'}]}}Copy the code
Asynchronous components
When analyzing createComponent above, we left out a few special cases, one of which was the case of asynchronous components. In this scenario, if ctor. cid is not defined, the asynchronous component creation process is directly followed, with the following code
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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
Before we dive into the details, let’s take a look at the official example to see how asynchronous components are used differently than normal components
// Common components
Vue.component('my-component-name', {
// ... options ...
})
// Asynchronous components
Vue.component('async-webpack-example'.function (resolve, reject) {
// This particular require syntax
// will instruct WebPack to automatically build the code,
// Split into different bundles and load them via Ajax requests.
require(['./my-async-component'], resolve)
})
Copy the code
In our example, the Vue generic component is an object, while the asynchronous component is a factory function that takes two arguments, a resolve callback to fetch the component-defined object from the server, and a Reject callback to indicate a load failure. In addition to the above, asynchronous components can be written in two other ways
// Promise asynchronous component
Vue.component(
'async-webpack-example'.// the 'import' function returns a Promise.
() = > import('./my-async-component'))// Advanced asynchronous components
const AsyncComponent = () = > ({
// Load the component (eventually return a Promise)
component: import('./MyComponent.vue'),
// An asynchronous component is loading
loading: LoadingComponent,
// Failed to load, display this component
error: ErrorComponent,
// Displays the delay time before loading components. Default value: 200ms.
delay: 200.// If a timeout is provided and the load time exceeds this timeout,
// Displays the error component. Default: Infinity.
timeout: 3000
})
Vue.component('async-component', AsyncComponent)
Copy the code
1, resolveAsyncComponent
ResolveAsyncComponent supports the three asynchronous component creation methods described above
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
) :Class<Component> | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if(owner && ! isDef(factory.owners)) {const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null; (owner: any).$on('hook:destroyed'.() = > remove(owners, owner))
const forceRender = (renderCompleted: boolean) = > {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if(timerLoading ! = =null) {
clearTimeout(timerLoading)
timerLoading = null
}
if(timerTimeout ! = =null) {
clearTimeout(timerTimeout)
timerTimeout = null}}}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)}else {
owners.length = 0}})const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
`Failed to resolve async component: The ${String(factory)}` +
(reason ? `\nReason: ${reason}` : ' '))if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)}})const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
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(() = > {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)}}if (isDef(res.timeout)) {
timerTimeout = setTimeout(() = > {
timerTimeout = null
if(isUndef(factory.resolved)) { reject( process.env.NODE_ENV ! = ='production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}
sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
Copy the code
First let’s look at how asynchronous components are loaded. Here we’ll skip over what resolveAsyncComponent does to the advanced asynchronous components we mentioned earlier in the first place.
Before looking at the resolveAsyncComponent asynchronous component creation logic, let’s take a look at some of the core methods used
-
ForceRender: Force the component to rerender, then remove the owners of the factory function when render is done, and remove timerLoading and timerTimeout.
$forceUpdate: Calls Watcher’s update method, which is a re-rendering of the component. In vUE, only data changes will trigger the view to be rerendered. In asynchronous components, data does not change during loading, so the component will not be rerendered by executing $forceUpdate to force it to be rerendered
const forceRender = (renderCompleted: boolean) = > {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if(timerLoading ! = =null) {
clearTimeout(timerLoading)
timerLoading = null
}
if(timerTimeout ! = =null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Copy the code
once
: Uses closures and an identity variablecalled
Ensure that its wrapped functions are executed only once
export function once (fn: Function) :Function {
let called = false
return function () {
if(! called) { called =true
fn.apply(this.arguments)}}}Copy the code
-
Resolve: The internal resolve function, which first executes ensureCtor and returns its resolved value for the factory. Then, if the sync asynchronous variable is false, execute forceRender directly to force the component to rerender, otherwise empty the owners
EnsureCtor is a function defined to ensure that a component object defined on an asynchronous component can be found. If it is found to be a normal object, it is converted directly to the component’s constructor via vue.extend
const resolve = once((res: Object | Class<Component>) = > {
factory.resolved = ensureCtor(res, baseCtor)
if(! sync) { forceRender(true)}else {
owners.length = 0}})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
reject
Internal:reject
Function that is executed when the asynchronous component load fails
const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
`Failed to resolve async component: The ${String(factory)}` +
(reason ? `\nReason: ${reason}` : ' '))if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)}})Copy the code
After looking at the core methods, let’s look at how asynchronous components are created.
- We learn from
resolveAsyncComponent
It is known from the definition that the method takes two arguments, one of which isfactory
The factory function, one isbaseCtor
, i.e.,Vue
. - Then the current render instance exists, and in
factory.owners
Exists in the case that the component enterspending
Phase, the current instance is thrown directly tofactory.owners
In the. - However, when an asynchronous component is initialized
factory
There will not beowners
Well, what should I do then? It’s simple. Just do itfactory
Factory function, and put the internally definedresolve
和reject
Function as an argument, so we can go right throughresolve
和reject
Doing something, this logic is exactly what is supported for normal asynchronous components, and the code is shown below
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}
if(owner && ! isDef(factory.owners)) {const owners = factory.owners = [owner]
let sync = true; (owner: any).$on('hook:destroyed'.() = > remove(owners, owner))
const forceRender = (renderCompleted: boolean) = > {
// ...
}
const resolve = once((res: Object | Class<Component>) = > {
// ...
})
const reject = once(reason= > {
// ...
})
const res = factory(resolve, reject)
// ...
}
Copy the code
Promise
Asynchronous components
In VUE, you can load components asynchronously using WebPack2 + + ES6, as shown below
Vue.component(
'async-webpack-example'.// the 'import' function returns a Promise.
() = > import('./my-async-component'))Copy the code
After res = factory(resolve, reject), res is the return value of import(‘./my-async-component’), which is a Promise object. The processing logic of the Promise asynchronous component is then entered. Resolve is executed if the asynchronous component is loaded successfully, and reject is executed if the asynchronous component fails
const res = factory(resolve, reject)
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
Copy the code
- Advanced asynchronous component
In the 2.3.0+ version, vue has added the loading state processing function, that is, it throws some configurable fields to the user, including Component, loading, error, delay, timeout, Component supports the form of Promise asynchronous component loading. The case code is as follows
const AsyncComponent = () = > ({
// Load the component (eventually return a Promise)
component: import('./MyComponent.vue'),
// An asynchronous component is loading
loading: LoadingComponent,
// Failed to load, display this component
error: ErrorComponent,
// Displays the delay time before loading components. Default value: 200ms.
delay: 200.// If a timeout is provided and the load time exceeds this timeout,
// Displays the error component. Default: Infinity.
timeout: 3000
})
Vue.component('async-component', AsyncComponent)
Copy the code
If res = factory(resolve, Reject) and res.component returns a Promise, execute the then method as follows
else if (isPromise(res.component)) {
res.component.then(resolve, reject)
}
Copy the code
This is followed by processing of the other four configurable fields
- First determine if it is custom
error
Component, if any, executeensureCtor(res.error, baseCtor)
And assigns the return value directly tofactory.errorComp
- Same thing if it’s passed in
loading
Component is executedensureCtor(res.loading, baseCtor)
And assigns the return value directly tofactory.loadingComp
- And then, in the definition
loading
Component logic, if setdelay
If the value is 0, thefactory.loading
A value fortrue
, otherwise delaydelay
To perform,delay
The default delay is 200ms - Finally, if the component load is set
timeout
Load time if the component is inres.timeout
If the load fails, run the command directlyreject
To throw the wrong
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(() = > {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)}}if (isDef(res.timeout)) {
timerTimeout = setTimeout(() = > {
timerTimeout = null
if(isUndef(factory.resolved)) { reject( process.env.NODE_ENV ! = ='production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
Copy the code
Finally, different values are returned by determining factory.loading. As we know from the processing of the custom field loading above, if the custom field delay is set to 0, it means that the loading component is directly rendered this time. Otherwise it will simply delay and execute to the forceRender method, which will trigger a re-rendering of the component, which will execute resolveAsyncComponent again
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
Copy the code
Then we return to the actions we skipped at the beginning of the resolveAsyncComponent
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
Copy the code
2, createAsyncPlaceholder
If you execute resolveAsyncComponent for the first time, the return value of resolveAsyncComponent will be undefined, unless you set delay to 0. In order to prevent the node information from being captured when Ctor is undefined, an annotated VNode is created directly from createAsyncPlaceholder, which will be used as a placeholder for asynchronous components and retain all the original information about the vNode. The specific code is as follows
export function createAsyncPlaceholder (
factory: Function, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag: ? string) :VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
Copy the code
Functional components
One issue left unaddressed when analyzing createComponent component creation is the functional Component, which is described in the following scenario
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
Copy the code
In case you don’t know what a functional component is, let me list two officially supported ways to write functional components
// render function
Vue.component('my-component', {
functional: true.// Props is optional
props: {
// ...
},
// To make up for missing instances
// We provide the second argument context as the context
render: function (createElement, context) {
// ...}})// template functional
<template functional></template>
Copy the code
For more information, go directly to the official documentation and read it carefully.
Official definition of a functional component: The component is marked functional and has no state, no responsive data, and no instance, i.e. no this context.
Let’s take the veil off functional components.
1, createFunctionalComponent
CreateFunctionalComponent main core is divided into three steps
- will
Ctor.options
In theprops
Merge to new objectprops
In the. ifCtor.options
There areprops
, directly traverses itprops
, the implementation ofvalidateProp
对Ctor.options.props
Current property validates and copies the current property toprops[key]
. ifCtor.options.props
If not defined, willdata
Well defined aboveattrs
和props
By performingmergeProps
Function merged into the new objectprops
On. - perform
new FunctionalRenderContext
instantiationfunctional
Component context, and executeoptions
On therender
Function instantiationvnode
node - For instantiated
vnode
Perform a special clone operation and return
export function createFunctionalComponent (
Ctor: Class<Component>,
propsData: ?Object,
data: VNodeData,
contextVm: Component,
children: ?Array<VNode>
) :VNode | Array<VNode> | void {
const options = Ctor.options
const props = {}
const propOptions = options.props
if (isDef(propOptions)) {
for (const key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject)
}
} else {
if (isDef(data.attrs)) mergeProps(props, data.attrs)
if (isDef(data.props)) mergeProps(props, data.props)
}
const renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
)
const vnode = options.render.call(null, renderContext._c, renderContext)
if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
} else if (Array.isArray(vnode)) {
const vnodes = normalizeChildren(vnode) || []
const res = new Array(vnodes.length)
for (let i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
}
return res
}
}
Copy the code
The two helper functions mentioned above are as follows
cloneAndMarkFunctionalResult
: To avoid reusing nodes,fnContext
Causes the named slot point to mismatch the case directly in the settingfnContext
The node is cloned before, and the cloned node is returnedvnode
mergeProps
:props
Merge strategy
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, renderContext) {
const clone = cloneVNode(vnode)
clone.fnContext = contextVm
clone.fnOptions = options
if(process.env.NODE_ENV ! = ='production') {
(clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext
}
if (data.slot) {
(clone.data || (clone.data = {})).slot = data.slot
}
return clone
}
function mergeProps (to, from) {
for (const key in from) {
to[camelize(key)] = from[key]
}
}
Copy the code
2, FunctionalRenderContext
We know from the documentation that functional components can be written in two ways, the first as a render function and the other as a < Template functional> single-file component. Render the function of the way in the treatment of createFunctionalComponent support has been done, it will direct execution Ctor. The options on the render method. The < Template functional> single-file component approach is also supported in the functional component render context constructor.
- First, it is designed to ensure that functional components
createElement
The function is able to get a unique context that will be clonedparent
Object to the contextvm
variablecontextVm
.contextVm._original
The assignment isparent
, as a token of its context source. One of the more critical cases is if the context is passed invm
It’s also a functional context. How do I do that? As long as you follow_uid
To reverse the logic,contextVm
receiveparent
.parent
receiveparent._original
Can, because go up to continue to look, always can find existence_uid
的parent
It isn’t. - The next step is to look at functional components
data
,props
,listeners
,injections
Support processing, here forslots
I’ve done a layer of transformation, and I’m going tonormal slots
Object converted toscoped slots
- Finally,
options._scopeId
The presence or absence of different scenarioscreateElement
Creating a node
export function FunctionalRenderContext (
data: VNodeData,
props: Object,
children: ?Array<VNode>,
parent: Component,
Ctor: Class<Component>
) {
const options = Ctor.options
let contextVm
if (hasOwn(parent, '_uid')) {
contextVm = Object.create(parent)
contextVm._original = parent
} else {
contextVm = parent
parent = parent._original
}
const isCompiled = isTrue(options._compiled)
constneedNormalization = ! isCompiledthis.data = data
this.props = props
this.children = children
this.parent = parent
this.listeners = data.on || emptyObject
this.injections = resolveInject(options.inject, parent)
this.slots = () = > {
if (!this.$slots) {
normalizeScopedSlots(
data.scopedSlots,
this.$slots = resolveSlots(children, parent)
)
}
return this.$slots
}
Object.defineProperty(this.'scopedSlots', ({
enumerable: true,
get () {
return normalizeScopedSlots(data.scopedSlots, this.slots())
}
}: any))
// support for compiled functional template
if (isCompiled) {
// exposing $options for renderStatic()
this.$options = options
// pre-resolve slots for renderSlot()
this.$slots = this.slots()
this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots)
}
if (options._scopeId) {
this._c = (a, b, c, d) = > {
const vnode = createElement(contextVm, a, b, c, d, needNormalization)
if (vnode && !Array.isArray(vnode)) {
vnode.fnScopeId = options._scopeId
vnode.fnContext = parent
}
return vnode
}
} else {
this._c = (a, b, c, d) = > createElement(contextVm, a, b, c, d, needNormalization)
}
}
Copy the code
5. Abstract components
I’ve written several articles about abstract components, so I won’t go over them here. Click on the portal to read for yourself.
- In Detail on Vue-Transition
- The Vue-Transition-Group
- Vue – Abstract Components In Action
conclusion
This article, after a large length of text analysis, we have a comprehensive understanding of vUE component creation (including asynchronous component creation, functional component creation and abstract component creation), component hook function, component configuration merge and so on.
Here I also hope you can friend after understanding the principle of component, in its own business development, can be the best combination of business component development practices, such as my personal for permissions in the business operation of unified management and use the individual thinks the best solution – abstract component, it is very good rights management is the business pain points
Front end communication group: 731175396, welcome to join