Skip to content

指令

Vue3保留Vue2的以下指令

  • v-text
  • v-html
  • v-show
  • v-if
  • v-else
  • v-else-if
  • v-for
  • v-on
  • v-bind
  • v-model
  • v-slot
  • v-pre
  • v-once
  • v-cloak

保留的指令大部分使用方法与v2一样,少数指令在保留原来语法的基础上,增加一些语法

1.改动指令

v-model

v-model的改变,体现在组件中的v-model

使用语法

vue
<template>
    <Child v-model="text" />
</template>

<script setup>
import Child from './views/Child.vue'
import { ref } from 'vue'

const text = ref('123')
</script>
vue
<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
    defineProps(['modelValue'])				// 接收props
    const emit = defineEmits(['update:modelValue'])			// 接受自定义事件
</script>

原理解析

上述父组件中使用的v-model会被编译为如下代码

vue
<Child
  :modelValue="text"
  @update:modelValue="$event => (text = $event)"
/>

也就是说,vue3组件中使用v-model绑定一个值相当于:

  • 给这个组件添加了一个名为modelValueprop,值为绑定的变量
    • 注意:传递的属性名一定为modelValue
  • 添加名为update:modelValue的自定义事件,作用是把新值赋值给双向绑定的变量
    • 在vue2中,给组件绑定的事件为input事件

在子组件中,依然是表单的value动态绑定,和input事件,而调用的是父组件的update:modelValue自定义事件把新的值传给父组件

v-model参数

组件上的 v-model 也可以接受一个参数:

vue
<template>
  <div>
    <Child v-model:name="myname"/>
  </div>
</template>

<script setup>
import Child from './views/Child.vue'
import { ref } from 'vue'

const myname = ref('456')
</script>
vue
<template>
  <div>
    {{ text }}
    <input :value="name" @input="emit('update:name', $event.target.value)" />
  </div>
</template>

<script setup>
defineProps(['name'])

const emit = defineEmits(['update:name'])
</script>

上述父组件调用子组件相当于以下语法

vue
<Child :name="myname" @update:name="$event=>(myname = $event)"/>

这样,子组件中接受的属性名就不用固定写成modelValue

多个v-model

同一个组件中,不能使用多个v-model,但是使用v-model参数的话,就可以多个使用

vue
<template>
  <Child v-model:value1="value1" v-model:value2="value2" />
</template>

<script setup>
import Child from './views/Child.vue'
import { ref } from 'vue'

const value1 = ref('123')
const value2 = ref('123')
</script>
vue
<template>
  <div>
    <input :value="value1" @input="emit('update:value1', $event.target.value)" />
    <input :value="value2" @input="emit('update:value2', $event.target.value)" />
  </div>
</template>

<script setup>
defineProps(['value1','value2'])

const emit = defineEmits(['update:value1','update:value2'])
</script>

不带参数的v-model和带参数的v-model可以一起使用,不过不带参数的v-model仍然是绑定的modelValue

vue
<template>
  <Child v-model="value1" v-model:value2="value2" />
</template>

<script setup>
import Child from './views/Child.vue'
import { ref } from 'vue'

const value1 = ref('123')
const value2 = ref('123')
</script>
vue
<template>
  <div>
    <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
    <input :value="value2" @input="emit('update:value2', $event.target.value)" />
  </div>
</template>

<script setup>
defineProps(['modelValue', 'value2'])

const emit = defineEmits(['update:modelValue', 'update:value2'])
</script>

处理v-model修饰符

我们知道 v-model 有一些内置的修饰符,例如 .trim.number.lazy。在某些场景下,你可能想要一个自定义组件的 v-model 支持自定义的修饰符。

我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

vue
<Child v-model.capitalize="myText" />
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="props.modelValue" @input="emitValue" />
</template>

带参数的 v-model 修饰符

vue
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>

defineModel

珍妮机的出现,引起了大量工人的不满

1.新增指令

v-memo

仅在 3.2+ 中支持

语法:

缓存一个模板的子树。接受一个数组,只有当数组中的变量更改时,该元素才会被重新渲染

vue
<div v-memo="[valueA, valueB]">
  ...
</div>

当组件重新渲染,如果 valueAvalueB 都保持不变,这个 <div> 及其子项的所有更新都将被跳过。实际上,甚至虚拟 DOM 的 vnode 创建也将被跳过,因为缓存的子树副本可以被重新使用。

正确指定缓存数组很重要,否则应该生效的更新可能被跳过。v-memo 传入空依赖数组 (v-memo="[]") 将与 v-once 效果相同。

与 v-for 一起使用

v-memo 仅用于性能至上场景中的微小优化,应该很少需要。最常见的情况可能是有助于渲染海量 v-for 列表 (长度超过 1000 的情况):

vue
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>

2.自定义指令

Vue3的<script setup>中,一个自定义指令由一个包含类似组件生命周期钩子的对象来定义

<script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。

比如一个名为vFocus的自定义指令,使用方式为v-focus

vue
<template>
	<div v-directive></div>
</template>

<script setup>
const vDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {},
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}
}
</script>

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

举例来说,像下面这样使用指令:

vue
<div v-example:foo.bar="baz">

binding 参数会是一个这样的对象:

js
{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` 的值 */,
  oldValue: /* 上一次更新时 `baz` 的值 */
}

和内置指令类似,自定义指令的参数也可以是动态的。举例来说:

vue
<div v-example:[arg]="value"></div>

这里指令的参数会基于组件的 arg 数据属性响应式地更新。

TIP

除了 el 外,其他参数都是只读的,不要更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现。

2.1 动态参数

正常给指令传递一个参数如下所示

vue
<div v-dir:name></div>

在指令中钩子函数的binding参数的arg可以得到此参数

js
{
    arg:'name',
    // ..
}

我们可以通过一个中括号,传递一个变量,也就是动态参数

vue
<template>
  <div v-dir:[arg]></div>
</template>

<script setup>
import {ref}  from 'vue'
    
const arg = ref( "name123")

const vDir = {
  mounted(el, binding) {
    console.log(binding)  // name123
  }
}
</script>

这样我们可以需要的时候修改arg的值,来达到动态控制参数的名字

WARNING

态参数值的限制

动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。

动态参数语法的限制

动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:

vue
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>

如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。

当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:

vue
<a :[someAttr]="value"> ... </a>

上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]。如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。单文件组件内的模板受此限制。

3.全局指令

一个全局指令需要在main.js中使用app.directives()来注册

3.1 详细形式

js
import { createApp } from 'vue'
const app = createApp(App)

const myDirective = {
  created(el, binding, vnode) {},
  beforeMount(el, binding, vnode) {},
  mounted(el, binding, vnode) {},
  beforeUpdate(el, binding, vnode, prevVnode) {},
  updated(el, binding, vnode, prevVnode) {},
  beforeUnmount(el, binding, vnode) {},
  unmounted(el, binding, vnode) {}
}

app.directives('directiveName',myDirective)

此时对象的名称可以随意定义,但directives()第一个参数名决定了指令最终使用的方式

3.2 简化形式

对于自定义指令来说,一个很常见的情况是仅仅需要在 mountedupdated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

<div v-color="color"></div>
js
app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

MIT Licensed