手把手教你使用Vue指令,亲手实现一个新闻标记和运动动画指令

3,004 阅读13分钟

前言

vue中的指令我们并不陌生,如v-model 、 v-on 、v-show 等等。指令的意义就在于能够对于一些特定的操作进行提取和封装。提高代码的复用性和可维护性

我们都知道,vue一直都希望能够减少dom的操作,但是有的时候又不得不进行对dom的操作。我们可以直接在组件中进行dom的操作,的确这没有什么问题,包括我自己在内,之前也是这么写的。但是当我重新审视,重新了解了directive之后,才发现原来自定义指令这么好用。

通过本篇文章你将会学习到自定义指令的用法,通过文章中的新闻标记和运动指令的例子,让你能够对自定义的功能有一个更深刻的印象,轻松实现一个自己的指令并不再是难事。

基本用法

vue给指令提供了几个钩子函数,方便再适当的时候操作dom。

  1. bind: 第一次绑定到元素时调用
  2. inserted: 绑定元素插入父节点的时候会调用
  3. update: 所在组件的VNode更新的时候调用
  4. componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  5. unbind: 指令于元素解绑时调用

每个生命周期函数都介绍四个参数。

  1. el:指令所绑定的元素,可以直接操作Dom
  2. binding:指令的描述对象。包含以下属性
  • name: 指令名,不包括v- 前缀
  • value: 指令的绑定值。
  • 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 }
  1. vnode
  2. oldVnode

实际使用与思考

通过官网对用法得介绍,我们不难发现以下几个特点

  1. 每个阶段都可以进行dom的操作
  2. 数据的传递十分灵活,数据类型分为 值、参数和修饰对象
  3. update和componentUpdated函数可以比较前后值和前后Vnode的变化
  4. inserted 可以拿到dom的位置和大小参数
  5. 也可以获取到对应的vm,通过vnode.context(并不建议直接操作vm)

使用自定义指令之后,代码的开发量并没有增加多少,指令的hook函数更加明确了所绑定dom的生命周期,操作dom更加直观,且可以高度复用,动态指令参数也使它的使用变得更加灵活。把dom操作部分代码单独抽离出去,也使代码更加好维护。

bind/inserted 函数中的运用

制作一个类似新闻 已读/未读 的指令

新建一个directive文件夹存放自定义指令

directive/pick.js

这里用localStorage做了一个缓存机制,示例中8秒后数据过期。

// 本地存缓存
function setStorage(id, maxAge) {
  maxAge = new Date().getTime() + maxAge;
  localStorage.setItem('news_' + id, maxAge)
}
//  判断是否过期
function isOverdue(id) {
  const preTime = localStorage.getItem('news_' + id)
  if (new Date().getTime() > + preTime) {
    localStorage.removeItem('news_' + id)
    return true
  } else {
    return false
  }
}
function pick(el, binding) {
  // 拿到新闻id
  const id = binding.value.id;
  // 拿到对应的处理函数
  const handle = binding.value.handle;
  // 是否传递了color参数,点击后文本的颜色,如果有用传递的,没有就用默认的#999 
  const color = binding.arg ? "#" + binding.arg : "#999";
  // 缓存的最大时间,如果有用传递的,没有就用默认的10000,10秒 
  const maxTime = binding.value.maxTime ? + binding.value.maxTime : 10000;
  // 判断是否有缓存
  if (localStorage.getItem('news_' + id)) {
    // 判断缓存有没有过期
    if (!isOverdue(id)) {
      // 有缓存且没有过期颜色设置#999
      el.style.color = color
    }
  }
  // 给dom添加点击事件
  el.onclick = function () {
    // 执行操作函数
    handle(id);
    // 设置字体颜色
    el.style.color = color;
    // 本地存缓存,并设置过期时间
    setStorage(id, maxTime)
  }
}

export default {
  bind(el, binding) {
    // 初始化pick
    pick(el, binding)
  },
  // inserted上也可以,感觉和bind区别不大
  // inserted(el, binding){
  //   pick(el, binding)
  // },
}

在main.js中使用,注册全局指令

import pick from '@/directive/pick.js'
Vue.directive('pick', pick)

在组件中定义directives使用,给当前组件注册指令

import pick from '@/directive/pick.js'
export default {
// ... 
directives: {
    pick: pick,
  },
}

全局的directive与组件中的directives冲突已组件的优先

组件中使用

views/Directive

<template>
  <div class='container'>
    <!-- :f40:传入的文本颜色
    handle:点击时进行的操作
    id:新闻的id
    maxTime:缓存的最大时间 -->
    <div v-pick:f40="{handle:handleToPage,id:item.id,maxTime:2000}"
      v-for="item in data"
      :key="item.id"
      class="card">
      <h3 class="title">{{item.title}}</h3>
      <div class="content">{{item.content}}</div>
    </div>
  </div>
</template>

<script>
import pick from '@/directive/pick.js'
export default {
  data () {
    return {
      data:[{
        title:'这是文章标题1',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'1'
      },{
        title:'这是文章标题2',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'2'
      },{
        title:'这是文章标题3',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'3'
      },{
        title:'这是文章标题4',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'4'
      },{
        title:'这是文章标题5',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'5'
      },{
        title:'这是文章标题6',
        content:'这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容这是文章内容',
        id:'6'
      }]
    }
  },
  directives: {
    pick: pick,
  },
  methods: {
    handleToPage(id){
      this.$router.push({name:'News',query:{id}})
    }
  }
}
</script>

