(一)場(chǎng)景:
例如百度的搜索框煎谍,當(dāng)我們輸入搜索內(nèi)容的時(shí)候會(huì)彈出搜索建議提示窗,然后當(dāng)我們點(diǎn)擊其他區(qū)域的時(shí)候這個(gè)彈窗就會(huì)收起。上一張圖看一下:
(二)先要知道的基礎(chǔ)點(diǎn):
- vm.$isServer
當(dāng)前 Vue 實(shí)例是否運(yùn)行于服務(wù)器。
該屬性不是直接定義在實(shí)例對(duì)象 vm 上,而是定義在原型對(duì)象上 => Vue.prototype.$isServer
該屬性根據(jù)當(dāng)前運(yùn)行的環(huán)境以及 process.ven.VUE_ENV 自動(dòng)設(shè)置。
node.contains( otherNode )
如果 otherNode 是 node 的后代節(jié)點(diǎn)或是 node 節(jié)點(diǎn)本身.則返回true , 否則返回 false.vue的自定義指令 官方文檔
示例寫法:
import Vue from 'vue'
Vue.directive('demo', {
// 當(dāng)被綁定的元素插入到dom時(shí)……
inserted (el) {},
// 只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置
bind (el, binding, vnode) {},
// 所在組件的 VNode 更新時(shí)調(diào)用
update (el, binding, vnode) {},
// 只調(diào)用一次尊残,指令與元素解綁時(shí)調(diào)用
unbind (el, binding, vnode) {}
})
使用:
<div v-demo>
bind,update,unbind等等這些都是鉤子函數(shù),它們會(huì)被傳入一些參數(shù)(下面說明的都是要用到的犀农,了解更多請(qǐng)移步官方文檔):
- el:指令所綁定的元素孟害,可以用來直接操作 DOM
- binding
- name:指令名
- value:綁定的值
- expression:指令表達(dá)式
- vnode:虛擬節(jié)點(diǎn)
(三)實(shí)現(xiàn):
以下代碼參考自elementui的源碼,會(huì)有解讀
import Vue from 'vue'
const ctx = 'clickoutside'
const nodeList = []
const isServer = Vue.prototype.$isServer
let seed = 0
let startClick
// 用來綁定事件的方法,它是一個(gè)自執(zhí)行的函數(shù),會(huì)根據(jù)是否運(yùn)行于服務(wù)器和是否支持addEventListener來返回一個(gè)function
const on = (function () {
if (!isServer && document.addEventListener) {
return function (element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
} else {
return function (element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler)
}
}
}
})()
// 返回一個(gè)方法用來在點(diǎn)擊的時(shí)候觸發(fā)函數(shù)(觸發(fā)之前會(huì)判斷該元素是不是el,是不是focusElment以及他們的后代元素,如果是則不會(huì)執(zhí)行函數(shù))
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.focusElment &&
(vnode.context.focusElment.contains(mouseup.target) ||
vnode.context.focusElment.contains(mousedown.target)))
) {
return
}
if (binding.expression &&
el[ctx].methodName &&
vnode.context[el[ctx].methodName]) {
vnode.context[el[ctx].methodName]()
} else {
el[ctx].bindingFn && el[ctx].bindingFn()
}
}
}
if (!isServer) {
on(document, 'mousedown', e => (startClick = e))
on(document, 'mouseup', e => {
// 循環(huán)所有的綁定節(jié)點(diǎn)揽乱,把它們的documentHandler屬性所綁定的函數(shù)執(zhí)行一次(這個(gè)時(shí)候得到的剛好是上面的那個(gè)判斷執(zhí)行的函數(shù))
nodeList.forEach(node => node[ctx].documentHandler(e, startClick))
})
}
Vue.directive('clickoutside', {
// 當(dāng)被綁定的元素插入到dom時(shí)……
inserted (el) {
console.log(el)
},
// 只調(diào)用一次撒犀,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置
// 把綁定的元素扔到nodeList里面,并給綁定元素設(shè)置屬性
// documentHandler屬性在nodeList.forEach的時(shí)候執(zhí)行并得到一個(gè)function
// bindingFn 是綁定的那個(gè)值,用來執(zhí)行的
bind (el, binding, vnode) {
nodeList.push(el)
const id = seed++
el[ctx] = {
id,
documentHandler: createDocumentHandler(el, binding, vnode),
methodName: binding.expression,
bindingFn: binding.value
}
},
// 所在組件的 VNode 更新時(shí)調(diào)用
update (el, binding, vnode) {
el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
el[ctx].methodName = binding.expression
el[ctx].bindingFn = binding.value
},
// 只調(diào)用一次蚌斩,指令與元素解綁時(shí)調(diào)用
unbind (el, binding, vnode) {
const 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]
}
})
使用:
<template lang='pug'>
.test-wrap
input.input(
v-model='text'
v-clickoutside='handleClose'
@focus='visible = true'
placeholder='請(qǐng)?zhí)顚懰阉鲀?nèi)容'
)
ul.list-wrap(v-show='visible' ref='listWrap')
li(v-for='item in list' @click='setValue(item.text)') {{item.text}}
</template>
<script>
export default {
data () {
return {
text: '',
visible: false,
list: [{
text: '1111',
id: 1
}, {
text: 'aaaaa',
id: 2
}]
}
},
computed: {
focusElment () {
return this.$refs.listWrap
}
},
methods: {
handleClose () {
this.visible = false
},
setValue (val) {
this.text = val
}
}
}
</script>
調(diào)用解析:
- template里面設(shè)置v-clickoutside='handleClose'調(diào)用這個(gè)自定義指令
- list-wrap用visible來控制顯示
- 把list-wrap設(shè)置為focusElment,目的是在點(diǎn)擊這個(gè)區(qū)域的時(shí)候不去觸發(fā)bindingFn,源碼中(vnode.context.focusElment &&
(vnode.context.focusElment.contains(mouseup.target) ||
vnode.context.focusElment.contains(mousedown.target))這段代碼解決這個(gè)問題 - handleClose函數(shù)設(shè)置visible的值為false,以隱藏下拉窗口
這個(gè)實(shí)現(xiàn)就分享這些幸缕,歡迎拍磚~