react实现 Modal组件前置技术:react-transtion-group源码分析

前言

之前写的react组件:

接下来要写Modal,或者叫Dialog组件,效果如下:

image.png

在写之前,我们可能需要一个淡出动画,所以跟大家介绍一个实现普通css动画的实用库,’react-transtion-group’中的CSSTranstion。

如果我们想把任意组件渲染到任意位置,比如我在一个div里弹框,想弹框到body元素下,就需要Portal组件了。

而Modal组件的实现都依赖上面两个通用组件,所以是前置知识点。我们这里只谈react-transtion-group的源码

开始

react-transition-group 共为我们提供了如下四个组件:

  • Transition
  • CSSTransition
  • SwitchTransition
  • TransitionGroup
  • 我们这里减少最常见的Transition和CSSTransition的用法

我们先看看 Transition这个组件如何使用

在线使用地址:
https://codesandbox.io/s/inspiring-fog-ix8229?file=/src/App.tsx

import { Transition } from 'react-transition-group'; 
 
 
function Demo() { 
  const [inProp, setInProp] = useState(false); // 用来控制子组件的挂载、卸载 
  return ( 
    <div> 
      <Transition in={inProp} timeout={2000}>  
      /* 
      *Transition子组件是一个函数、其接受state(代表挂载、卸载的不同阶段)作为参数 
      **/ 
        {state => ( 
          <h1> {state} </h1> 
        )} 
      </Transition> 
      <button onClick={() => setInProp(!inProp)}> 
        Click to Enter 
      </button> 
    </div> 
  ); 
} 

你会看到state初始值是entered

点击按钮后,会马上显示exiting然后过了2秒显示exited

再次点击,会马上显示entering然后过了2秒显示entered

手动实现一个简易版的Transition

首先一个基本的架子如下:

// 将用到的一些常量存储起来 
export const UNMOUNTED = 'unmounted' 
export const EXITED = 'exited' 
export const ENTERING = 'entering' 
export const ENTERED = 'entered' 
export const EXITING = 'exiting' 

export default class Transition extends Component { 
  static contextType = TransitionGroupContext 
 
  private nextCallback 
 
  constructor(props, context) { 
    super(props) 

    let initialStatus;
    if (props.in) {
      if (appear) {
        initialStatus = EXITED;
        this.appearStatus = ENTERING;
      } else {
        initialStatus = ENTERED;
      }
     }  else {
      if (props.unmountOnExit || props.mountOnEnter) {
        initialStatus = UNMOUNTED;
      } else {
        initialStatus = EXITED;
      }
    }
    this.state = { 
      status: initialStatus, // 用来存放过渡状态,初始态为EXITED 
    } 
  } 
   
   componentDidMount() {
     // 此处主要用以控制,Transition的子组件在首次挂载的时候是否执行进场动画
    this.updateStatus(true, this.appearStatus);
  }
 
  render() { 
    const { 
      children, 
      onEnter: _onEnter, 
      onEntering: _onEntering, 
      onEntered: _onEntered, 
      onExit: _onExit, 
      onExiting: _onExiting, 
      onExited: _onExited, 
      in: _in, 
      timeout: _timeout, 
      ...childProps 
    } = this.props 
    const { status } = this.state 
 
    return ( 
      <TransitionGroupContext.Provider value={null}> 
        {typeof children === 'function' 
          ? children(status) 
          : React.cloneElement( 
              React.Children.only(children) as React.ReactElement, 
              childProps, 
            )} 
      </TransitionGroupContext.Provider> 
    ) 
  } 
} 

很简单吧,刚开始,构造函数从in这里确认状态是展示还是不展示,in的类型是boolean,如果有appear参数,那么就会指定是否有进场动画,如果有进场动画的话会在componentDidMount声明周期里把 ENTERING 参数传入,表示要执行进场动画.

  // 更新状态统一收口在该处 
  updateStatus(mounting = false, nextStatus) { 
    if (nextStatus !== null) { 
      this.cancelNextCallback() // 先取消上次的回调函数 
      if (nextStatus === ENTERING) { 
        this.performEnter(mounting) // mounting 主要用来控制是否是初始进场动画 
      } else { 
        this.performExit() // 执行退场动画相关 
      } 
    } 
  } 

这个函数,调用了performEnter,还有一个this.cancelNextCallback我们一起看看

