rc-animate
rc-animate是這個庫在npm上的名字,他在github搜索的話得用animate去搜索。(下面就都簡稱為animate了)
animate是阿里的庫,ant-design也是使用的他完成動畫的一部分內容。
官網是這樣介紹的:
對單個元素根據狀態進行動畫顯示隱藏,需結合 css 或其它第三方動畫類一起使用
代碼結構
rc-animate的源代碼src文件夾下只有Animate,AnimateChild,ChildrenUtils,Utils這四個js文件,后兩個utils是工具類js,用來檢測組件的props是否含有某個值這樣的工作等,不過多介紹。
Animate.js是我們庫暴露給我們使用的組件,他會在內部做一些檢測,回調等等操作。然后調用AnimateChild組件,AnimateChild是實際實現動畫的地方。AnimateChild中使用了css-animate這個庫,css-animate才是最核心的操作,他將給dom元素添加指定的css類名。另一個animateUtils是上一層的utils.js這個文件,我寫錯了,這里說明一下。
先從rc-animate的最外層,我們最先接觸到的Animate.js講起。
Animate.js
簡書上寫大段的代碼太費勁了,我就一小段一小段的解釋吧。
constructor
這個沒什么難點,就是普通的初始化。currentlyAnimatingKeys和childrenRefs在后續會比較重要,使用次數比較多
constructor(props) {
super(props);
// 這個數組用來儲存正在進行動畫的dom節點的key值
this.currentlyAnimatingKeys = {};
// 這兩個數組就顧名思義啦
this.keysToEnter = [];
this.keysToLeave = [];
this.state = {
// 這里的toArrayChildren和getChildrenFromProps都是上面講的兩個工具類里的工具方法
children: toArrayChildren(getChildrenFromProps(props)),
};
// 儲存ref
this.childrenRefs = {};
}
componentDidMount
componentDidMount() {
const showProp = this.props.showProp;
let children = this.state.children;
// showProp是Animate組件提供的一個API,原文解釋為:'子級動畫的類型,顯示或隱藏'
// 完全不理解他的意思,剛好官網的代碼示例也沒有用帶這個參數,我們就暫且跳過他,給他一個null值吧
if (showProp) {
children = children.filter((child) => {
return !!child.props[showProp];
});
}
// 遍歷子節點,并調用performAppear方法,這個方法名翻譯一下就是:‘執行顯示’,所以這就是我們的css效果的第一個觸發點
// 完成組件掛載后,讓組件執行css動畫,也就是常見的淡入效果之類的。
children.forEach((child) => {
if (child) {
this.performAppear(child.key);
}
});
}
performAppear
performAppear這個方法是Animate組件自己的實例方法
performAppear = (key) => {
if (this.childrenRefs[key]) {
// 用key值找到目標組件,然后對currentlyAnimatingKeys數組做一個標識,告訴大家,這個組件正在進行動畫
this.currentlyAnimatingKeys[key] = true;
// 調用實例上的方法,這個componentWillAppear方法,是Ref對應的實例自己提供的一個實例方法(這里實例對應的Class是AnimateChild)
// 可以將componentWillAppear理解為AnimateChild的一個生命周期函數,Animate就是執行這個生命周期函數的框架
// 傳遞給可以將componentWillAppear的方法是一個回調函數
this.childrenRefs[key].componentWillAppear(
this.handleDoneAdding.bind(this, key, 'appear')
);
}
}
我想按照調用組件時代碼的執行順序來梳理整個庫的代碼,所以接下來不一一說明Animate類里的方法了,而是講他的render函數與AnimateChild.js的內容
render
render() {
const props = this.props;
this.nextProps = props;
const stateChildren = this.state.children;
let children = null;
// 先map拿到被AnimateChild包裝好的組件數組
if (stateChildren) {
children = stateChildren.map((child) => {
if (child === null || child === undefined) {
return child;
}
if (!child.key) {
throw new Error('must set key for <rc-animate> children');
}
// 返回的AnimateChild組件,childrenRefs數組里儲存的就是AnimateChild組件的實例
return (
<AnimateChild
key={child.key}
ref={node => this.childrenRefs[child.key] = node}
animation={props.animation}
transitionName={props.transitionName}
transitionEnter={props.transitionEnter}
transitionAppear={props.transitionAppear}
transitionLeave={props.transitionLeave}
>
{child}
</AnimateChild>
);
});
}
const Component = props.component;
// 這個component是暴露出來的api,用來指定需要替換的標簽
// 這里可以看到,如果沒有component的話,會只渲染組件數組的第一項
// 有component則會全部渲染,因該是為了保證有一個父級節點去包裹,以便進行動畫
if (Component) {
let passedProps = props;
if (typeof Component === 'string') {
passedProps = {
className: props.className,
style: props.style,
...props.componentProps,
};
}
return <Component {...passedProps}>{children}</Component>;
}
return children[0] || null;
}
AnimateChild
AnimateChild組件并沒有react原生的生命周期函數包括constructor,render函數也只是簡單的返回了this.props.children。由此可見,AnimateChild其實是一個類,提供一些接口。下面,主要了解一下他的接口都是做什么的。
先從我們上邊講的performAppear函數調用的componentWillAppear函數開始講
componentWillAppear
AnimateChild組件的componentWillAppear,componentWillEnter,componentWillLeave全都一樣,判斷一下是否做動畫,做就調用transition方法,不然就調用done。
然后說transition,這是AnimateChild的核心方法
componentWillAppear(done) {
if (animUtil.isAppearSupported(this.props)) {
this.transition('appear', done);
} else {
done();
}
}
transition
transition(animationType, finishCallback) {
const node = ReactDOM.findDOMNode(this);
const props = this.props;
const transitionName = props.transitionName;
const nameIsObj = typeof transitionName === 'object';
this.stop();
const end = () => {
this.stopper = null;
finishCallback();
};
// props.animation是如果使用第三方動畫類庫需要傳入的對象,props[transitionMap[animationType]]是一個布爾值,對應著transitionAppear等三個api
// 這個判斷翻譯過來就是,支持css動畫或者不使用css類庫并且有自己的css類名并且的確要做這一步的動畫
if ((isCssAnimationSupported || !props.animation[animationType]) &&
transitionName && props[transitionMap[animationType]]) {
// 這一大串,都是在拼接字符串,拼接出要添加的類名
const name = nameIsObj ? transitionName[animationType] : `${transitionName}-${animationType}`;
let activeName = `${name}-active`;
if (nameIsObj && transitionName[`${animationType}Active`]) {
activeName = transitionName[`${animationType}Active`];
}
// 核心中的核心,調用cssAnimate方法并傳入回調
this.stopper = cssAnimate(node, {
name,
active: activeName,
}, end);
} else {
this.stopper = props.animation[animationType](node, end);
}
}
cssAnimate
cssAnimate庫包含兩個js文件,一個index.js是提供給我們使用的,用來觸發動畫或者過渡。另一個event.js是支撐index.js的運行的,event.js在內部封裝了函數,用來檢測瀏覽器是否執行完了css動畫。(這是一個新的知識點,之前從未了解過。上邊講的cssAnimate方法是index.js提供的。下面我們先說他。
index
index.js里的工具方法與細枝末節就不說了,直接說cssAnimate這個函數
const cssAnimation = (node, transitionName, endCallback) => {
// 先是一大串字符串拼接,得出要添加的class類名
const nameIsObj = typeof transitionName === 'object';
const className = nameIsObj ? transitionName.name : transitionName;
const activeClassName = nameIsObj ? transitionName.active : `${transitionName}-active`;
let end = endCallback;
let start;
let active;
// 這個classes是引用的component-classes這個庫的api,用來跨瀏覽器操作dom元素的類名,理解為jq吧,但是只有類名的增刪改查
const nodeClasses = classes(node);
// 如果是對象,就解構一下,拿到真正的回調函數
if (endCallback && Object.prototype.toString.call(endCallback) === '[object Object]') {
end = endCallback.end;
start = endCallback.start;
active = endCallback.active;
}
// 這是cssAnimate自己定義的一個方法,動畫結束時的回調
if (node.rcEndListener) {
node.rcEndListener();
}
node.rcEndListener = (e) => {
if (e && e.target !== node) {
return;
}
if (node.rcAnimTimeout) {
clearTimeout(node.rcAnimTimeout);
node.rcAnimTimeout = null;
}
clearBrowserBugTimeout(node);
nodeClasses.remove(className);
nodeClasses.remove(activeClassName);
Event.removeEndEventListener(node, node.rcEndListener);
node.rcEndListener = null;
// Usually this optional end is used for informing an owner of
// a leave animation and telling it to remove the child.
if (end) {
end();
}
};
// 綁定動畫結束的監聽事件
Event.addEndEventListener(node, node.rcEndListener);
// 真正開始了
if (start) {
start();
}
nodeClasses.add(className);
// 這里延時加載了實現動畫效果的activeClassName,還有生命周期函數active
node.rcAnimTimeout = setTimeout(() => {
node.rcAnimTimeout = null;
nodeClasses.add(activeClassName);
if (active) {
setTimeout(active, 0);
}
fixBrowserByTimeout(node);
// 30ms for firefox
}, 30);
// node.rcEndListener在自己被調用之后會將自己置為空,我認為這里的返回是為了提供一個手動停止動畫的方法
return {
stop() {
if (node.rcEndListener) {
node.rcEndListener();
}
},
};
};