1. 程式人生 > >翻了翻element-ui原始碼,發現一個很實用的指令clickoutside

翻了翻element-ui原始碼,發現一個很實用的指令clickoutside

## 前言 指令(`directive`)在 `vue` 開發中是一項很實用的功能,指令可以繫結到某一元素或元件,使功能的顆粒度更精細。今天在翻 `element-ui` 的原始碼時,發現一個還挺實用的工具指令,跟大夥分享一下。 ## clickoutside 的使用及效果 該指令的原始碼在 `src/utils` 下的 `clickoutside.js`。它功能是指令需要接收一個函式,當用戶滑鼠點選的區域在繫結指令的元素之外時,會觸發該函式。 那麼使用這個指令能夠實現什麼功能呢?我想到一個功能,就像我們常用的抽屜元件,在點選抽屜之外的區域時,抽屜就會消失(但 `elementui` 中不是用這種方式,而是用一個遮罩層實現)。 接下來我們來看看怎麼玩這個指令,很簡單,只需要引入這個檔案註冊指令就好了。 ```js // main.js import Vue from 'vue' import clickoutside from 'element-ui/src/utils/clickoutside' Vue.directive('clickoutside', clickoutside) ``` 使用: ```html ``` ```js export default { data() { return { show: true } }, methods: { handler() { this.show = false } } } ``` 效果: ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/797ddf100c6141ed9992bc9d3272a48b~tplv-k3u1fbpfcp-zoom-1.image) ## 原始碼分析 `clickoutside` 看起來還挺不錯,下面看看它是如何實現的。首先是它的指令鉤子定義: ```js 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` 滑鼠事件進行監聽: ```js 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` 創建出來的: ```js 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` 下,導致在點選完選擇項後被判斷為區域外點選。 ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/372f6fdc6dca43bcac73ad647dc497c6~tplv-k3u1fbpfcp-zoom-1.image) 其實這也符合邏輯,因為點選的地方也確實在區域外,只是在這種場景下看起來像是“bug”一樣。然後我發現原始碼裡提供了一個選項解決這種問題。可以在使用指令的元件 `data` 裡定義 `popperElm` 屬性,它的值是一個 `dom`。 ```js export default { mounted() { this.popperElm = document.querySelector('.el-select-dropdown.el-popper') } } ``` 在原始碼裡會通過 `popperElm` 進行判斷: ```js 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】,專注於分享前端技術文章,和大家一起