rc-animate源碼淺析

原文地址:githubcss

前言

antd組件庫大量使用了react-component的組件,而antd咱們能夠理解爲是對react-component的上層封裝,好比Form,同時有大量的react-component組件並非像Form同樣被封裝一下使用,而是在其中起到了重要的協助做用,好比負責動畫效果的rc-animate組件,node

責任劃分

簡單的來看一下總體組件的架構,分工明顯
複製代碼
  1. Animate組件負責統籌規劃,全部子節點中每一個節點分別應該應用何種效果,推入相應隊列進行處理
  2. AnimateChild組件則負責對具體要執行效果的節點進行處理,包括對css-animate的調用,回調函數的封裝處理實際上回調函數是在Animate中封裝,AnimateChild更適合比做Animatecss-animate中的潤滑劑
  3. css-animate則負責具體元素的動畫執行,隨後調用各類傳入的回調,並不關心上層,只關心傳入的這個元素

經過上面的架構,咱們能夠看出rc-animate組件的責任劃分及其清晰明確,Animate組件做爲一個容器組件,隨後將更加細化的處理邏輯下放到AnimateChild中處理,而Animate則只處理總體子元素的處理,分別推入相應隊列後對各個隊列進行處理,咱們來詳細查看這三部分react

Animate

​ 咱們從應用初始化的步驟來看一下整個程序的邏輯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函數緩存

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

  1. 對當前全部子節點進行包裝,也就是經過AnimateChild包裝每個子節點,而後獲取其ref存儲至咱們前面所說的childrenRefs屬性中,(AnimateChild咱們稍後再說,目前只須要記住上面兩點就能夠)
  2. 對全部子節點進行再包裝,也就是咱們要傳入的component屬性,也就是咱們能夠自定義容器組件,這個沒什麼好說的,若是沒有傳入則使用span
componentDidMount

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

componentWillReceiveProps函數能夠說是當前整個組件的核心函數

咱們如今本身想象一下,若是咱們要實現一個動畫調節的容器組件,最重要也是最核心的就是咱們要分辨哪些元素應該應用哪些動畫,也就是說,咱們須要知道哪些是移入,哪些是移除,也就是咱們在初始化中提到的keysToEnterkeysToLeave兩個隊列.而要分辨的時機就是在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);
      }
    });
  }
複製代碼

咱們來逐步分析一下

  1. 首先,經過相同的方式解析新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中存儲下來的子節點

  2. 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,
        });
    複製代碼

    隨後進入到核心步驟,由於核心都在showProptrue的判斷項,咱們來看一下,咱們對上面獲取到的currentChildren進行遍歷,對每個組件根據key經過findChildInChildrenByKey函數在新props的子節點中進行查找,查找在新的子節點中是否還存在這個子節點,隨後繼續進行判斷,若是新節點再也不存在或者新節點的showProp屬性爲false,同時原先緩存子節點中存在該節點,則克隆一個showProptrue的子節點賦值給newChild,若是判斷未經過,則直接將nextChild賦值給newChild,隨後只要newChild存在值,則將其推入newChildren中,判斷2則對新props中的全部子節點進行遍歷,新節點的處理則很是簡單,若是當前節點值爲false或者是咱們以前緩存的節點中沒有找到新節點,則將其推入newChildren,

    如今咱們回過頭來看,判斷1主要是計算了以前沒有,或者以前沒顯示,也就是將要移入的又或者是一直存在的子節點,而判斷2則計算了將要移除的子節點,隨後將他們賦值到state.children,這也就是咱們前面說到的緩存的做用,他綜合了新舊兩個子節點中全部要執行動畫的子節點,緩存下來,等待後續的進一步處理

  3. 隊列處理

    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);
          }
        });
    複製代碼

    這段代碼應該很好理解,主要是根據各類屬性判斷將其推入相應隊列中,等待下一個生命週期函數進行處理

componentDidUpdate

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就是在作這個事情,咱們分別看一下performEnterperformLeave作了什麼

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實例的componentWillLeavecomponentWillEnter方法,並傳入相應的函數,從名稱來看應該是動畫結束的回調函數,那麼咱們來看看這兩個函數分別作了什麼

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組件,看看他做爲一個協調器的做用是如何工做的

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
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組件看下來,咱們能夠看到一個很常見的分治的思想,經過將不一樣的狀況規劃到不一樣的隊列,隨後分別調用處理函數來處理該狀態應有的動畫,大大下降了總體的複雜度,若是咱們沒有進行合理劃分整個組件的複雜度會呈指數級上升,同時也不利於維護.同時我在閱讀中也學到不少,最後仍是說盡信書不如無書,若有謬誤之處請不吝斧正.

相關文章
相關標籤/搜索