Vue|directive自定义指令咋个用?

vue中有很多豫定义的绑定指令,v-modle、v-onlick和v-bind等。如果咱需要自己的指令,该如何实现?这个问题就是当下要研究的方向 ~

在这里插入图片描述
当然研究的起始方向,依然是从组件开源库mint-ui的开源代码开始 ~

解读mint-ui源码文件

局部directive

/** packages/field/src/field.vue */

<template>
  <!-- 组件x-cell中使用自定义指令:v-clickoutside。当点击该组件外侧,则方法doCloseActive被激活并调用 -->
  <x-cell
    class="mint-field"
    :title="label"
    v-clickoutside="doCloseActive" <!-- 这里是v-clickoutside="doCloseAtive" 说明指令钩子函数bind中绑定的是click事件-->
    :class="[{
      'is-textarea': type === 'textarea',
      'is-nolabel': !label
    }]">
    ... ... ...
  </x-cell>
</template>

<script>
import XCell from 'mint-ui/packages/cell/index.js';
import Clickoutside from 'mint-ui/src/utils/clickoutside'; /** 引入自定义指令的js文件,触发该指令之后的业务逻辑 */
if (process.env.NODE_ENV === 'component') {
  require('mint-ui/packages/cell/style.css');
}

/**
 * mt-field
 * @desc 编辑器,依赖 cell
 * @module components/field
 *
 * @param {string} [type=text] - field 类型,接受 text, textarea 等
 * @param {string} [label] - 标签
 * @param {string} [rows] - textarea 的 rows
 * @param {string} [placeholder] - placeholder
 * @param {string} [disabled] - disabled
 * @param {string} [readonly] - readonly
 * @param {string} [state] - 表单校验状态样式,接受 error, warning, success
 *
 * @example
 * <mt-field v-model="value" label="用户名"></mt-field>
 * <mt-field v-model="value" label="密码" placeholder="请输入密码"></mt-field>
 * <mt-field v-model="value" label="自我介绍" placeholder="自我介绍" type="textarea" rows="4"></mt-field>
 * <mt-field v-model="value" label="邮箱" placeholder="成功状态" state="success"></mt-field>
 */
export default {
  name: 'mt-field',

  data() {
    return {
      active: false,
      currentValue: this.value
    };
  },
 methods: {
    // 自定义指令v-clickoutside,被触发所调用的方法
    doCloseActive() {
      this.active = false;
    },

     .... .... ...
  },

  directives: { /** 注意:这里通过该字段,进行局部的自定义指令的配置 */
    Clickoutside
  },

 ... ... ...
};
</script>

<style lang="css">
  @import "../../../src/style/var.css";
... ... ...
</style>

上面vue源码中通过import引入clickoutside.js源码文件

/** packages/src/utils/clickoutside.js */

/**
 * v-clickoutside
 * @desc 点击元素外面才会触发的事件
 * @example
 * ```vue
 * <div v-element-clickoutside="handleClose">
 * ```
 */
const clickoutsideContext = '@@clickoutsideContext'; // 定义一个字符串作为 'key'

export default {
  bind(el, binding, vnode) { // 指令钩子函数:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    const documentHandler = function(e) {
      if (vnode.context && !el.contains(e.target)) { // 判断用户点击的位置,在使用自定义指令的组件的外侧,则继续向下执行
        vnode.context[el[clickoutsideContext].methodName]();
      }
    };
    el[clickoutsideContext] = { // el是对象类型,可直接操作dom。这行代码意思是为该对象添加一些属性。且添加属性的目的是能够在全局使用属性,起到全局变量的作用。
      documentHandler,
      methodName: binding.expression,
      arg: binding.arg || 'click'
    };
    document.addEventListener(el[clickoutsideContext].arg, documentHandler);// 为当前使用自定义指令的组件所在dom,添加点击的监听
  },
// 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  update(el, binding) {
    el[clickoutsideContext].methodName = binding.expression; // 指令钩子函数:更新指令对应的方法名,比如上面的'doCloseActive'
  },

  unbind(el) { // 指令钩子函数:移除监听
    document.removeEventListener(
      el[clickoutsideContext].arg,
      el[clickoutsideContext].documentHandler);
  },

