Analysis of the implementation principle of Vue instructions

Analysis of the implementation principle of Vue instructions

1. Basic Use

Official website case:

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

2. Working Principle of Instructions

2.1. Initialization

When initializing the global API, under platforms/web, call createPatchFunction to generate the patch method that converts VNode to the real DOM. An important step in initialization is to define the hooks methods corresponding to the DOM nodes. During the creation (create), activation (avtivate), update (update), removal (remove), and destruction (destroy) of the DOM, the corresponding hooks methods will be polled and called respectively. Some of these hooks are the entrances to the instruction 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 modules in vue, including class, style, domListener, domProps, attrs, directive, ref, 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) {
    // ...
  }
}

2.2 Template compilation

Template compilation is to parse the instruction parameters. The specific deconstructed ASTElement is as follows:

{
  tag: 'input',
  parent: ASTElement,
  directives: [
    {
      arg: null, // parameter end: 56, // end character position of the instruction isDynamicArg: false, // dynamic parameter, v-xxx[dynamicParams]='xxx' form call modifiers: undefined, // instruction modifier name: "model",
      rawName: "v-model", // Instruction name start: 36, // Instruction start character position value: "inputValue" // Template },
    {
      arg: null,
      end: 67,
      isDynamicArg: false,
      modifiers: undefined,
      name: "focus",
      rawName: "v-focus",
      start: 57,
      value: ""
    }
  ],
  // ...
}

2.3. Generate rendering method

Vue recommends using instructions to operate DOM. Since custom instructions may modify DOM or attributes, avoid the impact of instructions on template parsing. When generating rendering methods, the first thing to process is instructions, such as v-model, which is essentially a syntactic sugar. When splicing rendering functions, value attributes and input events will be added to the elements (taking input as an example, this can also be customized by the user).

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 instructions},
        on: {
            "input": function($event) { // Custom event added when processing v-model directive if ($event.target.composing)
                    return;
                inputValue = $event.target.value
            }
        }
    })])
}

2.4. Generate VNode

The design of vue's instructions is to facilitate our operation of DOM. When generating VNode, the instructions do not do any additional processing.

2.5. Generate real DOM

During the vue initialization process, we need to remember two points:

  • The initialization of the state is parent -> child, such as beforeCreate, created, beforeMount, the calling order is parent -> child
  • The real DOM mounting order is child -> parent, such as mounted. This is because in the process of generating the real DOM, if a component is encountered, the component creation process will be followed. The generation of the real DOM is spliced ​​from child to parent level by level.

During the patch process, each time createElm is called to generate the real DOM, it will detect whether the current VNode has a data attribute. If it does, invokeCreateHooks will be called to execute the hook function created at the beginning. The core code is as follows:

// src/core/vdom/patch.js
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // ...
    // createComponent has a return value, which is the method for creating components. If there is no return value, continue with the following method if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    // ....
    if (isDef(data)) {
        // After the real node is created, update the node attributes, including the instruction // The instruction will call the bind method for the first time, and then initialize the subsequent hooks method of the instruction invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // From bottom to top, insert(parentElm, vnode.elm, refElm)
    // ...
  }

The above is the first entry of the directive hook method. It is time to unveil the mystery of directive.js . The core code is as follows:

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

// By default, all methods thrown are updateDirectives method export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    // When destroyed, vnode === emptyNode
    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 update is complete const dirsWithPostpatch = []

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

  if (dirsWithInsert.length) {
    // The real DOM is inserted into the page, and this callback method will be called const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    // VNode merge insert hooks
    if (isCreated) {
      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)
      }
    }
  }
}

For the first creation, the execution process is as follows:

1.oldVnode === emptyNode, isCreate is true, and all bind hook methods in the current element are called.

2. Check if there is an inserted hook in the instruction. If so, merge the insert hook into the VNode.data.hooks property.

3. After DOM mounting is completed, invokeInsertHook will be executed for all mounted nodes if there is an insert hook in VNode.data.hooks. It will be called, and the inserted method bound to the instruction will be triggered.

Generally, only bind and inserted methods are used for the first creation, while update and componentUpdated correspond to bind and inserted. When the component dependency status changes, the VNode diff algorithm will be used to patch the node. The calling process is as follows:

1. When responsive data changes, call dep.notify to notify data updates.

2. Call patchVNode to perform differential updates on the new and old VNodes, and fully update the current VNode attributes (including instructions, which will enter the updateDirectives method).

3. If the instruction has an update hook method, call the update hook method, initialize the componentUpdated callback, and mount the postpatch hooks into VNode.data.hooks.

4. After the current node and its child nodes are updated, the postpatch hooks will be triggered, that is, the componentUpdated method of the instruction

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
    // Fully update node attributes 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 postpatch hook if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

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

3. Notes

When using custom instructions, v-model still has some differences from ordinary template data binding. For example, although the parameter I pass (v-xxx='param') is a reference type, it cannot trigger the bind or inserted of the instruction when the data changes. This is because in the declaration cycle of the instruction, bind and inserted are only called once at initialization, and only update and componentUpdated will be used afterwards.

The declaration lifecycle execution order of a directive is bind -> inserted -> update -> componentUpdated. If a directive needs to depend on the content of a child component, 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, the calls are wrapped in try catch. The purpose of this is to prevent a processing method from reporting an error and causing the entire program to crash. This can be used as a reference in our development process.

IV. Summary

When I started to look at the entire Vue source code, I didn't know much about many of the details and methods. By sorting out the implementation of each specific function, I was able to gradually see the overall picture of Vue, and at the same time avoid some pitfalls in development and use.

The above is the detailed content of analyzing the implementation principle of Vue directive. For more information about the principle of Vue directive, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Getting Started with Vue 3.0 Custom Directives
  • Detailed explanation of Vue custom instructions and their use
  • How to build a drag and drop plugin using vue custom directives
  • Detailed explanation of custom instructions for Vue.js source code analysis
  • Vue custom v-has instruction to implement button permission judgment
  • Vue basic instructions example graphic explanation
  • Summary of Vue 3 custom directive development
  • Vue3.0 custom instructions (drectives) knowledge summary
  • 8 very practical Vue custom instructions
  • Detailed explanation of custom instructions in Vue

<<:  Detailed explanation of MySQL remote connection permission

>>:  Detailed explanation of the installation steps of the MySQL decompressed version

Recommend

How to fill items in columns in CSS Grid Layout

Suppose we have n items and we have to sort these...

Detailed explanation of Vue's keyboard events

Table of contents Common key aliases Key without ...

Detailed explanation of Linux system directories sys, tmp, usr, var!

The growth path from a Linux novice to a Linux ma...

Json advantages and disadvantages and usage introduction

Table of contents 1. What is JSON 1.1 Array liter...

How to create a child process in nodejs

Table of contents Introduction Child Process Crea...

Vue implements the magnifying glass function of the product details page

This article shares the specific code of Vue to i...

What is web design

<br />Original article: http://www.alistapar...

How to pull the docker image to view the version

To view the version and tag of the image, you nee...

Understanding Vuex in one article

Table of contents Overview Vuex four major object...

Let’s talk in detail about how browsers view closures

Table of contents Preface Introduction to Closure...

Complete steps for deploying confluence with docker

Confluence is paid, but it can be cracked for use...