react-transition-group 探究(四)

199 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情

这章讲的是react-transition-group这个动画库里的TransitionGroup组件的使用和实现。

TransitionGroup

文档:reactcommunity.org/react-trans…

管理列表 (<Transition> and <CSSTransition>)

与过渡组件一样,是一个状态机,随时间管理组件的安装和卸载

主要用来给一组元素添加进场和离场动画

原理:用新的children和老的进行对比,如果多了就添加进场动画,少了就添加离场动画,动画结束再对渲染的children进行真正的添加和删除

例子

import { useState } from "react";
import "./App.css";
import "./test.css";
import { CSSTransition, TransitionGroup } from "./react-transition-group";
let id = 4;
// 动画 样式

function App() {
  const [items, setItems] = useState([
    { id: 1, text: "Buy eggs" },
    { id: 2, text: "Pay bills" },
    { id: 3, text: "Invite friends over" },
  ]);
  return (
    <div className="App">
      <header className="App-header">
        <TransitionGroup className="todo-list">
          {items.map(({ id, text }) => (
            <CSSTransition key={id} timeout={500} classNames="fade">
              <p
                key={id}
                onClick={() =>
                  setItems((items) => items.filter((item) => item.id !== id))
                }
              >
                {id}
              </p>
            </CSSTransition>
          ))}
        </TransitionGroup>
        <button
          onClick={() => {
            id++;
            setItems((items) => [...items, { id: id, text: id }]);
          }}
        >
          Add Item
        </button>
      </header>
    </div>
  );
}

export default App;

实现

初始化

和前面组件类似的流程, 在 getDerivedStateFromProps 初始化元素,为每个 子元素赋予 { in: true }然后渲染出来

import TransitionGroupContext from "./TransitionGroupContext";
import React from "react";
class TransitionGroup extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      childrenn: {},
    };
  }

  static getDerivedStateFromProps(nextProps, state) {
    return {
        // 初始化子元素
      childrenn: getInitialChildMapping(nextProps.children),
    };
  }
  render() {
    const { children } = this.state;
        // {key1:CSSTransition in:true,...}
    const component = Object.values(children);
    return (
	// [CSSTransition in:true,CSSTransition in:true,CSSTransition in:true,]
        {component}

    );
  }
}

export default TransitionGroup;
function getChilrenMapping(children, mapFn = (c) => c) {
  const result = {};
  React.Children.forEach(children, (child) => {
    result[child.key] = mapFn(child);
  });
    // {key1:CSSTransition in:true,...}
  return result;
}
function getInitialChildMapping(children) {
  return getChilrenMapping(children, (c) =>
    React.cloneElement(c, { in: true })
  );
}

新加组件动画准备

同之前类似,需要上下文

import TransitionGroupContext from "./TransitionGroupContext";
。。。
  constructor(props) {
    super(props);
    this.state = {
      childrenn: {},
    };
    mounted = false;
  }
// 初始化的时候不需要动画,但是更新的时候需要,挂载完成后,this.mounted = true;
// 这样新追入的 in为true的子元素 就有动画了
  componentDidMount() {
    this.mounted = true;
  }
。。。
 <TransitionGroupContext.Provider value={{ mounted: this.mount }}>
        {component}
 </TransitionGroupContext.Provider>

添加和删除

对比新老子组件,删除则对组件进行 in:false的操作,动画时间后再在渲染组件里面去掉该元素。

class TransitionGroup extends React.Component {
     this.state = {
      children: {},
      // 第一次 初始化
      firstRender: true,
      handleExited: this.handleExited,
    };
  handleExited = (key) => {
      // children删除这个元素
    this.setState((state) => {
      const children = { ...state.children };
      delete children[key];
      return { children };
    });
  };
 static getDerivedStateFromProps(
    nextProps,
    { children, firstRender, handleExited }
  ) {
    return {
      children: firstRender
        ? getInitialChildMapping(nextProps.children)
        : getNextChildrenMapping(nextProps, children, handleExited),
      //初始化后第二次 改为 false,之后 走另一个方法getNextChildrenMapping
      firstRender: false,
    };
  }
    
function getNextChildrenMapping(nextProps, prevChildrenMapping, handleExited) {
  const result = {};
  // 获取 新传入的 列表
  const nextChildrenMapping = getChilrenMapping(nextProps.children);
  // 新旧列表 取 最大
  const mergeMapping =
    Object.keys(prevChildrenMapping).length >
    Object.keys(nextChildrenMapping).length
      ? prevChildrenMapping
      : nextChildrenMapping;
  // 依次 对比 最大列表的元素
  Object.keys(mergeMapping).forEach((key) => {
    const isNext = key in nextChildrenMapping;
    const isPrev = key in prevChildrenMapping;
    if (!isPrev && isNext) {
      // 旧的不存在,新的存在,说明是新加入的元素 添加进来,由于 mounted是true,所以是会触发进场动画的
      result[key] = React.cloneElement(nextChildrenMapping[key], { in: true });
    } else if (isPrev && !isNext) {
      // 旧的存在,新的不存在 说明是删除了这个元素
      // 需要把元素的in改为false,并且在动画结束后 讲 渲染的数组里把 该元素去掉
      result[key] = React.cloneElement(prevChildrenMapping[key], {
        in: false,
        onExited: () => {
            // handleExited必须通过传参的方式,因为这样才能绑定这个组件,在这里this.setState是指向不到的
            // 而且 要放state里,静态方法static获取不到this的东西
          handleExited(key);
        },
      });
      // 新老都有,说明无变化
    } else if (isPrev && isNext) {
      result[key] = React.cloneElement(nextChildrenMapping[key], { in: true });
    }
  });
  return result;
}