从样式功能来看,整体不是很复杂,alert 组件主要包括了主题色,title,关闭按钮,关闭事件,居中,加粗等
源码
- template
<template> <!-- 显示隐藏有动画效果 --> <!-- 开发没用过,不是很理解为什么使用v-show判断显示 --> <transition name="d-alert-fade"> <div class="d-alert" :class="[typeClass, center ? 'is-center' : '', 'is-' + effect]" v-show="visible" role="alert" > <!-- 左侧图标 --> <i class="d-alert__icon" :class="[iconClass, isBigIcon]" v-if="showIcon" ></i> <!-- title 和 描述 --> <div class="d-alert__content"> <span class="d-alert__title" :class="[isBoldTitle]" v-if="title || $slots.title" > <slot name="title">{{ title }}</slot> </span> <p v-if="$slots.default && !description" class="d-alert__description"> <slot></slot> </p> <p v-if="description && !$slots.default" class="d-alert__description"> {{ description }} </p> <i class="d-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'd-icon-close': closeText === '' }" v-show="closable" @click="close" >{{ closeText }}</i > </div> </div> </transition></template>
使用 role 属性告诉辅助设备(如屏幕阅读器)这个元素所扮演的角色。本质上是增强语义性,当现有的 HTML标签不能充分表达语义性的时候,就可以借助 role 来说明。
这里不是很理解为什么 title 和 description 使用了属性和 slot 判断,有清楚的朋友可以帮忙解答
- props 属性比较常规,这里就不介绍了哈
setup(props, { emit, slots }) { // 接受的属性转为响应式 const { description, type } = toRefs(props) // 使用 v-show 显示隐藏 const visible = ref(true) // 关闭事件 const close = () => { visible.value = false emit('close') } const typeClass = computed(() => { return `d-alert--${type.value}` }) const iconClass = computed(() => { return TYPE_CLASSES_MAP[type.value] || 'd-icon-info' }) const isBigIcon = computed(() => { return description.value || slots.default ? 'is-big' : '' }) const isBoldTitle = computed(() => { return description.value || slots.default ? 'is-bold' : '' }) return { close, visible, typeClass, iconClass, isBigIcon, isBoldTitle } }
组件介绍到这里就结束了,比较简单。为了凑字呢,这里在介绍下 transition 组件
transition
大部分朋友都了解这是设置组件动画的内置动画组件。通常有三种使用方式:
- CSS 过渡
- CSS 动画
- Javascript 钩子
CSS 过渡
我们通常使用的方法,css 配置 enter 和 leave
<template> <div class="app"> <button @click="show = !show"> Toggle render </button> <transition name="fade"> <p v-if="show">我是测试</p> </transition> </div></template><script> export default { data() { return { show: true } } }</script><style> .fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }</style>
CSS 动画
<template> <div class="app"> <button @click="show = !show">Toggle show</button> <transition name="bounce"> <p v-if="show">我是测试</p> </transition> </div></template><script> export default { data() { return { show: true } } }</script><style> .bounce-enter-active { animation: bounce-in 0.5s; } .bounce-leave-active { // reverse 很关键 animation: bounce-in 0.5s reverse; } @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } }</style>
js 钩子
监听 transition 组件的内置方法,js 控制动画
<template> <div class="app"> <button @click="show = !show"> Toggle render </button> <transition @before-enter="beforeEnter" @enter="enter" @before-leave="beforeLeave" @leave="leave" css="false" > <p v-if="show">hello</p> </transition> </div></template><script> export default { data() { return { show: true } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.transition = 'opacity 0.5s ease' }, enter(el) { this.$el.offsetHeight el.style.opacity = 1 }, beforeLeave(el) { el.style.opacity = 1 }, leave(el) { el.style.transition = 'opacity 0.5s ease' el.style.opacity = 0 } } }</script>
如果形参不指定 done ,则表明用户不手动控制动画的结束,而转由节点的 transition 或者 animationEnd 来标识动画结束,开始回调 afterEnter。
钩子函数的形参的个数大于1,表示形参中有 done, 也就是说用户必须手动控制动画何时结束。所以一旦你配置了 done 形参,则转由你告诉框架,动画何时结束。需要在合适的时机调用 done,否则 afterEnter 接口就没法被调用了。
动画触发条件
- 条件渲染(v-if)
- 条件展示(v-show)
- 动态组件
- 组件根节点
执行原理
实例
<template> <div class="app"> <button @click="show = !show"> Toggle render </button> <transition name="fade"> <p v-if="show">hello</p> </transition> </div></template>
编译生成的 render 函数(不是使用的模板组件)
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, Transition as _Transition, withCtx as _withCtx,} from "vue";export function render(_ctx, _cache, $props, $setup, $data, $options) { return ( // 收集动态节点 如 v-if v-for _openBlock(), _createBlock("template", null, [ _createVNode("div", { class: "app" }, [ _createVNode( "button", { onClick: ($event) => (_ctx.show = !_ctx.show), }, " Toggle render ", 8 /* PROPS */, ["onClick"] ), _createVNode( _Transition, { name: "fade" }, { // transition 只有一个子节点,默认插槽。多个子节点报错 default: _withCtx(() => [ _ctx.show ? (_openBlock(), _createBlock("p", { key: 0 }, "hello")) : _createCommentVNode("v-if", true), ]), _: 1, } ), ]), ]) );}
那么如何在组建创建和销毁的时候执行事件呢?————创建钩子函数 transition 组件返回的是处理过的第一个子节点
- 如果 Transition 组件内部嵌套的是 KeepAlive 组件,那么它会继续查找 KeepAlive 组件嵌套的第一个子元素节点,来作为渲染的元素节点。
- 如果 Transition 组件内部没有嵌套任何子节点,那么它会渲染空的注释节点。
trantion 组件定义
const Transition = (props, { slots }) => //esolveTransitionProps 函数主要作用是,在我们给 Transition 传递的 Props 基础上做一层封装,然后返回一个新的 Props 对象,由于它包含了所有的 Props 处理 h(BaseTransition, resolveTransitionProps(props), slots);const BaseTransition = { name: `BaseTransition`, props: { mode: String, appear: Boolean, persisted: Boolean, // enter onBeforeEnter: TransitionHookValidator, onEnter: TransitionHookValidator, onAfterEnter: TransitionHookValidator, onEnterCancelled: TransitionHookValidator, // leave onBeforeLeave: TransitionHookValidator, onLeave: TransitionHookValidator, onAfterLeave: TransitionHookValidator, onLeaveCancelled: TransitionHookValidator, // appear onBeforeAppear: TransitionHookValidator, onAppear: TransitionHookValidator, onAfterAppear: TransitionHookValidator, onAppearCancelled: TransitionHookValidator, }, setup(props, { slots }) { const instance = getCurrentInstance(); const state = useTransitionState(); let prevTransitionKey; return () => { const children = slots.default && getTransitionRawChildren(slots.default(), true); if (!children || !children.length) { return; } // Transition 组件只允许一个子元素节点,多个报警告,提示使用 TransitionGroup 组件 if (process.env.NODE_ENV !== "production" && children.length > 1) { warn( "<transition> can only be used on a single element or component. Use " + "<transition-group> for lists." ); } // 不需要追踪响应式,所以改成原始值,提升性能 const rawProps = toRaw(props); const { mode } = rawProps; // 检查 mode 是否合法 if ( process.env.NODE_ENV !== "production" && mode && !["in-out", "out-in", "default"].includes(mode) ) { warn(`invalid <transition> mode: ${mode}`); } // 获取第一个子元素节点 const child = children[0]; if (state.isLeaving) { return emptyPlaceholder(child); } // 处理 <transition><keep-alive/></transition> 的情况 const innerChild = getKeepAliveChild(child); if (!innerChild) { return emptyPlaceholder(child); } const enterHooks = resolveTransitionHooks( innerChild, rawProps, state, instance ); setTransitionHooks(innerChild, enterHooks); const oldChild = instance.subTree; const oldInnerChild = oldChild && getKeepAliveChild(oldChild); let transitionKeyChanged = false; const { getTransitionKey } = innerChild.type; if (getTransitionKey) { const key = getTransitionKey(); if (prevTransitionKey === undefined) { prevTransitionKey = key; } else if (key !== prevTransitionKey) { prevTransitionKey = key; transitionKeyChanged = true; } } if ( oldInnerChild && oldInnerChild.type !== Comment && (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged) ) { const leavingHooks = resolveTransitionHooks( oldInnerChild, rawProps, state, instance ); // 更新旧树的钩子函数 setTransitionHooks(oldInnerChild, leavingHooks); // 在两个视图之间切换 if (mode === "out-in") { state.isLeaving = true; // 返回空的占位符节点,当离开过渡结束后,重新渲染组件 leavingHooks.afterLeave = () => { state.isLeaving = false; instance.update(); }; return emptyPlaceholder(child); } else if (mode === "in-out") { leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { const leavingVNodesCache = getLeavingNodesForType( state, oldInnerChild ); leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; // early removal callback el._leaveCb = () => { earlyRemove(); el._leaveCb = undefined; delete enterHooks.delayedLeave; }; enterHooks.delayedLeave = delayedLeave; }; } } return child; }; },};
在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks 去定义组件创建和删除阶段的钩子函数对象,然后再通过 setTransitionHooks 函数去把这个钩子函数对象设置到 vnode.transition 上。
hooks定义
const hooks = { mode, persisted, beforeEnter(el) { let hook = onBeforeEnter; if (!state.isMounted) { if (appear) { hook = onBeforeAppear || onBeforeEnter; } else { return; } } if (el._leaveCb) { el._leaveCb(true /* cancelled */); } const leavingVNode = leavingVNodesCache[key]; if ( leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el._leaveCb ) { leavingVNode.el._leaveCb(); } callHook(hook, [el]); }, enter(el) { let hook = onEnter; let afterHook = onAfterEnter; let cancelHook = onEnterCancelled; if (!state.isMounted) { if (appear) { hook = onAppear || onEnter; afterHook = onAfterAppear || onAfterEnter; cancelHook = onAppearCancelled || onEnterCancelled; } else { return; } } let called = false; const done = (el._enterCb = (cancelled) => { if (called) return; called = true; if (cancelled) { callHook(cancelHook, [el]); } else { callHook(afterHook, [el]); } if (hooks.delayedLeave) { hooks.delayedLeave(); } el._enterCb = undefined; }); if (hook) { hook(el, done); if (hook.length <= 1) { done(); } } else { done(); } }, leave(el, remove) { const key = String(vnode.key); if (el._enterCb) { el._enterCb(true /* cancelled */); } if (state.isUnmounting) { return remove(); } callHook(onBeforeLeave, [el]); let called = false; const done = (el._leaveCb = (cancelled) => { if (called) return; called = true; remove(); if (cancelled) { callHook(onLeaveCancelled, [el]); } else { callHook(onAfterLeave, [el]); } el._leaveCb = undefined; if (leavingVNodesCache[key] === vnode) { delete leavingVNodesCache[key]; } }); leavingVNodesCache[key] = vnode; if (onLeave) { onLeave(el, done); if (onLeave.length <= 1) { done(); } } else { done(); } }, clone(vnode) { return resolveTransitionHooks(vnode, props, state, instance); }, };
钩子函数对象定义了 4 个钩子函数,分别是 beforeEnter,enter,leave 和 clone。在节点 patch 阶段的 mountElement 函数中,在插入节点前且存在过度会执行 vnode.transition 中的 beforeEnter 函数
//beforeEnter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,来执行 onBeforeEnter 函数或者是 onBeforeAppear 函数。appear 是否节点现实的时候执行动画beforeEnter(el) { let hook = onBeforeEnter if (!state.isMounted) { if (appear) { hook = onBeforeAppear || onBeforeEnter } else { return } } if (el._leaveCb) { el._leaveCb(true /* cancelled */) } const leavingVNode = leavingVNodesCache[key] if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el._leaveCb) { leavingVNode.el._leaveCb() } callHook(hook, [el])}
resolveTransitionProps 函数
function resolveTransitionProps(rawProps) { let { name = "v", type, css = true, duration, enterFromClass = `${name}-enter-from`, enterActiveClass = `${name}-enter-active`, enterToClass = `${name}-enter-to`, appearFromClass = enterFromClass, appearActiveClass = enterActiveClass, appearToClass = enterToClass, leaveFromClass = `${name}-leave-from`, leaveActiveClass = `${name}-leave-active`, leaveToClass = `${name}-leave-to`, } = rawProps; const baseProps = {}; for (const key in rawProps) { if (!(key in DOMTransitionPropsValidators)) { baseProps[key] = rawProps[key]; } } if (!css) { return baseProps; } const durations = normalizeDuration(duration); const enterDuration = durations && durations[0]; const leaveDuration = durations && durations[1]; const { onBeforeEnter, onEnter, onEnterCancelled, onLeave, onLeaveCancelled, onBeforeAppear = onBeforeEnter, onAppear = onEnter, onAppearCancelled = onEnterCancelled, } = baseProps; const finishEnter = (el, isAppear, done) => { removeTransitionClass(el, isAppear ? appearToClass : enterToClass); removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass); done && done(); }; const finishLeave = (el, done) => { removeTransitionClass(el, leaveToClass); removeTransitionClass(el, leaveActiveClass); done && done(); }; const makeEnterHook = (isAppear) => { return (el, done) => { const hook = isAppear ? onAppear : onEnter; const resolve = () => finishEnter(el, isAppear, done); hook && hook(el, resolve); nextFrame(() => { removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass); addTransitionClass(el, isAppear ? appearToClass : enterToClass); if (!(hook && hook.length > 1)) { if (enterDuration) { setTimeout(resolve, enterDuration); } else { whenTransitionEnds(el, type, resolve); } } }); }; }; return extend(baseProps, { onBeforeEnter(el) { onBeforeEnter && onBeforeEnter(el); addTransitionClass(el, enterActiveClass); addTransitionClass(el, enterFromClass); }, onBeforeAppear(el) { onBeforeAppear && onBeforeAppear(el); addTransitionClass(el, appearActiveClass); addTransitionClass(el, appearFromClass); }, onEnter: makeEnterHook(false), onAppear: makeEnterHook(true), onLeave(el, done) { const resolve = () => finishLeave(el, done); addTransitionClass(el, leaveActiveClass); addTransitionClass(el, leaveFromClass); nextFrame(() => { removeTransitionClass(el, leaveFromClass); addTransitionClass(el, leaveToClass); if (!(onLeave && onLeave.length > 1)) { if (leaveDuration) { setTimeout(resolve, leaveDuration); } else { whenTransitionEnds(el, type, resolve); } } }); onLeave && onLeave(el, resolve); }, onEnterCancelled(el) { finishEnter(el, false); onEnterCancelled && onEnterCancelled(el); }, onAppearCancelled(el) { finishEnter(el, true); onAppearCancelled && onAppearCancelled(el); }, onLeaveCancelled(el) { finishLeave(el); onLeaveCancelled && onLeaveCancelled(el); }, });}
我们来看 onBeforeEnter 函数,它的内部执行了基础 props 传入的 onBeforeEnter 钩子函数,并且给 DOM 元素 el 添加了 enterActiveClass 和 enterFromClass 样式。
其中,props 传入的 onBeforeEnter 函数就是我们写 Transition 组件时添加的 beforeEnter 钩子函数。enterActiveClass 默认值是 v-enter-active,enterFromClass 默认值是 v-enter-from,如果给 Transition 组件传入了 name 的 prop,比如 fade,那么 enterActiveClass 的值就是 fade-enter-active,enterFromClass 的值就是 fade-enter-from。(onBeforeAppear 和 onBeforeEnter 的逻辑类似,就不赘述了,它是在我们给 Transition 组件传入 appear 的 Prop,且首次挂载的时候执行的。执行完 beforeEnter 钩子函数,接着插入元素到页面,然后会执行 vnode.transition 中的 enter 钩子函数,上面的 hooks 中)
在 enter 函数内部,首先执行基础 props 传入的 onEnter 钩子函数,然后在下一帧给 DOM 元素 el 移除了 enterFromClass,同时添加了 enterToClass 样式(动画也就是所谓的样式交替改变)
Transition 组件允许我们传入 enterDuration 这个 prop,它会指定进入过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 finishEnter 函数
来看它的实现
const finishEnter = (el, isAppear, done) => { removeTransitionClass(el, isAppear ? appearToClass : enterToClass); removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass); done && done();};
其实就是给 DOM 元素移除 enterToClass 以及 enterActiveClass,同时执行 done 函数,进而执行 onAfterEnter 钩子函数
leave 钩子主要功能和 enter 相反。小伙伴们可自行查阅。
以上就是对 alert 组件的学习。如有不对欢迎指正。