前言
之前写的react组件:
-
Affix组件: [react组件库源码+ 单测解析(Affix 固钉组件)]
-
GridLayout组件:秒杀ant design布局组件
-
Button和ButtonGroup 按钮组件: react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)
-
日历组件:
日历基本功能
日历组件拖拽功能原理
接下来要写Modal,或者叫Dialog组件,效果如下:
在写之前,我们可能需要一个淡出动画,所以跟大家介绍一个实现普通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函数的基本实现
手动实现一个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,这个组件很简单。
暂无评论内容