Vue3 组件、组合式函数、自定义指令、插件

组件、组合式函数、自定义指令都是逻辑复用的手段
组件是主要的构建模块,组合式函数则侧重于有状态的逻辑,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑

编译宏

编译宏是<script setup>中的一种特殊代码,比如definePropsdefineEmitsdefineExposedefineModel
宏只在webpack/vite编译时运行,因此不需要通过import导入。
宏只能在<script setup>顶层中使用,且不能访问其他变量(因为在编译时整个表达式都会被移到外部的函数中)。

注册组件

同步注册
  • 全局注册:app.component('MyComponent', MyComponent)
  • 局部注册:<script setup>通过import引入后直接在模板中使用即可,否则通过components选项来注册。
异步注册

使用defineAsyncComponent,返回一个包装后的特殊组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。
该方法接收一个工厂函数作为参数,该函数应返回一个resolve(组件)的Promise对象。
该方法还支持传入一个对象来配置加载行为和失败回调。

  • 全局注册
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
  • 局部注册
<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
  components: {
    AdminPage: defineAsyncComponent(() =>
      import('./components/AdminPageComponent.vue')
    )
  }
}
</script>

<template>
  <AdminPage />
</template>

透传

指的是传递给一个组件,却没有被该组件声明为 propsemits 的属性和事件。最常见的例子就是 class、style 和 id。

Vue2中一个template中必须有一个根元素,根元素和组件元素属性共通。
Vue3中没有该限制,因此需要手动获取属性。

  • 模板中直接通过$attrs获取父元素属性:
<my-component class="baz" age="18"></my-component>

<template>
  <p :class="$attrs.class">Hi!</p>
  <span v-bind="$attrs">This is a child component</span>
</template>
  • 选项式API中,通过this.$attrs获取父元素属性
  • setup 方法中,attrs 会作为 setup() 上下文对象的一个属性暴露:
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
  • <script setup> 中,通过useAttrs方法获取父元素属性:
<script setup>
import { useAttrs } from 'vue';
let attrs = useAttrs()
</script>

<template>
  <button @click="attrs.onClick()">执行透传的事件</button>
</template>

注意,此处的$attrsuseAttrs虽然会实时更新,但并不算响应式,不能被侦听器监听。


接受父组件参数

参数可以设置默认值和校验规则

  • 非 <script setup> 使用props配置项
    setup方法内使用第一个参数来获取props
export default {
  //props: ['title'],
  props: {
    title: String,
    propB: [String, Number, null],
    propC: {
      type: String,
      required: true,
      default: "hello",
    },
    propF: {
      type: Object,
      // 对象或数组的默认值必须从一个工厂函数返回。
      // 该函数接收组件所接收到的原始 prop 作为参数。
      default(rawProps) {
        return { message: "hello" };
      },
    },
    // 自定义类型校验函数
    // 在 3.4+ 中完整的 props 作为第二个参数传入
    propG: {
      validator(value, props) {
        // The value must match one of these strings
        return ["success", "warning", "danger"].includes(value);
      },
    },
    // 函数类型的默认值
    propH: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return "Default function";
      },
    },
  },
  setup(props) {
    console.log(props.title);
  },
};
  • <script setup> 使用defineProps宏:
<script setup>
//const props = defineProps(['title'])
const props = defineProps({title: String})
console.log(props.title)
</script>

向父组件抛出事件

  • 非 <script setup> 使用emits配置项
    setup方法内使用emits参数抛出事件
  • <script setup>使用defineEmits宏:
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>

事件定义时,可以传入对象来设置校验规则

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

注意,Vue3删除了事件的.native修饰。如组件包含了原生事件同名的事件,则将其覆盖


v-model 双向绑定

v-model 是一个语法糖,本质上等价于:

<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

则子组件中应当对应有以下配置:

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

或一个具有setter的计算属性:

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

同理,组合式API中可以使用modelValue+update:modelValue的组合:

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

或直接使用编译宏defineModel() ,并返回一个 ref。
可以给该宏传入props选项进行配置:

<script setup>
const model = defineModel({ default: 0 })
</script>

<template>
  <input v-model="model" />
</template>
多个 v-model

以上modelValueupdate:modelValuev-model的默认值,如存在多个v-model时,可指定参数:

  • 选项式:
<MyComponent v-model:title="bookTitle" />

export default {
  props: ['title'],
  emits: ['update:title']
}
  • 组合式:
<script setup>
  const title = defineModel('title',{required:true})
</script>

<template>
  <input type="text" v-model="title" />
</template>
v-model 修饰符

内置修饰符包括:.trim.number.lazy(默认情况下,v-model 会在每次 input 事件后更新数据 (中日韩文拼字阶段除外)。添加 lazy 修饰符可改为在每次 change 事件后更新数据)。

Vue2 的 .sync 在 Vue3 中已被 v-model 代替
v-model 的默认 prop 不再是 value,而是modelValue
默认emit方法不再是input,而是update:modelValue
通过v-model:xxx来将modelValue改为xxx而不再使用options的model属性修改,因此也可以实现同一组件上共存多个v-model
新增通过modelModifiers自定义v-model修饰符


插槽 slot

用于将父组件的部分模板内容插入子组件模板。

