[day-ui] Alert 组件学习

368 阅读4分钟

从样式功能来看,整体不是很复杂,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 来说明。

这里不是很理解为什么 titledescription 使用了属性和 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

大部分朋友都了解这是设置组件动画的内置动画组件。通常有三种使用方式:

  1. CSS 过渡
  2. CSS 动画
  3. Javascript 钩子

CSS 过渡

我们通常使用的方法,css 配置 enterleave

<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 个钩子函数,分别是 beforeEnterenterleaveclone。在节点 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 添加了 enterActiveClassenterFromClass 样式。

其中,props 传入的 onBeforeEnter 函数就是我们写 Transition 组件时添加的 beforeEnter 钩子函数。enterActiveClass 默认值是 v-enter-activeenterFromClass 默认值是 v-enter-from,如果给 Transition 组件传入了 nameprop,比如 fade,那么 enterActiveClass 的值就是 fade-enter-activeenterFromClass 的值就是 fade-enter-from。(onBeforeAppearonBeforeEnter 的逻辑类似,就不赘述了,它是在我们给 Transition 组件传入 appearProp,且首次挂载的时候执行的。执行完 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 组件的学习。如有不对欢迎指正。