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.activeInstanceIs 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:_vnodeand$vnodeIt’s also a father-son relationship in which_vnodeSaid that the currentVNodeNode,$vnodeRepresents the parent node. So let’s go back_renderMethod, 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: nodeOpsIs the introduction of theweb/runtime/node-ops.jsIn 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: modulesisplatformModulesandbaseModulesThe result of merging two arrays, wherebaseModulesIt’s on the template labelrefanddirectivesEncapsulation of various operations.platformModulesIt’s on the template labelclass,style,attrAs well aseventsSuch operations as encapsulation.

Summary:

  1. inupdateIn this section, we learn about first render and distribute updates to re-renderpatchThere is a slight difference, the difference being that the root node provided for the first rendering is a real oneDOMElement, which is provided when the update is rerenderedVNodeThe 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
  1. 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,activeInstanceAlways 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 componentscreateandmountThe 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
  1. renderFunction execution will result in oneVNodeThe tree structure of,updateIs to make this virtualDOMThe node tree is converted to realDOMThe node tree. So with all of the previous introductions, we can get an example from initialization to final rendering to realityDOMA 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 deconstructionmodulesIt is an array, and each array element can be definedcreate,update,removeAs well asdestroyWait 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 getmodulesAfter usingforLoop throughmodules, the purpose is to makehooksAs akey.hooksAs a function ofvalueAfter 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 tocreateThe hook function, for example, will be called when appropriatecreateThe 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:createElmMethod starts by callingcreateComponentMethod tries to create a component node ifvnodeIt’s a componentvnodeIt returnstrueAnd in advancereturnTermination ofcreateElmMethod, otherwise returnsfalse. Let’s seecreateComponentMethod 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:VNodeThe instancetagIf the property is true, it is checked firsttagCheck whether the label is correct. If not, an invalid label is displayed. If so, it is called firstcreateChildrenMethod is called after the child nodes are processedinsertInsert 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 separatelycreateCommentandcreateTextNodeIn fact, these two methods are the originalDOMA 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 theforBefore traversing, use thepushTargetUsed after traversalpopTarget.pushTargetandpopTargetWe have introduced it in previous chapters, but here is the main oneissue 7573You are hereissueYou can see why these two pieces of code were added above.
  • Through thethis.$optionsGet on the objecthookParameters of the correspondingcallbackArray, and then useforLoop through, pass through in each loopinvokeWithErrorHandlingTo trigger the callback function.invokeWithErrorHandlingMethods are defined insrc/core/util/error.jsA 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
  • inforSo after the loop, it decidesvm._hasHookEventWhere, you might wonder, is this internal property defined? What do you do? ininitEventsMethod 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:

  • intemplateIn the template, we can use@hook:xxxWhen 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 writingVueWhen applying, we often need to be increated/mountedAnd so onresize/scrollWait for the event, and then inbeforeDestroy/destroyedLife 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$childrenTo 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 earliervm._watchersMaintains an array of observers, all of which areWatcherInstance, another onevm._watcherRefers to the current componentrender watcher. When the component is destroyed, these observers need to be removed, and they all passWatcherThe instanceteardownMethod, 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 removingWatcherAfter that, it calls latervm.__patch__The method, we had beforeupdate/patchThis method is described in the section. Note that the second argument is passednullLet’s reviewpatchMethod 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 ofcallHookThe triggerdestroyedAfter the lifecycle hook function, we also need to remove the associated event listener that it uses$offTo 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.