element-ui源碼閱讀-指令

element-ui源碼中運(yùn)用了四個(gè)指令,分別為點(diǎn)擊元素外,滾輪事件優(yōu)化單擊事件優(yōu)化,獲取ref指令。這些指令在平時(shí)的開發(fā)中也會(huì)經(jīng)常用到,下面就來一一介紹這些指令的實(shí)現(xiàn)方式以及用途。

1.什么是指令

在理解element-ui中相關(guān)的指令前,先來了解下什么是指令,內(nèi)置指令以及怎么創(chuàng)建自定義指令。

1.1 指令概念

vue中指令都是以v-開頭,作用于html標(biāo)簽,提供一些特殊的特性,當(dāng)指令被綁定到html元素的時(shí)候,指令會(huì)為被綁定的元素添加一些特殊的行為,可以將指令看成html的一種屬性,用于操作DOM。

1.2 內(nèi)置指令

vue中提供了一些內(nèi)置指令,如下所示:

  • v-text:更新元素的 textContent。
  • v-html:更新元素的 innerHTML。
  • v-show:根據(jù)表達(dá)式之真假值,切換元素的 display CSS property。
  • v-if:條件渲染,用于判斷是否顯示元素。在切換時(shí)元素及它的數(shù)據(jù)綁定 / 組件被銷毀并重建
  • v-else:配合v-if一起使用。
  • v-else-if:配合v-if一起使用。
  • v-for:基于源數(shù)據(jù)多次渲染元素或模板塊。
  • v-on:綁定事件監(jiān)聽器。
  • v-bind:動(dòng)態(tài)地綁定一個(gè)或多個(gè) attribute,或一個(gè)組件 prop 到表達(dá)式。
  • v-model:在表單控件或者組件上創(chuàng)建雙向綁定。
  • v-slot:提供具名插槽或需要接收 prop 的插槽。
  • v-pre:跳過這個(gè)元素和它的子元素的編譯過程??梢杂脕盹@示原始 Mustache 標(biāo)簽。跳過大量沒有指令的節(jié)點(diǎn)會(huì)加快編譯。
  • v-cloak:這個(gè)指令保持在元素上直到關(guān)聯(lián)實(shí)例結(jié)束編譯。
  • v-once:只渲染元素和組件一次。隨后的重新渲染,元素/組件及其所有的子節(jié)點(diǎn)將被視為靜態(tài)內(nèi)容并跳過。這可以用于優(yōu)化更新性能。

1.3 自定義指令

Vue 推崇數(shù)據(jù)驅(qū)動(dòng)視圖的理念(數(shù)據(jù)交互,狀態(tài)管理),但并非所有情況都適合數(shù)據(jù)驅(qū)動(dòng)( DOM 的操作)。自定義指令就是一種有效的補(bǔ)充和擴(kuò)展,不僅可用于定義任何的 DOM 操作,并且是可復(fù)用的。

1.3.1 指令定義

使用Vue.directive(id,definition)可以進(jìn)行指令定義。

  • id:指令id,定義好后,可以直接通過v-{id}來使用。
  • definition:對(duì)象,該對(duì)象提供了一些鉤子函數(shù)

1.3.2 鉤子函數(shù)

一個(gè)指令定義對(duì)象可以提供如下幾個(gè)鉤子函數(shù):

  • bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。
  • inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 。
  • update:所在組件的 VNode 更新時(shí)調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。
  • componentUpdated:指令所在組件的 VNode及其子 VNode全部更新后調(diào)用。
  • unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。

1.3.3 鉤子函數(shù)參數(shù)

指令鉤子函數(shù)會(huì)被傳入以下參數(shù):

  • el:指令所綁定的元素,可以用來直接操作 DOM。
  • binding:一個(gè)對(duì)象,包含以下 property:
    • name:指令名,不包括 v- 前綴。
    • value:指令的綁定值。
    • oldValue:指令綁定的前一個(gè)值,僅在 updatecomponentUpdated 鉤子中可用。無論值是否改變都可用。
    • expression:字符串形式的指令表達(dá)式。
    • arg:傳給指令的參數(shù),可選。
    • modifiers:一個(gè)包含修飾符的對(duì)象。
  • vnode:Vue 編譯生成的虛擬節(jié)點(diǎn)。
  • oldVnode:上一個(gè)虛擬節(jié)點(diǎn),僅在 updatecomponentUpdated 鉤子中可用。

2.點(diǎn)擊元素邊界外

該指令主要是用于判斷點(diǎn)擊的點(diǎn)是否在綁定元素的范圍內(nèi)。該指令一般用在彈窗中,如經(jīng)常用到的Popover組件,下拉搜索等。具體實(shí)現(xiàn)思路如下所示:

    1. 添加v-clickoutside="close"指令
  • 2.為document添加鼠標(biāo)按下彈起的事件。
  • 3.綁定元素,并創(chuàng)建相應(yīng)的鼠標(biāo)事件函數(shù)。
    1. 監(jiān)聽鼠標(biāo)彈起事件,判斷點(diǎn)擊的點(diǎn)是否在元素外。
    1. 執(zhí)行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;

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