v-slot(可简写为 #)
  • 通过namev-slot可设置为具名插槽。
    未设置名字则隐式地命名为default,即v-slot等价于v-slot:default
  • v-slot支持动态指令参数,以实现动态插槽名
  • 插槽内容本身无法访问子组件的数据。但可以通过v-slot="XXX"传入数据,实现作用域插槽
  • 通过$slots可以访问从父组件接收到的插槽
子组件
<div class="container">
  <header>
    <slot name="header" :age="18">默认内容</slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer v-if="$slots.footer">
    <slot name="footer"></slot>
  </footer>
</div>

父组件
<BaseLayout>
  <template v-slot:header="slotProps">
    <h1>Here might be a page title  {{ slotProps.age }}</h1>
  </template>

  <p>该元素自动进入默认插槽</p>
  <p>该元素自动进入默认插槽</p>

  <template v-slot:[dynamicSlotName]>
    动态插槽名
  </template>
</BaseLayout>

动态组件 <component>

<KeepAlive include="a,b" :max="10" >
  <component :is="currentTab"></component>
</KeepAlive>

其中传给 :is 的值可以是被注册的组件名或导入的组件对象

<KeepAlive> 组件

可以让被切换掉的组件仍然保持“存活”的状态

  • includeexclude用于指定仅对特定name的组件有效/无效
  • max用于限制可被缓存的最大组件实例数
  • 可通过activatedonActivated) 和 deactivatedonDeactivated)管理动态组件生命周期

<Teleport> 组件

用于向外“传送”部分组件内容,以解决dom结构嵌套问题。

  • to字段是一个 CSS 选择器字符串或者一个DOM元素对象,会将Teleport内容传递给这个DOM元素标签作为子元素。
  • disabled字段可用于禁用 <Teleport> 组件
<button @click="open = true">Open Modal</button>

<Teleport to="body" :disabled="isMobile">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

依赖注入

用于解决深层的子组件获取祖先组件中的数据时,不合理的逐级传递。
通过祖先提供(provide),子组件注入(inject)来传递数据

祖先提供依赖

注入名可以是字符串或者Symbol,注入值可以是普通类型或响应式依赖类型

  • 选项式API
export default {
  provide: {
    message: 'hello!'
  },
}

如果需要访问data中内容,需要使用provide函数返回值。此时注入会失去响应性,因此还需要使用computed方法:

import { computed } from 'vue'

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    return {
      // 显式提供一个计算属性
      message: computed(() => this.message)
    }
  }
}
  • <script setup>
<script setup>
import { ref, provide } from 'vue'

// 提供响应式的值
const count = ref(0)
provide('count', count)
</script>
  • setup 方法
import { provide } from 'vue'

export default {
  setup() {
    provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  }
}
  • 可以在应用级别提供全局依赖
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
子组件注入
  • 选项式API:
    注入会在组件自身的状态之前被解析,因此你可以在 data() 中访问到注入的属性
export default {
  inject: ['message'],
  data() {
    return {
      // 基于注入值的初始数据
      fullMessage: this.message
    }
  }
}

注入可以起别名和设置默认值。
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或副作用,可以使用工厂函数来创建默认值:

export default {
  inject: {
    message_custom_name: {
      from: 'message',
      default: 'default value'
    },
    user: {
      // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例
      // 需要独立数据的,请使用工厂函数
      default: () => ({ name: 'John' })
    }
  }
}
  • <script setup>
<script setup>
import { inject } from 'vue'

const message = inject('message', "这是默认值")
</script>
  • setup 方法
import { inject } from 'vue'

export default {
  setup() {
    //第三个参数表示将默认值视为工厂函数
    const message = inject('message', () => new ExpensiveClass(), true)
    return { message }
  }
}

组合式函数

组合式函数是用组合式API封装的有状态逻辑的函数,便于函数复用。
组合式函数相比于无渲染组件的主要优势是:不会产生组件实例的性能开销。
组合式函数只能在 <script setup> 或 setup() 钩子中被同步调用。
约定:小驼峰命名法命名,以“use”开头,返回包含多个ref的普通的非响应式对象(以方便调用者解构接收返回值):

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

自定义指令

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。
钩子函数会接收到指令所绑定元素及指令内容作为其参数:

钩子参数
  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-* directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
全局注册指令
app.directive('focus', {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
})

如只需要在 mountedupdated 时调用,可以简写,直接传入一个函数:

app.directive('color', (el, binding) => {
  el.style.color = binding.value
})
在 <script setup> 中使用指令

所有v开头驼峰式命名的变量都可以被用作一个自定义指令

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted(el) {el.focus()},
}
</script>

<template>
  <input v-focus />
</template>
在选项式API 或 setup() 中使用指令
const focus = {
  mounted: (el) => el.focus()
}

export default {
  directives: {
    // 在模板中启用 v-focus
    focus
  }
}

插件

插件可以是一个拥有 install() 方法的对象,也可以直接该函数本身。
插件通常包含了全局方法、全局组件、全局自定义指令、提供全局依赖。
插件通过app.use进行安装。

import { createApp } from 'vue'

const app = createApp({})

const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

app.use(myPlugin, {
  /* 可选的选项 */
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容