自定义指令种类

  1. 注册全局指令
    Vue.directive('my-directive', function (el, binding) {
      el.innerHTML = binding.value.toUpperCase();
    });
    
  2. 注册局部指令 (很少使用)
    export default {
      data() {
        return {};
      },
      directives: {
        'my-directive': {
          bind(el, binding) {
            el.innerHTML = binding.value.toupperCase();
          },
        },
      },
    }
    

钩子函数

  1. 一个指令定义对象可以提供如下几个钩子函数 (均为可选)

    1. bind:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性的初始化设置;
    2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
    3. update:所在组件的 VNode 更新时调用;
    4. componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用;
    5. unbind:只调用一次,指令与元素解绑时调用;
  2. 钩子函数参数:

    1. el:指令所绑定的元素,可以用来直接操作 DOM
    2. binding:一个对象,包含以下 property
      • name:指令名,不包括 v- 前缀;
      • value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2
      • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用,无论值是否改变都可用;
      • expression:字符串形式的指令表达式,例如 v-my-directive=“1 + 1” 中,表达式为 “1 + 1”
      • arg:传给指令的参数,可选,例如 v-my-directive:foo 中,参数为 “foo”
      • modifiers:一个包含修饰符的对象,例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
    3. vnodeVue 编译生成的虚拟节点;
    4. oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用;

案例

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 懒加载

JavaScript
HTML
JavaScript
// 服务端代码
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);
        })
    }
}

面试题

写过自定义指令吗?使用场景有哪些?

  1. Vue 有一组默认指令,比如 v-modelv-for,同时 Vue 也允许用户注册自定义指令来扩展 Vue 能力,自定义指令主要完成一些可复用低层级 DOM 操作;

  2. 使用自定义指令分为 「定义」「注册」「使用」 三步:

    1. 定义自定义指令有两种方式:
      • 对象形式,类似组件定义,有各种生命周期;
      • 函数形式,只会在 mountedupdated 时执行;
    2. 注册自定义指令类似组件,可以使用 app.directive() 全局注册,使用 { directives: { xxx } } 局部注册;
    3. 使用时在注册名称前加上 v- 即可,比如 v-focus
  3. 在项目中常用到一些自定义指令,例如:

    1. 复制粘贴: v-copy
    2. 防抖: v-debounce
    3. 图片懒加载: v-lazy
    4. 按钮权限: v-premission
    5. 页面水印: v-waterMarker
    6. 拖拽指令: v-draggable
  4. vue3 中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错,另外在 v3.2 之后,可以在 setup 中以一个小写 v 开头方便的定义自定义指令,更简单了!

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 自定义指令种类
  2. 2. 钩子函数
  3. 3. 案例
    1. 3.1. clickOutSide 指令
    2. 3.2. v-load 懒加载
  4. 4. 面试题
    1. 4.1. 写过自定义指令吗?使用场景有哪些?