componentization
If you think it is good, please send me a Star at GitHub
Vue2.0 source code analysis: componentization (on) next: Vue2.0 source code analysis: compilation principle (on)
Due to the word limit of digging gold article, we had to split the top and the next two articles.
Update and patch
To review the mountComponent method mentioned earlier, it has this code:
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
Copy the code
In the previous section, we introduced the _render method and its createElement and createComponent logic, knowing that _Render returns a VNode tree. The _update method takes advantage of the VNode tree to generate a real DOM node.
In this chapter, we will analyze the implementation of update method and patch logic.
$forceUpdate
Before we get into update/ Patch, let’s take a look at an API method: $forceUpdate, which is used to force components to re-render. While developing a Vue application, we may have encountered components that were not rendered correctly even though our responsive data had changed. When this happens, we can call $forceUpdate to force the component to re-render. Its implementation code is as follows:
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Copy the code
We can see that the code for the $forceUpdate method is very simple. It first checks whether vm._watcher exists, that is, whether the render Watcher of the current component exists, and if so, calls the Render Watcher update method. After calling the update method, the process is the same as issuing the update, because this is render Watcher, so it ends up calling the following code, which is the core update/patch of this chapter:
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
Copy the code
update
_update is an internal private method called at two times: the initial mount phase and the dispatch update phase. The code is defined in the lifecycleMixin method as follows:
export function lifecycleMixin (Vue: Class<Component>) {
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
// 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
There isn’t much _update code, and the core of it is to call the __Patch__ method. Before introducing __patch__, in order to better understand the subsequent logic, let’s introduce a few small points of knowledge.
activeInstance
: As you can see from the name, it means the currently active instance object. We know that component rendering is a recursive process, and the order of rendering is child after parent. So in this recursive rendering process, we must correctly guarantee a pair of references: the currently rendered component instance and its parent component instance.activeInstance
Is the currently rendered component instance, which is a module variable:
export let activeInstance: any = null
Copy the code
In the _update method, it uses setActiveInstance to set the currently active instance and restoreActiveInstance to restore. The setActiveInstance method is defined as follows:
const restoreActiveInstance = setActiveInstance(vm)
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () = > {
activeInstance = prevActiveInstance
}
}
Copy the code
We can see that in setActiveInstance, it first defines the closure variable holding the currently activeInstance, then sets the activeInstance to the current parameter vm, and finally returns a function, The purpose of this function is to restoreActiveInstance to the last activeInstance that was cached by calling the restoreActiveInstance method.
Now that the currently rendered instance is resolved, let’s take a look at how the parent ensures this process. During initLifecycle, there is this code:
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 vm.$root = parent ? parent.$root : vm/ /... Omit code
}
Copy the code
During the execution of initLifecycle, the parent and children relationship is stored through a while loop. For the parent, all of its children are stored in $children, and for the child, the parent can be retrieved from vm.$parent.
_vnode and $vnode
:_vnode
and$vnode
It’s also a father-son relationship in which_vnode
Said that the currentVNode
Node,$vnode
Represents the parent node. So let’s go back_render
Method, which has code like this:
Vue.prototype._render = function () {
/ /... Omit code
const { render, _parentVnode } = vm.$options
vm.$vnode = _parentVnode
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
}
vnode.parent = _parentVnode
return vnode
}
Copy the code
After introducing these two sets of mappings, let’s look at the implementation of the core __Patch__ method, which is common to multiple platforms, It in SRC/platforms/web/runtime/index, js and SRC/platforms/weex/runtime/index. The js files are defined, we mainly look at the first, the custom code is as follows:
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
In the above code, it uses inBrowser to determine if it is currently in a browser environment, if it is, it assigns path, otherwise it is noop empty. This is because Vue can also run on the Node server. Next, let’s look at how the path method is defined in path.js:
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
Here we can see that patch is the result of createPatchFunction call. We will not look at how createPatchFunction is defined, but we will look at the parameters it passes.
nodeOps
:nodeOps
Is the introduction of theweb/runtime/node-ops.js
In this file, let’s take some of it to illustrate what it is.
export function createElement (tagName: string, vnode: VNode) :Element {
const elm = document.createElement(tagName)
if(tagName ! = ='select') {
return elm
}
// false or null will remove the attribute but undefined will not
if(vnode.data && vnode.data.attrs && vnode.data.attrs.multiple ! = =undefined) {
elm.setAttribute('multiple'.'multiple')}return elm
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
Copy the code
We can see that the methods encapsulated in the Node-ops.js file are actually a layer of encapsulation for real DOM operations, and the purpose of passing the nodeOps is to facilitate the transition from the virtual DOM to the real DOM node.
modules
:modules
isplatformModules
andbaseModules
The result of merging two arrays, wherebaseModules
It’s on the template labelref
anddirectives
Encapsulation of various operations.platformModules
It’s on the template labelclass
,style
,attr
As well asevents
Such operations as encapsulation.
Summary:
- in
update
In this section, we learn about first render and distribute updates to re-renderpatch
There is a slight difference, the difference being that the root node provided for the first rendering is a real oneDOM
Element, which is provided when the update is rerenderedVNode
The logic of the difference here is in the following code:
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
Copy the code
- In the recursive rendering of the parent component, the child component is rendered first, and the parent component will be rendered after the child component is rendered. In this recursive process,
activeInstance
Always point to the currently rendered component instance. At the same time, according to the order of recursive rendering of the parent and child components, we can know about the parent and child componentscreate
andmount
The order of execution of the two life cycles:
// parent beforeCreate
// parent created
// parent beforeMount
// child beforeCreate
// child created
// child beforeMount
// child mounted
// parent mounted
Copy the code
render
Function execution will result in oneVNode
The tree structure of,update
Is to make this virtualDOM
The node tree is converted to realDOM
The node tree. So with all of the previous introductions, we can get an example from initialization to final rendering to realityDOM
A mainline flowchart to a view.
patch
In the previous chapter, we left a createPatchFunction method that has not been analyzed. In the patch chapter, our main task is to figure out the implementation principle of createPatchFunction.
Since the createPatchFunction method has a lot of code in V2.6.11, we use sections to illustrate it and recommend reading the article while learning from the source code.
Hooks function
At the beginning of createPatchFunction, it first handles some hooks functions like this:
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
}
Copy the code
Note: The hooks defined here are similar to our component’s lifecycle hook functions, but they do not deal with the component’s lifecycle. They are called during the execution of VNode hook functions or at other times, such as: The created hook functions need to be executed for VNode insertion and the remove/destroy hook functions need to be executed for VNode removal.
Code analysis:
- The first is through deconstruction
modules
It is an array, and each array element can be definedcreate
,update
,remove
As well asdestroy
Wait for the hook function, which might look like this:
const modules = [
{
created: function () {},
update: function () {}}, {update: function () {},
remove: function () {}}, {remove: function () {},
destroy: function () {}}]Copy the code
- Deconstruct to get
modules
After usingfor
Loop throughmodules
, the purpose is to makehooks
As akey
.hooks
As a function ofvalue
After the loop is completed, it might look like this:
/ / before traversal
const cbs = {}
/ / after traversal
const cbs = {
create: [ function () {}, function () {}].activate: [].update: [ function () {}, function () {}, function () {}].remove: [ function () {}].destroy: [ function () {}]}Copy the code
- In order to
create
The hook function, for example, will be called when appropriatecreate
The code is as follows:
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
Copy the code
In the invokeCreateHooks method, it iterates through the CBS. create hook function array with for and then calls each of these methods in turn. At the end of the method, it also calls VNode’s two hook functions, as mentioned in the createComponent section, vNode.data.hook.
const componentVNodeHooks = {
init: function () {}, // Triggered during initialization
prepatch: function () {}, // Patch is triggered before
insert: function () {}, // when inserted into the DOM
destroy: function () {} // Triggered before node removal. }Copy the code
Return patch function
Let’s review the previous code:
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
CreatePatchFunction (createPatchFunction); patch (createPatchFunction);
export function createPatchFunction (backend) {
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// ...
} else {
// ...
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
Copy the code
At the beginning of the patch return function, it determines whether vNode is undefined or null, and if it is and the oldVnode condition is true, it invokeDestroyHook. The invokeDestroyHook is executed to trigger the destruction of the child node, so it is obvious that this code will be executed when the component is destroyed. You can see the following code in the $destroy method (which we will cover in the component lifecycle section) :
Vue.prototype.$destroy = function () {
// ...
vm.__patch__(vm._vnode, null)
// ...
}
Copy the code
After judging vNode, we see that it also judges oldVnode, so there is an if/else branch logic. So when do you go to if branch logic? When do I branch off the else logic?
When the isUndef method is true for oldVnode logic, proving that there is no oldVnode at that time, it indicates that the component is being rendered for the first time, so the if branch logic will be followed. When the root instance is mounted or an update is dispatched, the oldVnode is present and else branch logic is followed. Since the branching logic of these two pieces is relatively complex, we will divide the module description separately in the future.
At the end of the return patch function, it invokeInsertHook, which triggers VNode’s insert hook function, as follows:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
Copy the code
VNode’s insert hook function triggers the component’s mounted hook function. The component series of life cycles, which we will cover in the next section, is just an overview.
The root instance patch
Let’s review the _update method, which has this code:
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
Copy the code
On the first rendering, it passes a real DOM node to the root instance, which means that in the patch return function, the first parameter oldVnode is not only true, but it is also a real DOM node. So in the patch return function, it follows the following code:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// ...
} else {
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// ...
} else {
if (isRealElement) {
// ...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// ...
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
}
Copy the code
Since the oldVnode argument is a real DOM node, the isRealElement variable is true, which calls emptyNodeAt. This method converts a real DOM into a VNode instance. The code looks like this:
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
Copy the code
This is followed by a call to the createElm method, which converts the VNode instance into a real DOM node. We’ll cover createElm separately in a later section, but only here.
At the end of the code, it calls different methods by determining whether parentElm is true, using the app.vue component generated by vue-CLI scaffolding as an example.
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm)
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}
Copy the code
As you can see, in the removeVnodes method, it removes the old node with an ID equal to app and creates a new node with an ID equal to app. All mount elements will be replaced by the DOM generated by Vue. Therefore, mounting root instances to HTML or body is not recommended
Component patch
In the patch return function, the rendering processing logic of the component for the first time and that of the update distribution is different, and the difference is reflected in the following code:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// Render the component for the first time
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// The component sends out updated renderings
patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
} else {
// ...}}}Copy the code
In the component Update/Patch section, we will not analyze the patchVnode method, but will put it in the later compilation section. In this case, we only need to look at the first rendering of the component. For the first rendering of the component, the createElm method is still called, but note that it only passes two arguments.
createElm
In the previous two sections, we mentioned createElm and its main function is to convert a VNode instance into a real DOM node. In this section, we’ll look at the createElm method in more detail.
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
// ...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__) {
// ...
} 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
You can see the condensed code for the createElm method above, which has several main steps: creating a component node, creating a plain node, creating a comment node, and creating a text node.
- Creating a component NodeIn:
createElm
Method starts by callingcreateComponent
Method tries to create a component node ifvnode
It’s a componentvnode
It returnstrue
And in advancereturn
Termination ofcreateElm
Method, otherwise returnsfalse
. Let’s seecreateComponent
Method implementation code:
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
For the component VNode, it determines that its data condition is satisfied, and when it does, it processes I and assigns it the value i.init. Vnode init hook function: vNode init hook 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)
}
}
// ...
}
Copy the code
When I method performs, it will be in the code to create by createComponentInstanceForVnode Vue instance, and then call instance methods of $mount to mount the child component. Because the child’s $mount method is called here, the child recurses through the update/patch process from the beginning, and when the child is finished, inserts the actual DOM node tree corresponding to the child into the parent at the location of the component’s placeholder. This builds a complete tree of components by iterating update/patch through layers of recursion.
- Creating a Common NodeIf:
VNode
The instancetag
If the property is true, it is checked firsttag
Check whether the label is correct. If not, an invalid label is displayed. If so, it is called firstcreateChildren
Method is called after the child nodes are processedinsert
Insert directly into the parent.
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
Copy the code
We can see that in the createChildren method, it first determines whether VNode’s children are arrays, if not creates a text node to insert into the parent, if so iterates through the array of children, and then recursively calls createElm. According to the above analysis, we can know that the process of node creation is a depth-first traversal process, in which the child node is created first and then inserted below its parent, and the parent node is the last. Because child nodes are created and inserted first, the child node first calls the insert method, which looks like this:
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
Copy the code
InsertBefore and appendChild are layers of encapsulation of the real DOM.
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
Copy the code
- Create comment nodes and create text nodesCreating a comment node and creating a text node are very simple, they are called separately
createComment
andcreateTextNode
In fact, these two methods are the originalDOM
A layer of encapsulation of the operation:
export function createTextNode (text: string) :Text {
return document.createTextNode(text)
}
export function createComment (text: string) :Comment {
return document.createComment(text)
}
Copy the code
Component life cycle
After introducing the patch chapter of the component, we have introduced the main line process from the new Vue instantiation to the final rendering of the real DOM to the view. So let’s review this process and look at the component life cycle. There is such a component life cycle flow chart in the vue.js official website.
callhook
Before introducing life-cycle function, we first take a look at callHook method of implementation, it is defined in SRC/core/instance/lifecycle. A method of js file, the code is as follows:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
Copy the code
Code analysis:
- We can see it in the
for
Before traversing, use thepushTarget
Used after traversalpopTarget
.pushTarget
andpopTarget
We have introduced it in previous chapters, but here is the main oneissue 7573You are hereissue
You can see why these two pieces of code were added above. - Through the
this.$options
Get on the objecthook
Parameters of the correspondingcallback
Array, and then usefor
Loop through, pass through in each loopinvokeWithErrorHandling
To trigger the callback function.invokeWithErrorHandling
Methods are defined insrc/core/util/error.js
A method in the file with the following code:
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if(res && ! res._isVue && isPromise(res) && ! res._handled) { res.catch(e= > handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true}}catch (e) {
handleError(e, vm, info)
}
return res
}
Copy the code
As you can see, the invokeWithErrorHandling method doesn’t have a lot of code. The core is this code, and the rest is exception handling.
res = args ? handler.apply(context, args) : handler.call(context)
Copy the code
- in
for
So after the loop, it decidesvm._hasHookEvent
Where, you might wonder, is this internal property defined? What do you do? ininitEvents
Method first defaults to set this property tofalse
, the code is as follows:
export function initEvents (vm: Component) {
// ...
vm._hasHookEvent = false
// ...
}
Copy the code
In the event-center $on method, it evaluates based on the re condition and assigns true if true, with the following code:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function) :Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true}}return vm
}
Copy the code
When the _hasHookEvent property is true, the component fires the corresponding lifecycle hook function, so we can use this functionality to do two things: listen for the child component lifecycle and listen for the component’s own lifecycle.
Suppose we have the following components:
<template>
<div id="app">
<hello-world @hook:created="handleChildCreated" :msg="msg" />
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
msg: 'message'
}
},
methods: {
handleChildCreated () {
console.log('child created hook callback')
}
},
created () {
const listenResize = () => {
console.log('window resize callback')
}
window.addEventListener('resize', listenResize)
this.$on('hook:destroyed', () => {
window.removeEventListener('resize', listenResize)
})
}
}
</script>
Copy the code
Code analysis:
- in
template
In the template, we can use@hook:xxx
When the corresponding lifecycle function is triggered, it will execute the provided callback function. This approach is very useful for requirements that need to listen for a lifecycle of the child component. - In writing
Vue
When applying, we often need to be increated/mounted
And so onresize/scroll
Wait for the event, and then inbeforeDestroy/destroyed
Life cycle removed. For this requirement, we can write the logic in the same place instead of splitting it between two lifecycles, which is also useful when we need to listen to our own lifecycles.
The life cycle
BeforeCreate and created
Let’s start by looking at the beforeCreate and created hook functions, which are triggered in the this._init method:
Vue.prototype._init = function () {
// ...
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')
// ...
}
Copy the code
Between the beforeCreate and created life cycles, it calls three methods that initialize the inject, data, props, methods, computed, watch, and provide configuration options. We can conclude that these properties are only accessible in Created and not in beforeCreate because they are not initialized.
BeforeMount and mounted
Before the $mount method we mentioned beforeMount and Mounted methods. They are triggered in mountComponent as follows:
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
// ...
callHook(vm, 'beforeMount')
let updateComponent
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
// ...
} else {
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
}
// ...
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')}return vm
}
Copy the code
As you can see, in the front of the mountComponent method, it calls the beforeMount method and then starts executing vm._update(), which is called when the parent component is recursively rendered when the component first renders and distributes updates.
$vode == null after rendering. Mounted == null Why do you do that, you might wonder? In the update/path section, we mentioned a parent-child relationship: vm._vnode and vm.$vnode, where vm.$vnode represents the parent vnode. When will vm.$vnode be null? The answer is only the root instance, because only the root instance satisfies this condition, that is, the root instance’s mounted method is triggered, not the component’s.
Based on the call timing of beforeMount and Mounted, we know that the beforeMount life cycle is called before vm._update(), so we can’t get the correct DOM at this time of life. The Mounted life cycle is executed after the vm._update() method, so we can retrieve the correct DOM during this life cycle.
Back in Patch, we mentioned that VNode has some hook functions, so let’s review:
const componentVNodeHooks = {
init: function () {},
prepatch: function () {},
insert: function (vnode) {
const { context, componentInstance } = vnode
if(! componentInstance._isMounted) { componentInstance._isMounted =true
callHook(componentInstance, 'mounted')}// ...
},
destroy: function () {}}Copy the code
When the INSERT hook function is triggered, it also triggers its component’s Mounted method, so the component’s mounted life cycle is called when VNode triggers the INSERT hook function.
BeforeUpdate and updated
BeforeUpdate and updated the pair of lifecycle hook functions that are triggered during the dispatch of updates. Recalling the dependency collection/dispatch update sections, setters are triggered when a reactive variable value is updated.
Object.defineProperty(obj, key {
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
})
Copy the code
The dep.notify() method is called in the setter to notify the observer of the update, and in the notify implementation, it iterates through its subs array and then calls the Update () method in turn.
export default class Dep {
// ...
notify () {
const subs = this.subs.slice()
if(process.env.NODE_ENV ! = ='production' && !config.async) {
subs.sort((a, b) = > a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
Updates to these Watcher instances end up in the flushSchedulerQueue method, where a callUpdatedHooks method is called
function flushSchedulerQueue () {
// ...
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if(vm._watcher === watcher && vm._isMounted && ! vm._isDestroyed) { callHook(vm,'updated')}}}Copy the code
In the callUpdatedHooks method, which iterates through the Queue’s Watcher instance queue, on each iteration the VM’s updated method is triggered. When the updated hook function is triggered, the update phase is complete.
This is the updated hook function, but beforeUpdate is handled when the Render Watcher is instantiated.
export function mountComponent () {
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)}Copy the code
We can see that when we instantiate the Render Watcher, it passes a before property to the fourth argument pass object, which is assigned to the before property of the Watcher instance. Then, when the flushSchedulerQueue method iterates through the queue, it checks whether watcher.before exists and calls it if it does.
function flushSchedulerQueue () {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
// ...
}
// ...
callUpdatedHooks(updatedQueue)
}
Copy the code
BeforeDestroy and destroyed
Both beforeDestroy and destroyed lifecycleMixin are triggered in the vm.$destroy instance method, which is defined in lifecycleMixin as follows:
export function lifecycleMixin (Vue) {
// ..
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if(parent && ! parent._isBeingDestroyed && ! vm.$options.abstract) { remove(parent.$children, vm) }// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null}}}Copy the code
As you can see, at the very beginning of the $Destroy method, it triggers the beforeDestroy lifecycle and then handles some other operations: removing itself from the parent’s $Children, removing its dependencies, triggering child destruction, and removing event listeners.
Next, we use the above steps to illustrate:
- The children in the parent component removes itself: When a component is destroyed, we need to retrieve it from its parent
$children
To remove itself from the list, use the following code as an example:
<template>
<div class="parent">
<child-component />
</div>
</template>
Copy the code
Before ChildComponent is destroyed, the ParentComponent’s $Children array holds its references. When ChildComponent is destroyed, to preserve the references properly, we need to remove them from the $Children list.
// Display the use of the actual VM instance
/ / remove before
const $children = ['child-component'. ]/ / removed
const $children = [...]
Copy the code
- Remove self dependencies: We mentioned it earlier
vm._watchers
Maintains an array of observers, all of which areWatcher
Instance, another onevm._watcher
Refers to the current componentrender watcher
. When the component is destroyed, these observers need to be removed, and they all passWatcher
The instanceteardown
Method, the code is as follows:
export default class Watcher {
// ...
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)}let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)}this.active = false}}}Copy the code
- Triggers the child component destruction action: in removing
Watcher
After that, it calls latervm.__patch__
The method, we had beforeupdate/patch
This method is described in the section. Note that the second argument is passednull
Let’s reviewpatch
Method implementation:
export function createPatchFunction (backend) {
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// ...}}Copy the code
In the Patch method, when the second argument we pass, vnode, is null, it calls the invokeDestroyHook method, which looks like this:
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
Copy the code
This method recursively calls the child VNode hook function destroy. Let’s look at what VNode hook function destroy does:
const componentVNodeHooks = {
// ...
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
deactivateChildComponent(componentInstance, true /* direct */)}}}}Copy the code
As you can see, in the destroy hook function, if you ignore the keep-alive logic, its core is to call the component’s $destroy() method.
Summary: The process of component destruction should start with the parent component and then recursively destroy the child components. When the child components are destroyed, the parent component has basically completed the destruction action. So the order of execution of the parent component’s beforeDestroy and destroyed lifecycle hook functions is as follows:
// parent beforeDestroy
// child beforeDestroy
// child destroyed
// parent destroyed
Copy the code
- Removing Event ListeningWe mentioned earlier that when the child completes its destruction, the parent completes its destruction almost as well. This is because of the use of
callHook
The triggerdestroyed
After the lifecycle hook function, we also need to remove the associated event listener that it uses$off
To implement, let’s review the code:
Vue.prototype.$off = function (event? : string |Array<string>, fn? :Function
) :Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// ...
return vm
}
Copy the code
When we pass no arguments, it simply assigns vm._events to an empty object, thus removing event listening.
Activated and deactivated
These two lifecycle methods are lifecycle hook functions that are strongly related to the keep-alive built-in components, so we’ll cover them later in the Keep-Alive section.
The component registration
When developing Vue applications, there are usually two ways to register components: global and local. The results of the two ways of registering components are different. Globally registered components can be used directly throughout the application, while locally registered components can only be used within the current component. In this section, we examine how components are registered locally and globally in Vue.
Note: There are some components in Vue that can be used without registration. These are the built-in components: keep-alive, transition, transition-group, and Component. These built-in components will not be covered in this chapter, but will be covered in a separate section in the following chapters.
For components that require global registration, we use the Vue.component method to register our component. This method is defined as initAssetRegisters in SRC /core/global-api/assets.js, with the following code:
export const ASSET_TYPES = ['component'.'directive'.'filter']
export function initAssetRegisters (Vue: GlobalAPI) {
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
Code analysis: When vue.component is correctly passed, it will go to the else branch. In the else branch, the component first uses validateComponentName to verify that the component name is valid. This code looks like this:
export function validateComponentName (name: string) {
if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}] * $`).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.')}if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
Copy the code
For a component name, it needs to be legal on the one hand and not a built-in or reserved HTML tag on the other. After validation, it calls the this.options._base.extend method, which is essentially the equivalent of calling vue.extend to convert a component object into a constructor. The extend method is implemented in detail earlier. After the constructor is converted, the corresponding options are assigned. According to the implementation of Vue.com Ponent method, we can use the following cases to express:
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'
/ / registered before
const options = {
components: {}}/ / register
Vue.component('HelloWorld', HelloWorld)
/ / after registration
const options = {
components: {
HelloWorld: function VueComponent () {... }}}Copy the code
Now that the component is registered, we have two questions: Where is the globally registered component? How is it found when using globally registered components?
To answer the first question, let’s review how the Components options are merged:
function mergeAssets (
parentVal: ?Object,
childVal: ?Object, vm? : Component, key: string) :Object {
const res = Object.create(parentVal || null)
if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
strats.component = mergeAssets
Copy the code
Because globally registered components are on the Vue.options.components option, according to the merge strategy above, we found that globally registered components are merged into the prototype of the components option of the child component, for example:
// After global registration
const baseVueOptions = {
components: {
HelloWorld: function VueComponent () {... }}}/ / after the merger
const childOptions = {
components: {
__proto__: {
HelloWorld: function VueComponent () {... }}}}Copy the code
With this code, we can answer the first question: globally registered components are reflected in the prototype of the subcomponent Components property object after the subcomponent configuration is merged.
Next, let’s look at the second problem. We go back to createElement. In this section, we notice the following code:
if (typeof tag === 'string') {
if (xxx) {
...
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
}
} else {
vnode = createComponent(tag, data, context, children)
}
Copy the code
When a template is compiled into a global component, the resolveAsset method attempts to obtain the component’s constructor.
export function resolveAsset (
options: Object, type: string, id: string, warnMissing? : boolean) :any {
/* istanbul ignore if */
if (typeofid ! = ='string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if(process.env.NODE_ENV ! = ='production'&& warnMissing && ! res) { warn('Failed to resolve ' + type.slice(0, -1) + ':' + id,
options
)
}
return res
}
Copy the code
For the Components option, it first tries to use the hasOwn method to look for it on its own object, and if none of the three methods exist, it finally looks in the prototype of components. For globally registered components, it will be found in the prototype. If it is not found in the prototype, it will be checked in the patch phase and an error will be thrown:
'Unknown custom element: xxx - did you register the component correctly? ' +
'For recursive components, make sure to provide the "name" option.'.Copy the code
After understanding the way of global registration component, all kinds of questions about local registration component are believed to be solved. Locally registered components are on the Components object, while globally registered components are reflected in the child component’s prototype of the Components object after the component merge is configured. This is the fundamental reason that globally registered components can be used anywhere.
If you think it is good, please send me a Star at GitHub
Vue2.0 source code analysis: componentization (on) next: Vue2.0 source code analysis: compilation principle (on)
Due to the word limit of digging gold article, we had to split the top and the next two articles.