Vue指令

发布于 大漠

Vue使用的模板语言是HTML的超集。在Vue的模板中除了使用插值({{}})之外还可以使用指令。在上一节中,我们主要学习和探讨了Vue模板相关的知识,在这一节中,我们将一起来了解Vue中的指令。在Vue中,指令基本上类似于添加到模板中的HTML属性。它们都是以**v-**开头,表示这是一个Vue特殊属性。Vue中的指令主要分为内嵌的指令和自定义指令。另外有一些指令还带有相应的修饰符。接下来我们主要围绕着这些点来展开,咱们先从内嵌的Vue指令开始。

Vue内嵌指令

在Vue中常见的内置嵌套指令主要有:

接下来,咱们简单的了解一下内嵌指令的使用。

v-text

Vue模板一节中,我们知道可以使用插值的语法{{}}向模板中插入字符串内容。

<!-- App.vue -->
<template>
    <h1>Vue插值语法:{{}}</h1>
    <div>{{ message }}</div>
</template>

<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'Hello Vue (^_^)'
            }
        }
    }
</script>

如果我们要更新元素的textContent。除了使用Vue插值语法之外,还可以使用**v-text**指令。他们最终达到的效果是一样的:

<!-- App.vue -->
<h1>v-text</h1>
<div v-text="message"></div>

v-html

有些时候,我们在data中定义的字段会带有HTML的标签,面对这样的场景,如果使用{{}}插值语法或者v-text指令,都会将数据中的HTML当做字符串插入到模板中。这并不是我们期望要的结果。这个时候,使用v-html指令可以解决。v-html指令会更新元素的innerHTML。比如下面这个示例:

<!-- App.vue -->
<h1>v-html</h1>
<div>{{ rawHtml }}</div>
<div v-html="rawHtml"></div>

<script>
export default {
    name: 'app',
    data () {
        return {
            rawHtml: 'Hello, <strong>Vue (^_^) </strong>'
        }
    }
}
</script>

在使用v-html相当于在网站上动态渲染任意HTML是非常危险的,因为容易导致XSS攻击。只在可信内容上使用v-html,永不用在用户提交的内容上。另外在单文件组件里,scoped的样式不会应用在v-html内部,因为那部分HTML没有被Vue的模板编译器处理。如果想对v-html的内容设置带作用域的CSS,可以替换为CSS Modules或用一个额外的全局<style>元素手动设置样式。如果你想在scoped设置v-html内标签元素的样式,需要借助Vue的另外特性,比如 >>>/deep/ 来设置样式。比如上例中的strong样式,我们可以像下面这样来设置:

<style scoped>
    div >>> strong {
        color: red;
    }

    div /deep/ strong {
        color: green;
    }
</style>

有关于这方面更详细的介绍可以阅读早前整理的一篇博文《Vue中的作用域CSS和CSS模块的差异》。有关于v-textv-html指令更详细的介绍可以阅读《v-textv-html》一文。

v-once

我们知道{{ message }}语法可以将data中的message插入到Vue的模板(或组件)中。当数据中的message发生变更时,模板中对应的值也会发生变更,比如:

<!-- App.vue -->
<h1>v-once</h1>
<div>{{ message }}</div>
<div v-once>{{ message }}</div>
<button @click="changeMessage">修改消息</button>

<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'Hello Vue (^_^)'
            }
        },
        methods: {
            changeMessage () {
                this.message = 'Hello, 大漠 !'
            }
        }
    }
</script>

当你点击修改消息按钮时,没有使用v-once指令的{{ message }}中的message值会发生变化,反之却不会有任何更新,如下图所示:

上面的示例再次验证了,v-once指令只渲染元素和组件一次。随后的重新渲染,元素或组件及其所有子节点将被视为静态内容并跳过。这可以用于优化更新性能。

v-bind