// 添加鼠標(biāo)點(diǎn)擊后彈起的事件,遍歷nodeList,執(zhí)行nodeList中元素添加的事件
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// 創(chuàng)建元素的點(diǎn)擊事件
function createDocumentHandler(el, binding, vnode) {
  // 以彈起和彈出作參數(shù)
  return function(mouseup = {}, mousedown = {}) {
    // 先判斷點(diǎn)擊的對(duì)象是否為指令綁定的元素本身或空元素對(duì)象
    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;

      // 獲取指令的表達(dá)式
    if (binding.expression &&
      el[ctx].methodName &&
      // 執(zhí)行指令綁定的方法
      vnode.context[el[ctx].methodName]) {
      vnode.context[el[ctx].methodName]();
    } else {
      el[ctx].bindingFn && el[ctx].bindingFn();
    }
  };
}

/**
 * v-clickoutside
 * @desc 點(diǎn)擊元素外面才會(huì)觸發(fā)的事件
 * @example
 * ```vue
 * <div v-element-clickoutside="handleClose">
 * ```
 */
export default {
  bind(el, binding, vnode) {
    nodeList.push(el);//將綁定的元素對(duì)象添加到數(shù)組中
    const id = seed++;
    // 給綁定的元素對(duì)象添加點(diǎn)擊觸發(fā)的方法,使用一個(gè)變量存儲(chǔ)
    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.單擊事件優(yōu)化

src/directives目錄下有一個(gè)repeat-click.js文件,該文件就是一個(gè)用于優(yōu)化單擊事件的指令,我們平時(shí)點(diǎn)擊時(shí),正常的點(diǎn)擊邏輯是這樣的:當(dāng)用戶按住鼠標(biāo)左鍵時(shí),會(huì)觸發(fā)mousedown的回調(diào)。但當(dāng)一直按住鼠標(biāo)左鍵不松手時(shí),就不會(huì)觸發(fā)mousedown的回調(diào),使用該指令就是為了實(shí)現(xiàn)一直按住鼠標(biāo)左鍵不松手時(shí),也能執(zhí)行對(duì)應(yīng)的事件,這指令主要用在InputNumber組件中,當(dāng)鼠標(biāo)點(diǎn)擊-+不松開時(shí),數(shù)字可以持續(xù)的進(jìn)行加減。下面就來看看指令是怎么實(shí)現(xiàn)的:

    1. 引入指令文件,import RepeatClick from 'element-ui/src/directives/repeat-click';
    1. directives中注冊(cè)指令。
    1. 使用指令,v-repeat-click="decrease"
    1. bind事件中定義一個(gè)clear的函數(shù),用于清除定時(shí)器。
  • 5.為綁定指令的元素添加moursedown事件。
  • 6.在moursedown回調(diào)事件中,定義一個(gè)定時(shí)器,每100秒執(zhí)行一次回調(diào)函數(shù)。
    1. 鼠標(biāo)彈起時(shí),執(zhí)行clear函數(shù)清除定時(shí)器。
import { once, on } from '@/utils/dom';

export default {
  bind(el, binding, vnode) {
    let interval = null;
    let startTime;
    // 獲取指令綁定的事件函數(shù)
    const handler = () => vnode.context[binding.expression].apply();
    // 定義一個(gè)清除定時(shí)器的函數(shù)
    const clear = () => {
        // 間隔時(shí)間小于100毫秒時(shí),繼續(xù)執(zhí)行回調(diào)函數(shù)
      if (Date.now() - startTime < 100) {
        handler();
      }
      clearInterval(interval);
      interval = null;
    };

    // 添加點(diǎn)擊事件
    on(el, 'mousedown', (e) => {
      if (e.button !== 0) return;
     //   緩存點(diǎn)擊時(shí)的時(shí)間
      startTime = Date.now();
      // 添加鼠標(biāo)彈起的事件
      once(document, 'mouseup', clear);
      // 清除定時(shí)器
      clearInterval(interval);
      // 100毫秒執(zhí)行一次回調(diào)函數(shù)
      interval = setInterval(handler, 100);
    });
  }
};

4.滾輪事件優(yōu)化

src/directives目錄下有一個(gè)mousewheel.js文件,該指令主要是對(duì)鼠標(biāo)滾動(dòng)事件進(jìn)行了優(yōu)化,使用normalize-wheel這個(gè)庫來解決不同瀏覽器之間的兼容性來獲取x方向和y方向的滾動(dòng)偏移量。

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/目錄下有一個(gè)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);
  }
};

總結(jié)

element-ui的指令基本上都介紹完了,平時(shí)開發(fā)的時(shí)候除了使用vue內(nèi)置的指令外,應(yīng)該盡可能多封裝一些組件來提升工作效率。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容