共计 4728 个字符,预计需要花费 12 分钟才能阅读完成。
Wave 组件效果预览
在上一篇文章 Button 组件的源码分析中遇到了一个 Wave 组件,Wave 组件在 Ant design 中提供了通用的表单控件点击效果,在自己阅读源码之前,也并没有过更多留心过在这些表单控件的动画效果是如何实现的,甚至可能有时都没注意到这些动画效果。下面先一起来看以下具体的效果:
看完 UI 效果之后我们大概已经知道是什么了,再看代码部分,由于代码书写顺序与阅读顺序并不一致,为了方便理解,我们在分析源码的过程中,会调整代码解释的顺序
源码分析
// 一个新的依赖,暂时不知道是什么,依据名字推测与动画效果有关
import TransitionEvents from ‘css-animation/lib/Event’;
export default class Wave extends React.Component<{insertExtraNode?: boolean}> {
//… some type code
// 我们发现 Wave 组件只提供组件逻辑,不参与 UI 展示,这种容器组件,往往在 DidMount 或 WillMount 声明周期中开始
// 构建组件逻辑,往下看
render() {
return this.props.children;
}
// 只有一行代码, 先看下方 bindAnimationEvent 方法
componentDidMount() {
this.instance = this.bindAnimationEvent(findDOMNode(this) as HTMLElement);
}
// 在组件卸载时,清除掉事件监听与定时器,避免内存泄漏
componentWillUnmount() {
if (this.instance) {
this.instance.cancel();
}
if (this.clickWaveTimeoutId) {
clearTimeout(this.clickWaveTimeoutId);
}
}
// 根据名字推测: 为 DOM 节点绑定动画效果, 进入函数内部查看
bindAnimationEvent = (node: HTMLElement) => {
//… some code
const onClick = (e: MouseEvent) => {
//… some code
// 无论是否正在执行动画,先清除掉动画效果(至于怎么清除,先不关注)
this.resetEffect(node);
// 从 target 取得颜色值
const waveColor =
getComputedStyle(node).getPropertyValue(‘border-top-color’) || // Firefox Compatible
getComputedStyle(node).getPropertyValue(‘border-color’) ||
getComputedStyle(node).getPropertyValue(‘background-color’);
// 在这里可以看到之前定义的私有变量 clickWaveTimeoutId,被用作储存一个定时器
this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0);
};
// 监听 node(this.props.children)的 onClick 事件
node.addEventListener(‘click’, onClick, true);
// 将移除监听事件的回调函数封装在一个对象中并作为返回值,看着这里应该想起之前定义的私有变量 instance,
// 回顾 DidMount 生命周期函数,你会发现这个返回的对象将会被储存在 instance 中
return {
cancel: () => {
node.removeEventListener(‘click’, onClick, true);
},
};
}
// 未完待续
我们通过观察上方 bindAnimationEvent 方法,其主要做了三件事,调用了两个外部函数 this.resetEffect 与 this.onClick 1、过滤不执行的条件(代码省略掉了,非主干逻辑)2、声明 onClick 函数,并作为 node 的点击事件触发时要执行的函数 3、返回一个储存了取消监听 click 事件方法的对象
个人认为 bindAnimationEvent 方法,做了太多的事情,而 ComponentDidMount 做的事情太少(单一原则)往下方,继续查看 this.resetEffect 与 this.onClick 分别是做什么的,以及如何实现的
// 这个函数是实现动画效果的核心,其主要有三个行为:1、创建内联 style 标签,2、插入 css 字符串 3、并将其插入到 document 中
// 我们知道 css 也是可以控制 DOM 变化的,比如伪类元素:after :before 这里正是通过:after 来实现效果的
onClick = (node: HTMLElement, waveColor: string) => {
//… some code 1
const {insertExtraNode} = this.props;
/* 创建了一个 div 元素 extraNode,装饰该 div,在 inserExtracNode= true 时,将 extraNode 作为 node 的子元素 */
/* 创建一个 div 元素,并缓存中私有变量 extraNode 中 */
this.extraNode = document.createElement(‘div’);
/* 这里用 let 更合适 */
const extraNode = this.extraNode;
extraNode.className = ‘ant-click-animating-node’;
// 可能有人好奇这个 extraNode 是干嘛的?
// 往之后的代码中看的时候会发现,在 insertExtraNode 为 false 时,Wave 通过插入伪类元素:after 来作为承载动画效果的 DOM 元素
// 在 insertExtraNode = true 时,会生成一个 div 替代:after 伪类元素,猜测是某些 this.props.children 可能自带:after,所以
// 使用 div 元素来替代:after 避免冲突,在这里我们只需要知道它是作为承载动画 css 的 DOM 元素即可
// 获取指定的 string insertExtraNode ? ‘ant-click-animating’ : ‘ant-click-animating-without-extra-node’;
const attributeName = this.getAttributeName();
// Element.removeAttribute(‘someString’); 从 element 中删除值为 someString 的属性
// Element.setAttribute(name, value); 为 element 元素值为 value 的 name 属性
node.removeAttribute(attributeName);
node.setAttribute(attributeName, ‘true’);
// 行为 1:这里创建了一个内联 style 标签
styleForPesudo = styleForPesudo || document.createElement(‘style’);
if (waveColor &&
waveColor !== ‘#ffffff’ &&
waveColor !== ‘rgb(255, 255, 255)’ &&
this.isNotGrey(waveColor) &&
/* 透明度不为 0 的任意颜色 */
!/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color
waveColor !== ‘transparent’) {
/* 给子元素加上 borderColor */
extraNode.style.borderColor = waveColor;
/* 行为 2:在内联 style 标签中插入样式字符串,利用伪元素:after 作为承载效果的 DOM */
styleForPesudo.innerHTML =
`[ant-click-animating-without-extra-node]:after {border-color: ${waveColor}; }`;
/* 行为 3:将 style 标签插入到 document 中 */
if (!document.body.contains(styleForPesudo)) {
document.body.appendChild(styleForPesudo);
}
}
/* 在 inserExtarNode 为 true 时,将 extraNode 插入到 node 子元素中 */
if (insertExtraNode) {
node.appendChild(extraNode);
}
/* 为元素增加动画效果 */
TransitionEvents.addEndEventListener(node, this.onTransitionEnd);
}
/**
* 重置效果
* 顾名思义:这个函数通过三个行为,致力于一件事情,取消动画效果
* 1、删除 node 的 attribute 属性 2、node 的子元素 3、删除对应的内联 style 标签
*/
resetEffect(node: HTMLElement) {
// come code …
const {insertExtraNode} = this.props;
const attributeName = this.getAttributeName();
/* 行为 1:删除 node 的 attribute 属性 */
node.removeAttribute(attributeName);
/* 行为 3:清空了为伪类元素内置的 style 标签 styleForPesudo */
this.removeExtraStyleNode();
if (insertExtraNode && this.extraNode && node.contains(this.extraNode)) {
// Node.removeChild() 方法从 DOM 中删除一个子节点。返回删除的节点。
node.removeChild(this.extraNode);
}
TransitionEvents.removeEndEventListener(node, this.onTransitionEnd);
}
// 删除内联 style 标签
removeExtraStyleNode() {
if (styleForPesudo) {
styleForPesudo.innerHTML = ”;
}
}
// 在每次动画执行结束后,清除掉状态,完成一个生命周期
onTransitionEnd = (e: AnimationEvent) => {
// todo
if (!e || e.animationName !== ‘fadeEffect’) {
return;
}
this.resetEffect(e.target as HTMLElement);
}
}
组件逻辑:
我们回过头来看第一部分的代码,组件逻辑体现在 componentDidMount 与 componentWillUnMount 中 1、在 componentDidMount 中为 this.props.children 的 click 事件绑定动画执行函数 this.onClick,2、在 componentWillUnMount 中清除与动画相关的状态避免内存泄漏。
组件运行逻辑:
此时,Wave 组件的代码与逻辑已经全部分析完了,整个 Wave 组件的运行逻辑可以通过下方这张图来概括
本篇完~