Vue插值语法只能在HTML标签中内容工作,而HTML属性中是不能使用它。如果要动态地给HTML标签绑定属性,需要使用v-bind。比如:

<!-- App.vue -->
<a :href="url">{{ linkText }}</a>

v-bind还有一个简写的方式,使用**:**来替代:

<a v-bind:href="url">{{ linkText }}</a>
<a :href="url">{{ linkText }}</a>

这两种写法是等效的。

v-bind不仅仅绑定一个物性,还可以绑定多个物特或一个组件prop到表达式。在绑定classstyle特性时,支持其它类型的值,比如数组对象。在绑定prop时,prop必须在子组件中声明。可以用修饰符指定不同的绑定类型。没有参数时,可以绑定到一个包含键值的对象。

<!-- 绑定一个属性 -->
<img v-bind:src="imageSrc">

<!-- 动态特性名 (2.6.0+) -->
<button v-bind:[key]="value"></button>

<!-- 缩写 -->
<img :src="imageSrc">

<!-- 动态特性名缩写 (2.6.0+) -->
<button :[key]="value"></button>

<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName">

<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">

<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>

<!-- 绑定一个有属性的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- 通过 prop 修饰符绑定 DOM 属性 -->
<div v-bind:text-content.prop="text"></div>

<!-- prop 绑定。“prop”必须在 my-component 中声明。-->
<my-component :prop="someThing"></my-component>

<!-- 通过 $props 将父组件的 props 一起传给子组件 -->
<child-component v-bind="$props"></child-component>

<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>

比如上面的示例中,v-bind中还使用了修饰符,常见的修饰符主要有:

  • .prop:被用于绑定DOM属性
  • .camel:将kebab-case特性名转换为camelCase
  • .sync:这是一个语法糖,会扩展成一个更新父组件绑定值的v-on侦听器

注意,在使用字符串模板或通过vue-loadervueify编译时,不需要使用.camel修饰符。

有关于v-bind指令更详细的介绍可以阅读《v-bind》一文。

v-model

对Vue有一定了解的同学,都知道Vue中可以轻易的实现数据的双向绑定。v-model主要用在表单控件或者组件上创建双向绑定。也就是说,v-model可以绑定一个输入表单(比如一个input),当用户更改input输入的内容时,对应元素的内容也会更新。比如下面这个示例:

<!-- App.vue -->
<input v-model="message" placeholder="输入一个信息" />
<div>你输入的信息:{{ message }}</div>

<select v-model="selected">
    <option disabled value="">选择你想要的选项</option>
    <option>CSS</option>
    <option>JavaScript</option>
    <option>Node</option>
    <option>HTML</option>
</select>
<div>你喜欢选项:{{ selected }}</div>

使用v-model指令的时候,也可以添加一些修饰符,比如:

  • .lazy:取代input监听change事件
  • .number:输入字符串转为有效的数字
  • .trim:输入首尾空格过滤

有关于v-model更详细的介绍可以阅读早前整理的一篇学习笔记《v-model》。

v-show

使用v-show指令可以根据Vue实例的某个特定属性的值(该值是一个条件值,它的值是truefalse)来展示元素。 当值为true时,DOM元素显示;反之为false时,该元素不显示。

<!-- App.vue -->
<div v-show="isTrue">{{ message }}</div>
<button @click="toggleShow">{{ isTrue ? '隐藏' : '显示' }}</button>

在Vue中使用v-show指令时,当Vue实例中某个特定属性值为false时(比如上例中的isTruefalse)时,div元素{{ message }}并不会在DOM中渲染,相当于该元素设置了display: none的效果。 简单地说,v-show只是简单地切换元素的CSS属性display(即blocknone之间的切换)

注意,v-show不支持<template>无素,也不支持v-else

v-ifv-else-ifv-else

v-ifv-else-ifv-else指令属性条件渲染。v-if指令用于条件性的渲染一个元素或一块内容。当Vue实例中data的某个属性值为true时,对应的元素或内容块就会被渲染。另外还可以配合v-else-ifv-else指令添加另外一块显示内容。有点类似于JavaScript中的:

if (conditions A) {
    content A // 当conditions A为true时,显示content A
} else if (conditions B) {
    content B // 当conditions B为true时,显示content B
} else {
    content C // 当 conditions A 和 conditions B 都为 false 时显示 content C
}

在Vue中,我们可以像下面这样使用:

<!-- App.vue -->
<div v-if="isTrue">Hello, {{ message }} !</div>
<div v-else>Hi, {{ message }} !</div>
<button @click="toggleShow">切换</button>

使用v-if的时候,还可以使用在<template>元素上。这样就可以根据条件渲染分组。比如:

<template v-if="isTrue">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
</template>

另会,在使用v-if的时候,还可以使用key管理可复用的元素,而且可以更高效地渲染。例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" />
</template>
<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" />
</template>

在上面的代码中切换loginType将不会清除用户已输入的内容。因为两个模板使用了相同的元素,<input>不会被替换掉,仅仅替换了它的placeholder。事实上这两个元素是完全独立的,不要复用它们。只需要添加一个具有唯一值的key属性即可:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input" />
</template>
<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input" />
</template>

v-showv-if指令看上去都是根据条件来显示(渲染)或隐藏元素。但他们之间还是有一定的差异的:

  • v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
  • v-if也是惰性的,如果在初始渲染时条件为false时,则什么也不做,直到条件第一次为true时,才会开始渲染条件块
  • v-show相对要简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS的display属性进行切换

一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好。

有关于v-ifv-show指令更详细的介绍可以阅读早前整理的相关学习笔记《v-ifv-show》。

v-for

v-for指令是基于数据多次渲染元素或模板块。该指令必须使用特定语法 alias in expression,为当前遍历的元素提供别名:

<!-- App.vue -->
<ul>
    <li v-for="item in items">{{ item }}</li>
</ul>

data () {
    return {
        items: ['CSS', 'JavaScript', 'HTML', 'TypeScript']
    }
}

另外也可以为数组索引指定别名(或用于对象的键):

<div v-for="(item, index) in items"></div>
<div v-for="(val, key) in object"></div>
<div v-for="(val, key, index) in object"></div>

v-for默认行为试着不改变整体,而是替换元素。迫使其重新排序的元素,你需要提供一个key的特殊属性:

<div v-for="item in items" :key="item.id">
    {{ item.text }}
</div>

自V2.6版本起,v-for也可以在实现了 可迭代协议 的值上使用,包括原生的MapSet。另外,当和v-if一起使用时,v-for的优先级比v-if更高。

有关于v-for更多的介绍还可以阅读:

v-on

v-on指令允许你侦听DOM事件,并在事件发生时触发一个方法。

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!--  串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

v-on用在普通元素上时,只能监听 原生DOM事件。用在自定义元素组件上时,也可以监听子组件触发的 自定义事件

在使用v-on指令帮定事件时,也可以附上一些修饰符,和v-on配合在一起常见的修饰符主要有:

  • .stop: 调用 event.stopPropagation()
  • .prevent: 调用 event.preventDefault()
  • .capture: 添加事件侦听器时使用 capture 模式
  • .self: 只当事件是从侦听器绑定的元素本身触发时才触发回调。
  • .{keyCode | keyAlias}: 只当事件是从特定键触发时才触发回调。
  • .native: 监听组件根元素的原生事件
  • .once: 只触发一次回调
  • .left: 只当点击鼠标左键时触发 (v2.2.0)
  • .right:只当点击鼠标右键时触发 (v2.2.0)
  • .middle:只当点击鼠标中键时触发 (v2.2.0)
  • .passive:以 { passive: true } 模式添加侦听器 (v2.3.0)

另外,在使用v-on可以使用简写的方式,即用@来替代,比如下面这个示例,两者起到的作用是相同的:

