动画
Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果。
没有动画的情况下,整个内容的显示和隐藏会非常的生硬: 如果我们希望给单元素或者组件实现过渡动画,可以使用 transition
内置组件来完成动画。
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)条件展示 (使用 v-show)
- 动态组件
- 组件根节点
当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
- 自动嗅探目标元素是否应用了CSS过渡(transition)或者动画(animation),如果有,那么在恰当的时机添加/删除 CSS类名。
- 如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用。
- 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行。将不会有动画效果。
<button @click="isShow = !isShow">显示/隐藏</button> <transition name="zh"> <h2 v-if="isShow">Hello World</h2> </transition> <style scoped> .zh-enter-from, .zh-leave-to { opacity: 0; } .zh-enter-to, .zh-leave-from { opacity: 1; } // 为添加过度属性,将不会出现动画效果。 .zh-enter-active, .zh-leave-active { /* transition: opacity 2s ease; */ } </style>
class属性添加时机
过渡动画class
Vue就是帮助我们在这些class之间来回切换完成的动画:
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。 当我们没有对transition组件命名时,即给出name属性,那么它将默认使用v当做name属性值。即所有的class是以v-
作为默认前缀。给定name属性后,将会根据name属性值多为class前缀。
css动画和css过度
vue中,完成动画我们需要借助动画animation
和过度transition
来完成。我们需要在 v--enter-active
和 v-leave-active
class属性值中定义上面两个属性即可。
// css过度 .zh-enter-from, .zh-leave-to { opacity: 0; } .zh-enter-to, .zh-leave-from { opacity: 1; } .zh-enter-active, .zh-leave-active { transition: opacity 2s ease; }
// css动画 .zh-enter-active { animation: bounce 1s ease; } .zh-leave-active { animation: bounce 1s ease reverse; } @keyframes bounce { 0% { transform: scale(0) } 50% { transform: scale(1.2); } 100% { transform: scale(1); } }
transition组件属性
name
属性
为了设置class前缀。
type
属性
Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:
- 如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听。但是如果我们同时使用了过渡和动画呢?
- 并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束。
- 在这种情况下,我们可以设置
type
属性为animation
或者transition
来明确的告知Vue监听的类型。并且可以通过duration
显式的指定动画时间。
<transition name="zh" type="transition" :duration="{enter: 800, leave: 1000}"> <h2 class="title" v-if="isShow">Hello World</h2> </transition>
duration
属性
设置过度动画的时间。
duration可以设置两种类型的值:
- pnumber类型:同时设置进入和离开的过渡时间。
- pobject类型:分别设置进入和离开的过渡时间。注意:显式设置的值会覆盖css过度和动画中指定的值。
mode
属性
默认情况下,进入和离开同时发生。如果想改变默认状态。我们就需要添加mode属性。
in-out
: 新元素先进行过渡,完成之后当前元素过渡离开。
out-in
: 当前元素先进行过渡,完成之后新元素过渡进入。大多数情况下,我们需要前一个动画结束时,后一个动画开始。所以需要使用out-in
appear
属性
默认情况下,首次渲染的时候是没有动画的,如果我们希望给他添加上去动画,那么就可以增加appear;ture
。
css
属性
当我们通过js来操作动画的时候,我们就不需要vue来检测css中的动画了。所以需要将css设置为false。默认情况下css: true
。
JavaScript 钩子
当我们想要在动画执行的各个阶段,做一些事情。我们就可以使用这个钩子。
- @before-enter="beforeEnter",执行到v-enter-from阶段
- @enter="enter",执行到v-enter-active
- @after-enter="afterEnter",执行到v-enter-to阶段
- @enter-cancelled="enterCancelled"
- @before-leave="beforeLeave",执行到v-leave-to阶段
- @leave="leave",执行到v-leave-active阶段
- @after-leave="afterLeave",执行到v-leave-to阶段
- @leave-cancelled="leaveCancelled",执行到v-enter-to阶段 当只用 JavaScript 过渡的时候,在
enter
和leave
钩中必须使用done
进行回调。否则,它们将被同步调用,过渡会立即完成。这时,我们可以添加:css="false"
,让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
上面的钩子可以和一些js动画库来实现动画。例如jsap库。 它可以通过JavaScript为CSS属性、SVG、Canvas等设置动画,并且是浏览器兼容的。其中有两个比较重要的API来实现动画。
jsap.from(el, options)
: 表示动画从什么状态开始。
jsap.to(el, options)
: 表示动画以什么状态结束。 其中el表示动画作用的元素。options表示动画css属性。
// 进入动画 enter(el, done) { console.log('enter') // from是表示开始的位置 gsap.from(el, { scale: 0, x: 200, onComplete: done, }) }, // 离开动画 leave(el, done) { console.log('leave') // to表示结束的位置 gsap.to(el, { scale: 0, x: 200, onComplete: done, }) },
我们来使用jsap库实现一个滚动数字动画。
<template> <div class="app"> <input type="number" step="100" v-model="counter"> <h2>当前计数: {{showNumber.toFixed(0)}}</h2> </div> </template> <script> import gsap from 'gsap'; export default { data() { return { counter: 0, showNumber: 0 } }, watch: { counter(newValue) { gsap.to(this, {duration: 1, showNumber: newValue}) } } } </script>
其他动画知识,请访问官网,讲的非常详细。
composition API
一下使用的API都需要在vue中导入。
Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
- Options API的一大特点就是在对应的属性中编写对应的功能模块。比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
- 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中。
- 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散。
- 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人)。
composition API介绍
composition API的容器
其中composition API都是写在我们setup
函数中的。并且在setup函数中是不能使用this的,因为vue内部再调用setup函数的时候没有绑定this。
下面我们就来研究一些setup函数。
- 它主要有两个参数:
- 第一个参数:
props
。组件接收的属性
- 第二个参数:
context
。组件上下文对象
- attrs:所有的非prop的attribute。
- slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用)。
- emit:当我们组件内部需要发出事件时会用到emit。
- expose:当通过ref获取该组件时,向外暴露的一些setup中的数据。那我们如何定义响应式数据呢?
composition API处理数据
reactive
: 将多个数据变成响应式数据
- 当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集。
- 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)。
- 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的。
const state = reactive({ counter: 100, name: 'zh' })
ref
: 将单个数据变成响应式。
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告。所以我们需要使用ref。
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源。
- 它内部的值是在ref的 value 属性中被维护的。
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用。
- 在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式。注意:ref对象在模板中的解包是浅层的解包
readonly
: 返回一个传入的对象的只读代理
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改。例如我们想要将provide提供数数据传递给子孙组件,我们就可以使用readonly,让其是只读的,不能再子孙组件中修改。
该API返回普通对象, ref对象, reactive对象的只读代理。
- readonly返回的对象都是不允许修改的。
- 但是经过readonly处理的原来的对象是允许被修改的。
- 其实本质上就是readonly返回的对象的setter方法被劫持了而已。
// 1.普通对象 const info1 = {name: "zh"}; const readonlyInfo1 = readonly(info1); // 2.响应式的对象reactive const info2 = reactive({ name: "zh" }) const readonlyInfo2 = readonly(info2); // 3.响应式的对象ref const info3 = ref("zh"); const readonlyInfo3 = readonly(info3);
toRefs
: 将传入的对象变成ref对象
当我们想要对reactive对象做解构的时候,直接解构,将使数据失去响应式。如果我们用toRefs将其包裹后解构,数据依然是响应式的。这种做法相当于已经在reactive中的属性和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化
const info = reactive({ name: 'zh', age: 22 }) // 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接 let { name, age } = toRefs(info)
toRef
: 将指定传入的对象那个属性变成ref对象
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法。
const info = reactive({ name: 'zh', age: 22 }) // 将info对象中的age属性变成ref对象。 let age = toRef(info, 'age')
isProxy
- 检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
- 检查对象是否是由 reactive创建的响应式代理。
- 如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。
isReadonly
- 检查对象是否是由 readonly 创建的只读代理。
toRaw
- 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
- 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
- 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
unref
- 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法。
- 如果参数是一个 ref,则返回内部值,否则返回参数本身。
- 这是 val = isRef(val) ? val.value : val 的语法糖函数。
isRef
- 判断值是否是一个ref对象。
shallowRef
- 创建一个浅层的ref对象。只有修改了ref对象,他才是响应式的。如果修改内部对象,将不是响应式的。 这个api和shallowReactive不一样。后者是将传入的对象第一层变成一个响应式的,修改第一层对象属性依旧是可以做到响应式的。但是这个api只是修改ref对象才会是响应式的。
const info = shallowRef({ name: 'zh' }) const changeInfo = () => { // 只有这样修改才是响应式的 info.value = { name: 'llm' } // 这样改不是响应式的 info.value.name = 'llm' }
triggerRef
- 手动触发和 shallowRef 相关联的副作用。
const info = shallowRef({name: "zh"}) const changeInfo = () => { info.value.name = "llm"; // 让其是响应式的 triggerRef(info); }
customRef
: 自定义ref对象
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
- 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数。
- 并且应该返回一个带有 get 和 set 的对象。
import { customRef } from 'vue'; // 自定义ref export default function(value, delay = 300) { let timer = null; return customRef((track, trigger) => { return { get() { track(); return value; }, set(newValue) { clearTimeout(timer); timer = setTimeout(() => { value = newValue; trigger(); }, delay); } } }) } // 直接使用 const message = debounceRef("Hello World");
获取当前组件上下文 getCurrentInstance
由于setup函数中,没有绑定this。所以我们获取不到this,即当前组件对象。
如果我们想要获取呢?
vue提供了getCurrentInstance
, 可以让我们获取当前组件对象。调用该API即可。
如果想要获取组件提供的全局属性。我们需要获取全局对象。
getCurrentInstance().appContext.config.globalProperties
计算属性 computed
当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理。
computed可以传入两种参数:
- 接收一个getter函数,并为 getter 函数指定返回值
- 接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象。当修改computed时非常有用。
- computed返回一个ref对象。
// 1.用法一: 传入一个getter函数 // computed的返回值是一个ref对象 const fullName = computed(() => firstName.value + " " + lastName.value); // 2.用法二: 传入一个对象, 对象包含getter/setter const fullName = computed({ get: () => firstName.value + " " + lastName.value, set(newValue) { const names = newValue.split(" "); firstName.value = names[0]; lastName.value = names[1]; } });
监听数据 watch / watchEffect
当数据变化时执行某一些操作。我们可以通过watch / watchEffect来监听。
二者的区别:
watchEffect
不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而watch
只能监听指定的属性而做出变更(v3开始可以同时指定多个)。
watch
可以获取到新值与旧值(更新前的值),而watchEffect
是拿不到的。
watchEffect
如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed
同理),而后收集到的依赖发生变化,这个回调才会再次执行,而watch
不需要,因为他一开始就指定了依赖。watchEffect
- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖。
- 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行。
- 并且返回一个函数,这个函数可以用来取消
watchEffect
的监听。或者组件卸载后自动停止监听。
// watchEffect: 自动收集响应式的依赖 const name = ref("zh"); const age = ref(20); const stop = watchEffect(() => { console.log("name:", name.value, "age:", age.value); }); const changeName = () => name.value = "llm" const changeAge = () => { age.value++; // 当age属性大于30时,就取消watchEffect的监听。 if (age.value > 30) { stop(); } }
watchEffect的执行时机。默认情况下,watchEffect是在视图更新之前执行副作用函数。如果我们想要改变他的执行时机,怎么改变呢?
watchEffect还可以传入第二个参数,为一个对象。设置flush
顺序性就可以改变watchEffect的执行时机。
flush: 'pre'(默认,在视图更新前执行) 'post'(在视图更新后执行) 'sync'(同步触发,会出现问题,少用)
const title = ref(null); watchEffect(() => { console.log(title.value); // 获取dom元素。 }, { flush: "post" // 在视图更新后执行,会减少watchEffect无效的执行。因为第一次执行会获取null })
watchEffect还可以清除副作用
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。
在我们给watchEffect
传入的函数被回调时,其实可以获取到一个参数:onInvalidate, 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数。我们可以在传入的回调函数中,执行一些清楚工作。就类似于节流函数。
watchEffect((onInvalidate) => { const timer = setTimeout(() => { console.log("网络请求成功~"); }, 2000) onInvalidate(() => { // 在这个函数中清除额外的副作用 clearTimeout(timer); console.log("onInvalidate"); }) });
watch
watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用。
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调。 监听单个数据源
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)。
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref)。 如果传入的是一个ref响应式对象或者getter并返回一个普通对象(通过...结构),监听函数中都是普通对象 / 普通值。而非ref, reactive对象。
如果传入的是一个reactive对象,那么监听函数中是reactive对象。并且新值和旧值都是一样的。
// 改变name const info = reactive({ name: 'zh', age: 20 }) watch(info, (newValue, oldValue) => { // newValue, oldValue都是reactive对象,并且返回的都是新值。 console.log('newValue:', newValue, 'oldValue:', oldValue) })
监听多个值
可以将多个源放在数组中。
// 1.定义可响应式的对象 const info = reactive({ name: 'zh', age: 20 }) const name = ref('zh') // 2.侦听器watch watch( [() => ({ ...info }), name], ([newInfo, newName], [oldInfo, oldName]) => { console.log(newInfo, newName, oldInfo, oldName) } )
watch的选项
如果想要深度监听对象,我们就需要给watch传入第二个参数。用于深度监听或者立即执行。
默认情况下,watch对于监听展开的reactive对象不能深度监听,但是我们如果先改变第一层的属性即info.name
,那么info.friend.name
也会被改变。但是如果只改变info.friend.name
,是不会触发watch回调的。只有配置了deep: true
,才会被监听到。
const info = reactive({ name: 'zh', age: 18, friend: { name: 'jcl', }, }) // 2.侦听器watch watch( () => ({ ...info }), (newInfo, oldInfo) => { console.log(newInfo, oldInfo) }, { // deep: true, // immediate: true, } ) const changeData = () => { // info.name = 'llm' info.friend.name = 'zheng' }
但是对于直接监听reactive对象,他会自动深度监听,内部有设置deep: true。
const info = reactive({ name: 'zh', age: 18, friend: { name: 'jcl', }, }) watch(info, (newInfo, oldInfo) => { console.log(newInfo, oldInfo) }) const changeData = () => { info.friend.name = 'zheng' }
对于ref传入的对象,也是默认没有深度监听的。并且监听函数中参数都是一个对象。
const info = ref({ name: 'zh', age: 18, friend: { name: 'jcl', }, }) watch( info, (newInfo, oldInfo) => { console.log(newInfo, oldInfo) }, { // 需要加上,才会深度监听 deep: true, } ) const changeData = () => { info.value.friend.name = 'zheng' }
生命周期
options API中生命周期和composition API中生命周期对比
选项式 API | Hook inside setup |
beforeCreate |
Not needed* |
created |
Not needed* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
activated |
onActivated |
deactivated |
onDeactivated |
provide 和 inject
如果我们想要向子孙组件传递数据,我们就可以通过provide
API来完成。用inject
API来接收传递的数据。
由于我们向下传递的数据,不需要子孙组件修改,只允许我们自己修改,然后影响下层组件,所以。可以使用readonly
API来传递只读数据。
provide可以传入两个参数:
- 第一个是提供的属性名称
- 第二个是传入的数据 inject可以传入两个参数:
- 第一个是接收到provide传递的属性名
- 第二个是提供默认值
// 向下传递 const obj = reactive({ name: 'zh', age: 20, }) const name = ref('llm') provide('obj', readonly(obj)) provide('name', readonly(name))
// 接收 let obj = inject('obj') let name = inject('name')
如果我们真的想要在子孙组件中修改数据,我们可以提供一个函数,接收子孙组件的数据,然后在该祖先组件中修改。
// provide //这个也传入到inject中,供其修改 const updateName = (e, val) => { console.log('e---------------', e, val) name.value = val }
// inject let updateName = inject('updateName') <button @click="(e) => {updateName(e, '子孙组件中修改name的数据')}">改变name</button>
渲染函数 render
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器。
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM)。
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode。
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode。
我们可以通过vue提供的h
函数来实现。h() 函数是一个用于创建 vnode 的一个函数。其实准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数。
下面就是h函数的参数传递和用法。
h( // {String | Object | Function} tag // 一个 HTML 标签名、一个组件、一个异步组件、或 // 一个函数式组件。 // // 必需的。 'div', // {Object} props // 与 attribute、prop 和事件相对应的对象。 // 这会在模板中用到。 // // 可选的。 {}, // {String | Array | Object} children // 子 VNodes, 使用 `h()` 构建, // 或使用字符串获取 "文本 VNode" 或者 // 有插槽的对象。 // // 可选的。 [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ] )
注意:如果没有props,那么通常可以将children作为第二个参数传入。如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入。
h
函数的基本使用。h函数可以在两个地方使用:
- render函数选项中。作为render函数的返回值。如果想要使用事件。我们可以传入on+事件名的属性,函数作为他的值。
data() { return { counter: 0 } }, render() { return h("div", {class: "app"}, [ h("h2", null, `当前计数: ${this.counter}`), h("button", { onClick: () => this.counter++ }, "+1"), h("button", { onClick: () => this.counter-- }, "-1"), ]) }
- setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode)。作为setup函数的返回值。
setup() { const counter = ref(0); return () => { return h("div", {class: "app"}, [ h("h2", null, `当前计数: ${counter.value}`), h("button", { onClick: () => counter.value++ }, "+1"), h("button", { onClick: () => counter.value-- }, "-1"), ]) } }
h函数中使用插槽。可以通过三元运算符,提供默认插槽内容。
HelloWorld.vue // 子组件提供插槽和插槽prop setup() { const instance = getCurrentInstance().ctx.$slots return () => h('div', {}, [ instance.first ? instance.first({ first: 'first=========' }) : '默认插槽first', instance.second ? instance.second({ second: 'second=========' }) : '默认插槽second', ]) },
// 使用渲染函数插槽 setup() { return () => h( HelloWorld, {class: 'hello-world'}, { // 向HelloWorld传入插槽内容,并使用提供的插槽数据 first: (slotProps) => h('span', slotProps.first), second: (slotProps) => h('span', slotProps.second), } ) },
以上只是一些个人实验,如果想要了解更多,请访问官网v3.cn.vuejs.org/guide/rende…
自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。用来复用代码,方便操作。
注意:在Vue中,代码的复用和抽象主要还是通过组件通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令。
自定义指令分为两种:
- 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用。
- 自定义全局指令:app的 directive 方法,可以在任意组件中被使用。 下面我们来自定义一个自动获取焦点的指令。
局部指令: 直接在dom上通过v-focus使用即可
// 局部指令 directives: { focus: { mounted(el, bindings, vnode, preVnode) { el.focus(); } } }
全局指令
app.directive("focus", { mounted(el, bindings, vnode, preVnode) { el.focus(); } })
下面我们就来介绍一下自定义指令中的生命周期函数
一个指令定义的对象,Vue提供了如下的几个钩子函数:注意: 这些生命周期函数名称和vue2有一些不同
created
:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted
:在绑定元素的父组件被挂载后调用;
beforeUpdate
:在更新包含组件的 VNode 之前调用;
updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount
:在卸载绑定元素的父组件之前调用;
unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次; 生命周期函数的参数:其中el, binding比较常用
el
: 指令绑定到的元素。这可用于直接操作 DOM。
binding
: 包含以下 property 的对象。
instance
:使用指令的组件实例。
value
:传递给指令的值。例如,在v-my-directive="1 + 1"
中,该值为2
。
oldValue
:先前的值,仅在beforeUpdate
和updated
中可用。值是否已更改都可用。
arg
:参数传递给指令 (如果有)。例如在v-my-directive:foo
中,arg 为"foo"
。
modifiers
:包含修饰符 (如果有) 的对象。例如在v-my-directive.foo.bar
中,修饰符对象为{foo: true,bar: true}
。
dir
:一个对象,在注册指令时作为参数传递。(就是提供的生命周期函数)
vnode
: 上面作为 el 参数收到的真实 DOM 元素的蓝图。
prevNode
: 上一个虚拟节点,仅在beforeUpdate
和updated
钩子中可用。
下面我们来封装一个格式化时间戳的指令
import dayjs from 'dayjs'; app.directive("format-time", { // 做一些初始化操作 created(el, bindings) { bindings.formatString = "YYYY-MM-DD HH:mm:ss"; // 当用户传入格式化字符串,我们将使用用户传入的格式 if (bindings.value) { bindings.formatString = bindings.value; } }, mounted(el, bindings) { const textContent = el.textContent; let timestamp = parseInt(textContent); if (textContent.length === 10) { // 将时间戳转化为毫秒 timestamp = timestamp * 1000 } el.textContent = dayjs(timestamp).format(bindings.formatString); } }) // 可以让用户自定义格式化 <h2 v-format-time="'YYYY/MM/DD'">{{timestamp}}</h2>
插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个
install
的函数,该函数会在安装插件时执行。install
函数将接受两个参数,一是全局对象。二是用户传入的配置对象
- 函数类型:一个function,这个函数会在安装插件时自动执行。将接受两个参数,一是全局对象。二是用户传入的配置对象。
插件可以完成的功能没有限制,比如下面的几种都是可以的:
- 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现。
- 添加全局资源:指令/过滤器/过渡等。
- 通过全局 mixin 来添加一些组件选。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。 使用插件调用全局对象的use方法即可。并将插件对象传入给use方法。
// plugin.js // 对象类型 export default { install(app) { app.config.globalProperties.$name = "zh" } } // 函数类型 export default function(app) { console.log(app); }
// 使用 const app = createApp(App); app.use(plugin)