preface

Custom directives are the second most commonly used component in vUE, with bind, INSERTED, Update, componentUpdated, and unbind lifecycle hooks. This article will introduce the working principle of VUE instruction. From this article, you will get:

  • How instructions work
  • Note on the use of instructions

The basic use

Official website case:

<div id='app'>
  <input type="text" v-model="inputValue" v-focus>
</div>
<script>
  Vue.directive('focus', {
    // called the first time the element is bound
    bind () {
      console.log('bind')},// When the bound element is inserted into the DOM...
    inserted: function (el) {
      console.log('inserted')
      el.focus()
    },
    // Called when the component VNode is updated
    update () {
      console.log('update')},// call after the component's VNode and its child VNodes are all updated
    componentUpdated () {
      console.log('componentUpdated')},// Only called once, when the instruction is unbound from the element
    unbind () {
      console.log('unbind')}})new Vue({
    data: {
      inputValue: ' '
    }
  }).$mount('#app')
</script>
Copy the code

Principle of instruction

Initialize the

Initializing global API, under the platforms/web, call createPatchFunction generated VNode into real DOM patch method, comparatively important step is to define the initialization and DOM node corresponding hooks method, In the DOM, create, avtivate, Update, remove, and destroy are polled to invoke the corresponding hooks, some of which are entrances to the directive declaration cycle.

// src/core/vdom/patch.js
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]] = []
    // modules correspond to vue modules, including class, style, domListener, domProps, attrs, directive, ref, and transition
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // Finally convert hooks to {hookEvent: [cb1, cb2... . } form
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  / /...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...}}Copy the code
Copy the code

Template compilation

Template compilation parses the parameters of the directive, and the ASTElement looks like this:

{
  tag: 'input'.parent: ASTElement,
  directives: [{arg: null./ / parameters
      end: 56.// Command end character position
      isDynamicArg: false.// Dynamic parameter,v-xxx[dynamicParams]=' XXX 'form call
      modifiers: undefined.// Directive modifiers
      name: "model".rawName: "v-model".// Directive name
      start: 36.// Command start character position
      value: "inputValue" / / template
    },
    {
      arg: null.end: 67.isDynamicArg: false.modifiers: undefined.name: "focus".rawName: "v-focus".start: 57.value: ""}].// ...
}

Copy the code

Generate render method

Vue recommends using instructions to operate DOM. Since custom instructions may modify DOM or attributes, the influence of instructions on template parsing is avoided. When generating a rendering method, instructions such as V-Model are processed first, which is essentially a syntax sugar. The element is assigned a value attribute and an input event (in the case of input, this can also be customized).

with (this) {
    return _c('div', {
        attrs: {
            "id": "app"
        }
    }, [_c('input', {
        directives: [{
            name: "model".rawName: "v-model".value: (inputValue),
            expression: "inputValue"
        }, {
            name: "focus".rawName: "v-focus"}].attrs: {
            "type": "text"
        },
        domProps: {
            "value": (inputValue) // Attributes added when processing v-model directives
        },
        on: {
            "input": function($event) { // A custom event added while processing the V-model directive
                if ($event.target.composing)
                    return;
                inputValue = $event.target.value
            }
        }
    })])
}

Copy the code

Generate VNode

The instructions for vUE are designed so that we can manipulate the DOM easily and do no extra processing when generating vNodes.

Generating a real DOM

During vUE initialization, we need to keep two things in mind:

  • Initializations of states are parent -> child, as inbeforeCreate,created,beforeMountThe call order is parent -> child
  • realDOMThe mount order is child -> parent, as inmountedThis is because reality is being generatedDOMProcess, if encountered component, will go through the process of component creation, trueDOMThe generation of is a hierarchical splicing from child to parent.

When createElm is used to create a real DOM, it checks whether VNode has a data attribute. If it does, it calls invokeCreateHooks.