<a v-on:click="handleClick">Click me!</a>
<a @click="handleClick">Click me!</a>

有关于Vue中v-on更详细的介绍,可以阅读早前整理的一篇学习笔记《v-on》。

v-pre

v-pre指令可以跳过这个元素和它的子元素的编译过程。可以用来显示原始 插值。跳过大量没有指令的节点会加快编译。

<!-- App.vue -->
<div v-pre>{{ message }}</div>

在使用v-pre时,不需要任何的表达式。

v-cloak

v-cloak指令保持在元素上直接到关联实例结束编译。和CSS规则 如 [v-cloak] {display: none}一起用时,这个指令可以隐藏未编译的插值标签,直到实例准备完毕。

<!-- App.vue -->
<div v-cloak>{{ message }}</div>

[v-cloak] {
    display: none
}

v-slot

v-slot指令是v2.6新增加的一个指令。结合了slotslot-scope的功能,同时也是scoped slots的简写。具体使用的示例如下:

<!-- 具名插槽 -->
<base-layout>
    <template v-slot:header>
        Header content
    </template>

    Default slot content

    <template v-slot:footer>
        Footer content
    </template>
</base-layout>

<!-- 接收 prop 的具名插槽 -->
<infinite-scroll>
    <template v-slot:item="slotProps">
        <div class="item">
            {{ slotProps.item.text }}
        </div>
    </template>
</infinite-scroll>

<!-- 接收 prop 的默认插槽,使用了解构 -->
<mouse-position v-slot="{ x, y }">
    Mouse position: {{ x }}, {{ y }}
</mouse-position>

有关于v-slot更详细的介绍,可以阅读前面整理的学习笔记《Vue新指令:v-slot

自定义指令

对Vue稍微有点了解的同学都知道,Vue虽然是用数据来驱动视图,但并非所有情况都能适合用数据来驱动视图(也就是对DOM的操作)。不过,Vue提供了一种指令的操作,可以对DOM进行操作。除了上面介绍的内嵌指令之外,还有自定义指令。而这种自定义指令可以用于定义任何的DOM操作,并且是可以复用的。

在Vue中的自定义指令主要分为全局指令局部指令

它们之间的共同点都是具有一个”指令名称“和”指令钩子函数“。在使用的时候和内嵌指令类似,以**v-**前缀开头。不同的是:

  • 全局指令是全局注册,在任何组件中都可以使用全局注册的自定义指令
  • 局部指令是局部注册,只能在当前组件使用该自定义指令

注册全局指令时,一般在项目的main.js中使用Vue.directive()来注册,比如:

<!-- main.js -->
Vue.directive('my-custom-directive', {
    // 自定义指令将要做的一些事情,
    // 使用自定义指令的钩子函数
})

就是这么简单地全局注册了一个名称叫my-custom-directive的Vue自定义指令。对于局部注册的自定义指令,是在对应Vue实例中的options选项中的directives中来完成,比如:

<!-- App.vue -->
export default {
    name: 'App',
    directives: {
        'my-custom-dirctive': {
            // 自定义指令要完成的事情
            // 自定义指令中的钩子函数
        }
    }
}

当然,为了更好的维护自定义的指令,在创建全局自定义指令的时候,还可以在src目录下创建一个独立的文件夹,比如directives,用来管理你需要的自定义指令。就上面的示例,我们可以在src/directives/下创建一个my-custom-directive.js文件,在这个文件中输入你需要的自定义指令要做的事情。比如:

<!-- my-custom-directive.js -->
export default {
    // 自定义指令要做的事情
    // 钩子函数
}

这种方式在注册全局自定义指令和局部自定义指令也略有不同。比如全局自定义指令,在main.js中先引入my-custom-directive.js,然后再使用Vue.directive()来注册,比如:

<!-- main.js -->
import MyCustomDirective from './directives/my-custom-directive.js'

