VUE3的内置指令用法实例以及生命周期函数的使用和原理探索
const text = "这是测试v-text"
<h1 v-text="text"></h1>
const html = "这是测试v-html"
<h1 v-html="html"></h1>
const id = "123"
const flag = true
const style = {color: "green"}
<h1 :id="id">这是v-bind绑定id</h1>
<h1 class="c" :class="['a','b']">这是v-bind绑定class,测试动态增加class</h1>
<h1 :class="[flag ? 'a' : 'b']">这是v-bind绑定class,测试表达式</h1>
<h1 :style="style">这是v-bind绑定style</h1>
<style>
.a { color:red; }
.b{ font-size: smaller; }
.c { font-family: 'Times New Roman', Times, serif; }
</style>
h1标签加了v-once之后,无论按钮点击多少下,值始终为0,不会改变。v-once的使用场景为:当多次触发某些操作时,又不想频繁更新某一个组件时,就可以使用v-once来限制对应的组件。
import { ref } from 'vue'
let count = ref<number>(0)
const once = () => {
count.value++;
console.log(count.value)
}
<button @click="once" style="height: 100px; width: 100px; background: green;"></button>
<div>
<h1 v-text="count" v-once></h1>
</div>
v-memo的作用场景和v-once的使用场景相似,但是比v-once更加灵活,v-memo可以通过自定义限制条件,通过条件是否满足,进而来处理组件。以下场景:当点击了按钮之后,clicked的值变成了0,并且同时我们改变了第一条数据和第二条数据的值,但是发现,渲染的时候只改变了第一条数据。而第二条数据并没有发生变化,这就说明,v-memo中条件成立的数据才会被重新渲染。
import { ref, reactive } from 'vue'
const clicked = ref(1)
const list = reactive([
{ id: 0, name: "000" },
{ id: 1, name: "111" },
{ id: 2, name: "222" },
{ id: 3, name: "333" },
]);
const memo = () => {
clicked.value = 0;
list[0].name = "1"; //修改结果 1
list[2].name = "2"; // 修改结果 2
};
<button @click="memo" style="height: 100px; width: 100px; background: green">memo</button>
<div v-for="(item, index) in list" v-memo="[index == clicked]" :key="index">
<p>index: {{ item }} - selected: {{ clicked }}</p>
</div>
<div v-for="(item, index) in list" :key="index">
<p> {{ item }} </p>
</div>
用来绑定事件,可以是一个方法名,也可以是一个表达式。对于普通函数的调用,还可以通过以下修饰符来处理自定义事件,比如使用.stop来阻止组件的事件触发。
.stop
:阻止父组件事件的触发。也就是只执行自己的函数事件。.{keyAlias}
:只在某些按键下触发处理函数。.prevent
:阻止表单默认事件,会阻止form当中的action行为<button v-on:click="change" style="width: 100px; height: 30px; background: green">改变值</button>
<button @click="change" style="width: 100px; height: 30px; background: green">改变值</button>
// .once只会让事件触发一次
<button @click.once="change" style="width: 100px; height: 30px; background: green">改变值</button>
// .stop会阻止parent事件的触发
<div @click="parent">
<p>{{ message }}</p>
<button @click.stop="change1" style="width: 100px; height: 30px; background: green">改变值</button>
</div>
// .prevent阻止跳转到https://www.baidu.com
<form action="https://www.baidu.com">
<button @click.prevent="change2" style="width: 100px; height: 30px; background: green">
改变值
</button>
</form>
import { ref } from 'vue'
const message = ref('This is a page about stuff')
const change = () => {
console.log('change被点击了')
message.value = 'Hello Vue 3!'
}
const change2 = () => {
console.log('change2被点击了')
message.value = 'Hello Vue 3!'
}
动态事件:
<button v-on:[event]="change" style="width: 100px; height: 30px; background: green">改变值</button>
<button @[event]="change" style="width: 100px; height: 30px; background: green">改变值</button>
<input v-model="message" />
import { ref } from 'vue'
const message = ref('This is a page about stuff')
<span v-pre>{{ const a = 1; const b = 2; }}</span>
v-show的原理是在组件上增加display样式属性,来达到显示或者隐藏组件的目的。频繁的操作显示隐藏的场景时,使用v-show性能更好。
<button @click="isShow = !isShow" style="width: 100px; height: 30px; background: green">显示隐藏</button>
<span v-show="isShow">显示了</span>
const isShow = ref(true)
v-if的原理是通过创建或者销毁组件,来达到显示或者隐藏组件的目的,一般用在组件在第一次加载的场景时,使用v-if性能更好。
<button @click="isShow = !isShow" style="width: 100px; height: 30px; background: green">显示隐藏</button>
<span v-if="isShow">显示了</span>
const isShow = ref(true)
vue中的插槽使得组件设计更加灵活,可以在组件中使用插槽设计灵活的满足组件上动态的化的展示。
当我们直接使用
定义插槽的时候,此时的插槽称为匿名插槽。也就是说没有名字的插槽,实际上VUE会这个插槽默认设置一个default的名字。
// SoltComponetents.vue
<template>
<div class="header">
<slot>这是子组件header内容</slot>
</div>
</template>
<script setup lang="ts"></script>
<style></style>
// PComponents.vue
<template>
<SoltComponetents>
<template #default></template>
</SoltComponetents>
</template>
<script setup lang="ts">
import SoltComponetents from '../solt/SoltComponetents.vue'
</script>
<style>
.header {
background: turquoise;
width: 100px;
height: 100px;
}
.main {
background: rgb(226, 148, 92);
width: 100px;
height: 100px;
}
.bottom {
background: rgb(43, 31, 204);
width: 100px;
height: 100px;
}
</style>
当我们直接使用
定义插槽的时候,此时的插槽称为具名插槽。也就是说给插槽起了个名字叫做main。父组件在使用插槽的时候,使用使名称为
main
的插槽生效,其中#main
是v-solt:main
的缩写。
// SoltComponetents.vue
<template>
<div class="main">
<slot name="main">这是子组件main内容</slot>
</div>
</template>
<script setup lang="ts"></script>
<style></style>
// PComponents.vue
<template>
<SoltComponetents>
<template #main></template>
</SoltComponetents>
</template>
<script setup lang="ts">
import SoltComponetents from '../solt/SoltComponetents.vue'
</script>
<style>
.header {
background: turquoise;
width: 100px;
height: 100px;
}
.main {
background: rgb(226, 148, 92);
width: 100px;
height: 100px;
}
.bottom {
background: rgb(43, 31, 204);
width: 100px;
height: 100px;
}
</style>
当我们直接使用
定义插槽的时候,可以在插槽内定义数据,并将数据传递到父组件使用
// SoltComponetents.vue
<template>
<div class="bottom">
<slot name="bottom" :data="data" :count="count">{{ pData }}</slot>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref('这是子组件定义的数据')
const count = ref(100)
</script>
<style></style>
// PComponents.vue
<template>
<SlotComponetents>
// {{ pros.data }} -- {{ pros.count }} // 如果参数比较多,接受的时候直接使用对象接受,比如使用pros接受,使用pros.count获取对象中count属性的值
// 如果参数比较少,接受的时候直接使用ES6的{}进行解构接受,这样看起来比较直观
<template #bottom="{ data, count }"> {{ data }} -- {{ count }} </template>
</SlotComponetents>
</template>
<script setup lang="ts">
import SlotComponetents from '../slot/SlotComponetents.vue'
</script>
<style>
.header {
background: turquoise;
width: 100px;
height: 100px;
}
.main {
background: rgb(226, 148, 92);
width: 100px;
height: 100px;
}
.bottom {
background: rgb(43, 31, 204);
width: 100px;
height: 100px;
}
</style>
动态插槽就是动态的设置插槽的名称,以达到动态设置插槽的目的,实际项目中经常使用,这里不做介绍。
注意:
或者
标签,那么匿名插槽将不会展示。
标签或者一旦指定了任意插槽的名字,即使当前指定的插槽的名称不是匿名插槽的名字,匿名插槽也会被展示。VUE3 setup
语法糖模式下,主要有以下几个生命周期函数。
缓存组件时,当组件被加载后会执行这个函数
缓存组件时,当组件被销毁后会执行这个函数// // PComponents.vue
<template>
<button
@click="show = !show"
style="width: 80px; height: 40px; background: green"
>
删除组件
</button>
<keep-alive>
<SlotComponetents v-if="show">
<template #bottom="pro">
{{ pro.data }} -- {{ pro.count }}
</template>
</SlotComponetents>
</keep-alive>
</template>
<script setup lang="ts">
import SlotComponetents from '../slot/SlotComponetents.vue'
import { ref } from 'vue'
const show = ref(true)
</script>
<style>
.header {
background: turquoise;
width: 600px;
height: 100px;
}
.main {
background: rgb(226, 148, 92);
width: 600px;
height: 100px;
}
.bottom {
background: rgb(43, 31, 204);
width: 600px;
height: 100px;
}
</style>
// SoltComponetents.vue
<template>
<div class="bottom" ref="mainDiv">
<slot name="bottom" :data="data" :count="count"></slot>
<button
@click="add"
style="width: 80px; height: 40px; background: green"
>
加1
</button>
</div>
</template>
<script setup lang="ts">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated
} from 'vue'
const data = ref('这是子组件定义的数据')
const count = ref(100)
const mainDiv = ref<HTMLElement>()
const add = () => {
count.value = count.value + 1
}
// 页面加载完成前执行这个生命周期函数,此时不可以获取操作DOM
onBeforeMount(() => console.log('before mount', '获取DOM:', mainDiv.value))
// 页面加载完成后执行这个生命周期函数,此时可以获取操作DOM
onMounted(() => console.log('mounted', '获取DOM:', mainDiv.value))
// 在改变变量的值之前执行这个生命周期函数
onBeforeUpdate(() => console.log('before update', '获取DOM:', mainDiv.value?.innerHTML))
// 在改变变量的值之后执行这个生命周期函数
onUpdated(() => console.log('updated', '获取DOM:', mainDiv.value?.innerHTML))
// 页面加载销毁前执行这个生命周期函数,此时可以获取操作DOM
onBeforeUnmount(() => console.log('before unmount', '获取DOM:', mainDiv.value))
// 页面加载销毁后执行这个生命周期函数,此时不可以获取操作DOM
onUnmounted(() => console.log('unmounted', '获取DOM:', mainDiv.value))
// 使用 缓存组件时,当组件被加载后会执行这个函数
onActivated(() => console.log('active', '获取DOM:', mainDiv.value?.innerHTML))
// 使用 缓存组件时,当组件被销毁后会执行这个函数
onDeactivated(() => console.log('inactive', '获取DOM:', mainDiv.value?.innerHTML))
</script>
<style></style>
VUE3的生命周期函数的定义在apiLifecycle.ts
这个文件中定义的,在这个文件中,通过一下方式导出了常用的生命周期函数,每一个生命周期函数都调用了createHook
这个函数,参数实际上是各个生命周期函数的缩写。
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
createHook
函数实际上会调用injectHook
,injectHook
中有一个特殊的参数target
代表的是当前组件的实例currentInstance
,而之后,所有的生命周期函数都会被注册到当前实例currentInstance
上:// 主要是将生命周期函数挂载到当前实例上,以便于后期渲染页面时调用
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
// 先从当前实例上获取生命周期函数,如果没有那就初始化为一个空的数组
const hooks = target[type] || (target[type] = []);
// cache the error handling wrapper for injected hooks so the same hook
// can be properly deduped by the scheduler. "__weh" stands for "with error
// handling".
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
// 如果当前实例被销毁了,那就不用注册了,直接返回,相当于做了一个缓存
if (target.isUnmounted) {
return;
}
// disable tracking inside all lifecycle hooks
// since they can potentially be called inside effects.
// 这里先取消依赖收集,防止重复收集依赖,因为在组件初始化的时候,就已经进行过依赖收集了。
pauseTracking();
// Set currentInstance during hook invocation.
// This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
// 将target设置为当前实例
setCurrentInstance(target);
// 并且执行相应的生命周期函数
const res = callWithAsyncErrorHandling(
hook,
target,
type,
args
);
// 释放当前实例
unsetCurrentInstance();
// 恢复依赖收集
resetTracking();
return res;
});
if (prepend) {
hooks.unshift(wrappedHook);
} else {
hooks.push(wrappedHook);
}
return wrappedHook;
} else if (__DEV__) {
const apiName = toHandlerKey(
ErrorTypeStrings[type].replace(/ hook$/, "")
);
warn(
`${apiName} is called when there is no active component instance to be ` +
`associated with. ` +
`Lifecycle injection APIs can only be used during execution of setup().` +
(__FEATURE_SUSPENSE__
? ` If you are using async setup(), make sure to register lifecycle ` +
`hooks before the first await statement.`
: ``)
);
}
}
生命周期函数的调用在renderer.ts
中,componentUpdateFn
函数中会进行函数的调用
const componentUpdateFn = () => {
// 先判断下当前组件是不是已经挂载了
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined;
const { el, props } = initialVNode;
const { bm, m, parent } = instance;
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode);
toggleRecurse(instance, false);
// beforeMount hook
// bm就是beforeMount钩子的缩写,如果当前实例上有bm,则先执行bm
if (bm) {
invokeArrayFns(bm);
}
// ………………省略部分源码………………
if (el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
// ………………省略部分源码………………
// 此时还没有DOM的化,执行下边的逻辑之后,就会将DOM挂载到VNode上,之后就存在DOM了,执行mounted操作就可以操作DOM了。
} else {
if (__DEV__) {
startMeasure(instance, `render`);
}
const subTree = (instance.subTree = renderComponentRoot(instance));
if (__DEV__) {
endMeasure(instance, `render`);
}
if (__DEV__) {
startMeasure(instance, `patch`);
}
// 将VNode装载到容器当中,此后就会有DOM
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
);
if (__DEV__) {
endMeasure(instance, `patch`);
}
initialVNode.el = subTree.el; // 挂载DOM
}
// mounted hook
// m就是mounted的缩写,这里执行onMounted生命周期函数
if (m) {
queuePostRenderEffect(m, parentSuspense);
}
// ………………省略部分源码………………
} else {
// ………………省略部分源码………………
// beforeUpdate hook
// bu就是beforeUpdate的缩写,这里开始执行onBeforeUpdate生命周期函数
if (bu) {
invokeArrayFns(bu);
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode);
}
// ………………省略部分源码………………
// updated hook
// u就是updated,这里开始执行onUpdated生命周期函数
if (u) {
queuePostRenderEffect(u, parentSuspense);
}
// ………………省略部分源码………………
}
};
// unmount进行组件卸载,这里调用unmountComponent函数清空收集的所有依赖
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
// ………………省略部分源码………………
// 清空收集的所有依赖
unmountComponent(vnode.component!, parentSuspense, doRemove)
// 清空所有的依赖之后,就会执行onBeforeUnmount生命周期函数
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, "beforeUnmount");
}
// 接着还会调用该函数删除DOM下边的所有子树
unmountChildren(dynamicChildren, parentComponent, parentSuspense, false, true);
// 最后执行onUnmounted函数,此时表明组件已经卸载完成
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
// ………………省略部分源码………………
};