前言
指令(directive)在 vue 開發中是一項很實用的功能,指令可以系結到某一元素或組件,使功能的顆粒度更精細,今天在翻 element-ui 的原始碼時,發現一個還挺實用的工具指令,跟大伙分享一下,
clickoutside 的使用及效果
該指令的原始碼在 src/utils 下的 clickoutside.js,它功能是指令需要接收一個函式,當用戶滑鼠點擊的區域在系結指令的元素之外時,會觸發該函式,
那么使用這個指令能夠實作什么功能呢?我想到一個功能,就像我們常用的抽屜組件,在點擊抽屜之外的區域時,抽屜就會消失(但 elementui 中不是用這種方式,而是用一個遮罩層實作),
接下來我們來看看怎么玩這個指令,很簡單,只需要引入這個檔案注冊指令就好了,
// main.js
import Vue from 'vue'
import clickoutside from 'element-ui/src/utils/clickoutside'
Vue.directive('clickoutside', clickoutside)
使用:
<div v-show="show" v-clickoutside="handler"><div>
export default {
data() {
return {
show: true
}
},
methods: {
handler() {
this.show = false
}
}
}
效果:
原始碼分析
clickoutside 看起來還挺不錯,下面看看它是如何實作的,首先是它的指令鉤子定義:
const nodeList = [];
const ctx = '@@clickoutsideContext';
let seed = 0;
export default {
// 指令系結時觸發
bind(el, binding, vnode) {
// 每次系結時會把dom元素存放到 nodeList 中
nodeList.push(el);
// 創建遞增id標識
const id = seed++;
// 在dom元素上設定一些屬性和方法
// ctx的作用是一個標識,為了不和原生的屬性沖突
el[ctx] = {
id,
// 這個是點擊元素區域外時會執行的函式,后面會提到
documentHandler: createDocumentHandler(el, binding, vnode),
// 系結的值運算式,值相當于上面例子中的 "handler" 字串
methodName: binding.expression,
// 系結的值,值相當于上面例子中的 handler 函式
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;
// 找到對應的dom元素,從 nodeList 移除它
for (let i = 0; i < len; i++) {
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
// 移除之前添加的自定義屬性
delete el[ctx];
}
};
原始碼內部會對 docuemnt 滑鼠事件進行監聽:
let startClick;
// 滑鼠按下時 記錄按下元素的事件物件
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
// 滑鼠松開時 遍歷 nodeList 中的元素,執行 documentHandler
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
接下來最核心的就是 documentHandler 函式,它是由 createDocumentHandler 創建出來的:
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 是組件實體背景關系
// 就像開頭的例子,methodName 是 "handler",通過索引背景關系的屬性找到 methods 中定義的 handler 函式
vnode.context[el[ctx].methodName]();
} else {
el[ctx].bindingFn && el[ctx].bindingFn();
}
};
}
至此整個指令流程分析就完了,
小插曲
在經過一些demo的使用后,發現該指令在某些場景下會出現不理想的效果,例如:抽屜內有 el-select 選擇欄時,選擇欄的 dom 是掛載到 body 下,導致在點擊完選擇項后被判斷為區域外點擊,
其實這也符合邏輯,因為點擊的地方也確實在區域外,只是在這種場景下看起來像是“bug”一樣,然后我發現原始碼里提供了一個選項解決這種問題,可以在使用指令的組件 data 里定義 popperElm 屬性,它的值是一個 dom,
export default {
mounted() {
this.popperElm = document.querySelector('.el-select-dropdown.el-popper')
}
}
在原始碼里會通過 popperElm 進行判斷:
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;
如果 popperElm 包含滑鼠點擊的 dom 則跳出邏輯,
然后我又想到了一個問題,popperElm 只能設定一個,當有多個選擇欄組件時,還是會出現上面所說的情況,我的想法是,把 clickoutside 給 copy 一份下來,把 popperElm 改成可以接受陣列型別,判斷時去回圈判斷,這樣應該可以解決問題,
還有一件有趣的事,我在全域搜索時發現 element-ui 里好像沒有用到這個指令,
結語
clickoutside 不止抽屜的場景,只要你想在點擊某個元素區域之外做些事情,都可以考慮它,
除了這個,還有很多優秀的第三方指令,例如 element-ui 中的 v-loading 可以實作區域的加載影片,常用的 vue-lazyload 中的 v-lazy 可以實作圖片的懶加載,
個人認為指令屬于那種用得少但很實用的東西,可能在開發功能時都沒有考慮到用指令來實作,如果你還不了解指令,趕快學起來,
感謝閱讀
歡迎關注公眾號【奔跑的前端er】,專注于分享前端技術文章,和大家一起進步,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/167671.html
標籤:JavaScript