Vue.directive('my-custom-directive', MyCustomDirective)

局部注册自定义指令如下:

<!-- App.vue -->
import MyCustomDirective from './directives/my-custom-directive.js'

export default {
    name: 'App',
    directives: {
        'my-custom-directive': MyCustomDirective
    }
}

注意,Vue自定义指令的名称可以随你喜欢的取,但最好还是取一些和功能更接近,带有语意化的名称。如果自定义指令取名时使用的是驼峰命名,比如MyCustomDirective,在使用的时候要将驼峰转为小写,并且每个单词之间使用-符号来连接,比如v-my-custom-directive

钩子函数和参数

在学习Vue实例时,我们知道每个Vue都有生命周期,而生命周期中有相应的钩子函数。Vue自定义指令有点类似,正如前面所提到的,使用Vue.directive(name,{})注册自定义指令时,除了第一个参数用来命名指令之外,第二个参数就是自定义指令的相应的钩子函数了。自定义指令该具备的特性(能做啥事)都是在这些钩子函数中完成的。

一个指令定义对象主要有以五个钩子函数,但这五个钩子函数可以根据自己的需要选用:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
  • update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

比如我们上面所说的my-custom-directive指令,将钩子函数插入到第二个对象参数当中,看起来就像下面这样:

<!-- main.js -->
Vue.directive('my-custom-directive', {
    bind: function(){
        // 指令第一次绑定到元素时调用,做绑定的准备工作
        // 比如添加事件监听器,或是其他只需要执行一次的复杂操作
    },
    inserted: function(){
        // 被绑定标签的父节点加入 DOM 时立即触发
    },
    update: function(){
        // 根据获得的新值执行对应的更新
        // 对于初始值也会调用一次
    },
    componentUpdated: function(){
        // 指令所在组件的 VNode 及其子 VNode 全部更新后调用,一般使用 update 即可
    },
    unbind: function(){
        // 做清理操作
        // 比如移除bind时绑定的事件监听器
    }
})

注意,五个钩子函数是可选的。在很多时候,你可能想在 bindupdate 时触发相同行为,而不关心其它的钩子,那么可以简化写法:

Vue.directive('my-custom-directive', function(){
    // update 内的代码块
})

用张图来表示Vue自定义指令中的钩子函数:

特别声明,上图来自于@Sarah Drasner的《The Power of Custom Directives in Vue》一文。

Vue自定义指中所有的钩子函数会被复制到实际的指令对象中,而这个指令对象将会是所有钩子函数的this上下文环境。指令对象上暴露了一些有用的公开属性。也就是说,钩子函数中每一个都可以具有相同的参数,比如elbindingvnode,而且除了updatecomponentUpdated钩子函数之外,还可以暴露oldVnode参数,用于区分传递的旧值和新值。

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding: 一个对象,包含以下属性namevalueoldValueexpressionargmodifiers
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在updatecomponentUpdated钩子函数中可用

另外binding参数中包含属性相应的解释如下:

  • name:指令名,不包括 v- 前缀
  • value:指令的绑定值,例如:v-my-custom-directive="1 + 1" 中,绑定值为 2
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用
  • expression:字符串形式的指令表达式。例如 v-my-custom-directive="1 + 1" 中,表达式为 "1 + 1"
  • arg:传给指令的参数,可选。例如 v-my-custom-directive:foo 中,参数为 "foo"
  • modifiers:一个包含修饰符的对象。例如:v-my-custom-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

我们来看一个小示例,通过这个小示例来了解每个钩子函数怎么运行。比如我们来模仿一个v-show指令,从前面介绍的内容我们可以得知,v-show指令等于给DOM元素添加了display:none的效果,用来控制DOM元素显示和隐藏。

