vue中有很多豫定义的绑定指令,v-modle、v-onlick和v-bind等。如果咱需要自己的指令,该如何实现?这个问题就是当下要研究的方向 ~
当然研究的起始方向,依然是从组件开源库mint-ui
的开源代码开始 ~
解读mint-ui源码文件
局部directive
/** packages/field/src/field.vue */
<template>
<!-- 组件x-cell中使用自定义指令:v-clickoutside。当点击该组件外侧,则方法doCloseActive被激活并调用 -->
<x-cell
class="mint-field"
:title="label"
v-clickoutside="doCloseActive" <!-- 这里是v-clickoutside="doCloseAtive" 说明指令钩子函数bind中绑定的是click事件-->
:class="[{
'is-textarea': type === 'textarea',
'is-nolabel': !label
}]">
... ... ...
</x-cell>
</template>
<script>
import XCell from 'mint-ui/packages/cell/index.js';
import Clickoutside from 'mint-ui/src/utils/clickoutside'; /** 引入自定义指令的js文件,触发该指令之后的业务逻辑 */
if (process.env.NODE_ENV === 'component') {
require('mint-ui/packages/cell/style.css');
}
/**
* mt-field
* @desc 编辑器,依赖 cell
* @module components/field
*
* @param {string} [type=text] - field 类型,接受 text, textarea 等
* @param {string} [label] - 标签
* @param {string} [rows] - textarea 的 rows
* @param {string} [placeholder] - placeholder
* @param {string} [disabled] - disabled
* @param {string} [readonly] - readonly
* @param {string} [state] - 表单校验状态样式,接受 error, warning, success
*
* @example
* <mt-field v-model="value" label="用户名"></mt-field>
* <mt-field v-model="value" label="密码" placeholder="请输入密码"></mt-field>
* <mt-field v-model="value" label="自我介绍" placeholder="自我介绍" type="textarea" rows="4"></mt-field>
* <mt-field v-model="value" label="邮箱" placeholder="成功状态" state="success"></mt-field>
*/
export default {
name: 'mt-field',
data() {
return {
active: false,
currentValue: this.value
};
},
methods: {
// 自定义指令v-clickoutside,被触发所调用的方法
doCloseActive() {
this.active = false;
},
.... .... ...
},
directives: { /** 注意:这里通过该字段,进行局部的自定义指令的配置 */
Clickoutside
},
... ... ...
};
</script>
<style lang="css">
@import "../../../src/style/var.css";
... ... ...
</style>
上面vue源码中通过import引入clickoutside.js
源码文件
/** packages/src/utils/clickoutside.js */
/**
* v-clickoutside
* @desc 点击元素外面才会触发的事件
* @example
* ```vue
* <div v-element-clickoutside="handleClose">
* ```
*/
const clickoutsideContext = '@@clickoutsideContext'; // 定义一个字符串作为 'key'
export default {
bind(el, binding, vnode) { // 指令钩子函数:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
const documentHandler = function(e) {
if (vnode.context && !el.contains(e.target)) { // 判断用户点击的位置,在使用自定义指令的组件的外侧,则继续向下执行
vnode.context[el[clickoutsideContext].methodName]();
}
};
el[clickoutsideContext] = { // el是对象类型,可直接操作dom。这行代码意思是为该对象添加一些属性。且添加属性的目的是能够在全局使用属性,起到全局变量的作用。
documentHandler,
methodName: binding.expression,
arg: binding.arg || 'click'
};
document.addEventListener(el[clickoutsideContext].arg, documentHandler);// 为当前使用自定义指令的组件所在dom,添加点击的监听
},
// 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
update(el, binding) {
el[clickoutsideContext].methodName = binding.expression; // 指令钩子函数:更新指令对应的方法名,比如上面的'doCloseActive'
},
unbind(el) { // 指令钩子函数:移除监听
document.removeEventListener(
el[clickoutsideContext].arg,
el[clickoutsideContext].documentHandler);
},
/**
*该install(vue)方法,目的是可以进行全局配置。只要在main.js中进行Vue.use()配置即可。
*如果要在某个vue页面通过components:{}配置,则是局部的配置。
*/
install(Vue) {
Vue.directive('clickoutside', {
bind: this.bind,
unbind: this.unbind
});
}
};
详尽的了解还是要看上面对源码进行的注释的,然后简要叙述下使用逻辑。
通过编码clickoutside.js文件来自定义directive指令逻辑,如上第14行的bind()方法中第25行,对使用指令的组件所在的dom添加点击事件的监听。若监听的事件被激活(点击了页面某个地方,可以是组件可以是外侧),则会执行第16、17行 , 判断是否点击了组件外围,若是,则执行回调方法vnode.context[el[clickoutsideContext].methodName]();
。即对应上面vue源码中的doCloseActive()方法。
全局directive
全局配置的使用directive自定义指令的mint-ui开源组件源码文件
/** directive.js文件 scroll滑动,滑动加载更多的自定义指令 */
import Vue from 'vue';
const ctx = '@@InfiniteScroll';
var throttle = function(fn, delay) {
var now, lastExec, timer, context, args; //eslint-disable-line
var execute = function() {
fn.apply(context, args);
lastExec = now;
};
return function() {
context = this;
args = arguments;
now = Date.now();
if (timer) {
clearTimeout(timer);
timer = null;
}
if (lastExec) {
var diff = delay - (now - lastExec);
if (diff < 0) {
execute();
} else {
timer = setTimeout(() => {
execute();
}, diff);
}
} else {
execute();
}
};
};
var getScrollTop = function(element) {
if (element === window) {
return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop);
}
return element.scrollTop;
};
var getComputedStyle = Vue.prototype.$isServer ? {} : document.defaultView.getComputedStyle;
var getScrollEventTarget = function(element) {
var currentNode = element;
// bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
var overflowY = getComputedStyle(currentNode).overflowY;
if (overflowY === 'scroll' || overflowY === 'auto') {
return currentNode;
}
currentNode = currentNode.parentNode;
}
return window;
};
// 获取可见高度
var getVisibleHeight = function(element) {
if (element === window) {
return document.documentElement.clientHeight;
}
return element.clientHeight;
};
var getElementTop = function(element) {
if (element === window) {
return getScrollTop(window);
}
return element.getBoundingClientRect().top + getScrollTop(window);
};
// 判断el(dom节点)是否已挂载
var isAttached = function(element) {
var currentNode = element.parentNode;
while (currentNode) {
if (currentNode.tagName === 'HTML') {
return true;
}
if (currentNode.nodeType === 11) {
return false;
}
currentNode = currentNode.parentNode; // 一直循环获取当前node节点的父节点
}
return false;
};
/** 滚动监听注册,且滚动时有函数throttle回调,然后再调用doCheck方法,最后根据滚动条件会回调自定义指令中绑定的方法bind.value */
var doBind = function() {
if (this.binded) return; // eslint-disable-line
this.binded = true;
var directive = this; // this等于doBind.call(el[ctx], args);中的el[ctx]
var element = directive.el;// 被指令绑定的组件节点dom
directive.scrollEventTarget = getScrollEventTarget(element);
directive.scrollListener = throttle(doCheck.bind(directive), 200);
// scroll 滚动事件绑定,滚动时回调throttle()方法
directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener);
var disabledExpr = element.getAttribute('infinite-scroll-disabled');// 获取class为'infinite-scroll-disabled'的变量值
var disabled = false;
if (disabledExpr) {
this.vm.$watch(disabledExpr, function(value) {
directive.disabled = value;
if (!value && directive.immediateCheck) {
doCheck.call(directive);
}
});
disabled = Boolean(directive.vm[disabledExpr]);
}
directive.disabled = disabled;
var distanceExpr = element.getAttribute('infinite-scroll-distance');
var distance = 0;
if (distanceExpr) {
distance = Number(directive.vm[distanceExpr] || distanceExpr);
if (isNaN(distance)) {
distance = 0;
}
}
directive.distance = distance;
var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check');
var immediateCheck = true;
if (immediateCheckExpr) {
immediateCheck = Boolean(directive.vm[immediateCheckExpr]);
}
directive.immediateCheck = immediateCheck;
if (immediateCheck) {
doCheck.call(directive);
}
var eventName = element.getAttribute('infinite-scroll-listen-for-event');
if (eventName) {
directive.vm.$on(eventName, function() {// this.$on() 监听当前实例上的自定义事件`eventName`。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
doCheck.call(directive); // doCheck方法中上下文this该换作directive
});
}
};
/** scroll滚动时,会回调到方法doCheck这里 */
var doCheck = function(force) {
var scrollEventTarget = this.scrollEventTarget;
var element = this.el;
var distance = this.distance;
if (force !== true && this.disabled) return; //eslint-disable-line
var viewportScrollTop = getScrollTop(scrollEventTarget);
var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget);
var shouldTrigger = false;
if (scrollEventTarget === element) {
shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance;
} else {
var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;
shouldTrigger = viewportBottom + distance >= elementBottom;
}
if (shouldTrigger && this.expression) {
this.expression(); // 即binding.value,当滚动到的条件符合上面if时,执行自定义指令的回调方法 this.expression()
}
};
export default {
bind(el, binding, vnode) { // 指令钩子函数:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
el[ctx] = { // 属性绑定
el,
vm: vnode.context,
expression: binding.value
};
const args = arguments;
var cb = function() {
el[ctx].vm.$nextTick(function() {// 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
if (isAttached(el)) {// 判断el(dom节点)是否已挂载
doBind.call(el[ctx], args);
}
el[ctx].bindTryCount = 0;
var tryBind = function() { // 注意:每隔50s执行一次,循环判断dom节点el是否已经mount
if (el[ctx].bindTryCount > 10) return; //eslint-disable-line
el[ctx].bindTryCount++;
if (isAttached(el)) {
doBind.call(el[ctx], args);// 调用doBind方法,doBind方法上下文环境指定为el[ctx]
} else {
setTimeout(tryBind, 50);
}
};
tryBind();
});
};
if (el[ctx].vm._isMounted) {
cb();
return;
}
el[ctx].vm.$on('hook:mounted', cb); // 监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
},
unbind(el) {
if (el[ctx] && el[ctx].scrollEventTarget) {
el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
}
}
};
使其进行全局配置,还有另一个js文件 infinite-scroll.js
/** infinite-scroll.js */
import InfiniteScroll from './directive'; // 指令逻辑文件
import 'mint-ui/src/style/empty.css';
import Vue from 'vue';
const install = function(Vue) {// 第一步:install方法中进行Vue.directive全局自定义指令配置
Vue.directive('InfiniteScroll', InfiniteScroll);
};
/** 如果这里没有下面if判断行,作全局配置。
则需要在工程中的main.js中引入install文件 即:import install from '**directive.js',
然后再调用Vue.use(install);就能完成全局配置*/
if (!Vue.prototype.$isServer && window.Vue) {
window.infiniteScroll = InfiniteScroll;
/** 安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。 */
Vue.use(install); // eslint-disable-line // 第二步: Vue.use(install); 全局配置
}
InfiniteScroll.install = install;
export default InfiniteScroll; // 第三步: 对外可通过import引入该模块
我们看到的infinite-scroll.js文件,是对自定义指令的全局配置。全局配置我们可以舍弃nfinite-scroll.js这一步,直接在本地工程中的main.js中进行配置。当然利用上面的infinite-scroll.js文件,然后再在main.js中作配置会更合适。