antd wave组件解析

1,791 阅读3分钟

前言

wave是通过伪类的方式,为传入的props.children添加波纹扩散动画效果的组件。antd4.X中使用的是类组件的方式

源码分析

componentDidMount函数中,获取节点的引用并添加事件监听

 componentDidMount() {
    const node = this.containerRef.current as HTMLDivElement;
    // nodeType = 1表示是元素节点ELEMENT_NODE
    if (!node || node.nodeType !== 1) {
      return;
    }
    // 给节点加上事件监听,bindAnimationEvent返回的是cancel函数方便取消
    this.instance = this.bindAnimationEvent(node);
}

componentWillUnmount中取消动画,清理定时器

componentWillUnmount() {
    //  取消动画
    if (this.instance) {
      this.instance.cancel();
    }
    // 清理定时器
    if (this.clickWaveTimeoutId) {
      clearTimeout(this.clickWaveTimeoutId);
    }

    this.destroyed = true;
}

接下来看看添加事件监听的函数bindAnimationEvent

  bindAnimationEvent = (node: HTMLElement) => {
    // 如果元素是禁用状态直接返回
    if (
      !node ||
      !node.getAttribute ||
      node.getAttribute('disabled') ||
      node.className.indexOf('disabled') >= 0
    ) {
      return;
    }
    const onClick = (e: MouseEvent) => {
      // Fix radio button click twice
      // 否则是input框或者隐藏起来的元素就不展示动画
      if ((e.target as HTMLElement).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) {
        return;
      }
      // 重置node上绑定的所有东西
      this.resetEffect(node);
      // Get wave color from target(获取波纹颜色)
      const waveColor =
        getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible
        getComputedStyle(node).getPropertyValue('border-color') ||
        getComputedStyle(node).getPropertyValue('background-color');
      // clickWaveTimeoutId是调用onClick函数放进微任务队列的定时器
      this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0);
      // TODO:看上去raf是控制动画的
      raf.cancel(this.animationStartId);
      this.animationStart = true;

      // Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this.
      this.animationStartId = raf(() => {
        this.animationStart = false;
      }, 10);
    };
    node.addEventListener('click', onClick, true);
    return {
      cancel: () => {
        node.removeEventListener('click', onClick, true);
      },
    };
  };

上面的函数中clickWaveTimeoutId是触发this.onClick的定时器,因此看下这个函数做了什么

onClick = (node: HTMLElement, waveColor: string) => {
    // 异常条件不作处理
    if (!node || isHidden(node) || node.className.indexOf('-leave') >= 0) {
      return;
    }
    // insertExtraNode为false时,Wave通过插入伪类元素:after 来作为承载动画效果的DOM元素
    // 为true时,就把下面新建的extraNode作为承载动画效果的DOM元素
    // 为什么要提供insertExtraNode属性来控制是否插入新的节点:因为有的已经原本就定义了::after,
    // 比如Switch组件,用的rc-switch,但原本的rc-switch已经通过:after定义了样式;
    // 因此在antd实现这个switch时就传了insertExtraNode=true
    const { insertExtraNode } = this.props;
    
    // 新建一个extraNode节点,为其添加样式
    this.extraNode = document.createElement('div');
    const { extraNode } = this;
    const { getPrefixCls } = this.context;
    extraNode.className = `${getPrefixCls('')}-click-animating-node`;
    const attributeName = this.getAttributeName();
    node.setAttribute(attributeName, 'true');
    // Not white or transparent or grey
    if (
      waveColor &&
      waveColor !== '#ffffff' &&
      waveColor !== 'rgb(255, 255, 255)' &&
      isNotGrey(waveColor) &&
      !/rgba\((?:\d*, ){3}0\)/.test(waveColor) && // any transparent rgba color
      waveColor !== 'transparent'
    ) {
      extraNode.style.borderColor = waveColor;
      // 获取元素根节点(body)
      const nodeRoot = node.getRootNode?.() || node.ownerDocument;
      const nodeBody: Element =
        nodeRoot instanceof Document ? nodeRoot.body : (nodeRoot.firstChild as Element) ?? nodeRoot;
      // 将::after样式更新到body上去
      styleForPseudo = updateCSS(
        `
      [${getPrefixCls('')}-click-animating-without-extra-node='true']::after, .${getPrefixCls(
          '',
        )}-click-animating-node {
        --antd-wave-shadow-color: ${waveColor};
      }`,
        'antd-wave',
        { csp: this.csp, attachTo: nodeBody },
      );
    }
    if (insertExtraNode) {
      node.appendChild(extraNode);
    }
    // 为加点添加动画事件的监听
    ['transition', 'animation'].forEach(name => {
      node.addEventListener(`${name}start`, this.onTransitionStart);
      node.addEventListener(`${name}end`, this.onTransitionEnd);
    });
  };

resetEffect函数做了几件事:

  1. 去掉样式,重置styleForPseudo
  2. 移除节点
  3. 移除动画的事件监听

render函数很简单

renderWave = ({ csp }: ConfigConsumerProps) => {
    const { children } = this.props;
    this.csp = csp;
    // 检查合法性
    if (!React.isValidElement(children)) return children;

    let ref: React.Ref<any> = this.containerRef;
    if (supportRef(children)) {
      ref = composeRef((children as any).ref, this.containerRef as any);
    }
    // 使用React.cloneElement克隆节点
    return cloneElement(children, { ref });
};

render() {
    // ConfigConsumer就是ConfigContext.Consumer
    return <ConfigConsumer>{this.renderWave}</ConfigConsumer>;
}

React.cloneElement的使用场景

React.cloneElement(
  element,
  [props],
  [...children]
)
  1. 克隆原始节点,同时可添加新的props进行浅合并
  2. 保留原始的key和ref
  3. 若传入children,将替代原有的children 因此,当在创建通用组件时,可以在组件内部获取children利用该函数做通用处理。比如列表最后 一项设置特殊classname等

css

[ant-click-animating-without-extra-node='true'] {
  position: relative;
}

[ant-click-animating-without-extra-node='true']::after {
  position: absolute;
  top: -1px;
  right: -1px;
  bottom: -1px;
  left: -1px;
  display: block;
  border: 0 solid @primary-color;
  border-radius: inherit;
  opacity: 0.2;
  animation: fadeEffect 2s @ease-out-circ, waveEffect 0.4s @ease-out-circ;
  animation-fill-mode: forwards;
  pointer-events: none;
  content: '';
}

@keyframes waveEffect {
  100% {
    top: -@wave-animation-width;
    right: -@wave-animation-width;
    bottom: -@wave-animation-width;
    left: -@wave-animation-width;
    border-width: @wave-animation-width;
  }
}

@keyframes fadeEffect {
  100% {
    opacity: 0;
  }
}

技巧学习:

通过children获取传入组件,在不破坏原有dom结构前提下,使用css伪元素实现效果