patch
patch
The overall process of patch: createComponent -> sub-component initialization -> sub-component render -> sub-component patch
activeInstance
Is currently activevm
Instance;vm.$vnode
Is a placeholder for the component;vm._vnode
Render for the componentvnode
- Nested components are inserted in child before parent order
When we create the component VNode using createComponent, we go to vm._update and execute vm.__ Patch__ to convert the VNode to a real DOM node. This process has been examined before, but for a normal VNode, let’s take a look at how component VNodes differ.
The patch procedure calls ==createElm== to create the element node. Review the implementation of createElm, which is defined in SRC /core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
Copy the code
createComponent
We delete the redundant code, only keep the key logic, it can determine the createComponent (vnode, insertedVnodeQuiui parentElm, refElm) return values, if is true end directly, So let’s look at the implementation of the createComponent method
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
The createComponent function first makes some judgments about vnode.data:
let i = vnode.data
if (isDef(i)) {
// ...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
// ...
}
// ..
}
Copy the code
If vnode is a component vnode, then the condition will be satisfied and I will be the init hook function. Recall from the previous section that when we created a component vnode, we incorporated the init hook function into the merged hook function. SRC /core/vdom/create-component.js:
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) } },Copy the code
Init hook function also is very simple, we won’t consider keepAlive, create a Vue createComponentInstanceForVnode instance, and then call $mount method to mount components, The realization of the 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
}
// 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
CreateComponentInstanceForVnode function structure of an internal component parameters, and then execute the new vnode.com ponentOptions. Ctor (options). Here vnode.com ponentOptions. Ctor corresponding is the child component constructor, we have analyzed in the previous section it is inherited in a constructor Sub Vue, equivalent to a new Sub (options) and several key parameters to note a few points here, _isComponent true means it’s a component, and parent means the currently active component instance.
So the child component instantiation is actually in the actual execution, and it will perform the instance _init method, this process has some and pick out different places need to say, before the code to the SRC/core/instance/init. In js:
const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV ! == 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV ! == 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV ! == 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } }Copy the code
_isComponent is true, so go to initInternalComponent. The implementation of this function is also a brief look:
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
In this process, we can mainly remember the following points: Opts.parent = opts.parent, opts._parentvNode = parentVnode, They are put before us by createComponentInstanceForVnode function to the merger of several parameters of internal options in the $options.
Let’s look at the last execution of the _init function:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
$componentVNodeHooks () {$componentVNodeHooks (); $componentVNodeHooks (); $componentVNodeHooks (); $componentVNodeHooks (); Child.$mount(hydrating? vnode.elm : – undefined, hydrating is true for server rendering, we only consider client rendering. So $mount equals child.$mount(undefined,false), which eventually calls the mountComponent method and executes the vm._render() method:
ComponentVNodeHooks Function
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
It ends up calling the mountComponent method, which executes the vm._render() method:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode
return vnode
}
Copy the code
_parentVnode is the parent VNode of the current component, and the render function generates the VNode of the current component. The parent of VNode points to _parentVnode, i.e. Vm.$VNode. They are a father-son relationship.
After vm._render is executed to generate vNodes, vm._update is executed to render vNodes. Take a look at what need to pay attention to in the process of component rendering, vm. _update defined in SRC/core/instance/lifecycle. In js:
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 // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (! prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }Copy the code
Vm. _vnode = vnode, which renders vnode using the component returned by vm._render(). Vm. _vnode and vm.$vnode have a parent-child relationship. Vm._vnode. parent === vm.$vnode. There’s another interesting piece of code
const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (! prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook.Copy the code
This activeInstance is an instance of the Vue that holds the current context. This is a global variable in the Lifecycle module and is defined as export let activeInstance: Any = null, and when we call createComponentInstanceForVnode method before, was obtained from the lifecycle module and passed as a parameter. Because JavaScript is actually a single thread, the entire initialization of a Vue is a deep traversal process. In instantiating the child component, it needs to know what the Vue instance of the current context is and treat it as the parent Vue instance of the child component. We mentioned earlier that the instantiation of child components will call initInternalComponent(VM, options) to merge options, storing parent in **vm. Options ∗∗, in options**, in options∗∗, The initLifecycle(VM) method is called before mount:
export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent let parent = options.parent if (parent && ! options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent vm.$root = parent ? parent.$root : vm vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false }Copy the code
Push (vm) is used to store the current VM in the parent instance of $children.
During the vm._update process, PrevActiveInstance: assigns the current VM to activeInstance and preserves the previous activeInstance with const prevActiveInstance = activeInstance. PrevActiveInstance has a parent-child relationship with the current VM. After the current VM instance completes the patch or update process for all its subtrees, the activeInstance returns to its parent. Thus perfect guarantee the createComponentInstanceForVnode the entire depth traversal process, we in the instance, anti-fuzzy components can pass into the current child components of parent Vue instance, and in the process of _init, through vm. $parent keep the father-son relationship.
Back to _update, the final call to render VNode is __patch__.
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// ...
}
// ...
}
Copy the code
The function that renders the DOM is createElm. Note that we only pass two arguments, so parentElm is undefined. Let’s look at its definition:
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
Note that the vnode we pass in here is the component rendered vnode (vm._vnode). If the root node of the component is a normal element, then vm._vnode is also a normal vnode. CreateComponent (vNode, insertedVnodeQueue, parentElm, refElm) returns false. Create a parent node placeholder and recursively call createElm through all the child VNodes. If a child VNode is a component VNode, repeat the procedure we started in this section. In this way, the entire component tree can be completely built in a recursive manner. Since parentElm we passed in at this time is empty, the component insert in createComponent has this logic:
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
Insert (parentElm, vnode.elm, refElm) to insert the DOM of the component after completing the patch process. If a child component is created during the patch process, the DOM insertion sequence is child before parent.
conclusion
So here, a component of the VNode is how to create, initialize, render the process is also introduced. Now that we have an overview of componentization implementation, let’s go through some of the details. We know that writing a component is actually writing a JavaScript object, which describes the various configurations. We mentioned earlier that the initial phase of _init is the merge options logic, so the next section will analyze the merge configuration process from a source point of view