因项目需要,开始了解与学习 vue。这里用于记录 vue3 的学习历程。根据以往的经验,感觉还是边学边记,对我比较适合一点。因为当前对 vue 一点都不了解,因此这个时间节点的目标:编写出从 0 到 1,由浅入深的一系列学习查询文档。好了,这篇将是基本语法的快速查询文档,自己开始吧。
自己提前使用npm create vue@latest
,先将框架初始化好,方便语法学习测试:
javascript
// main.ts
import { createApp } from 'vue'
import AppVue from './App.vue';
const app = createApp(AppVue);
app.mount('#app');
// App.vue
<script setup lang="ts">
</script>
<template>
<div class="start">
Hello, Let's start to learn Vue
</div>
</template>
<style scoped>
.start {
color: red;
}
</style>
接下来,我们开始细细学习具体基础语法吧。
1. 基础语法与指令
语法类型 | 实例 |
---|---|
Mustache 双大括号语法 ,纯文本插入 |
<span>你好,{{yehonzi}}</span> |
v-html 指令,插入 html |
<span v-html="rawHtml"></span> 动态渲染 html 比较危险,容易造成 XSS 漏洞 |
v-bind 指令,属性绑定 |
单一绑定: <div v-bind:id="customId">hello</div> 简写 => <div :id="customId">hello</div> 动态绑定: const obj = { id:'cusId', class: 'cusCls'}, <div v-bind="obj">hello</div> 等价 => <div id="cusId" class="cusCls">hello</div> |
js 表达式,{{}}中,或v- 开头的指令中 |
只支持单一表达式。{{ isTrue ? 'yes' : 'no'}} ,{{ message.split('').reverse().join('') }} |
动态参数绑定 | const obj ='href';<a :[obj]="url">hello</a> 等价=> <a :href="url">hello</a> |
class 绑定 | 对象绑定: const cusCls = reactive({isActive=true, isHidden = false}); <div class="defaultCls" :class=cusCls></div> 等价 => <div class="defaultCls active"></div> 数组绑定:const cusCls = ['active', 'isShow']; <div class="defaultCls" :class=cusCls></div> 等价 => <div class="defaultCls active isShow"></div> |
组件设置 class | 组件:<p class="foo">Hi!</p> , 组件使用<Compnent :class={active: true} class="custCls"/> => 渲染:<p class="foo custCls active">Hi!</p> |
绑定内联样式(推荐驼峰编写) | 对象绑定:const cusStyle = reactive({color: 'red', fontsize: fontSize+'px'}); const fontSize = 10; <div :style="cusStyle"></div> 数组绑定:<div :style="[baseStyles, overridingStyles]"></div> |
v-if /v-else-if / v-else | <h1 v-if="type===A">A</h1><h1 v-else-if="type===B">B</h1><h1 v-else="type=C">C</h1> 当期望显示的是一整个模块,可以将v-if 使用在<template> 上:<template v-if="isShow">.....</template> |
v-show | <h1 v-show=isShow">A</h1> 当期望显示的是一整个模块,可以将v-show 使用在<template> 上:<template v-if="show">.....</template> |
v-for 列表渲染 | 1:遍历顺序基于 Object.keys()返回值: <ul><li :key="item.index" v-for="item in items"><span :Key="index" v-for="{ index, childName } in item.children">{{ item.name }}: {{ childName }}</span></li></ul> 2:v-for 可比遍历一个整数,可用于 template。<template v-for="n in 10"><span>{{ n }}</span></template> 3:组件不自动注入 v-for 迭代数据: <MyComponent v-for="(item, index) in items" :item="item :key="item.id" /> 当需要循环的数组不是原始数据,而是需要经过处理的数据,可以使用 computed 进行过滤处理。 |
v-on:eventName 简写 @eventName | 1. 内联事件:const count = ref(1);<button @click="count++">click me</button> 3:内联事件,使用event 或箭头函数中的 event 传递事件对象:`fuction myName(event,name) {console.log(name)}; |
事件修饰符 | <a @click.stop.prevent="doThat"></a> 或者 <form @submit.prevent="onSubmit">...</form> .stop :阻止事件冒泡 .prevent : 阻止默认事件 .self : 元素本身才出发事件 .capture : 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。指向内部元素的事件,在被内部元素处理前,先被外部处理 .once : 只触发一次 passive : 永远不会调用preventDefault() |
按键修饰符 | <input @keyup.page-down="onPageDown" /> ,当$event.key='PageDown' 时调用 提供以下按键别名:.enter,.tab, .delete, .esc, .space, .down, .left, .right, .up |
系统修饰符 | Alt + Enter: <input @keyup.alt.enter="clear"/> 提供以下修饰符:.ctrl, .alt, .shift, .meta |
.exact | 允许控制触发一个事件所需的确定组合的系统按键修饰符。 @click.ctrl : 按下的键只要包含ctrl 就触发 @click.ctrl.exact : 只有按下 ctrl 键才触发 @click.exact :什么键都不按触发 |
鼠标按键修饰符 | .left, .right, .middle |
表单绑定基础 | text: <input v-model="message" placeholder="edit" /> textarea : <textarea v-model="message" placeholder="multiple lines"></textarea> checkbox: <input type="checkbox" id="checkbox" v-model="checked" /> checkbox 值绑定(选中 yes,未选中 no):<input type="checkbox" id="checkbox" v-model="checked" true-value="yes" false-value="no" radio: <input type="radio" id="one" value="one" v-model="selected" /><label for="one">One</label> raido 值绑定(选中后 pick 的值为 first 的值):<input type="radio" v-model="pick" :value="first" /> multiCheckbox: const names = ref([]); <input type="checkbox" id="jack" value="Jack" v-model="names" <label for="jack">Jack</label> <input type="checkbox" id="john" value="John" v-model="names"> <label for="john">John</label> select: <select value="selected"><option v-for="option in options" :key="option.value" :value="option.value">{{ option.text }}</option></select> select 值绑定(选中后,selected 对象值变为{number:123}):<select v-model="selected"><option :value="{ number: 123 }">123</option></select> |
表单修饰符 | .lazy(每次 change 事件后更新数据): <input v-model.lazy="msg"/> .trim(自动去掉用户输入的两端空格):<input v-model.trim="msg"/> .number(用户输入内容自动转为 number,无法转换时返回原始值):<input v-model.number="msg"/> |
侦听器 watch | watch 追踪明侦听的 dataOrign 数据源。watch(dataOrgin, (newData, oldData) => { ...业务操作}, { deep: true, immediate: true,flush: 'post'}) dataOrgin:ref, 响应式数据、数组、getter 函数 deep:深层侦听器,不设置时只会监听对象的地址变化,属性变化没有效果 immediate:创建立马执行一次 flush:'post' : dom 更新完成后在执行。 |
侦听器 watchEffect | 自动跟踪回调的响应式依赖,首次立即执行。同步执行所有响应式数据被追踪,异步追踪 await 执行前的数据源 const unwatch = watchEffect(async() => { const res = await fetch(http://ww.com/$dataOrgin})}, {flush: 'post'}); .... unwatch(); // 停止侦听器 flush:'post' : dom 更新完成后在执行。可以使用 watchPostEffect 实现该效果。 |
v-show vs v-if
v-if
有更高的切换开销,v-show
有公告的初始化渲染开销。频繁切换请使用v-show
。
v-if
是真实的
按条件渲染。条件区块内所有事件监听器与子组件都将被销毁和重建v-show
是懒惰
的,如果初始值为 false 将不会渲染任何内容,只有为 true 才会被渲染。渲染后,不会被销毁,使用display
属性被切换。
v-for vs v-if
v-if 与 v-for 不建议同时使用,因为优先级不明显。但是当一起使用时,v-if
优先级更高,因此v-if
中访问不了v-for
的变量别名。使用以下方式解决:
javascript
<template v-for="todo in todos">
<li v-if="!todo.isShow">
{{ todo.name }}
</li>
</template>
Vue 侦听响应式数组变更
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。以下方法都会触发数组的变更:
- push、pop、shift、unshift、splice、sort、reverse
tips: filter, contat, slice 不会改变原数组。
:key 的作用
2. 组件的使用(组件复用主要是构建模块)
2.1 组件注册实例
- 全局注册(可链式注册)
javascript
import { createApp } from 'vue'
import GlobalRegisterComponentOne from './GlobalRegisterComponentOne.vue'
import GlobalRegisterComponentTwo from './GlobalRegisterComponentTwo.vue'
const app = createApp({})
app
.component('GlobalRegisterComponentOne', GlobalRegisterComponentOne)
.component('GlobalRegisterComponentTwo', GlobalRegisterComponentTwo)
- 局部注册
javascript
<script setup>
import ComponentA from './ComponentA.vue';
</script>
<template>
<CompnentA />
</tempate>
2.2 组件的 Props 传递实例 (defineProps 定义)
- 单向数据流: 父组件的值变化,子组件自动变化,
不能
在子组件中改变props
的值,但是能修改 props 数组内部值,对象的属性值,只是不能更改 props 的值的地址。 - 子组件可将 props 作为初始值,定义内部数据。也可以依赖 props 值进行 computed 计算。
- 属性传递时建议使用
kebab-case
形式, example: 传递时使用user-school
, 使用时是userSchool
javascript
// ParentComponent
<script setup>
import ChildCompnent from './ChildCompnent.vue'
import { ref } from 'vue'
const user = ref({ name: 'yezi', age: 10});
const userSchool= ref('chengdu');
const objectInfo = { id: "100", loveFruit: "香蕉"};
</script>
<template>
<ChildCompnent
:user="user"
:user-school="userSchool"
is-boy {/* is-body: boolean类型,默认转为true */}
:array-data="['跳舞', '唱歌']" {/* 虽然是静态数据,不是动态数据,但是因为数组是一个表达式,不是一个字符串,因此需要动态绑定。 object对象也一样,静态object也需要动态绑定 */}
v-bind="objectInfo" {/* 绑定对象,会被解析为 :id="objectInfo.id" :love-fruit="objectInfo.loveFruit" */}
/>
</template>
// ChildCompnent
<script setup>
{/* type可以是String, Number, Boolean, Array, Object, Data, Funciton, Symbol, 自定义Persion类等。Vue通过 instanceof来校验 */}
const props = defineProps({
user: Object,
userSchool: {
type: String,
required: true, // 传入值必填,不填会报错
},
isBoy: {
type: [Boolean, Number], // 类型允许多类型,但是转换会按类型顺序进行转换。
default: 'defaultValue'
}, // Boolean默认值为false, 其他类型默认为undefined。 可以自定义设置default值
arrayData: Array,
id: String,
loveFruit: string,
});
{/* script中访问props */}
console.log(props.user.name)
{/* 可以将传入的props作为自己内部的值的初始值,操作修改,不影响父级 */}
const selfLoveFruit = ref(props.loveFruit);
{/* 可以对传入值进行computed计算 */}
const selfId = computed(() => props.id + 'self');
{/* 可改变数组的值,不能改变地址 */}
props.arrayData.push('睡觉');
</script>
<template>
<div>id: {{id}}</div>
<div>name: {{user.name}} </div>
<div>userSchool:{{userSchool}} </div>
<div>isBody:{{isBoy}} </div>
<div>兴趣:{{arrayData}} </div>
<div>喜欢的水果:{{loveFruit}}</div>
</template>
2.3 组件事件实例(defineEmits)
- 同 props 一样,推荐使用
kebab-case
形式来编写监听器。 - 组件触发事件没有冒泡机制,同级组件通信需要外部的事件总线或利用
全局状态管理方案
javascript
// ParentCompnent
<script setup>
const addMethod = (a, b) => {
alert(a + b);
}
</script>
<template>
<ChildComponet @add-method="addMethod"/>
</template>
// ChildComponent
<script setup>
const emit = defineEmits(['addMethod']);
const selfAddMethod = () => {
{/* script setup 中不能使用$emit, 需要使用defineEmits的返回值进行调用 */}
emit('addMethod', 10, 11)
}
</script>
<template>
<button @click="selfAddMethod">click me</button>
{/* 方法中调用 */}
<button @click="$emit('addMethod', 10, 11)">click me</button>
</template>
2.4 v-model 自定义修饰符
javascript
// ParentCompnent
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const customName = ref('zhaoyezi');
</script>
<template>
{/* 有参数custom-name, 生成的props名称名称 `customNameModifiers` */}
{/* 没有参数,生成props名称:modelModifiers */}
<ChildComponent v-model:custom-name.upper="customName" v-model.upper="customName"/>
</template>
// ChildComponent
<script setup>
const props = defineProps({
customName: String,
customNameModifiers: { default: () => ({}) },
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(['update:modelValue', 'update:customName']);
const upper = (e, type, needUper) => {
let value = e.target.value;
if (needUper) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
type === 'modelValue' ? emit(`update:modelValue`, value) : emit(`update:modelValue`, value);
};
</script>
<template>
<input type="text" :value="modelValue" @input="(e) => upper(e, 'modelValue', modelModifiers)"/>
<input type="text" :value="customName" @input="(e) => upper(e, 'customName', customNameModifiers)"/>
</template>
v-bind 与 v-model:
- v-model: 双向数据绑定,用于表单控件,如果用在表单控件之外的标签没有效果
- v-bind: 单向数据绑定,会将数据投影到绑定的地方。但是绑定的地方对数据更改时,原始数据不会更改。html 中的属性、css 的样式、对象、数组、number 类型、bool 类型都支持。
javascript
<script setup>
import { ref} from 'vue';
const test = ref('aaa');
</script>
<template>
<input v-model="test" />
<input v-model="test" />
{/* v-model 等价于 */}
<input :value="test" @input="test = $event.target.value"/>
</template>
2.5 v-model 在自定义组件工作(实现双向绑定)
javascript
// ParentComponent
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const customName = ref('zhaoyezi');
const title = ref('zhaoyezi--title');
</script>
<template>
<ChildComponent v-model="customName" v-model:title="title"/>
{/* 等价: */}
<CustomInput
:modelValue="customName"
@update:modelValue="newValue => customName = newValue"
:title="title"
@update:title="newValue => Value = newValue"/>
</template>
// ChildComponent
<script setup>
defineProps({
modelValue: String,
title: String,
})
defineEmits({
'update:modelValue': Function,
'update:title': Function,
})
</script>
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"/>
<input :value="modelValue"/>
<input :value="title" @input="$emit('update:title', $event.target.value)"/>
<input :value="title" />
</template>
2.6 Attributes 的透传
透传Attribute
: 传递给子组件的属性或者事件,没有在子组件中声明为props
或emits
或者 v-on 的事件监听器, 将被透传(可以跨组件层级,父组件属性透传给 子子组件)。
javascript
// ParentComponent
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const customName = ref('zhaoyezi');
const clickEven = () => {
alert(customName.value)
}
</script>
<template>
<ChildComponent v-model="customName" class="wrapperCls" style="color:red" @click="clickEven" />
</template>
// ChildComponent
<script setup>
defineProps({
modelValue: String,
})
</script>
<template>
<button class="selfCls">{{modelValue}}</button>
</template>
最后子组件 button dom 中简析内容为, 并且 click 事件注册在该 button 上。如果子组件嵌套的是另一个子组件,那么同样的道理,会透传给下一层的子组件:
javascript
<button class="wrapperCls selfCls" style="color: red;">
zhaoyezi
</button>
- 禁用 Attributes: 使用
inheritAttrs
属性,并使用$attrs
访问。如果父级中使用wrapper-cls
,则通过$attrs['wrapper-cls']
获取。@click
事件使用$attrs.onClick
获取 - 多根节点访问:默认解析到子组件的根节点,如果想在子组件的子节点中访问,使用
$attrs
进行 - script 中访问透传 attribute, 使用
useAttrs()
javascript
// ParentComponent
<HomeView v-model="customName" custom="custom" class="wrapperCls" style="color:red" @click="clickEven" />
// ChildComponent
<script setup>
import { useAttrs } from 'vue';
defineProps({ modelValue: String})
defineOptions({
inheritAttrs: false
})
const attrs = useAttrs();
console.log(attrs.class)
</script>
<template>
<div class="parent">
<span v-bind="$attrs">{{ $attrs.custom }}</span>
<button>{{ modelValue }}</button>
<TheWelcome />
</div>
</template>
2.7 插槽 Slots
<slot></slot>
可以想象成在子组件中的一个占位符,占位符可提供默认的内容。父级传入的内容将占位符
替换掉。- 插槽的内容无法访问到子组件的状态。可通过插槽像组件那样传递属性,则可在父级中访问子组件的属性
javascript
// ParentCompoennt
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from '../components/ChldComponent.vue'
const dynamicSoltName = ref('footer');
</script>
<template>
<ChildComponent>
{/* 子级传递数据给父级 */}
<template v-slot:header="header"> {{ header.text }} parent header <button @click="header.click">click me</button></template>
{/* slot没有name, 默认名字为default */}
<template #default>没有soltname, 默认解析为 default 的 插槽。 但是建议写上#default</template>
{/* 没有传递内容,使用slot默认内容 */}
<template #left></template>
{/* 动态slot Name */}
<template #[dynamicSoltName]>parent footer</template>
</ChildComponent>
</template>
// ChildComponet
<script setup lang="ts">
import { ref } from 'vue';
const clickChild = () => { alert('parent click child')}
const childText = ref('parent show child content')
</script>
<template>
<div class="chid">
<header>
{/* 传递属性给父级,使得父级可访问自己的数据 */}
<slot name="header" @click="clickChild" :text="childText">headerDetault</slot>
</header>
<main>
<slot name="left">leftDefault</slot>
<slot>content default</slot> <!-- default -->
</main>
<footer>
<slot name="footer">footerDefault</slot>
</footer>
</div>
</template>
<style scoped>
.chid {
display: flex;
flex-direction: column;
}
</style>
2.8 依赖注入
props
都是逐级透传。我们希望父组件定义的属性,无论层级多深,后代组件都能够访问。可使用provide
(依赖提供者)和inject
(注入)来解决。
- 建议尽可能将任何对响应式状态的变更都保持在
供给方组件
中。如果要在注入方
更改数据,可在供给方
提供函数方法 - 希望提供的数据不被
注入方
变更,课使用readonly
包装提供的值 - 如果项目过大,请将注入明使用 Symbol 数据类型,专门放入一个文件中管理
javascript
// ParentComponent
<script setup lang="ts">
import { provide, ref, readonly } from 'vue';
import ChildComponent from '../components/ChldComponent.vue'
const parentSelf = ref('parentSelf');
const location = ref('chengdu');
const updateLocation = (event) => {
location.value = event.target.value;
}
provide('prodiveName', parentSelf);
provide('provideUpdate', {
location: readonly(location),
updateLocation
})
</script>
<template>
parentSelf: <input v-model="parentSelf"/>
parentLocation: <input v-model="location"/>
<ChildComponent />
</template>
// 第一层ChildComponent
<script setup lang="ts">
import Child2Component from './Child2Component.vue';
</script>
<template>
<Child2Component />
</template>
// 第二层component
<script setup lang="ts">
import { inject } from 'vue';
const message = inject('prodiveName', 'default value');
const { location, updateLocation } = inject('provideUpdate') as any;
</script>
<template>
childself: <input v-model="message"/>
childlocation: <input :value="location" @input="updateLocation"/>
{/* 不能变更,因为设置了readonly */}
<input v-model="location"/>
</template>
2.9 异步组件
当希望将应用拆分为更小的块,并实现按需加载,在需要的时候才从服务器加载相关组件。可使用defineAsyncComponent
javascript
// ParentComponet
<script setup lang="ts">
import AsyncComponent from '@/components/AsyncComponent.vue';
import { ref } from 'vue';
const loadChild2 = ref(false);
</script>
<template>
<input type="checkbox" v-model="loadChild2" />
<input type="text" :value="loadChild2"/>
<AsyncComponent v-if="loadChild2" />
</template>
// 异步Component
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import LoadingComponent from './LoadingComponent.vue';
const AsyncCompnent = defineAsyncComponent({
loader: () => import('./Child2Component.vue'),
loadingComponent: LoadingComponent, // 加载异步组件时使用的组件
delay: 200, // 展示加载组件前的延迟时间,默认为 200ms
// 加载失败后展示的组件
// errorComponent: ErrorComponent,
timeout: 3000 // 如果提供了一个 timeout 时间限制,并超时也会显示这里配置的报错组件,默认值是:Infinity
})
</script>
<template>
<AsyncCompnent />
</template>
显示结果:可以看到,当点击 radios 选中,才回去加载Child2Component
组件。
3. 逻辑的复用
3.1 组合式函数(复用主要是有状态的逻辑封装)
利用 Vue 的组合式 API 来封装和复用 有状态逻辑
的函数。例如以下例子,注册事件与清除事件在项目中是比较常见的,将其抽取为组合式函数
进行使用:
- 组合式函数约定用驼峰命名法命名,并以"use"作为开头
- toValue: 输入
组合是函数
的参数是ref
会getter
,请利用toValue()
工具函数。将可能的 ref 或 getter 解包。 组合式函数
中推荐使用ref()
,而不是reactive()
。因为ref()
函数作为返回之后能在组建中正确解构
后仍然可以保持响应式
- 只能在
<script setup>
或者setup()钩子
中被调用。有时可以在onMounted()
这样的生命周期钩子中调用
javascript
// Composables.js 事件钩子
import { onMounted, onUnmounted } from "vue"
export const useEventComposables = (target, event, callback) => {
onMounted(() => {
target.addEventListener(event, callback);
});
onUnmounted(() => {
target.removeEventListener(event, callback);
})
}
// MouseComposables.js 鼠标移动钩子
import { ref, toValue, watch, watchEffect } from 'vue';
import { useEventComposables } from './Composables'
export const useMouseComposables = (url) => {
const mouseX = ref(0);
const mouseY = ref(0);
const newUrl = ref(toValue(url) + '_' + (new Date().valueOf()) );
// watch(url, (newOption, oldOption) => {
// newUrl.value = newOption + '_' + (new Date().valueOf());
// console.log(newUrl.value)
// });
watchEffect(() => {
newUrl.value = toValue(url) + '_' + (new Date().valueOf());
})
// 内部使用组合式函数
useEventComposables(window, 'mousemove' , (event) => {
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});
return { mouseX, mouseY, newUrl }
}
// ParentComponent.vue 钩子使用
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useMouseComposables } from '../components/MouseComposables';
// 组件式函数可以接收响应式状态数据,内部使用监听函数处理。例如异步请求可以按这种方式封装
const url = ref('customURL');
const { mouseX, mouseY, newUrl } = useMouseComposables(url);
// 如果喜欢以对象的形式访问,可以使用reactive包裹,mouseInfo的mouseX, mouseY, newUrl也能正确解构
const mouseInfo = reactive(useMouseComposables(url));
</script>
<template>
鼠标位置:<br>
<div>mouseX: {{ mouseX }}</div>
<div>mouseY: {{ mouseY }}</div>
模拟URL 外部变化
<input v-model="url"/>
<div>newUrl通过组合是函数变化值: {{ newUrl }}</div>
</template>
3.2 自定义指令(重用涉及普通元素的底层 DOM 访问的逻辑)
定义格式(各个方法参数通简格式一致):
javascript
const myDirective = {
// 在绑定元素的 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) {},
}
app.directive('customDirective', myDirective)
简写格式:
javascript
app.directive('customDirective', (el, binding, vnode, prevNode) => {})
- el: 绑定到的元素。这可以用于直接操作 DOM
- binding:
- value: 传递给指令的值。
v-customDirective="zhaoyezi"
, 则 value 为zhaoyezi
- oldValue: 更改前的值。仅在
beforeUpdate
和updated
中可用 - arg: 传递给指令的参数。
v-customDirective:foo="zhaoyezi"
, 则arg
是foo
- modifiers: 修饰符对象。
v-customDirective:foo.stop.once="zhaoyezi"
, 则修饰符是{stop:true, once:true}
- instance: 指令的组件实例
- dir:指令的定义对象(不是简写里的那些生命周期方法 mounted, beforeMount)
节流函数指令(防止表单重复提交)
网上的指令实例普遍有 3 个,节流函数,图片延时加载,一键 copied 功能。这里使用节流函数实例。
javascript
// 指令注入
app.directive('throttle', (el, binding, vnode, prevNode) => {
const throttleTime = binding.value || 2000;
let func;
el.addEventListener('click', event => {
if (!func) {
func = setTimeout(() => {
func = null;
}, throttleTime)
} else {
// stopImmediatePropagation: 多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。
event && event.stopImmediatePropagation();
}
}, true)
})
// 指令使用
<script setup>
const helloEvent = () => {
console.log('hello yezi');
}
</script>
<template>
<button @click="helloEvent" v-throttle>zhaoyezi</button>
</template>
3.3 插件
插件就是一种可以为Vue
添加全局功能的工具代码。插件主要应用的场景:
- 通过
app.component()
或app.directive()
注册一到多个全局组件或自定义指令 - 通过
app.provide()
使得资源被注入到整个应用 - 向
app.config.globalProperties
中添加一些全局实例属性或方法 - 可能是以上 3 中的任意组合或者全部功能都有(例如 vue-router)
基本格式:
javascript
const app = createApp(AppVue)
app.use(
{
// options: 第二个参数options就是app的第二个自己输入的参数{ customOptions: 'xxx'}
install: (app, options) => {
// $zhaoyezi: 将在全局可以使用
app.config.globalProperties.$zhaoyezi = { a: 1 }
console.log(options)
},
},
{ customOptions: 'xxx' },
)
// 还可以使用以下方式定义
app.use(
{
install(app, options) {
app.config.globalProperties.$zhaoyezi = { a: 1 }
console.log(options)
},
},
{ customOptions: 'xxx' },
)
各种类型的注入,使用的时候不再需要引入:
javascript
// 入口main.ts
import { createApp } from 'vue'
import ChildComponent from './ChildComponent.vue';
import LoadingComponentVue from './components/LoadingComponent.vue';
const app = createApp(AppVue);
const myPligun = {
install: (app, options) => {
console.log(options)
// 添加全局属性
app.config.globalProperties.$zhaoyehong = { buttonText: 'click me' };
// 添加全局指令
app.directive('throttle', (el, binding, vnode, prevNode) => {
const throttleTime = binding.value || 2000;
let func;
el.addEventListener('click', event => {
if (!func) {
func = setTimeout(() => {
func = null;
}, throttleTime)
} else {
event && event.stopImmediatePropagation();
}
}, true)
});
// 依赖注入
app.provide('i18n', options);
// 组件注入
app.component('LoadingComponent', LoadingComponentVue)
}
}
app.use(myPligun, { mutiName: '多国语文件' })
app.mount('#app');
// 消费组件ChildComponent
<script setup lang="ts">
import { inject } from 'vue';
const helloEvent = () => {
console.log('hello yezi');
}
const i18n = inject('i18n');
</script>
<template>
<button @click="helloEvent" v-throttle>{{$zhaoyehong.buttonText}}</button>
<div>多国语文件消费:{{ i18n.mutiName }}</div>
{/* 直接使用,不引入 */}
<LoadingComponent/>
</template>
4. 内置组件
4.1 <Transition>
组件
组件class
- v-enter-from: 进入动画的起始状态。元素插入之前添加,元素插入完成的下一帧移除
- v-enter-active: 进入动画的生效状态。整个动画阶段应用。元素插入之前添加,动画过渡或完成后移除。
- v-enter-to: 进入动画结束状态。v-enter-from 移除时添加,动画过度完成后移除
- v-leave-from:起来动画的起始状态。离开过渡效果被触发时添加,在一帧后被移除
- v-leave-active: 离开动画的生效状态,整个离开动画阶段应用。在过渡或动画完成后移除。
- v-leave-to: 离开动画的结束状态。v-leave-to移除的时候添加,在过渡或动画完成之后移除
v-enter-active, v-leave-active: 提供了为进入和离开动画指定不同速度曲线的能力。
组件钩子函数
可通过监听<Transition/>
组件事件的方式在过度过程中挂上钩子函数。
javascript
// 在元素被插入到 DOM 之前被调用。用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用。用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用。 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用。用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
</Transition>
组件应用(css版本)
- 可以自定义 Transition name。 class会将由
v-enter-active
=>customName-enter-active
- 可自定义class名称:
enter-active-class="animate_animated"
, 则将使用animate_animated
替换enter-active-class
。有以下class可以自定义enter-from-class/enter-active-class/enter-to-class/leave-from-class/leave-active-class/leave-to-class
- 解决内嵌dom的动画:因为Transition只能监听过渡根元素上的第一个
transitionend
和animationend
来判断过渡动画何时结束,而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成,因此需要向组件传递duration
来指定过度动画的持续时间(例子请看下面的第二个动画)。
javascript
<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(false);
</script>
<template>
<button @click="isShow = !isShow">change is SHOW</button>
<Transition>
<div v-if="isShow">Transation</div>
</Transition>
<!-- 也可以详细指定enter, leave时间 :duration="{ enter: 500, leave: 800 }" -->
<Transition
duration="1000"
name="custom-transition"
enter-active-class="animate_animated"
>
<div v-if="isShow" class="outer"> <div class="inner"> Transation 内嵌</div></div>
</Transition>
<div>
<div :class="isShow ? 'v-enter-show' : 'v-enter-hidden'">Transation 2</div>
</div>
</template>
<style scoped>
/* 原始名字 */
.v-enter-active, .v-leave-active {
transition: all 1s cubic-bezier(1, 0.5, 0.8, 1);
}
.v-enter-from, .v-leave-to {
transform: translateX(20px);
opacity: 0;
}
/* 自定义名字,自定义class */
.animate_animated, .animate_animated .inner, .custom-transition-leave-active, .custom-transition-leave-active .inner{
transition: all 1s cubic-bezier(1, 0.5, 0.8, 1);
}
.animate_animated .inner {
transition-delay: 0.25s;
}
.custom-transition-enter-from, .custom-transition-enter-from .inner, .custom-transition-leave-to, .custom-transition-leave-to .inner {
transform: translateX(20px);
opacity: 0;
}
.outer {
background-color: blanchedalmond;
}
/* 自己实现普通的动画效果 */
.v-enter-show {
opacity: 1;
}
.v-enter-hidden {
opacity: 0;
transform: translateX(20px);
}
.v-enter-show, .v-enter-hidden {
transition: all 1s cubic-bezier(1, 0.5, 0.8, 1);
}
</style>
组件应用 mode="out-in"
等上一个动画结束再进行下一个动画,感觉和使用动画延迟属性实现的效果一样。
javascript
<script setup lang="ts">
import { ref } from 'vue'
const showType = ref(-1)
const changeType = () => {
if (showType.value === 2) {
showType.value = 0
return
}
showType.value++
}
</script>
<template>
<button @click="changeType">click me</button>
<div style="height: 50px;display: flex; flex-direction: column; justify-content: center; border: 1px solid #ddd; width: 500px;overflow: hidden;">
<Transition mode="out-in">
<button v-if="showType === 0">Saved</button>
<button v-else-if="showType === 1">edited</button>
<button v-else-if="showType===2">editing</button>
</Transition>
</div>
</template>
<style scoped>
button {
width: 100px;
}
.v-enter-active,
.v-leave-active {
transition: all 0.4s ease-in;
}
{/* 不添加mode="out-in",添加这个效果差不多 */}
{/* .v-enter-active {
transition-delay: 0.3s;
} */}
.v-enter-from {
transform: translateY(30px);
opacity: 0;
}
.v-leave-to {
transform: translateY(-30px);
opacity: 0;
}
</style>
4.2 内置组件 <TransitionGroup>
组件
与<Transition>
组件基本有相同的props、css过度class,和JavaScript钩子监听函数。但是有以下区别:
- 默认
<TransitionGroup>
该容器是不渲染,可通过tag
属性,指定将父容器渲染为什么标签。例如tag='ul'
, 则TransitionGrop
将被渲染为ul标签
- 过度模式
mode="out-in"
不可用 - 列表的每个元素需要有唯一的
key
- css过度class被应用在列表元素上,而不是容器元素上
javascript
<script setup lang="ts">
import { ref } from 'vue'
const showData = ref([1, 2, 3, 4, 5])
const removeData = () => {
showData.value.shift();
}
const addData = () => {
showData.value.push(new Date().valueOf())
}
</script>
<template>
<button @click="removeData">removeData</button>
<button @click="addData">addData</button>
<div style="display: flex; flex-direction: column; justify-content: center; border: 1px solid #ddd; width: 500px;overflow: hidden;">
<TransitionGroup tag="ul">
<li v-for="item in showData" :key="item">{{ item }}</li>
</TransitionGroup>
</div>
</template>
<style scoped>
.v-move,.v-enter-active .v-leave-active {
transition: all 0.4s ease-in;
}
.v-enter-from,.v-leave-to {
transform: scaleY(0.01) translate(30px, 0);
}
.v-leave-active {
position: absolute;
}
</style>
4.3 内置组件 <KeepAlive>
组件
- 在多个组件相互切换过程中,可以缓存被移除的组件实例。将保留之前组件所有的状态。
KeepAlive
默认保留所有的组件实例,但是可以通过include
和exclude
来包含或排出。例如下面的例子:只有当值为B或者C的时候,才进行缓存:max="10"
:被缓存的最大组件实例数。缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间- onActivated 与 onDeactivated: 缓存实例的生命周期。疑问(为什么没有执行)?
javascript
// Child.vue
<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue'
const witchBox = ref('A')
onActivated(() => {
console.log('调用时机为首次挂载以及每次从缓存中被重新插入时');
})
onDeactivated(() => {
console.log('在从 DOM 上移除、进入缓存.以及组件卸载时调用');
})
</script>
<template>
<input type="text" v-model="witchBox" />
</template>
<style scoped></style>
// Parent.vue
<script setup lang="ts">
import { ref } from 'vue'
import Child from '../components/Child.vue'
const witchBox = ref('A')
</script>
<template>
<input type="radio" id="A" value="A" v-model="witchBox" /><label for="A">A</label>
<input type="radio" id="B" value="B" v-model="witchBox" /><label for="B">B</label>
<input type="radio" id="C" value="C" v-model="witchBox" /><label for="C">C</label><br />
<KeepAlive :max="10" include="A"><Child v-if="witchBox === 'B' || witchBox === 'C'" /> </KeepAlive><br />
<Child v-if="witchBox === 'A'" /><br />
</template>
<style scoped></style>
4.4 内置组件 <Teleport>
组件
- 将一个组件内部的一部分模板"传送"到该组件的 DOM 结构外层的位置去。
- 多个Teleport挂在到同一个目标元素(例子中的#root,记得在index.html模板中必须有#root 元素)
:disabled
: 禁用Teleport,不会挂在到外部元素
javascript
<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(true);
</script>
<template>
<div>this is vue content</div>
<Teleport to="#root">
<div class="modal">
this is modal1
</div>
</Teleport>
<Teleport to="#root">
<div class="modal">
this is modal2
</div>
</Teleport>
<Teleport to="body" :disabled="isShow">
<div class="modal">
this is modal
</div>
</Teleport>
</template>
<style scoped>
.modal {
border: 1px solid;
}
</style>