/** 
  *该install(vue)方法,目的是可以进行全局配置。只要在main.js中进行Vue.use()配置即可。
  *如果要在某个vue页面通过components:{}配置,则是局部的配置。
 */
  install(Vue) { 
    Vue.directive('clickoutside', {
      bind: this.bind,
      unbind: this.unbind
    });
  }
};

详尽的了解还是要看上面对源码进行的注释的,然后简要叙述下使用逻辑。
通过编码clickoutside.js文件来自定义directive指令逻辑,如上第14行的bind()方法中第25行,对使用指令的组件所在的dom添加点击事件的监听。若监听的事件被激活(点击了页面某个地方,可以是组件可以是外侧),则会执行第16、17行 , 判断是否点击了组件外围,若是,则执行回调方法vnode.context[el[clickoutsideContext].methodName](); 。即对应上面vue源码中的doCloseActive()方法。

全局directive

全局配置的使用directive自定义指令的mint-ui开源组件源码文件

/** directive.js文件 scroll滑动,滑动加载更多的自定义指令 */

import Vue from 'vue';
const ctx = '@@InfiniteScroll';

var throttle = function(fn, delay) {
  var now, lastExec, timer, context, args; //eslint-disable-line

  var execute = function() {
    fn.apply(context, args);
    lastExec = now;
  };

  return function() {
    context = this;
    args = arguments;

    now = Date.now();

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (lastExec) {
      var diff = delay - (now - lastExec);
      if (diff < 0) {
        execute();
      } else {
        timer = setTimeout(() => {
          execute();
        }, diff);
      }
    } else {
      execute();
    }
  };
};

var getScrollTop = function(element) {
  if (element === window) {
    return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop);
  }

  return element.scrollTop;
};

var getComputedStyle = Vue.prototype.$isServer ? {} : document.defaultView.getComputedStyle;

var getScrollEventTarget = function(element) {
  var currentNode = element;
  // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
  while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
    var overflowY = getComputedStyle(currentNode).overflowY;
    if (overflowY === 'scroll' || overflowY === 'auto') {
      return currentNode;
    }
    currentNode = currentNode.parentNode;
  }
  return window;
};
// 获取可见高度
var getVisibleHeight = function(element) {
  if (element === window) {
    return document.documentElement.clientHeight;
  }

  return element.clientHeight;
};

var getElementTop = function(element) {
  if (element === window) {
    return getScrollTop(window);
  }
  return element.getBoundingClientRect().top + getScrollTop(window);
};
// 判断el(dom节点)是否已挂载
var isAttached = function(element) {
  var currentNode = element.parentNode;
  while (currentNode) {
    if (currentNode.tagName === 'HTML') {
      return true;
    }
    if (currentNode.nodeType === 11) {
      return false;
    }
    currentNode = currentNode.parentNode; // 一直循环获取当前node节点的父节点
  }
  return false;
};

/** 滚动监听注册,且滚动时有函数throttle回调,然后再调用doCheck方法,最后根据滚动条件会回调自定义指令中绑定的方法bind.value */
var doBind = function() {
  if (this.binded) return; // eslint-disable-line
  this.binded = true;

  var directive = this; //  this等于doBind.call(el[ctx], args);中的el[ctx]
  var element = directive.el;// 被指令绑定的组件节点dom

  directive.scrollEventTarget = getScrollEventTarget(element);
  directive.scrollListener = throttle(doCheck.bind(directive), 200);
  // scroll 滚动事件绑定,滚动时回调throttle()方法
  directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener);

  var disabledExpr = element.getAttribute('infinite-scroll-disabled');// 获取class为'infinite-scroll-disabled'的变量值
  var disabled = false;

  if (disabledExpr) {
    this.vm.$watch(disabledExpr, function(value) {
      directive.disabled = value;
      if (!value && directive.immediateCheck) {
        doCheck.call(directive);
      }
    });
    disabled = Boolean(directive.vm[disabledExpr]);
  }
  directive.disabled = disabled;

  var distanceExpr = element.getAttribute('infinite-scroll-distance');
  var distance = 0;
  if (distanceExpr) {
    distance = Number(directive.vm[distanceExpr] || distanceExpr);
    if (isNaN(distance)) {
      distance = 0;
    }
  }
  directive.distance = distance;

  var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check');
  var immediateCheck = true;
  if (immediateCheckExpr) {
    immediateCheck = Boolean(directive.vm[immediateCheckExpr]);
  }
  directive.immediateCheck = immediateCheck;

  if (immediateCheck) {
    doCheck.call(directive);
  }

  var eventName = element.getAttribute('infinite-scroll-listen-for-event');
  if (eventName) {
    directive.vm.$on(eventName, function() {// this.$on() 监听当前实例上的自定义事件`eventName`。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
      doCheck.call(directive); // doCheck方法中上下文this该换作directive
    });
  }
};

