Vue2 source code analysis (componentized)
One of the main ideas of Vue is componentization. In normal development, a complete Vue project is usually composed of one Vue component module after another. Let’s first look at how Vue initializes a component:
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app'.// h is the createElement method
render: h= > h(App)
})
Copy the code
Here the component calls the Render method, passing the App argument into the createElement function:
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
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
The createElement function will eventually call _createElement.
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
`Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render! ',
context
)
return createEmptyVNode()
}
// Get the component is property to get the corresponding component
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if(! tag) {// Create a comment node in tag if not pure
return createEmptyVNode()
}
// Process component slot slots
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// The children vNode array is flattened, if it is a template-generated render, the flattening scheme is selected according to the parameter, and the user-built render array is recursively flattened
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children) // Deep recursively flattens the array
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children) // From 2d to 1d
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// Check whether it is a platform standard tag
if (config.isReservedTag(tag)) {
if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
// Create the label vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// Create component vNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// Create vNodes with tag names
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// Create component vNode
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
Copy the code
Vnode = createComponent(Tag, data, context, children);
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
// Create a Vue subclass constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
....
// Handle asynchronous components
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
....
// Install the component hook function
installComponentHooks(data)
const name = Ctor.options.name || tag
// Create a component Vnode
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 createComponent method does three things:
- Pass the component object
Ctor = baseCtor.extend(Ctor)
To build aVue
The subclass constructor of - Pass the component’s hook function
installComponentHooks(data)
Method will becomponentVNodeHooks
Merges the hook function todata.hook
In the. - Create the component Vnode and return the Vnode.
After creating the component Vnode, the vm._update function executes the vm. __Patch__ (prevVnode, Vnode) method to convert the Vnode to the real DOM. This method is defined in SRC /core/vdom/patch.js:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // Delete the old node
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // Create a new node if the old one does not exist
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else{... } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm
}
Copy the code
Here, since we are a new component Vnode, we call createElm(Vnode, insertedVnodeQueue) :
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {...if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return}... }Copy the code
Here because we are component vNodes, createComponent(Vnode, insertedVnodeQueue, parentElm, refElm) returns true:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// Execute component Vnode hook function init
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)}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
SRC /core/vdom/create-component.js: SRC /core/vdom/create-component.js
init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// If the component is keepAlive
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)
}
},
Copy the code
If the component is keepAlive is executed componentVNodeHooks. Prepatch (mountedNode mountedNode) method, if not create a Vue createComponentInstanceForVnode instance, Then call the $mount method to mount the child component:
export function createComponentInstanceForVnode (vnode: any, parent: any) :Component {
const options: InternalComponentOptions = {
_isComponent: true._parentVnode: vnode,
parent
}
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
Function to build a component parameters, and then execute the new vnode.com ponentOptions. Ctor (options), the vnode.com ponentOptions. Ctor is inherited the son of the Vue constructor before, The _isComponent argument is true to indicate that it is a component, and parent indicates the current component instance, which is the parent component. So the instantiation of the child component is performed at this point, and then the _init method in the constructor is executed:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Copy the code
Component instance passes _isComponent true and executes initInternalComponent(VM, options) :
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
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
This function basically merges the parameters we passed into the option $options. The _init function finally executes:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
ComponentVNodeHooks init ($mount) {child.$mount(hydrating? Elm: undefined, hydrating), so it calls the mountComponent method and finally executes the vm._render() method:
Vue.prototype._render = function () :VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
vm.$vnode = _parentVnode
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode
return vnode
}
Copy the code
The parent of the render Vnode refers to _parentVnode (vm.$Vnode). The parent of the render Vnode refers to _parentVnode (vm.$Vnode). Execute vm._update to render the corresponding vnode:
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if(! prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating,false /* removeOnly */)}else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
Copy the code
Const restoreActiveInstance = setActiveInstance(VM) holds the Vue instance of the current context:
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () = > {
activeInstance = prevActiveInstance
}
}
Copy the code
PrevActiveInstance holds the parent Vue instance of the current VM instance. ActiveInstance saves the current Vue instance and returns a function to refer the activeInstance back to the parent Vue instance of the current instance after the patch of the child component is completed. Because Vue initialization is a deep traversal process, the current Vue instance needs to be known when instantiating the child component. Make it the parent Vue instance of the child component and the initLifecycle(VM) method is called before the child component $mount is mounted:
export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
if(parent && ! options.abstract) {while(parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent . }Copy the code
Here the current VM is stored in the parent instance’s $children, thus guaranteeing the parent-child relationship between the VM instance and all of its subtrees. $el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// ...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
// ...
} 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
The difference here is that the vnode we pass in here is a component render VNode. Because our render vNode root element is a normal element, it is a normal Vnode, not the previous component vNode, so instead of using the previous logic to create a component instance, we use the following normal VNode logic. Check whether vNode contains a tag parameter. If yes, check whether vNode contains a tag parameter. Then call platform DOM operation to create a placeholder element:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
Copy the code
Then call the createChildren method to create the child elements, and then use invokeCreateHooks(vnode, InsertedVnodeQueue) triggers all create hook functions and pushes vnodequeue to insertedVnodeQueue:
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
Copy the code
Finally, we call insert(parentElm, vnode.elm, refElm), because parentElm we passed is empty, so we don’t insert here, Instead, execute the initComponent function after component initialization in createComponent to assign the component rendering vNode’s $EL to component VNode’s elm:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
/ /...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)}// ...
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
Finally, execute insert(parentElm, vnode.elm, refElm) to insert component DOM.
conclusion
When Vue renders the whole project according to the VNode tree, if it is a normal Vnode, it will directly create element inserts, but if it is a component Vnode, it will first initialize the component Vue instance and point its parent to the parent instance, and then patch the component to insert the generated real DOM into the location of the component. This is how the component initializes rendering.