前言
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函数做了几件事:
- 去掉样式,重置styleForPseudo
- 移除节点
- 移除动画的事件监听
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]
)
- 克隆原始节点,同时可添加新的props进行浅合并
- 保留原始的key和ref
- 若传入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伪元素实现效果