<!-- main.js -->
Vue.directive('my-show', {
    // 只调用一次,指令第一次绑定到元素时调用
    bind: function (el, binding, vnode) {
        console.log('bind钩子函数 =>el', el)
        console.log('bind钩子函数 =>binding', binding)
        console.log('bind钩子函数 =>vnode', vnode)
        
        el.style.display = binding.value ? 'block' : 'none'
    },

    // 被绑定元素插入父节点时调用
    inserted: function (el, binding, vnode) {
        console.log('inserted 钩子函数 =>el', el)
        console.log('inserted 钩子函数 =>binding', binding)
        console.log('inserted 钩子函数 =>vnode', vnode)
    },

    // 所在组件的 VNode 更新时调用
    update: function (el, binding, vnode, oldVnode) {
        console.log('update 钩子函数 el=>', el)
        console.log('update 钩子函数 binding=>', binding)
        console.log('update 钩子函数 vnode=>', vnode)
        console.log('update 钩子函数 oldVnode=>', oldVnode)

        el.style.display = binding.value ? 'block' : 'none'
    },

    // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
    componentUpdated: function (el, binding, vnode, oldVnode) {
        console.log('componentUpdated 钩子函数 el=>', el)
        console.log('componentUpdated 钩子函数 binding=>', binding)
        console.log('componentUpdated 钩子函数 vnode=>', vnode)
        console.log('componentUpdated 钩子函数 oldVnode=>', oldVnode)
    },

    // 指令与元素解绑时调用
    unbind: function (el, binding, vnode) {
        console.log('unbind 钩子函数 el=>', el)
        console.log('unbind 钩子函数 binding=>', binding)
        console.log('unbind 钩子函数 vnode=>', vnode)
    }
})

我们调用my-show指令时,可以像下面这样:

<!-- App.vue -->
<div v-my-show="isTrue">{{ message }}</div>

从效果上我们可以看出来,这个自定义指令v-my-show和Vue内置指令v-show是等效的。通过上面的小示例,我们对自定义指令中五个钩子函数的触发时机有了初步的认识。有疑问的是bindinsertedupdatecomponentUpdated的区别。

先来看bindinserted两个指令之间的区别。为了更好的看出他们之间的差异,在上面的指令的钩子函数中添加一段测试代码:

bind: function (el, binding, vnode) {
    console.log('bind钩子函数 =>el', el)
    console.log('bind钩子函数 =>binding', binding)
    console.log('bind钩子函数 =>vnode', vnode)
    
    el.style.display = binding.value ? 'block' : 'none'
    el.style.color = 'red'
},

// 被绑定元素插入父节点时调用
inserted: function (el, binding, vnode) {
    console.log('inserted 钩子函数 =>el', el)
    console.log('inserted 钩子函数 =>binding', binding)
    console.log('inserted 钩子函数 =>vnode', vnode)
    el.style.color = 'green'
},

bind钩子函数中设置了:

el.style.color = 'red'

inserted钩子函数中设置了:

el.style.color = 'green'

其中el.style.colorbind钩子函数中并未生效,这是因为bind钩子函数被调用时,bind的第一个参数el拿到对应的DOM元素,但此时DOM元素并未插入到DOM中,因此在这个时候el.style.color并未生效。哪怕是你在加载页面的一瞬间,你也看到的红色的文本。

当DOM元素被插入到DOM树中时,inserted钩子函数就会被调用(生效),此时,inserted钩子中的el.style.color就被执行,也就生效了。在客户端看到的文本颜色是绿色(green)。

虽然我们的指令中写了五个钩子函数,但从页面加载到页面加载完,我们只能看到bindinserted两个钩子函数对应输出的内容,如下图所示:

接着我们再来看看updatecomponentUpdated两个钩子函数的区别。为了更好的向大家演示他们之间的差异,同样在这两个钩子函数中来设置文本的颜色:

// 所在组件的 VNode 更新时调用
update: function (el, binding, vnode, oldVnode) {
    el.style.display = binding.value ? 'block' : 'none'
    el.style.color = 'orange'
},