<style lang="scss" scoped>
.card{
  width400px;
  border1px solid #f40;
  margin-top20px;
  cursor: pointer;
}
</style>

写了个简单的页面展示新闻id信息

views/News

<template>
  <div class="container">{{msg}}</div>
</template>

<script>
export default {
  data() {
    return {
      msg"",
    };
  },
  created() {
    this.getNewsData(this.$route.query.id);
  },
  methods: {
    getNewsData(id) {
      if (!id) {
        alert("新闻id不存在,当前新闻id为:" + id);
        throw new Error("新闻id不存在,当前新闻id为:" + id);
      } else {
        this.msg = id;
      }
    },
  },
};
</script>

效果如下:

点击后标记为已读(字红色),每一个news8秒后刷新还原成未读状态

update/componentUpdated 函数中的运用

dom动画,当数据改变时div运动的指定位置

directive/animation.js

// 动画函数
const animationType = {
  skipTimeMath.floor(1000 / 60),
  // 获取dom上的属性
  getStyle(dom, attr) {
    if (window.getComputedStyle) {
      return window.getComputedStyle(dom, null)[attr];
    } else {
      return dom.currentStyle[attr];
    }
  },
  // 匀速直线运动
  uniform(dom, attr, target, speed) {
    clearInterval(dom.timer);
    let iSpeed = null, iCur = parseInt(this.getStyle(dom, attr));
    iSpeed = target - iCur > 0 ? speed : -speed;
    dom.timer = setInterval(() => {
      iCur = parseInt(this.getStyle(dom, attr));
      if (Math.abs(target - iCur) < Math.abs(iSpeed)) {
        clearInterval(dom.timer);
        dom.style[attr] = target + 'px';
      } else {
        dom.style[attr] = iCur + iSpeed + 'px';
      }
    }, this.skipTime )
  },
  // 弹性直线运动
  elastic(dom, attr,target) {
    clearInterval(dom.timer);
    let iSpeed = 0,
      // 运动中的速度和加速度实际上都是用dom和target的距离来决定的,在缓冲运动中iSpeed随着距目标点的位移矢量来决定iSpeed的方向为大小
      // 这里不用iSpeed直接控制,弹性运动中为了实现在目标线左右来回运动用a做变化的矢量来控制iSpeed的方向和大小
      a = 1,
      u = 0.8;
      dom.timer = setInterval() => {
      a = (target - parseInt(this.getStyle(dom, attr))) / 5;
      iSpeed += a;
      iSpeed *= u;
      if (Math.abs(iSpeed) < 1 && Math.abs(target -parseInt(this.getStyle(dom, attr))) < 1) {
        // 经测如果if里填dom.style.left == target + 'px'也是可以进入if停止定时器的,不会永远不到target
        // 到一定程度系统直接取到target值
        dom.style[attr] = target + 'px';
        clearInterval(dom.timer);
      } else {
        dom.style[attr] = parseInt(this.getStyle(dom, attr)) + iSpeed + 'px';
      }
    }, this.skipTime + 4)
  }
}
// 初始化运动函数
function animation(el, binding, positon = '0',direction='x', mode = 'uniform') {
  el.style.position = 'absolute';
  // 选择方向
  switch (direction) {
    case 'x':
      direction = 'left';
      break;
    case 'y':
      direction = 'top';
      break;
    default:
      throw new Error('direction 的只能传x或y')
  }
  // 选择运动函数
  switch (mode) {
    case 'uniform':
      animationType.uniform(el, direction, positon, 8)
      break;
    case 'elastic':
      animationType.elastic(el,direction,positon)
      break;
    default:
      throw new Error('没有注册运动函数')
  }
}

export default {
  update(el, binding) {
    // 位移量
    const position = binding.value;
    // 方向
    const direction = binding.arg;
    // 运动模式
    if(Object.keys(binding.modifiers).length > 1) {
      throw new Error('运动模式只能有一个')
    }
    const mode = Object.keys(binding.modifiers)[0] ? Object.keys(binding.modifiers)[0] : 'uniform';
    animation(el, binding, position,direction,mode)
  },
  // componentUpdated 和 update 差不太多
  // componentUpdated(el, binding){
  //   const position = binding.value;
  //   animation(el, binding, position,binding.arg)
  // },
}

在全局使用,注册全局指令

main.js

import animation from '@/directive/animation.js'
Vue.directive('animation', animation)

在组件中定义directives使用,给当前组件注册指令

import animation from '@/directive/animation.js'
export default {
// ... 
directives: {
    animation: animation,
  },
}

在指定组件中使用

views/animation

<template>
  <div class='container'>
    <input  type="text" v-model.lazy="positionX">
    <br/>
    <br/>
    <div class="boxWrapper">
      <!-- elastic:运动模式
      x:运动方向
      positionX:位移值 -->
      <div v-animation:x.elastic="positionX" class="box">{{positionX}}</div>
    </div>
  </div>
</template>

<script>
import animation from '@/directive/animation.js'
export default {
  data () {
    return {
      positionX0
    }
  },
  directives: {
    animation: animation,
  },
}
</script>

<style lang="scss" scoped>
.boxWrapper{
  position: relative;
}
.box{
  width100px;
  height100px;
  background-color: red;
  text-align: center;
  line-height100px;
}
</style>

效果如下:

elastic模式 uniform(默认)模式

以上就是对Vue directive 自定义指令的总结和实现。如果有什么不足的地方欢迎小伙伴们在评论区里补充