请勿直接爬走,本文地址:https://www.cnblogs.com/xiaoxuStudy/p/13208406.html
目录:
1. 怎么创建自定义指令
2. 什么时候用自定义指令
3. 钩子函数
4. 实操
通过 Vue.directive 全局创建指令,Vue.directive 的第一个参数定义了指令的名称,如下代码创建了一个名为 resize 的指令。
Vue.directive("resize", { });
在全局注册这个指令之后,意味着可以任意组件中使用这个指令,可以直接在单文件组件的模板中直接使用指令,也可以在 JSX 中使用指令。按照约定,指令名字有 “v-” 前缀,前缀用于标明这是一个前缀。
关于什么时候用自定义指令,其逻辑与使用事件修饰符的逻辑是一样的。
使用事件修饰符很大程度上是为了让我们的代码显得是数据驱动并且易于测试的,将 DOM 的逻辑单独委托出来,约定成一些特定的修饰符。(事件修饰符相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/13233379.html#oneone)
其实,自定义指令也是一样的逻辑,当我们的 methods 中存在操作 DOM/BOM 相关的逻辑的时候,就该思考是否可以将其抽象成一个自定义指令,以便于业务逻辑与相关 DOM 操作解耦,并且使之更容易被单元测试。
Vue 在这里严格遵循了设计模式中的开闭原则,通过约定的钩子函数来让开发者可以在不同的时机中去操作组件。(Vue官网钩子函数相关:https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0)
1. 钩子函数
Vue.directive("resize", { //只调用一次,指令第一次绑定元素时调用 //在这里可以进行一次性的初始化设置 bind: function(el, binding, value){}, //被绑定元素插入父节点时调用 //(仅保证父节点存在,但不一定已被插入文档中) inserted: function(el, binding, vnode){}, //所在组件的 Vnode 更新时调用 //但是可能发生在其子 VNode 更新之前 //指令的值可能发生了变化,也可能没有 //但是可以通过比较更新前后的值来忽略不必要的模板更新 update: function(el, binding, vnode, oldVnode){}, //指令所在的 VNode 及其子 VNode 全部更新后调用 componentUpdated: function(el, binding, vnode, oldVnode){}, //只调用一次,指令与元素解绑时调用 unbind: function(el, binding, vnode){}, });钩子函数例子
先来看第一对钩子函数 bind 与 unbind 函数,顾名思义,这两个钩子函数是在当前这个指令声明的元素绑定和解绑时被调用的,并且需要记住的是,bind 与 unbind 都只会被调用一次。
接下来看钩子函数 inserted。通常情况下,inserted 会在 bind 之后被调用。
bind 跟 inserted 的区别是:bind 中参数 el.parentNode 为 null,inserted 中可以通过 el.parentNode 访问当前节点的父节点。当有信息需要存放在父节点上、需要访问父节点时,使用 inserted 的频率高于 bind 。
接下来看最后一组钩子函数 update 跟 componentUpdate,这对钩子函数会在 vnode 更新前后被调用。
与其他钩子函数相比,update 跟 componentUpdate 传入的参数多一个 oldVnode,oldVnode 代表之前的 Virtual DOM 节点信息,vnode代表当前的Virtual DOM 节点信息。可以根据比较 oldVnode 和 vnode 之间的差异来判断模板是否需要更新,以减少不必要的模板更新,从而一定程度提高组件性能。
2. 钩子函数参数
function( // 指令所绑定的元素,可以用来直接操作 DOM el, // binding 一个对象,包含以下属性 { // 指令名,不包括 -v 前缀 name, // 指令的绑定值,例如:v-my-directive="1+1"中,绑定值为 2 value, // 指令绑定的前一个值 // 仅在 update 和 componentUpdated 钩子中可用 oldValue, //字符串形式的指令表达式 //例如 v-my-directive="1+1" 中,表达式为 "1+1" expression, //例如指令的参数,可选。 //例如 v-my-directive:foo 中,参数为 "foo" arg, //一个包含修饰符的对象 //例如:v-my-directive.foo.bar 中, //修饰符对象为 {foo: true, bar: true} modifiers }, //Vue 编译生成的虚拟节点 vnode, //上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用 oldVnode )钩子函数参数
除了 el 之后,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
需求
需求1. 完成一个 v-resize 指令,去监听浏览器大小的改变,改变的时候通过监听 onResize 响应。然后,将高度或宽度打印到页面上。
需求2. 自定义一个参数 direction, 控制监听页面高度或者宽度的变化。
需求3. 添加完成一个修饰符 .quiet,来控制是否在指令初始化的时候响应 onResize 函数。
实现
本例主要编写三个文件,分别是 main.js、App.vue、SDirectivePage.vue,main.js 是入口文件,index.html 是一级页面,App.vue 是二级页面,SDirectivePage.vue 是 App.vue 的子组件。
需求1
在 main.js 注册全局自定义指令 v-resize, 写钩子函数的时候要选择写 bind 或者 inserted,bind 跟 inserted 的区别是 inserted 能访问父节点 bind 不能,因为不确定需不需访问父节点,先选用 inserted。在 inserted 里面写函数,当页面加载时,执行该函数。给该函数传入参数 el 跟 binding,将回调函数从 binding 中取出来,binding.value 对应在 SDirectivePage.vue 的模板里使用自定义指令 v-resize 时定义指令的表达式 onResize,onResize 是一个方法,通过 binding 取出回调函数后将回调函数 onResize 赋值给 callback 变量,然后使用 addEventListener 在 window 上添加 resize 的事件处理程序,用于监听浏览器的 onResize 事件,给回调函数传入窗口的文档显示区的宽度 window.innerWidth。因为绑定事件之后需要销毁,所以在 unbind 函数内销毁绑定的事件,要销毁绑定的事件首先需要获得绑定的事件,所以,在 inserted 钩子函数内将回调函数放到 el 上,之所以放到 el 上,是因为 el 外的参数都是只读的。给 el 新建属性 _onResize,将回调函数赋给 _onResize,然后在 unbind 函数中移除事件处理程序,删除 el._onResize。( addEventListener方法相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/13128639.html#three)
在 SDirectivePage.vue 的模板里使用自定义指令 v-resize、定义 v-resize 指令的表达式 onResize, onResize 是一个方法。
<template> <div> <s-directive-page /> </div> </template> <script> import SDirectivePage from "./components/SDirectivePage" export default{ components: { "s-directive-page": SDirectivePage } } </script>App.vue
import Vue from 'vue' import App from './App.vue' Vue.directive("resize", { inserted(el, binding){ //将回调函数从参数 binding 中取出来 const callback = binding.value; //监听浏览器的 resize 事件 window.addEventListener("resize", ()=>{callback(window.innerWidth)}); //可以把共享的数据放到 el 上,因为 el 外的参数都是只读的 el._onResize = callback; }, //绑定事件的话必须将其销毁,在 unbind 函数内销毁 unbind(el){ if(!el._onResize) return; window.removeEventListener("resize", el._onResize); delete el._onResize; } }); new Vue({ render: h => h(App), }).$mount('#app')main.js
为了熟悉钩子函数参数,顺便在 SDirectivePage.vue 上标出了钩子函数的参数的对应值。
<template> <div v-resize="onResize">window width is: {{ length }}</div> </template> <script> export default { data(){ return{ direction: "vertical", length: 0 }; }, methods:{ onResize(length) { this.length = length; } } } </script>>SDirectivePage.vue
页面表现:
一开始,显示的数值是 0
如果,拖动窗口边边改变高度,数值会变为窗口宽度,数值不会随着窗口高度的改变而改变,数值会一直都是窗口宽度
如果,拖动窗口边边改变宽度,数值会随着窗口的宽度改变而改变
至此,已经完成一个 v-resize 指令,去监听浏览器大小的改变,改变的时候通过监听 onResize 响应。然后,将宽度打印到页面上。
如果想要监听高度的改变,需要将 main.js 第 9 行传入的参数由 window.innerWidth 改为 window.innerHeight。
需求2
实现的方式与需求 1 大致相同。
在 main.js 中通过 binding.arg 获得指令的参数 direction,写一个函数 result, 返回值是 direction, 判断 direction 的值是不是 "veritcal",如果是的话, 返回高度,否则,返回宽度。将 result 的返回值作为参数传入到回调函数 callback 即 onResize 中。
指令的参数 binding.arg 是 SDirectivePage.vue 模板中 <div v-resize:[direction]="onResize">window height is: {{ length }}</div> 中的 [direction]。在 SDirectivePage.vue 中可以在 data 中设置 direction 的值,本例中设置成了 vertical。
import Vue from 'vue' import App from './App.vue' Vue.directive("resize", { inserted(el, binding){ const callback = binding.value; // 从参数 binding 中取出指令的参数 const direction = binding.arg; // 定义一个函数判断 direction 是不是 vertical ,如果是 direction 就是 // window.innerHeight, 否则是 window.innerWidth const result = () => { return direction === "vertical" ? window.innerHeight : window.innerWidth; } const onResize = () => callback(result()); window.addEventListener("resize", onResize); el._onResize = onResize; }, unbind(el){ if(!el._onResize) return; window.removeEventListener("resize", el._onResize); delete el._onResize; } }); new Vue({ render: h => h(App), }).$mount('#app')main.js
<template> <div v-resize:[direction].quiet="onResize">window height is:{{ length }}</div> </template> <script> export default { data(){ return{ direction: "vertical", length: 0 }; }, methods:{ onResize(length) { this.length = length; } } } </script>SDirection.vue
页面表现:
一开始,显示的数值是 0
如果,拖动窗口边边改变宽度,数值会变为窗口高度,数值不会随着窗口宽度的改变而改变,数值会一直都是窗口高度。
因为 SDirectionPage.vue 中 direction 的值是 vertical ,所以只监听高度变化。
如果,拖动窗口边边改变高度,数值会随着窗口的高度改变而改变
至此,已经完成了自定义一个参数 direction, 控制监听页面高度的变化。
如果想要监听宽度的改变,需要将 SDirectivePage.vue 中第 9 行 direction 的值修改为 vertical 以外的值。
需求3
首先解释一下需求的意思,添加完成一个修饰符 .quiet,来控制是否在指令初始化的时候响应 onResize 函数。在前面的例子中,页面加载时,显示的数值是0,现在要实现如果使用了 .quiet 修饰符,则页面加载时显示的数值是0,如果没有使用 .quiet 修饰符,显示的数值是宽度或者高度。
在需求2的基础上实现需求3。
在 main.js 中,通过 binding 取得一个包含修饰符的对象。如果在 SDirectivePage.vue 使用修饰符的语句是 <div v-resize:[direction].quiet ="onResize">window height is: {{ length }}</div> ,那么 binding.modifiers 是 {quiet: true},如果没有使用修饰符,语句是 <div v-resize:[direction]="onResize">window height is: {{ length }}</div> ,那么 binding.modifiers 是空对象{}。取得 binding.modifiers 之后判断它是否为空,如果它非空或者说它不包含 quiet 属性,那么就执行 onResize 函数,这个 onResize 函数不是在 SDirectivePage.vue 定义的那个,是在 main.js 重新定义的,执行 onReszie 的作用是在页面上显示宽度或者高度的数值。
import Vue from 'vue' import App from './App.vue' Vue.directive("resize", { inserted(el, binding){ const callback = binding.value; const direction = binding.arg; // 从参数 binding 中取出一个包含修饰符的对象 modifiers const modifiers = binding.modifiers; const result = () => { return direction === "vertical" ? window.innerHeight : window.innerWidth; } const onResize = () => callback(result()); window.addEventListener("resize", onResize); if(!modifiers || !modifiers.quiet){ onResize(); } el._onResize = onResize; }, unbind(el){ if(!el._onResize) return; window.removeEventListener("resize", el._onResize); delete el._onResize; } }); new Vue({ render: h => h(App), }).$mount('#app')main.js
<template> <div v-resize:[direction].quiet="onResize">window height is: {{ length }}</div> <!-- <div v-resize:[direction]="onResize">window height is: {{ length }}</div> --> </template> <script> export default { data(){ return{ direction: "vertical", length: 0 }; }, methods:{ onResize(length) { this.length = length; } } } </script>SDirectivePage.vue
页面表现:
1. 使用了 .quiet 修饰符时,初始显示的数值为 0
2. 不使用 .quite 修饰符时,初始显示的数值为宽度或者高度,这个例子显示的是高度