/** scroll滚动时,会回调到方法doCheck这里 */
var doCheck = function(force) {
  var scrollEventTarget = this.scrollEventTarget;
  var element = this.el;
  var distance = this.distance;

  if (force !== true && this.disabled) return; //eslint-disable-line
  var viewportScrollTop = getScrollTop(scrollEventTarget);
  var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget);

  var shouldTrigger = false;

  if (scrollEventTarget === element) {
    shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance;
  } else {
    var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;

    shouldTrigger = viewportBottom + distance >= elementBottom;
  }

  if (shouldTrigger && this.expression) {
    this.expression(); // 即binding.value,当滚动到的条件符合上面if时,执行自定义指令的回调方法 this.expression()
  }
};

export default {
  bind(el, binding, vnode) { // 指令钩子函数:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    el[ctx] = { // 属性绑定
      el,
      vm: vnode.context,
      expression: binding.value
    };
    const args = arguments;
    var cb = function() {
      el[ctx].vm.$nextTick(function() {// 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
        if (isAttached(el)) {// 判断el(dom节点)是否已挂载
          doBind.call(el[ctx], args);
        }

        el[ctx].bindTryCount = 0;

        var tryBind = function() { // 注意:每隔50s执行一次,循环判断dom节点el是否已经mount
          if (el[ctx].bindTryCount > 10) return; //eslint-disable-line
          el[ctx].bindTryCount++;
          if (isAttached(el)) {
            doBind.call(el[ctx], args);// 调用doBind方法,doBind方法上下文环境指定为el[ctx]
          } else {
            setTimeout(tryBind, 50);
          }
        };

        tryBind();
      });
    };
    if (el[ctx].vm._isMounted) {
      cb();
      return;
    }
    el[ctx].vm.$on('hook:mounted', cb); // 监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
  },

  unbind(el) {
    if (el[ctx] && el[ctx].scrollEventTarget) {
      el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
    }
  }
};

使其进行全局配置,还有另一个js文件 infinite-scroll.js

/** infinite-scroll.js */

import InfiniteScroll from './directive'; // 指令逻辑文件
import 'mint-ui/src/style/empty.css';
import Vue from 'vue';

const install = function(Vue) {// 第一步:install方法中进行Vue.directive全局自定义指令配置
  Vue.directive('InfiniteScroll', InfiniteScroll);
};

/** 如果这里没有下面if判断行,作全局配置。
则需要在工程中的main.js中引入install文件 即:import install from '**directive.js',
然后再调用Vue.use(install);就能完成全局配置*/
if (!Vue.prototype.$isServer && window.Vue) {
  window.infiniteScroll = InfiniteScroll;
  /** 安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。 */
  Vue.use(install); // eslint-disable-line // 第二步: Vue.use(install); 全局配置
}

InfiniteScroll.install = install;
export default InfiniteScroll; // 第三步: 对外可通过import引入该模块

我们看到的infinite-scroll.js文件,是对自定义指令的全局配置。全局配置我们可以舍弃nfinite-scroll.js这一步,直接在本地工程中的main.js中进行配置。当然利用上面的infinite-scroll.js文件,然后再在main.js中作配置会更合适。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值