9. 探究Vue的自定义指令

464 阅读2分钟

自定义指令的注册

Vue的全局API: directive、filter和component注册如下:

ASSET_TYPES.forEach(function (type) {
      Vue[type] = function (
        id,
        definition
      ) {
        if (!definition) {// 过滤器注册
          return this.options[type + 's'][id]
        } else {
          /* istanbul ignore if */
          if (type === 'component') {
            validateComponentName(id);
          }
          // 如果是全局注册组件,通过Vue.extend生成子组件构造函数
          if (type === 'component' && isPlainObject(definition)) {
            definition.name = definition.name || id;
            definition = this.options._base.extend(definition);
          }
          // 注册或获取全局指令,此时并未失效,如果definition是函数则默认监听bind和update事件
          if (type === 'directive' && typeof definition === 'function') {
            definition = { bind: definition, update: definition };
          }
          this.options[type + 's'][id] = definition;
          return definition
        }
      };
    });
}

以模板为例:

<div id="app"><input type="text" class="form-control" v-model="keywords" v-focus></div>

自定义指令在编译阶段的处理

在模板编译阶段,处理属性的过程中,会把v-model和v-focus这种解析成:

[
    {arg: null, end: 72, isDynamicArg: false, modifiers: undefined, name: "model",rawName: "v-model",start: 54, value: "keywords"},
    {arg: null,end: 83,isDynamicArg: false,modifiers: undefined,name: "focus",rawName: "v-focus",start: 73, value: ""}
]

存在el.directives属性中,生成的渲染函数如下:

with(this){return _c('div',{
        attrs:{"id":"app"}
    },[
        _c('input',{
            directives:
            [
                {
                    name:"model",
                    rawName:"v-model",
                    value:(keywords),
                    expression:"keywords"
                },
                {
                    name:"focus",
                    rawName:"v-focus"
                }
            ],
            staticClass:"form-control",
            attrs:{"type":"text"},
            domProps:{"value":(keywords)},
            on:{"input":function($event){
                if($event.target.composing)
                return;
                keywords=$event.target.value
            }
        }
    })
])}

input生成的Vnode如下:

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: undefined,
    componentInstance: undefined,
    componentOptions: undefined,
    context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
    data: {directives: Array(2), staticClass: "form-control", attrs: {type: "text"}, domProps: {value: undefined}, on: {input: f}},
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "input",
    text: undefined,
    child: undefined
}

自定义指令在渲染阶段

在渲染阶段,patchVnode过程中,会执行

for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }

更新属性的函数为

[
    ƒ updateAttrs(oldVnode, vnode),
    ƒ updateClass(oldVnode, vnode),
    ƒ updateDOMListeners(oldVnode, vnode),
    ƒ updateDOMProps(oldVnode, vnode),
    ƒ updateStyle(oldVnode, vnode),
    ƒ update(oldVnode, vnode),
    ƒ updateDirectives(oldVnode, vnode)
]

其中指令相关的处理逻辑

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

_update函数为:

function _update (oldVnode, vnode) {
    var isCreate = oldVnode === emptyNode;
    var isDestroy = vnode === emptyNode;
    var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
    var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);

    var dirsWithInsert = [];
    var dirsWithPostpatch = [];

    var key, oldDir, dir;
    for (key in newDirs) {
      oldDir = oldDirs[key];
      dir = newDirs[key];
      if (!oldDir) {
        // new directive, bind
        // 新的指令触发bind方法
        callHook$1(dir, 'bind', vnode, oldVnode);
        if (dir.def && dir.def.inserted) {
          dirsWithInsert.push(dir);
        }
      } else {
        // existing directive, update
        // 指令存在,触发update
        dir.oldValue = oldDir.value;
        dir.oldArg = oldDir.arg;
        callHook$1(dir, 'update', vnode, oldVnode);
        // 判断是否设置componentUpdated方法,有则将其添加到列表中,确保组件的VNode和其子VNode全部更新后再调用指令的componentupdated方法
        if (dir.def && dir.def.componentUpdated) {
          dirsWithPostpatch.push(dir);
        }
      }
    }
    // 指令添加到dirsWithInsert,保证所有的bind执行完毕再执行insert函数
    if (dirsWithInsert.length) {
      var callInsert = function () {
        for (var i = 0; i < dirsWithInsert.length; i++) {
          callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
        }
      };
      // 判断虚拟节点是否是新创建节点
      if (isCreate) {
        // 可以将一个钩子函数与虚拟节点现有的钩子函数合并在一起,当虚拟节点触发钩子函数时,新增的钩子也会触发,这里应该等到元素被插入到父节点后再执行指令的insert
        mergeVNodeHook(vnode, 'insert', callInsert);
      } else {
        // 不需要延迟到绑定元素插入到父节点后进行,直接执行callInsert
        callInsert();
      }
    }

    if (dirsWithPostpatch.length) {
        // 触发update钩子后,再触发postpatch
      mergeVNodeHook(vnode, 'postpatch', function () {
        for (var i = 0; i < dirsWithPostpatch.length; i++) {
            // componentUpdated指令推迟到vnode和oldVnode更新后
          callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
        }
      });
    }

    if (!isCreate) {
        // 新创建的虚拟节点不需要解绑,如果newDirs中不存在,则触发unbind方法
      for (key in oldDirs) {
        if (!newDirs[key]) {
          // no longer present, unbind
          callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
        }
      }
    }
}

这里经过normalizeDirectives函数处理后变成:

[
    {
        v-focus: {
            def: {bind: ƒ, inserted: ƒ, updated: ƒ},
            modifiers: {},
            name: "focus",
            oldArg: undefined,
            oldValue: undefined,
            rawName: "v-focus"
        }
    },
    v-model:{
        def: {inserted: ƒ, componentUpdated: ƒ},
        expression: "keywords",
        modifiers: {},
        name: "model",
        oldArg: undefined,
        oldValue: "wewe",
        rawName: "v-model",
        value: "wewew"
    }
]

callHook如何执行的钩子函数如下:

function callHook$1 (dir, hook, vnode, oldVnode, isDestroy) {
    var fn = dir.def && dir.def[hook];
    if (fn) {
      try {
        fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
      } catch (e) {
        handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook"));
      }
    }
}

更新完directive后继续进行diff,最后挂载到页面。