好了,我们接着看最重要的this.updateStatus方法的实现

// 执行进场相关操作 
  performEnter(mounting) { 
    // console.log('执行进场动画') 
 
    const { onEnter, onEntering, onEntered, timeout } = this.props 
 
    // eslint-disable-next-line react/no-find-dom-node 
    const node = ReactDOM.findDOMNode(this) as Element 
 
    onEnter(node, mounting) 
     
    //先更新状态为ENTERING,然后在指定时间间隔timeout之后更新状态为 ENTERED  
    this.safeSetState({ status: ENTERING }, () => { 
      onEntering(node, mounting) 
      this.onTransitionEnd(timeout, () => { 
        this.safeSetState({ status: ENTERED }, () => { 
          onEntered(node, mounting) 
        }) 
      }) 
    }) 
  } 
 

从上面我们可以看到,进场的话,会先去执行onEnter操作,然后马上跟着执行onEntering,过了timeout后就执行onEntered

这里有一些方法我们需要了解,第一个是this.safeSetState是啥,第二个是this.onTransitionEnd

  // 用以确保setState异步回调函数可以被取消 
  safeSetState(state, callback) { 
    callback = this.setNextCallBack(callback) 
    this.setState(state, callback) 
  } 

这个方法其实就是setState改变当前状态,需要注意的 是这里有一个不错的学习点this.setNextCallBack帮我们装饰一个callback变为一个可中断的callback

  // 利用闭包特性来控制callbakc可以随时被取消 
  setNextCallBack(callback) { 
    let active = true 
 
    this.nextCallback = (event) => { 
      if (active) { 
        active = false 
        this.nextCallback = null 
        callback(event) 
      } 
    } 
 
    this.nextCallback.cancel = () => { 
      active = false 
    } 
 
    return this.nextCallback 
  } 

可以看到,被装饰的callback拥有了cancel方法,用来取消操作

我们接着看onTransitionEnd是什么函数,其实很简单,就是指定timeout时间间隔以后执行callback

 
  // 在指定timeout时间间隔以后执行callback 
  onTransitionEnd(timeout, callback) { 
    if (timeout !== null) { 
      callback = this.setNextCallBack(callback) 
      setTimeout(callback, timeout) 
    } 
  } 
   

我们再回到上面提到的包装callback的问题,为啥要包装,主要是为了增加取消callback执行的操作,比如我点了一下按钮,从entering变化到entered,那么在变化过程中,我可以继续点按钮,本来entered是在两秒后执行的,是不是两秒后我要取消执行的函数才合理,对吧

我们接着看下全部代码的Transition函数的基本实现

我们再回到上面提到的包装callback的问题,为啥要包装,主要是为了增加取消callback执行的操作,比如我点了一下按钮,从entering变化到entered,那么在变化过程中,我可以继续点按钮,本来entered是在两秒后执行的,是不是两秒后我要取消执行的函数才合理,对吧

我们接着看下全部代码的Transition函数的基本实现

98104801-ad32-4e2e-9c7b-0ac48857402b.png

手动实现一个CSSTransition

看一个示例

import React, { useState } from 'react' 
import { CSSTransition } from 'react-transition-group' 
 
export default function CSSTransitionDemo () { 
  const [inProp, setInProp] = useState(false) 
  return ( 
    <> 
      <CSSTransition 
        in={inProp} 
        timeout={2000} 
        classNames="my-node" 
       > 
        <div id="test"> 
          {"I'll receive my-node-* classes"} 
        </div> 
      </CSSTransition> 
      <button type="button" onClick={() => setInProp(!inProp)}> 
        Click to Enter 
      </button> 
    </> 
  ) 
} 

然后我们可以看到,第一次点击按钮,审查元素div#test标签的类名发生了变化:

依次为”my-node-enter“、”my-node-enter my-node-enter-active“ 2 秒后

“my-node-enter-down”

第二次点击的时候,类名变化如下:

依次为 ”my-node-exit“, “my-node-exit my-node-exit-active” 2秒后

”my-node-exit-done“

接着我们看一下代码,可以看到cssTransition就是封装的Transition,并且在onEnter,

onEntering,

onEntered,

onExit,

onExiting,

onExited

这些声明周期添加了css类名而已,太简单了,直接上代码吧

