自定义指令种类
- 注册全局指令
Vue.directive('my-directive', function (el, binding) { el.innerHTML = binding.value.toUpperCase(); });
- 注册局部指令 (很少使用)
export default { data() { return {}; }, directives: { 'my-directive': { bind(el, binding) { el.innerHTML = binding.value.toupperCase(); }, }, }, }
钩子函数
-
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性的初始化设置;
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中);
- update:所在组件的 VNode 更新时调用;
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用;
- unbind:只调用一次,指令与元素解绑时调用;
-
钩子函数参数:
- el:指令所绑定的元素,可以用来直接操作 DOM;
- binding:一个对象,包含以下 property:
- name:指令名,不包括 v- 前缀;
- value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2;
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用,无论值是否改变都可用;
- expression:字符串形式的指令表达式,例如 v-my-directive=“1 + 1” 中,表达式为 “1 + 1”;
- arg:传给指令的参数,可选,例如 v-my-directive:foo 中,参数为 “foo”;
- modifiers:一个包含修饰符的对象,例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true };
- vnode:Vue 编译生成的虚拟节点;
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用;
案例
clickOutSide 指令
<div id="app">
<div v-click-outside="blur">
<input type="text" @focus="focus">
<div v-show="visible">面板</div>
</div>
</div>
<script>
Vue.directive('clickOutside', {
bind(el, bindings, vnode) {
el.handler = function (e) {
if (!el.contains(e.target)) {
let method = bindings.expression;
// vnode.context 为当前 vue 实例
vnode.context[method]();
}
}
document.addEventListener('click', el.handler)
},
unbind(el) {
document.removeEventListener('click', el.handler)
}
});
// 指令的目的就是 dom 操作
let vm = new Vue({
el: '#app',
data() {
return { visible: false, my: 'ok' }
},
methods: {
focus() {
this.visible = true;
},
blur() {
this.visible = false
}
}
});
vm.my = 'no ok';
</script>
v-load 懒加载
// 服务端代码
const express = require('express');
const app = express();
app.use(express.static(__dirname))
app.listen(4500);
const arr = [];
for (let i = 10; i <= 20; i++) {
arr.push(`http://localhost:4500/images/${i}.jpeg`)
}
app.get('/api/img', (req, res) => { res.json(arr) })
<!-- 插件使用 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="node_modules/vue/dist/vue.js"></script>
<script src="./vue-lazyload.js"></script>
<script src="node_modules/axios/dist/axios.js"></script>
<div id="app">
<div class="box">
<li v-for="img in imgs" :key="img">
<img v-lazy="img" alt="">
</li>
</div>
</div>
<script>
const loading = 'http://localhost:4500/images/1.gif'
Vue.use(VueLazyload, {
preLoad: 1.3, // 预渲染1.3屏,默认窗口的 1.3倍
loading
});
let vm = new Vue({
el: '#app',
data: {
imgs: []
},
created() {
axios.get('http://localhost:4500/api/img').then(res => {
this.imgs = res.data;
})
}
});
</script>
<style>
.box {
width: 600px;
height: 600px;
overflow: scroll;
}
img {
width: 300px;
height: 300px;
}
</style>
</body>
</html>
// 获取带有滚动的盒子
const getScrollParent = (el) => {
let parent = el.parentNode;
while (parent) {
if (/(?:scroll)|(?:auto)/.test(getComputedStyle(parent)['overflow'])) {
return parent;
}
parent = parent.parentNode;
}
return parent;
}
const loadImageAsync = (src, resolve, reject) => {
let image = new Image();
image.src = src;
image.onload = resolve;
image.onerror = reject;
}
const throttle = (cb, delay) => {
let prev = Date.now();
return () => {
let now = Date.now();
if (now - prev >= delay) {
cb();
prev = Date.now();
}
}
}
const Lazy = (Vue) => {
class ReactiveListener { // 每一个图片元素 都构造成一个类的实例
constructor({ el, src, options, elRender }) {
this.el = el;
this.src = src;
this.options = options;
this.elRender = elRender
this.state = { loading: false } // 没有加载过
}
checkInView() { // 检测这个图片是否在可视区域内
let { top } = this.el.getBoundingClientRect();
return top < window.innerHeight * (this.options.preLoad || 1.3);
}
load() { // 用来加载这个图片
// 先加载loading
// 如果加载的ok的话 显示正常图片
this.elRender(this, 'loading');
// 懒加载的核心 就是 new Image
loadImageAsync(this.src, () => {
this.state.loading = true;
this.elRender(this, 'finish');
}, () => {
this.elRender(this, 'error');
});
}
}
return class LazyClass {
constructor(options) {
// 保存用户传入的属性
this.options = options;
this.bindHandler = false; // 绑定标识
this.listenerQueue = [];
}
handleLazyLoad() {
// 这里应该看一下,是否应该显示这个图片
// 计算当前图片的位置
this.listenerQueue.forEach(listener => {
if (!listener.state.loading) {
let catIn = listener.checkInView();
catIn && listener.load();
}
})
}
add(el, bindings, vnode) {
// 找到父亲元素
Vue.nextTick(() => {
// 带有滚动的盒子 infiniteScroll
let scrollParent = getScrollParent(el);
if (scrollParent && !this.bindHandler) {
this.bindHandler = true;
// 节流 通过节流来进行优化
this.lazyHandler = throttle(this.handleLazyLoad.bind(this), 100);
scrollParent.addEventListener('scroll', this.lazyHandler.bind(this));
}
// 需要判断当前这个元素是否在容器可视区域中,如果不是就不用渲染
const listener = new ReactiveListener({
el,
src: bindings.value,
options: this.options,
elRender: this.elRender.bind(this)
})
// 所有的人都创建一个实例,放到数组中
this.listenerQueue.push(listener);
this.handleLazyLoad()
})
}
elRender(listener, state) { // 渲染方法
let el = listener.el;
let src = ''
console.log(state);
switch (state) {
case 'loading':
src = listener.options.loading || ''
break;
case 'error':
src = listener.options.error || '';
break;
default:
src = listener.src;
break;
}
el.setAttribute('src', src)
}
}
}
const VueLazyload = {
install(Vue, options) {
// 把所有逻辑进行封装 类,把类在封装到函数中
const LazyClass = Lazy(Vue);
const lazy = new LazyClass(options);
Vue.directive('lazy', {
bind: lazy.add.bind(lazy);
})
}
}
面试题
写过自定义指令吗?使用场景有哪些?
-
Vue 有一组默认指令,比如 v-model 或 v-for,同时 Vue 也允许用户注册自定义指令来扩展 Vue 能力,自定义指令主要完成一些可复用低层级 DOM 操作;
-
使用自定义指令分为 「定义」、「注册」 和 「使用」 三步:
- 定义自定义指令有两种方式:
- 对象形式,类似组件定义,有各种生命周期;
- 函数形式,只会在 mounted 和 updated 时执行;
- 注册自定义指令类似组件,可以使用 app.directive() 全局注册,使用 { directives: { xxx } } 局部注册;
- 使用时在注册名称前加上 v- 即可,比如 v-focus;
- 定义自定义指令有两种方式:
-
在项目中常用到一些自定义指令,例如:
- 复制粘贴: v-copy;
- 防抖: v-debounce;
- 图片懒加载: v-lazy;
- 按钮权限: v-premission;
- 页面水印: v-waterMarker;
- 拖拽指令: v-draggable;
- …
-
vue3 中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错,另外在 v3.2 之后,可以在 setup 中以一个小写 v 开头方便的定义自定义指令,更简单了!
vue2.x✍️ 内置指令
上一篇