background

I read a few articles about custom commands, but I don’t see many articles exploring how they work, so I decided to write one.

How do I customize directives?

In fact, there is a good example of this issue in the official document, let’s first take a look at it.

In addition to the core functionality’s default built-in directives (V-Model and V-show), Vue also allows you to register custom directives. Note that in Vue2.0, the main form of code reuse and abstraction is components. However, in cases where you still need low-level manipulation of normal DOM elements, custom directives are used. An example of focusing an input box is as follows:

When the page loads, this element gets focus (note: AutoFocus does not work on the mobile version of Safari). In fact, as long as you haven’t clicked on anything since opening the page, the input field should remain in focus. Now let’s implement this function with instructions:

// Register a global custom directive 'v-focus'
Vue.directive('focus', {
// When the bound element is inserted into the DOM...
    inserted: function (el) {
        // Focus the element
        el.focus()
    }
})
Copy the code

If you want to register local directives, the component also accepts a directives option:

directives: {
  focus: {
    // Directive definition
    inserted: function (el) {
      el.focus()
    }
  }
}
Copy the code

Then you can use the new V-Focus Property on any element in the template, as follows:

<input v-focus>
Copy the code

The hook function provided within the directive

A directive definition object can provide the following hook functions (all optional) :

  • bind:It is called only once, the first time a directive is bound to an element. This is where you can do one-time initialization.
  • inserted:Called when the bound element is inserted into the parent node (only the parent node is guaranteed to exist, but not necessarily has been inserted into the document).
  • update:Called when the component’s VNode is updated, ** but may occur before its children are updated. The value of the ** directive may or may not have changed. However, you can ignore unnecessary template updates by comparing the values before and after the update (see below for more details on the hook function arguments).
  • componentUpdated:VNode of the component where the directive residesHis son VNodeCalled after all updates.
  • unbind:It is called only once, when the instruction is unbound from the element.

Hook function parameters

The directive hook function is passed the following arguments:

  • el:The element bound by the directive can be manipulated directlyDOM
  • Binding:An object containing the following property:
  1. Name:Directive name, excludingv-Prefix.
  2. Value:Directive binding values, such as:v-my-directive="1 + 1", the binding value is 2.
  3. OldValue:Instruction bound to the previous value, only inupdatecomponentUpdatedAvailable in hooks. It is available regardless of whether the value has changed.
  4. Expression:Instruction expressions in the form of strings. For example,v-my-directive="1 + 1", the expression is"1 + 1".
  5. Arg:Parameter passed to the instruction, optional. For example,v-my-directive:foo, the parameter is “foo”.
  6. Modifiers:An object that contains a modifier. Such as:v-my-directive.foo.bar, the modifier object is{ foo: true, bar: true }.
  • Vnode: VueCompile the generated virtual node. See the VNode API on the website for more details.
  • OldVnode:Last virtual node, only inupdatecomponentUpdatedAvailable in hooks.

All parameters except EL should be read-only and should not be modified. If you need to share data between hooks, it is recommended to do so through the element’s dataset.

So let’s do a demo,

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Copy the code
Vue.directive('demo', {
    bind: function (el, binding, vnode) {
        var s = JSON.stringify
        el.innerHTML =
        'name: '       + s(binding.name) + '<br>' +
        'value: '      + s(binding.value) + '<br>' +
        'expression: ' + s(binding.expression) + '<br>' +
        'argument: '   + s(binding.arg) + '<br>' +
        'modifiers: '  + s(binding.modifiers) + '<br>' +
        'vnode keys: ' + Object.keys(vnode).join(', ')}})new Vue({
    el: '#hook-arguments-example'.data: {
        message: 'hello! '}})Copy the code

Take a look at the result:

name: "demo"
value: "hello!"
expression: "message"
argument: "foo"
modifiers: {"a":true."b":true}
vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder
Copy the code

Instruction implementation principle analysis

Through the above official website examples and our usual coding, we basically understand how the vue instruction is used, and then we analyze the principle of its implementation from the perspective of source code.

Definition of vue. directive:

function initAssetRegisters(Vue) {
    ASSET_TYPES.forEach(function (type) {
        Vue[type] = function ( id, definition ) {
            if(! definition) {return this.options[type + 's'][id]
            } else {
                if (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') {
                    // Tip: pass parameter compatibility
                    definition = {
                        bind: definition,
                        update: definition
                    };
                }
                // Tip: store a ['component', 'directive', 'filter']
                this.options[type + 's'][id] = definition;
                return definition
            }
        };
    });
}
Copy the code

For example, vm.$options.directives. Demo = {XXX}. We will see how this directive works.

Without further analysis of the source code, we can make a rough guess as to how the instructions from the definition will be implemented. In the template compilation phase, the attributes of the element are parsed to the attribute of the instruction, and the different custom logic in the custom instruction is called at different life cycle element stages. Next, with the analysis of the source code, the parsing and effective of this instruction can be divided into three stages: template compilation stage, VNode generation stage, and real Dom generation patch stage.

Take the following code snippet as an example:

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Copy the code

Template compilation phase

Those of you who are not familiar with template compilation may want to review what is being done at this stage. I won’t go into the details here, just focus on the instructions. Directives are part of an element’s attributes, so when parsing a tag element, they are put into the ATTRs attribute of the Ele Ast object. The above example will be resolved as follows:

[{name"id".value"hook-arguments-example".start5.end32},
    {name"v-demo:foo.a.b".value"message".start33.end57}]Copy the code

These properties are further processed as directives when matched to the end tag. For example, they are cached as directives on the Ele Ast object.

When endTagMatch matches the end tag, the parseEndTag function is called. Inside this function, the parseHtml configuration item is called options.end. It goes back to calling closeElement.

function closeElement(element) {
    // ...
    if(! inVPre && ! element.processed) { element = processElement(element, options); }// ...
}
Copy the code

Notice the processElement method here, which does all sorts of processing on elements in the parsing process. Let’s look at the code for processElement.

function processElement( element, options ) {
    processKey(element);
    processRef(element);
    processSlotContent(element);
    processSlotOutlet(element);
    processComponent(element);
    // ...
    processAttrs(element);
    return element
}
Copy the code

Focusing on the processing of a bunch of element attributes, we need to focus on the processAttrs method, which deals with instructions and modifiers. Let’s look at the pseudo-code for processAttrs:

function processAttrs(el) {
    var list = el.attrsList;
    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
    for (i = 0, l = list.length; i < l; i++) {
        name = rawName = list[i].name;
        value = list[i].value;
        / / Tip: parsing instructions dirRE = / ^ v - | ^ @ | ^ : | ^ # /;
        if (dirRE.test(name)) {
            // ...
            if (bindRE.test(name)) {
                // Handle the V-bind case
                if((modifiers && modifiers.prop) || ( ! el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) { addProp(el, name, value, list[i], isDynamic); }else{ addAttr(el, name, value, list[i], isDynamic); }}else if (onRE.test(name)) { 
                // Handle the V-ON case
                addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
            } else { 
                // Handle the general instruction case
                // Tip: Adds the directives attribute to the element being resolvedaddDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]); }}else {
            / /... Handling Literal Attributes}}}Copy the code

There is a for loop that goes to the attrsList of Ele Ast and then resolves them according to the different re, dealing with v-bind, V-ON, and V-xx cases respectively. For custom directives, add directives to the Ele Ast with addDirectives as follows:

directives = [
    {
        arg"foo" , end57.isDynamicArgfalse.modifiers: { atrue.btrue }, name"demo".rawName"v-demo:foo.a.b".start33.value"message"}]Copy the code

In the first phase of template parsing the instructions are resolved to look like this. In the second phase of template parsing generate generates function strings that generate VNodes from the Ele Ast that is parsed. The custom instruction is now converted to the following form as the second argument to the _c function.

"{directives:[{name:"demo",rawName:"v-demo:foo.a.b",value:(message),expression:"message",arg:"foo",modifiers:{"a":true," b":true}}],attrs: {"id":"hook-arguments-example"}}"Copy the code

Generate the vNode phase

During the render generation phase of the vNode, the resulting directive string is mounted to the vNode.data.directives property.

vNode.data.directives = [{
    arg: "foo"
    expression: "message"
    modifiers: { a: true.b: true }
    name: "demo"
    rawName: "v-demo:foo.a.b"
    value: "hello!"
}]
Copy the code

The patch phase to generate the real Dom

In this phase of generating the real Dom from vNode, createElm will call invokeCreateHooks (call crate functions), and will call updateDirectives. This will eventually call _update. Let’s look at the code:

function _update(oldVnode, vnode) {
    var isCreate = oldVnode === emptyNode;
    
    // Tip: get a global custom instruction function
    var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
    var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);

    for (key in newDirs) {
        if(! oldDir) {// new directive, bind
            callHook$1(dir, 'bind', vnode, oldVnode);
            if(dir.def && dir.def.inserted) { dirsWithInsert.push(dir); }}else {
            callHook$1(dir, 'update', vnode, oldVnode); }}if (dirsWithInsert.length) {
        var callInsert = function () {
            for (var i = 0; i < dirsWithInsert.length; i++) {
                callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); }};if (isCreate) {
            mergeVNodeHook(vnode, 'insert', callInsert);
        } else{ callInsert(); }}if (dirsWithPostpatch.length) {
        mergeVNodeHook(vnode, 'postpatch'.function () {
            for (var i = 0; i < dirsWithPostpatch.length; i++) {
                callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode); }}); }if(! isCreate) {for (key in oldDirs) {
            if(! newDirs[key]) {// no longer present, unbind
                callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); }}}}Copy the code

The normalizeDirectives$1 is important in _UPDATE because it corresponds to the current directives that we started with a global custom function. In addition, different custom instruction functions will be called according to different conditions in different life cycles. For example, if there is no oldDir, the initialized bind will be called.

conclusion

Directive is a global function defined in Vue. Directive and the different hook functions are defined in the document. We have a general idea. Through the step by step exploration of the implementation of custom instructions, I have a further understanding of the entire VUE process. In addition, I was impressed by the organization of the entire code logic, which is worth exploring and learning.

Refer to the link

  • Vue custom instruction
  • These 15 custom Vue instructions will make your project development more exciting
  • Vue.js Template parser Principles – From Chapter 9 of Vue.js
  • Vue diff algorithm analysis
  • Interviewer: Have you read about the DIff algorithm in Vue? Say that see