import React from 'react' 
import Transition, { TransitionPropTypes } from './transition' 
 
import { addClassNames, removeClassNames } from './utils' 
 
interface CSSTransitionPropTypes extends TransitionPropTypes { 
  classNames: string 
} 
 
export default function CSSTransition(props: CSSTransitionPropTypes) { 
  const { 
    classNames, 
    onEnter, 
    onEntering, 
    onEntered, 
    onExit, 
    onExiting, 
    onExited, 
    ...otherProps 
  } = props 
 
  // 返回 base active done 状态的类名 
  const getClassNames = (status) => { 
    const { classNames } = props 
    const baseClassName = `${classNames}-${status}` 
    const activeClassName = `${classNames}-${status}-active` 
    const doneClassName = `${classNames}-${status}-done` 
    return { 
      base: baseClassName, 
      active: activeClassName, 
      done: doneClassName, 
    } 
  } 
 
  // 给给定的dom节点添加类名、并控制浏览器是否强制重绘 
  const addClassNamesAndForceRepaint = ( 
    node, 
    classNames, 
    forceRepaint = false, 
  ) => { 
    // 此处主要是为了强制浏览器重绘 
    if (forceRepaint) { 
      node && node.offsetLeft 
    } 
    addClassNames(node, classNames) 
  } 
  // 移除其他的类名并添加进场开始类名 
  const _onEnter = (node, maybeAppear) => { 
    // 移除上一次的类名 
    const exitClassNames = Object.values(getClassNames('exit')) 
    removeClassNames(node, exitClassNames) 
     
    // 添加新的类名 
    const enterClassName = getClassNames('enter').base 
    addClassNamesAndForceRepaint(node, enterClassName) 
 
    if (onEnter) { 
      onEnter(node, maybeAppear) 
    } 
  } 
  // 添加进场进行时类名 
  const _onEntering = (node, maybeAppear) => { 
     
    // 添加新的类名 
    const enteringClassName = getClassNames('enter').active 
    addClassNamesAndForceRepaint(node, enteringClassName, true) 
 
    // 执行回调函数 
    if (onEntering) { 
      onEntering(node, maybeAppear) 
    } 
  } 
  // 移除其他类名、添加进场结束类名 
  const _onEntered = (node, maybeAppear) => { 
     
    // 移除旧的类名 
    const enteringClassName = getClassNames('enter').active 
    const enterClassName = getClassNames('enter').base 
    removeClassNames(node, [enterClassName, enteringClassName]) 
     
    // 添加新的类名 
    const enteredClassName = getClassNames('enter').done 
    addClassNamesAndForceRepaint(node, enteredClassName) 
 
    // 执行回调函数 
    if (onEntered) { 
      onEntered(node, maybeAppear) 
    } 
  } 
   
  // 移除其他类名、添加退场开始类名 
  const _onExit = (node) => { 
    // 移除上一次的类名 
    const enteredClassNames = Object.values(getClassNames('enter')) 
    removeClassNames(node, enteredClassNames) 
     
    // 添加新的类名 
    const exitClassName = getClassNames('exit').base 
 
    addClassNamesAndForceRepaint(node, exitClassName) 
    if (onExit) { 
      onExit(node) 
    } 
  } 
   
  // 添加退场进行时类名 
  const _onExiting = (node) => { 
   
    const exitingClassName = getClassNames('exit').active 
    addClassNamesAndForceRepaint(node, exitingClassName, true) 
 
    if (onExit) { 
      onExit(node) 
    } 
  } 
  // 添加退场完成时类名 
  const _onExited = (node) => { 
    const exitingClassName = getClassNames('exit').active 
    const exitClassName = getClassNames('exit').base 
    removeClassNames(node, [exitClassName, exitingClassName]) 
 
    const exitedClassName = getClassNames('exit').done 
    addClassNamesAndForceRepaint(node, exitedClassName) 
 
    if (onExited) { 
      onExited(node) 
    } 
  } 
 
  return ( 
    <Transition 
      {...otherProps} 
      onEnter={_onEnter} 
      onEntering={_onEntering} 
      onEntered={_onEntered} 
      onExit={_onExit} 
      onExiting={_onExiting} 
      onExited={_onExited} 
    > 
      {props.children} 
    </Transition> 
  ) 
} 

接着下篇文章实现Portal,这个组件很简单。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容