element-ui源码阅读-指令

element-ui源码中运用了四个指令,分别为点击元素外滚轮事件优化单击事件优化获取ref指令。这些指令在平时的开发中也会经常用到,下面就来一一介绍这些指令的实现方式以及用途。

1.什么是指令

在理解element-ui中相关的指令前,先来了解下什么是指令,内置指令以及怎么创建自定义指令。

1.1 指令概念

vue中指令都是以v-开头,作用于html标签,提供一些特殊的特性,当指令被绑定到html元素的时候,指令会为被绑定的元素添加一些特殊的行为,可以将指令看成html的一种属性,用于操作DOM

1.2 内置指令

vue中提供了一些内置指令,如下所示:

  • v-text:更新元素的 textContent
  • v-html:更新元素的 innerHTML
  • v-show:根据表达式之真假值,切换元素的 display CSS property
  • v-if:条件渲染,用于判断是否显示元素。在切换时元素及它的数据绑定 / 组件被销毁并重建
  • v-else:配合v-if一起使用。
  • v-else-if:配合v-if一起使用。
  • v-for:基于源数据多次渲染元素或模板块。
  • v-on:绑定事件监听器。
  • v-bind:动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。
  • v-model:在表单控件或者组件上创建双向绑定。
  • v-slot:提供具名插槽或需要接收 prop 的插槽。
  • v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
  • v-cloak:这个指令保持在元素上直到关联实例结束编译。
  • v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

1.3 自定义指令

Vue 推崇数据驱动视图的理念(数据交互,状态管理),但并非所有情况都适合数据驱动( DOM 的操作)。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。

1.3.1 指令定义

使用Vue.directive(id,definition)可以进行指令定义。

  • id:指令id,定义好后,可以直接通过v-{id}来使用。
  • definition:对象,该对象提供了一些钩子函数

1.3.2 钩子函数

一个指令定义对象可以提供如下几个钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
  • componentUpdated:指令所在组件的 VNode及其子 VNode全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

1.3.3 钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值。
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。
    • arg:传给指令的参数,可选。
    • modifiers:一个包含修饰符的对象。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

2.点击元素边界外

该指令主要是用于判断点击的点是否在绑定元素的范围内。该指令一般用在弹窗中,如经常用到的Popover组件,下拉搜索等。具体实现思路如下所示:

    1. 添加v-clickoutside="close"指令
  • 2.为document添加鼠标按下弹起的事件。
  • 3.绑定元素,并创建相应的鼠标事件函数。
    1. 监听鼠标弹起事件,判断点击的点是否在元素外。
    1. 执行v-clickoutside绑定的close事件。
import Vue from 'vue';
import { on } from 'element-ui/src/utils/dom';

const nodeList = [];
const ctx = '@@clickoutsideContext';

let startClick;
let seed = 0;

// 添加鼠标按下的事件,并缓存event
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));

// 添加鼠标点击后弹起的事件,遍历nodeList,执行nodeList中元素添加的事件
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// 创建元素的点击事件
function createDocumentHandler(el, binding, vnode) {
  // 以弹起和弹出作参数
  return function(mouseup = {}, mousedown = {}) {
    // 先判断点击的对象是否为指令绑定的元素本身或空元素对象
    if (!vnode ||
      !vnode.context ||
      !mouseup.target ||
      !mousedown.target ||
      el.contains(mouseup.target) ||
      el.contains(mousedown.target) ||
      el === mouseup.target ||
      (vnode.context.popperElm &&
      (vnode.context.popperElm.contains(mouseup.target) ||
      vnode.context.popperElm.contains(mousedown.target)))) return;

      // 获取指令的表达式
    if (binding.expression &&
      el[ctx].methodName &&
      // 执行指令绑定的方法
      vnode.context[el[ctx].methodName]) {
      vnode.context[el[ctx].methodName]();
    } else {
      el[ctx].bindingFn && el[ctx].bindingFn();
    }
  };
}

