原文地址:githubcss
antd
組件庫大量使用了react-component
的組件,而antd
咱們能夠理解爲是對react-component
的上層封裝,好比Form
,同時有大量的react-component
組件並非像Form
同樣被封裝一下使用,而是在其中起到了重要的協助做用,好比負責動畫效果的rc-animate
組件,node
簡單的來看一下總體組件的架構,分工明顯
複製代碼
AnimateChild
組件則負責對具體要執行效果的節點進行處理,包括對css-animate
的調用,回調函數的封裝處理實際上回調函數是在Animate
中封裝,AnimateChild
更適合比做Animate
與css-animate
中的潤滑劑css-animate
則負責具體元素的動畫執行,隨後調用各類傳入的回調,並不關心上層,只關心傳入的這個元素經過上面的架構,咱們能夠看出rc-animate
組件的責任劃分及其清晰明確,Animate
組件做爲一個容器組件,隨後將更加細化的處理邏輯下放到AnimateChild
中處理,而Animate
則只處理總體子元素的處理,分別推入相應隊列後對各個隊列進行處理,咱們來詳細查看這三部分react
咱們從應用初始化的步驟來看一下整個程序的邏輯git
constructor(props) {
super(props);
this.currentlyAnimatingKeys = {};
this.keysToEnter = [];
this.keysToLeave = [];
this.state = {
children: toArrayChildren(getChildrenFromProps(props)),
};
this.childrenRefs = {};
}
複製代碼
咱們能夠看到,constructor
中初始化了多個屬性,從名稱上咱們就能夠看到其做用,包括正在進行動畫的元素key
,將要移入移出的隊列,對子元素引用的map
,隨後將當前子元素節點緩存至state
中,github
咱們繼續往下看一下render
函數緩存
render() {
const props = this.props;
this.nextProps = props;
const stateChildren = this.state.children;
let children = null;
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');
}
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;
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;
}
複製代碼
在render
函數中,大體作了兩件事,antd
AnimateChild
包裝每個子節點,而後獲取其ref
存儲至咱們前面所說的childrenRefs
屬性中,(AnimateChild
咱們稍後再說,目前只須要記住上面兩點就能夠)component
屬性,也就是咱們能夠自定義容器組件,這個沒什麼好說的,若是沒有傳入則使用span
render
結束後,componentDidMount
生命週期函數會被調起,架構
componentDidMount() {
const showProp = this.props.showProp;
let children = this.state.children;
if (showProp) {
children = children.filter((child) => {
return !!child.props[showProp];
});
}
children.forEach((child) => {
if (child) {
this.performAppear(child.key);
}
});
}
複製代碼
在componentDidMount
中咱們看到其首先獲取咱們以前緩存的子元素節點,隨後經過showProp
屬性篩選出來全部配置爲顯示項的子節點,推入children
隊列,隨後遍歷調用performAppear
方法,能夠看到componentDidMount
生命週期函數是極其簡單的,只是作了兩件事,篩選和遍歷,而performAppear
咱們從字面意思上來看是執行本來就存在的動畫,那咱們先無論他,跟隨React
的生命週期繼續往下app
componentWillReceiveProps
函數能夠說是當前整個組件的核心函數
咱們如今本身想象一下,若是咱們要實現一個動畫調節的容器組件,最重要也是最核心的就是咱們要分辨哪些元素應該應用哪些動畫,也就是說,咱們須要知道哪些是移入,哪些是移除,也就是咱們在初始化中提到的keysToEnter
和keysToLeave
兩個隊列.而要分辨的時機就是在componentWillReceiveProps
生命週期中,咱們能夠對新舊兩組子元素進行對比,這也就是state.children
的做用,咱們能夠認爲state.children
是一個緩衝區,它存儲了新舊子節點中全部節點,這其中包括咱們提到了,沒存在將要移入的,存在將要移除的,本來一直存在的,咱們具體看一下代碼的處理
componentWillReceiveProps(nextProps) {
this.nextProps = nextProps;
const nextChildren = toArrayChildren(getChildrenFromProps(nextProps));
const props = this.props;
// exclusive needs immediate response
if (props.exclusive) {
Object.keys(this.currentlyAnimatingKeys).forEach((key) => {
this.stop(key);
});
}
const showProp = props.showProp;
const currentlyAnimatingKeys = this.currentlyAnimatingKeys;
// last props children if exclusive
const currentChildren = props.exclusive ?
toArrayChildren(getChildrenFromProps(props)) :
this.state.children;
// in case destroy in showProp mode
let newChildren = [];
if (showProp) {
currentChildren.forEach((currentChild) => {
const nextChild = currentChild && findChildInChildrenByKey(nextChildren, currentChild.key);
let newChild;
if ((!nextChild || !nextChild.props[showProp]) && currentChild.props[showProp]) {
newChild = React.cloneElement(nextChild || currentChild, {
[showProp]: true,
});
} else {
newChild = nextChild;
}
if (newChild) {
newChildren.push(newChild);
}
});
nextChildren.forEach((nextChild) => {
if (!nextChild || !findChildInChildrenByKey(currentChildren, nextChild.key)) {
newChildren.push(nextChild);
}
});
} else {
newChildren = mergeChildren(
currentChildren,
nextChildren
);
}
// need render to avoid update
this.setState({
children: newChildren,
});
nextChildren.forEach((child) => {
const key = child && child.key;
if (child && currentlyAnimatingKeys[key]) {
return;
}
const hasPrev = child && findChildInChildrenByKey(currentChildren, key);
if (showProp) {
const showInNext = child.props[showProp];
if (hasPrev) {
const showInNow = findShownChildInChildrenByKey(currentChildren, key, showProp);
//以前存在可是showProp爲false 因此未顯示,如今要顯示了
if (!showInNow && showInNext) {
this.keysToEnter.push(key);
}
} else if (showInNext) {
this.keysToEnter.push(key);
}
} else if (!hasPrev) {
this.keysToEnter.push(key);
}
});
currentChildren.forEach((child) => {
const key = child && child.key;
if (child && currentlyAnimatingKeys[key]) {
return;
}
const hasNext = child && findChildInChildrenByKey(nextChildren, key);
if (showProp) {
const showInNow = child.props[showProp];
if (hasNext) {
const showInNext = findShownChildInChildrenByKey(nextChildren, key, showProp);
if (!showInNext && showInNow) {
this.keysToLeave.push(key);
}
} else if (showInNow) {
this.keysToLeave.push(key);
}
} else if (!hasNext) {
this.keysToLeave.push(key);
}
});
}
複製代碼
咱們來逐步分析一下
首先,經過相同的方式解析新props
中的子節點,隨後判斷是否傳入了exclusive
,也就是是否只容許一組動畫進行,若是是,則調用下面的語句
Object.keys(this.currentlyAnimatingKeys).forEach((key) => {
this.stop(key);
});
複製代碼
咱們從字面意思中能夠看到,對currentlyAnimatingKeys
隊列,也就是正在執行的動畫隊列每一個元素調用中止,咱們姑且這樣認爲,繼續向下
const currentChildren = props.exclusive ?
toArrayChildren(getChildrenFromProps(props)) :
this.state.children;
複製代碼
這個咱們暫且無論,咱們當作咱們並無傳入exclusive
,那麼取值爲this.state.children;
,也就是在constructor
中存儲下來的子節點
if (showProp) {
currentChildren.forEach((currentChild) => {
const nextChild = currentChild && findChildInChildrenByKey(nextChildren, currentChild.key);
let newChild;
//判斷1
if ((!nextChild || !nextChild.props[showProp]) && currentChild.props[showProp]) {
newChild = React.cloneElement(nextChild || currentChild, {
[showProp]: true,
});
} else {
newChild = nextChild;
}
if (newChild) {
newChildren.push(newChild);
}
});
//判斷2
nextChildren.forEach((nextChild) => {
if (!nextChild || !findChildInChildrenByKey(currentChildren, nextChild.key)) {
newChildren.push(nextChild);
}
});
} else {
newChildren = mergeChildren(
currentChildren,
nextChildren
);
}
// need render to avoid update
this.setState({
children: newChildren,
});
複製代碼
隨後進入到核心步驟,由於核心都在showProp
爲true
的判斷項,咱們來看一下,咱們對上面獲取到的currentChildren
進行遍歷,對每個組件根據key
經過findChildInChildrenByKey
函數在新props
的子節點中進行查找,查找在新的子節點中是否還存在這個子節點,隨後繼續進行判斷,若是新節點再也不存在或者新節點的showProp
屬性爲false
,同時原先緩存子節點中存在該節點,則克隆一個showProp
爲true
的子節點賦值給newChild
,若是判斷未經過,則直接將nextChild
賦值給newChild
,隨後只要newChild
存在值,則將其推入newChildren
中,判斷2則對新props
中的全部子節點進行遍歷,新節點的處理則很是簡單,若是當前節點值爲false
或者是咱們以前緩存的節點中沒有找到新節點,則將其推入newChildren
,
如今咱們回過頭來看,判斷1主要是計算了以前沒有,或者以前沒顯示,也就是將要移入的又或者是一直存在的子節點,而判斷2則計算了將要移除的子節點,隨後將他們賦值到state.children
,這也就是咱們前面說到的緩存的做用,他綜合了新舊兩個子節點中全部要執行動畫的子節點,緩存下來,等待後續的進一步處理
隊列處理
nextChildren.forEach((child) => {
const key = child && child.key;
if (child && currentlyAnimatingKeys[key]) {
return;
}
const hasPrev = child && findChildInChildrenByKey(currentChildren, key);
if (showProp) {
const showInNext = child.props[showProp];
if (hasPrev) {
const showInNow = findShownChildInChildrenByKey(currentChildren, key, showProp);
//以前存在可是showProp爲false 因此未顯示,如今要顯示了
if (!showInNow && showInNext) {
this.keysToEnter.push(key);
}
} else if (showInNext) {
this.keysToEnter.push(key);
}
} else if (!hasPrev) {
this.keysToEnter.push(key);
}
});
currentChildren.forEach((child) => {
const key = child && child.key;
if (child && currentlyAnimatingKeys[key]) {
return;
}
const hasNext = child && findChildInChildrenByKey(nextChildren, key);
if (showProp) {
const showInNow = child.props[showProp];
if (hasNext) {
const showInNext = findShownChildInChildrenByKey(nextChildren, key, showProp);
if (!showInNext && showInNow) {
this.keysToLeave.push(key);
}
} else if (showInNow) {
this.keysToLeave.push(key);
}
} else if (!hasNext) {
this.keysToLeave.push(key);
}
});
複製代碼
這段代碼應該很好理解,主要是根據各類屬性判斷將其推入相應隊列中,等待下一個生命週期函數進行處理
當render
結束,進入componentDidUpdate
生命週期,這個週期中作的事情就簡單多了
componentDidUpdate() {
const keysToEnter = this.keysToEnter;
this.keysToEnter = [];
keysToEnter.forEach(this.performEnter);
const keysToLeave = this.keysToLeave;
this.keysToLeave = [];
keysToLeave.forEach(this.performLeave);
}
複製代碼
這裏咱們能夠看到,只是對移入移出兩個隊列分別調用不一樣的函數,
前面咱們說了,state.Children
中存儲了三種類型的子元素,移入,移出,本來就存在的,那麼在更新的時候咱們只須要處理移入移出,那麼如今當總體從新render
結束,咱們要開始應用動畫,咱們能夠從字面意思上看出componentDidUpdate
就是在作這個事情,咱們分別看一下performEnter
和performLeave
作了什麼
performEnter = (key) => {
// may already remove by exclusive
if (this.childrenRefs[key]) {
this.currentlyAnimatingKeys[key] = true;
this.childrenRefs[key].componentWillEnter(
this.handleDoneAdding.bind(this, key, 'enter')
);
}
}
performLeave = (key) => {
// may already remove by exclusive
if (this.childrenRefs[key]) {
this.currentlyAnimatingKeys[key] = true;
this.childrenRefs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key));
}
}
複製代碼
咱們從這能夠看到,不過是根據key
去遍歷調用咱們以前存儲的AnimateChild
實例的componentWillLeave
和componentWillEnter
方法,並傳入相應的函數,從名稱來看應該是動畫結束的回調函數,那麼咱們來看看這兩個函數分別作了什麼
handleDoneAdding = (key, type) => {
const props = this.props;
delete this.currentlyAnimatingKeys[key];
// if update on exclusive mode, skip check
if (props.exclusive && props !== this.nextProps) {
return;
}
const currentChildren = toArrayChildren(getChildrenFromProps(props));
if (!this.isValidChildByKey(currentChildren, key)) {
// exclusive will not need this
this.performLeave(key);
} else if (type === 'appear') {
if (animUtil.allowAppearCallback(props)) {
props.onAppear(key);
props.onEnd(key, true);
}
} else if (animUtil.allowEnterCallback(props)) {
props.onEnter(key);
props.onEnd(key, true);
}
}
handleDoneLeaving = (key) => {
const props = this.props;
delete this.currentlyAnimatingKeys[key];
// if update on exclusive mode, skip check
if (props.exclusive && props !== this.nextProps) {
return;
}
const currentChildren = toArrayChildren(getChildrenFromProps(props));
// in case state change is too fast
if (this.isValidChildByKey(currentChildren, key)) {
this.performEnter(key);
} else {
const end = () => {
if (animUtil.allowLeaveCallback(props)) {
props.onLeave(key);
props.onEnd(key, false);
}
};
if (!isSameChildren(this.state.children,
currentChildren, props.showProp)) {
this.setState({
children: currentChildren,
}, end);
} else {
end();
}
}
}
複製代碼
咱們能夠看到,這兩個函數大同小異,核心確實是跟咱們按照名稱猜想的同樣是去獲取傳入的各類動畫狀態的結束回調,值得一提的是,這兩個函數都會調用this.isValidChildByKey
函數來檢測當前的props
中是否存在當前key
的子節點,上面註釋也說的很清楚是爲了防止狀態過快變更,咱們假設一個很簡單的例子就很好理解了,
若是一個子節點經歷了,移入=>移出=>再移入,按照咱們上面說的處理流程來講,若是數據變動過快極有可能出現上面預防的狀況,也就是再移入已經生效了,移出特效纔剛剛結束,移出回調被調用,這是就要作出必定的補救措施,這也就是這兩個函數這麼作的緣由,
好咱們上面說了這麼多Animate
組件,咱們再來回頭看看AnimateChild
組件,看看他做爲一個協調器的做用是如何工做的
在Animate
組件中,咱們介紹了,Animate
會調用AnimateChild
組件實例上的某些方法,他們名稱相似於React
原有的生命週期函數,因此我爲了順口叫作自定義生命週期,(不要在乎),
componentWillEnter(done) {
if (animUtil.isEnterSupported(this.props)) {
this.transition('enter', done);
} else {
done();
}
}
componentWillAppear(done) {
if (animUtil.isAppearSupported(this.props)) {
this.transition('appear', done);
} else {
done();
}
}
componentWillLeave(done) {
if (animUtil.isLeaveSupported(this.props)) {
this.transition('leave', done);
} else {
done();
}
}
複製代碼
咱們看到,這三個函數其實都是同樣的,都是調用了this.transition
同時傳入動畫類型和回調函數,也就是咱們上面說的performEnter
等三個處理函數中傳入的handleDoneLeaving
等函數,那麼咱們來看看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();
};
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`];
}
this.stopper = cssAnimate(node, {
name,
active: activeName,
}, end);
} else {
this.stopper = props.animation[animationType](node, end);
}
}
stop() {
const stopper = this.stopper;
if (stopper) {
this.stopper = null;
stopper.stop();
}
}
複製代碼
咱們能夠看到,transition
的核心就是構建cssAnimate
須要的參數,隨後經過CSSAnimate
去完成動畫,由於整個Animate
組件動畫能夠經過多種方式配置,因此transition
作了多種判斷來尋找各類狀態下的css類,
通篇Animate
組件看下來,咱們能夠看到一個很常見的分治的思想,經過將不一樣的狀況規劃到不一樣的隊列,隨後分別調用處理函數來處理該狀態應有的動畫,大大下降了總體的複雜度,若是咱們沒有進行合理劃分整個組件的複雜度會呈指數級上升,同時也不利於維護.同時我在閱讀中也學到不少,最後仍是說盡信書不如無書,若有謬誤之處請不吝斧正.