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)
}
}
}
复制代码
其实能够发现,这几个指令基本上都是为了优化体验而编写,而这样的功能在一个系统中确定是大量存在的,因此使用指令,能够极大的节省代码,从而提高工程的健壮性。