vue2自定义指令的实现

# 自定义指令的概念理解

vue2指令有四个钩子函数,分别是inserted、update、componentUpdated、bind、unbind。官方的解释有点不大好理解,我类比一下,大家就容易懂了。

inserted => mounted
bind => created
unbind => beforeDestroy
update => 当前节点更新时
componentUpdated => 当前节点及其子节点全部更新完成时调用
1
2
3
4
5

五个函数分别可接受el、binding、vnode、oldVnode四个入参,oldVnode是只有update、componentUpdated函数才有的入参。

vnode.context,可以拿到当前组件实例,v-model底层实现就是利用了这个去修改当前实例的参数。

# v-myModel的实现

以下来实现一下双向绑定,只考虑input,不考虑其他表单组件

<template>
  <input v-myModel="input" />
</template>

<script>
export default {
  name: 'App',
  directives: {
    myModel: {
      bind(el, binding, vnode) {
        console.log('kjlkkj', binding.value);
        el.value = binding.value;
        el.addEventListener('input', function () {
          vnode.context[binding.expression] = this.value;
        });
      },
      update(el, binding) {
        el.value = binding.value;
      },
    },
  },
  data() {
    return {
      input: '222',
    };
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

指令还分全局与局部,只是注册的位置不同,此处省略。

我多举几个常用的例子。

# v-permission的实现

实现v-permission指令。实现这一指令,可以在用户登录时,请求一次用户的权限fetchPermissions,存到localstorage,这样就方便指令去拿权限信息了。绑定的指令先判断当前绑定权限是否包含,没有则隐藏,当然处理隐藏还可以做其它处理,比如禁止点击。另外,绑定的值的类型可以做个判断,兼容下,传入数组的情况。

permisson: {
  inserted(el, binding) {
    let val = binding.value
    if(binding.value) {
      // 这里的用户权限是从接口返回
      let fetchPermissions = ['add', 'delete', 'fix']
      let hasPer = fetchPermissions.some(i => i === val)
      // 没权限则隐藏
      if(!hasPer) el.style.display = 'none'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# v-empty的实现

实现v-empty指令,假设需要接受图片地址、文字信息,这样我们就需要创建跟真实节点插入到当前节点,那么这个要怎么创建真实节点呢?在vue2里面可以借助Vue.extend()得到vue组件,在利用$mount().$el()得到真实节点。明白了这点,下面我们来实现一遍。

empty: {
  update(el, binding, vnode) {
    // 子绝父相 + 居中
    el.style.position = el.style.position || 'relative'
    const { offsetHeight, offsetWidth } = el
    const { visible, content, img } = binding.value
    const image = img ? `<img src="${img}" height='30%' width="30%"/>` : ''
    const defaultStyle = "position: absolute;top:0;left:0;z-index:9999;background:#fff;display:flex;align-items:center;justify-content:center;"
    const empty = Vue.extend({
      template: `<div style='height: ${offsetHeight}px;width:${offsetWidth}px;${defaultStyle}'>
        <div style='text-align:center'>
          <div>
            ${image}
          </div>
          <div>
            ${content || '暂无数据'}
          </div>
        </div>
      </div>
      `
    })
    const component = new empty().$mount().$el
    visible ? el.appendChild(component) : el.removeChild(el.lastChild)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

删除子元素使用了el.removeChild(el.lastChild),不会有太大问题,当前节点下只可能是我传入的img节点。这里要更严谨点,可能innerHtml = ''更好。实际使用时,优化下即可。

# v-preventReClick的实现

该指令的作用是为了防止频繁点击按钮导致请求频繁,优化下,可以做成类似于防抖/节流的功能。根据当前按钮是否disabled来决定是否让用户能点击。

preventReClick: {
  inserted(el, binding, vnode) {
    el.addEventListener('click', function() {
      if(!el.disabled) {
        el.disabled = true
        setTimeout(() => { el.disabled = false }, binding.value || 2000)
      }
    })
  }
}
1
2
3
4
5
6
7
8
9
10

# 结语

vue指令的更多实现,可参考Vue-Demo-Collection (opens new window)

Buy me a cup of coffee ☕.