// src/core/vdom/patch.js
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    // ...
    CreateComponent has a return value, which is the method that created the component. If it does not return a value, proceed to the following method
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    / /...
    if (isDef(data)) {
        // After the real node is created, update node properties, including instructions
        // The directive first calls the bind method and then initializes the subsequent hooks methods of the directive
        invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // Insert from bottom up
    insert(parentElm, vnode.elm, refElm)
    // ...
  }
Copy the code

That’s the first entry to the directive hook method. It’s time to unmask directive.js. The core code is as follows:

// src/core/vdom/modules/directives.js

// By default, both are updateDirectives
export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    Vnode === emptyNode when destroyed
    updateDirectives(vnode, emptyNode)
  }
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

function _update (oldVnode, vnode) {
  const isCreate = oldVnode === emptyNode
  const isDestroy = vnode === emptyNode
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
  // Callback after insertion
  const dirsWithInsert = [
  // Callback after the update is complete
  const dirsWithPostpatch = []

  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
    // New element directive, which executes a INSERTED hook method
    if(! oldDir) {// new directive, bind
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
    } else {
      // existing directive, update
      // The element already exists, the componentUpdated hook method is executed once
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    // When the real DOM is inserted into the page, this callback method is called
    const callInsert = () = > {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    // VNode merges insert hooks
    if (isCreate) {
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch'.() = > {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }

  if(! isCreate) {for (key in oldDirs) {
      if(! newDirs[key]) {// no longer present, unbind
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

Copy the code

For the first time, perform the following operations:

  1. oldVnode === emptyNode.isCreatefortrueCall all elements in the current elementbindHook method.
  2. Check for presence in instructioninsertedHook, if present, willinsertHook merge intoVNode.data.hooksAttribute.
  3. DOMThe command is executed after the mount is completeinvokeInsertHook, all mounted nodes, ifVNode.data.hooksininsertHook. Is called, at which point the instruction binding is triggeredinsertedMethods.

Normally the first creation only takes the BIND and INSERTED methods, whereas update and componentUpdated correspond to bind and INSERTED. When the component dependency state changes, VNode DIff algorithm is used to patch the node. The call flow is as follows:

  1. Reactive data changes, calldep.notifyTo notify data updates.
  2. callpatchVNodeFor old and newVNodeDifferentiated update, and full update currentVNodeAttributes, including directives, enterupdateDirectivesMethods).
  3. If the instruction existsupdateHook method, calledupdateHook method and initializecomponentUpdatedThe callback will bepostpatch hooksMount to theVNode.data.hooksIn the.
  4. Triggered when the current node and its child nodes are updatedpostpatch hooks, i.e., of instructionscomponentUpdatedmethods

The core code is as follows:

// src/core/vdom/patch.js
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    // ...
    const oldCh = oldVnode.children
    const ch = vnode.children
    // Update all attributes of the node
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // ...
    if (isDef(data)) {
    // Call the postPatch hook
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
Copy the code

The unbind method calls invokeDestroyHook when the node is destroyed, which is not described here.

Matters needing attention

While I pass the parameter (V-xxx =’param’) as a reference type, NO bind or INSERTED instruction can be triggered when the data changes, because within the declaration period of the instruction, Bind and INSERTED are called only once during initialization, and are followed only by Update and componentUpdated.

The directive declaration cycle is executed in the order bind -> INSERTED -> Update -> componentUpdated. If the directive depends on the content of a subcomponent, it is recommended to write the corresponding business logic in componentUpdated.

In VUE, many methods are called in a loop, such as hooks methods, event callbacks, etc. Generally, calls are wrapped with try catch. The purpose of this method is to prevent one method from failing and causing the whole program to crash, which can be used for reference in the development process.

summary

Began to look at the whole VUE source code, for many details of the method are not how to understand, by combing the implementation of each specific function, gradually can see the whole VUE panorama, but also to avoid the development of some pits in use.

GitHub