/**
 * v-clickoutside
 * @desc 点击元素外面才会触发的事件
 * @example
 * ```vue
 * 
* ``` */ export default { bind(el, binding, vnode) { nodeList.push(el);//将绑定的元素对象添加到数组中 const id = seed++; // 给绑定的元素对象添加点击触发的方法,使用一个变量存储 el[ctx] = { id, documentHandler: createDocumentHandler(el, binding, vnode), methodName: binding.expression, bindingFn: binding.value }; }, // 更新 update(el, binding, vnode) { el[ctx].documentHandler = createDocumentHandler(el, binding, vnode); el[ctx].methodName = binding.expression; el[ctx].bindingFn = binding.value; }, // 解除绑定 unbind(el) { let len = nodeList.length; for (let i = 0; i < len; i++) { if (nodeList[i][ctx].id === el[ctx].id) { nodeList.splice(i, 1); break; } } delete el[ctx]; } };

3.单击事件优化

src/directives目录下有一个repeat-click.js文件,该文件就是一个用于优化单击事件的指令,我们平时点击时,正常的点击逻辑是这样的:当用户按住鼠标左键时,会触发mousedown的回调。但当一直按住鼠标左键不松手时,就不会触发mousedown的回调,使用该指令就是为了实现一直按住鼠标左键不松手时,也能执行对应的事件,这指令主要用在InputNumber组件中,当鼠标点击-+不松开时,数字可以持续的进行加减。下面就来看看指令是怎么实现的:

    1. 引入指令文件,import RepeatClick from 'element-ui/src/directives/repeat-click';
    1. directives中注册指令。
    1. 使用指令,v-repeat-click="decrease"
    1. bind事件中定义一个clear的函数,用于清除定时器。
  • 5.为绑定指令的元素添加moursedown事件。
  • 6.在moursedown回调事件中,定义一个定时器,每100秒执行一次回调函数。
    1. 鼠标弹起时,执行clear函数清除定时器。
import { once, on } from '@/utils/dom';

export default {
  bind(el, binding, vnode) {
    let interval = null;
    let startTime;
    // 获取指令绑定的事件函数
    const handler = () => vnode.context[binding.expression].apply();
    // 定义一个清除定时器的函数
    const clear = () => {
        // 间隔时间小于100毫秒时,继续执行回调函数
      if (Date.now() - startTime < 100) {
        handler();
      }
      clearInterval(interval);
      interval = null;
    };

    // 添加点击事件
    on(el, 'mousedown', (e) => {
      if (e.button !== 0) return;
     //   缓存点击时的时间
      startTime = Date.now();
      // 添加鼠标弹起的事件
      once(document, 'mouseup', clear);
      // 清除定时器
      clearInterval(interval);
      // 100毫秒执行一次回调函数
      interval = setInterval(handler, 100);
    });
  }
};

4.滚轮事件优化

src/directives目录下有一个mousewheel.js文件,该指令主要是对鼠标滚动事件进行了优化,使用normalize-wheel这个库来解决不同浏览器之间的兼容性来获取x方向和y方向的滚动偏移量。

import normalizeWheel from 'normalize-wheel';

const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;

const mousewheel = function(element, callback) {
  if (element && element.addEventListener) {
    element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
      const normalized = normalizeWheel(event);
      callback && callback.apply(this, [event, normalized]);
    });
  }
};

export default {
  bind(el, binding) {
    mousewheel(el, binding.value);
  }
};

5.获取ref指令

packages/popover/src/目录下有一个directive.js文件,该指令主要是用于Popover组件,用于获取Popover组件的ref,

const getReference = (el, binding, vnode) => {
  const _ref = binding.expression ? binding.value : binding.arg;
  const popper = vnode.context.$refs[_ref];
  if (popper) {
    if (Array.isArray(popper)) {
      popper[0].$refs.reference = el;
    } else {
      popper.$refs.reference = el;
    }
  }
};

export default {
  bind(el, binding, vnode) {
    getReference(el, binding, vnode);
  },
  inserted(el, binding, vnode) {
    getReference(el, binding, vnode);
  }
};

总结

element-ui的指令基本上都介绍完了,平时开发的时候除了使用vue内置的指令外,应该尽可能多封装一些组件来提升工作效率。

你可能感兴趣的:(element-ui源码阅读-指令)