// 指令所在组件的 VNode 及其子 VNode 全部更新后调用
componentUpdated: function (el, binding, vnode, oldVnode) {
    el.style.color = 'lime'
},

按钮来回点击时,看到的效果如下:

从效果上看,在update指令上设置的文本颜色,也就是橙色并没有看到,而componentUpdated指令上设置的文本颜色可以看到。简单地说,update 钩子函数触发时机是自定义指令所在组件的 VNode 更新时, componentUpdated 触发时机是指令所在组件的 VNode 及其子 VNode 全部更新后。此处使用 el.style.color设置文本颜色值,从运行结果上看 updatecomponentUpdated 是 DOM 更新前和更新后的区别。

将上面介绍的内容浓缩到一个知识图谱中来,如下所示:

纠正:上图中的v-clock应该是v-cloak。谢谢@E0大大指正!

自定义指令示例

了解了Vue中的自定义指令是怎么一回事,来整个示例,加强一下这方面的练习,通过实例更好的来理解Vue的自定义指令。首先来看一个图片延迟加载的案例。

图片延迟加载

在Web中,图片延迟加载是非常常见的一种技术。如果你从未接触过,可以先阅读下面这些文章来了解Web应用中图片延迟加载的相关原理:

使用Vue自定义指令,声明一个lazyload的指令:

<!-- main.js -->
Vue.directive('lazyload', {
    inserted: el => {
        function loadImage () {
            const imageElement = Array.from(el.children).find(el => el.nodeName === 'IMG')

            if (imageElement) {
                imageElement.addEventListener('load', () => {
                    setTimeout(() => el.classList.add('loaded'), 100)
                })
                imageElement.addEventListener('error', () => console.log('error'))
                imageElement.src = imageElement.dataset.url
            }
        }

        function handleIntersect (entries, observer) {
            entries.forEach(entry => {
                if (!entry.isIntersecting) {
                    return
                } else {
                    loadImage()
                    observer.unobserve(el)
                }
            })
        }

        function createObserver () {
            const options = {
                root: null,
                threshold: '0'
            }

            const observer = new IntersectionObserver(handleIntersect, options)

            observer.observe(el)
        }

        if (!window['IntersectionObserver']) {
            loadImage()
        } else {
            createObserver()
        }
    }
})

我们在main.js中声明了Vue指令,在加载图片的时候,我们可以在图片的容器上使用v-lazyload来实现图片的延迟加载效果:

<!-- App.vue -->
<div class="box" v-lazyload v-for="(item, index) in imageSoures" :key="index">
    <div class="ripple">
        <div class="ripple__circle"></div>
        <div class="ripple__circle ripple__inner-circle"></div>
    </div>
    <img :data-url="item.url" :alt="item.title" />
</div>

data () {
    return {
        imageSoures: [
            {
            title: 'Homee Sales Surge',
            url: '//www.nar.realtorhttps://static.fedev.cn/sites/default/files/condo-building-GettyImages-152123643-1200w-628h.jpg'
            },
            // ...
        ]
    }
}

具体效果如下:

该示例详细的代码可以在Github上app-vue-directives仓库的step3分支查看。

有关于Vue针对图片延迟加载相关的自定义指令更详细的介绍可以阅读:

小结

这篇文章主要介绍了Vue的指令。从Vue的内置指令v-textv-htmlv-showv-on等入手。了解了内置指令的使用和相应的作用。但很多时候,这些内置的指令并不能完全满足我们来操作DOM。但Vue还提供了自定义指令的机制,通过这篇文章的学习,我们知道自定义指令的钩子函数和相应的在数,以及如何用这些自定义指令的钩子函数来创建我们所需要的指令。通过自定义指令让我们更好的操作DOM。最后希望这篇文章对初学Vue的同学有所帮助,如果您在这方面有更好的经验欢迎在下面的评论中与我们共享。Nike React Element 87 Moss