directive是一個容易被人遺忘的vue屬性,由於vue的開發思想不推崇直接對dom進行操做,可是適當的dom操做有利於提高工程的健壯性。html
關於指令的具體講解請看官方文檔vue
https://cn.vuejs.org/v2/guide/custom-directive.html算法
其中bind函數使用較爲頻繁,如下使用幾個示例進行講解數組
代碼:緩存
Vue.directive('selectTextOnFocus', {
bind: function(el, binding) {
function bindDom(el) {
if (el.tagName !== 'INPUT') {
[...el.children].forEach(dom => {
return bindDom(dom)
})
} else {
el.onfocus = function() {
setTimeout(() => {
el.select()
}, 30)
}
return true
}
}
bindDom(el)
}
})
複製代碼
大體的思路就是從父元素遞歸的查找input子元素(對於組件也可使用),若是找到input子元素,那麼就綁定focus事件,而且在input focus時將元素select。閉包
對於數字輸入框聚焦後按下鍵盤方向鍵或者滾動鼠標滾輪,數字會自動遞增或者遞減,這一功能可能會在用戶不經意的狀況下改變輸入的值,致使提交錯誤的數據,可使用以下代碼解決這個問題。app
Vue.directive('removeMouseWheelEvent', {
bind: function(el, binding) {
el.onmousewheel = function(e) {
el.blur()
}
el.onkeydown = function(e) {
if ([38, 40, 37, 39].includes(e.keyCode)) {
e.preventDefault()
}
}
}
})
複製代碼
對於以上兩個指令,使用方式以下dom
<input type="number" v-selectTextOnFocus v-removeMouseWheelEvent>
複製代碼
很方便的就給輸入框添加了這兩個功能異步
element ui的表格組件不提供一個滾動加載的功能,可是既想使用element ui的table組件又想得到滾動加載的功能,那麼就須要指令來完成這一功能,先看看指令的寫法。ide
Vue.directive('scrollLoad', {
bind: function(el, binding) {
let lastPotion = 0
const scrollWrap = el.querySelector('.el-table__body-wrapper')
scrollWrap.onscroll = function() {
const distanceRelativeToBottom = this.scrollHeight - this.scrollTop - this.clientHeight
const direction = getDirection(lastPotion, this.scrollTop)
lastPotion = this.scrollTop
binding.value({
direction,
scrollTop: this.scrollTop,
distanceRelativeToBottom
})
}
function getDirection(last, now) {
return now - last > 0 ? 'down' : 'up'
}
}
})
複製代碼
首先找到 .el-table__body-wrapper 這一元素,這是element ui 表格的容器(除去表頭),其次給它添加onscroll事件,在滾動時進行位置的計算,而且將計算獲得的方向以及位置信息傳遞給傳入的回調函數,由回調函數來判斷是否應該進行數據請求。
<el-table v-scrollLoad="scrollLoad">
複製代碼
binding.value({
direction,
scrollTop: this.scrollTop,
distanceRelativeToBottom
})
複製代碼
一個表單的優化體驗功能,在一兩個月前對於這個功能,大體是這樣實現的
<tr v-for="(goods, index) in tableData">
<td class="t3">
<input :ref="getRef(index, 1)" :data-ref="getRef(index, 1)" @focus="inputFocus($event)">
</td>
<td class="t4">
<input :ref="getRef(index, 2)" :data-ref="getRef(index, 2)" @focus="inputFocus($event)">
</td>
<td class="t5">
<input :ref="getRef(index, 3)" :data-ref="getRef(index, 3)" @focus="inputFocus($event)">
</td>
</tr>
複製代碼
inputFocus(e) {
this.nowInputAt = e.target.getAttribute('data-ref')
},
getRef(row, column) {
return row + ':' + column
},
keydown(event) {
let [row, column] = this.nowInputAt.split(':').map(value => parseInt(value))
let pos = {
row,
column
}
if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
event.preventDefault()
}
function up() {
if (pos.row === 0) return
pos.row -= 1
}
function down() {
if (pos.row === this.tableData.length - 1) return
let a = pos.row + 1
pos.row = a
}
function left() {
if (pos.row === 0 && pos.column === 1) return
if (pos.column === 1) {
pos.row -= 1
pos.column = 3
} else {
pos.column -= 1
}
}
function right() {
if (pos.row === this.tableData.length - 1 && pos.column === 3) return
if (pos.column === 3) {
pos.row += 1
pos.column = 1
} else {
pos.column += 1
}
}
switch (event.keyCode) {
case 38: up.call(this); break
case 40: down.call(this); break
case 37: left.call(this); break
case 39: right.call(this); break
case 13: right.call(this); break
}
this.$nextTick(() => {
this.nowInputAt = pos.row + ':' + pos.column
this.$refs[pos.row + ':' + pos.column][0].focus()
})
},
複製代碼
大體的作法就是給每個input設置一個座標信息,當輸入框聚焦時存儲當前的座標,當方向鍵按下時利用存儲的座標信息進行計算獲得下一個輸入框同時進行聚焦。計算座標的算法有點相似於推箱子游戲。
現在一樣的需求出如今了另外一個表單,若是複製一份代碼,就很不優雅,因而決定使用指令來完成這一需求,先看看實現,接下來拆分代碼進行講解。
import _ from 'lodash'
export default function() {
let gridSquare = []
let pos = {
column: 0,
row: 0
}
let parentEl = null
let keyUpDebounceFn = null
function findRow(element) {
if (!element) return
if (element.dataset && 'sokobanrow' in element.dataset) {
const row = []
const findCol = function(htmlEl) {
if (!htmlEl) return
if (htmlEl.dataset && 'sokobancol' in htmlEl.dataset) {
row.push(htmlEl)
} else {
[...htmlEl.childNodes].forEach(dom => {
findCol(dom)
})
}
}
findCol(element)
gridSquare.push(row)
} else {
[...element.childNodes].forEach(dom => {
findRow(dom)
})
}
}
function registerGrid() {
findRow(parentEl)
}
function bindEvent() {
bindFocusEvent()
bindKeyDownEvent()
}
function bindFocusEvent() {
gridSquare.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
cell.addEventListener('focus', function() {
pos = {
column: cellIndex,
row: rowIndex
}
})
})
})
}
function bindKeyDownEvent() {
const keyEvent = function(event) {
// 上 38
// 下 40
// 左 37
// 右 39
// 回車 13
if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
event.preventDefault()
}
function up() {
if (pos.row === 0) return
pos.row -= 1
}
function down() {
if (pos.row === gridSquare.length - 1) return
pos.row += 1
}
function left() {
if (pos.row === 0 && pos.column === 0) return
if (pos.column === 0) {
pos.row -= 1
pos.column = gridSquare[pos.row].length - 1
} else {
pos.column -= 1
}
}
function right() {
if (pos.row === gridSquare.length - 1 && pos.column === gridSquare[gridSquare.length - 1].length - 1) return
if (pos.column === gridSquare[pos.row].length - 1) {
pos.row += 1
pos.column = 1
} else {
pos.column += 1
}
}
switch (event.keyCode) {
case 38: up(); break
case 40: down(); break
case 37: left(); break
case 39: right(); break
case 13: right(); break
}
try {
gridSquare[pos.row][pos.column].focus()
} catch (e) {}
}
keyUpDebounceFn = _.debounce(keyEvent, 100)
window.addEventListener('keyup', keyUpDebounceFn)
}
return {
bind(el, binding) {
parentEl = el
},
unbind() {
gridSquare = null
pos = null
parentEl = null
window.removeEventListener('keyup', keyUpDebounceFn)
keyUpDebounceFn = null
}
init() {
gridSquare = []
pos = {
column: 0,
row: 0
}
registerGrid()
bindEvent()
}
}
}
複製代碼
首先定義一個閉包函數用於緩存dom節點,以及當前聚焦的位置信息等相關信息。其次閉包函數返回vue指令須要的對象,同時在此對象中,包含了自定義的init函數。這個函數的做用在於,由於對於動態渲染的dom節點,bind函數是沒法獲取到最新的dom節點,那麼就須要暴露出init函數,用於延時綁定。其實指令也提供了 update componentUpdated 函數用於檢測dom的改變,可是若是dom節點有一些例如v-if v-show 或者style的改變,都會觸發這兩個事件,因此這裏暫不使用這兩個事件進行初始化,會下降性能,同時提供 unbind 鉤子以供元素銷燬時釋放閉包內的變量,代碼以下:
import _ from 'lodash'
export default function() {
const gridSquare = []
let pos = {
column: 0,
row: 0
}
let parentEl = null
let keyUpDebounceFn = null
function registerGrid() {}
function bindEvent() {}
return {
bind(el, binding) {
parentEl = el
},
unbind() {
gridSquare = null
pos = null
parentEl = null
window.removeEventListener('keyup', keyUpDebounceFn)
keyUpDebounceFn = null
}
init() {
registerGrid()
bindEvent()
}
}
}
複製代碼
其次初始化時,進行input輸入框的二維座標模型的創建,具體作法是,首先給每一行定義一個自定義屬性 data-sokobanrow 以及每一列定義自定義屬性 data-sokobancol,其次深度優先遞歸查找相關dom,若是是行元素,那麼就新建一個數組(X軸),若是是列元素(Y軸),那麼就將此dom push到X軸數組中,最後將X軸數組push到網格數組中,最終獲得一個內部存放input DOM的二維數組。
<table v-sokoban>
<tr data-sokobanrow v-for="(goods, index) in tableData">
<td>
<input data-sokobancol>
</td>
<td>
<input data-sokobancol>
</td>
<td>
<input data-sokobancol>
</td>
</tr>
</table>
複製代碼
const gridSquare = []
function findRow(element) {
if (!element) return
if (element.dataset && 'sokobanrow' in element.dataset) {
const row = []
const findCol = function(htmlEl) {
if (htmlEl.dataset && 'sokobancol' in htmlEl.dataset) {
row.push(htmlEl)
} else {
[...htmlEl.childNodes].forEach(dom => {
findCol(dom)
})
}
}
findCol(element)
gridSquare.push(row)
} else {
[...element.childNodes].forEach(dom => {
findRow(dom)
})
}
}
function registerGrid() {
findRow(parentEl)
}
複製代碼
而後再進行相關的事件綁定,在input focus時存儲當前座標信息,在keyup時計算相關座標獲得下一input座標而且使其focus,代碼以下:
function bindEvent() {
bindFocusEvent()
bindKeyDownEvent()
}
function bindFocusEvent() {
gridSquare.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
cell.addEventListener('focus', function() {
pos = {
column: cellIndex,
row: rowIndex
}
})
})
})
}
function bindKeyDownEvent() {
const keyEvent = function(event) {
// 上 38
// 下 40
// 左 37
// 右 39
// 回車 13
if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
event.preventDefault()
}
function up() {
if (pos.row === 0) return
pos.row -= 1
}
function down() {
if (pos.row === gridSquare.length - 1) return
pos.row += 1
}
function left() {
if (pos.row === 0 && pos.column === 0) return
if (pos.column === 0) {
pos.row -= 1
pos.column = gridSquare[pos.row].length - 1
} else {
pos.column -= 1
}
}
function right() {
if (pos.row === gridSquare.length - 1 && pos.column === gridSquare[gridSquare.length - 1].length - 1) return
if (pos.column === gridSquare[pos.row].length - 1) {
pos.row += 1
pos.column = 1
} else {
pos.column += 1
}
}
switch (event.keyCode) {
case 38: up(); break
case 40: down(); break
case 37: left(); break
case 39: right(); break
case 13: right(); break
}
try {
gridSquare[pos.row][pos.column].focus()
} catch (e) {}
}
keyUpDebounceFn = _.debounce(keyEvent, 100)
window.addEventListener('keyup', keyUpDebounceFn)
}
複製代碼
最終用法以下:
<table v-sokoban>
<tr data-sokobanrow v-for="(goods, index) in tableData">
<td>
<input data-sokobancol>
</td>
<td>
<input data-sokobancol>
</td>
<td>
<input data-sokobancol>
</td>
</tr>
</table>
複製代碼
import sokobanDirectiveGenerator from '@/directives/sokoban'
const sokoban = sokobanDirectiveGenerator()
export default {
directives: {
sokoban
},
methods: {
getServerData() {
setTimeout(() => { // 一個異步請求
this.$nextTick(() => {
sokoban.init() // 頁面渲染後進行初始化
})
}, 1000)
}
}
}
複製代碼
其實能夠發現,這幾個指令基本上都是爲了優化體驗而編寫,而這樣的功能在一個系統中確定是大量存在的,因此使用指令,能夠極大的節省代碼,從而提高工程的健壯性。