25-实践篇-自定义弹窗
课程
1
写给想要进阶的你
已学完
学习时长: 4分28秒
2
基础篇-认识 jsx
已学完
学习时长: 40分51秒
3
基础篇-起源 Component
已学完
学习时长: 32分50秒
4
基础篇-玄学 state
已学完
学习时长: 33分37秒
5
基础篇-深入 props
已学完
学习时长: 43分58秒
6
基础篇-理解 lifeCycle
学习时长: 72分46秒
7
基础篇-多功能 Ref
已学完
学习时长: 71分17秒
8
基础篇-提供者 context
已学完
学习时长: 53分16秒
9
基础篇-模块化 css
学习时长: 35分20秒
10
基础篇-高阶组件
学习时长: 43分29秒
11
优化篇-渲染控制
学习时长: 50分4秒
12
优化篇-渲染调优
学习时长: 35分8秒
13
优化篇-处理海量数据
学习时长: 28分1秒
14
优化篇-细节处理(持续)
学习时长: 32分19秒
15
原理篇-事件原理
学习时长: 30分48秒
16
原理篇-调度与时间片
学习时长: 23分47秒
17
原理篇- 调和与 fiber
学习时长: 35分54秒
18
原理篇-Hooks 原理
已学完
学习时长: 34分34秒
19
生态篇-React-router
学习时长: 50分9秒
20
生态篇-React-redux
学习时长: 50分34秒
21
生态篇-React-mobx
学习时长: 65分3秒
22
实践篇-实现mini-Router
学习时长: 37分19秒
23
实践篇-表单验证上
学习时长: 41分49秒
24
实践篇-表单验证下
学习时长: 46分31秒
25
实践篇-自定义弹窗
学习时长: 35分4秒
26
实践篇-自定义 Hooks 设计(持续)
学习时长: 32分13秒
27
实践篇-自定义 Hooks 实践(持续)
学习时长: 58分47秒
28
总结篇-如何有效阅读源码
学习时长: 7分18秒
29
原理篇-Context原理
学习时长: 28分27秒
30
原理篇-beginWork和render全流程
学习时长: 38分16秒
31
V18特性篇-useMutableSource(已被取缔)
学习时长: 28分37秒
32
V18特性篇-transition
学习时长: 38分15秒
33
原理篇-更新流程:进入调度任务
学习时长: 30分20秒
34
v18特性篇-concurrent 下的 state更新流程
学习时长: 29分27秒
35
v18特性篇-订阅外部数据源
学习时长: 16分53秒
36
原理篇-v18commit全流程
学习时长: 45分40秒
37
v18新特性-Offscreen
学习时长: 1秒
38
实践篇-设计并实现 keepalive 功能
学习时长: 71分13秒
juejin_logo copyCreated with Sketch.

一 前言

本章节,我们一起来设计一个自定义的弹窗组件,会包含如下知识点:

  • 弹窗组件设计;
  • ReactDOM.createPortal 使用;
  • 组件静态方法使用;
  • 不依赖父组件实现挂载/卸载组件。

二 设计思路

1 建立目标

要实现的具体功能下:

编写的自定义 Modal 可以通过两种方式调用:

  • 第一种通过挂载组件方式,动态设置 visible 属性。
<Modal  title={'《React进阶实践指南》'}  visible={visible}  >
    <div> hello,world </div>
</Modal>
  • 第二种通过 Modal 静态属性方法,控制 Modal 的显示/隐藏。
 Modal.show({ /* 自定义弹窗的显示 */
    content:<p>确定购买《React进阶指南小册》吗</p>,
    title:'《React进阶实践指南》',
    onOk:()=>console.log('点击确定'),
    onCancel:()=>console.log('点击取消'),
    onClose:()=> Modal.hidden() /* 自定义弹窗的隐藏 */
})

如上,Modal.show 控制自定义弹窗的显示,可以通过 Modal.hidden 控制弹窗的隐藏,业务层不需要挂载组件。

其他要求:

  • 自定义弹窗要有渐变的动画效果。

2 设计思路

1 props的设定

实现的 Modal 组件需要 props 配置项如下。

props 属性属性描述属性类型
visible当前 modal 是否显示boolean
onOk 回调函数当点击确定按钮触发function
onCancel 回调函数当点击取消按钮触发function
closeCb 回调函数当弹窗完全关闭后触发function
width弹窗宽度number
okTest确定按钮文案string
cancelText取消按钮文案string
titleModal标题string
footer自定义底部内容React Element
childrenModal 内容(插槽模式)React Element
contentModal 内容( props 属性模式)React Element

2 组件之外渲染

需要把弹窗组件渲染到挂载的容器之外,这样不受到父组件的影响。这里可以通过 ReactDOM.createPortal API解决这个问题。

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。createPortal 可以把当前组件或 element 元素的子节点,渲染到组件之外的其他地方。

createPortal 接受两个参数:

ReactDOM.createPortal(child, container)
  • 第一个: child 是任何可渲染的 React Element元素。
  • 第二个: container 是一个 DOM 元素。

3 不依赖父组件实现挂载/卸载组件

挂载组件

一个 React 应用,可以有多个 root Fiber, 所以可以通过 ReactDOM.render 来实现组件的自由挂载。

卸载组件

上面既然完成了挂载组件,下面需要在隐藏 Modal 的时候去卸载组件。 可以通过 ReactDOM.unmountComponentAtNode 来实现这个功能。

unmountComponentAtNode 从 DOM 中卸载组件,会将其事件处理器和 state 一并清除。 如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true ,如果没有组件可被移除将会返回 false 。

三 代码实现

1 组件层面

Modal——分配 props ,渲染视图

import Dialog from './dialog'

class Modal extends React.PureComponent{
    /* 渲染底部按钮 */
    renderFooter=()=>{
        const { onOk , onCancel , cancelText , okText, footer  } = this.props
        /* 触发 onOk / onCancel 回调  */
        if(footer && React.isValidElement(footer)) return footer
        return <div className="model_bottom" >
            <div className="model_btn_box" >
                <button className="searchbtn"  onClick={(e)=>{ onOk && onOk(e) }} >{okText || '确定'}</button>
                <button className="concellbtn" onClick={(e)=>{ onCancel && onCancel(e) }} >{cancelText || '取消'}</button>
            </div>
        </div>
    }

    /* 渲染顶部 */
    renderTop=()=>{
        const { title , onClose  } = this.props
        return <div className="model_top" >
            <p>{title}</p>
            <span className="model_top_close"  onClick={()=> onClose && onClose()} >x</span>
        </div>
    }

    /* 渲染弹窗内容 */
    renderContent=()=>{
        const { content , children } = this.props
        return  React.isValidElement(content) ? content
                : children ? children : null
    }
    render(){
        const { visible, width = 500 ,closeCb , onClose  } = this.props
        return <Dialog
            closeCb={closeCb}
            onClose={onClose}
            visible={visible}
            width={width}
               >
           {this.renderTop()}
           {this.renderContent()}
           {this.renderFooter()}
     </Dialog>
    }
}

设计思路:

  • Modal 组件的设计实际很简单,就是接收上述的 props 配置,然后分配给 Top, Foot, Content 等每个部分。
  • 这里通过 Dialog 组件,来实现 Modal 的动态显示/隐藏,增加动画效果。
  • 绑定确定 onOk ,取消 onCancel ,关闭 onClose 等回调函数。
  • 通过 PureComponent 做性能优化。

Dialog——控制显示隐藏

import React , { useMemo , useEffect ,useState  } from 'react'
import ReactDOM from 'react-dom'

 /* 控制弹窗隐藏以及动画效果 */
 const controlShow = (f1,f2,value,timer)=> {
    f1(value)
    return  setTimeout(()=>{
        f2(value)
    },timer)
}
export default function Dialog(props){
    const { width , visible , closeCb , onClose  } = props
    /* 控制 modelShow 动画效果 */
    const [ modelShow , setModelShow ] = useState(visible)
    const [ modelShowAync , setModelShowAync ] = useState(visible)
    const renderChildren = useMemo(()=>{
        /* 把元素渲染到组件之外的 document.body 上  */
        return ReactDOM.createPortal(
          <div style={{ display:modelShow ? 'block' : 'none'  }} >
              <div className="model_container" style={{ opacity:modelShowAync ? 1 : 0  }}  >
                <div className="model_wrap" >
                    <div  style={{ width:width + 'px'}}  > {props.children} </div>
                </div>
              </div>
              <div  className="model_container mast"  onClick={()=> onClose && onClose()} style={{ opacity:modelShowAync ? 0.6 : 0  }}  />
          </div>,
          document.body
         )
    },[ modelShowAync, modelShow ])
    useEffect(()=>{
        let timer
        if(visible){
            /* 打开弹窗,需要先让 */
           timer = controlShow(setModelShow,setModelShowAync,visible,30)
        }else{
           timer = controlShow(setModelShowAync,setModelShow,visible,1000)
        }
        return function (){
            timer && clearTimeout(timer)
        }
    },[ visible ])
    /* 执行关闭弹窗后的回调函数 closeCb */
    useEffect(()=>{
        !modelShow && typeof closeCb  === 'function' && closeCb()
    },[ modelShow ])
    return renderChildren

设计思路:

需要把元素渲染到组件之外,用 createPortal 把元素直接渲染到 document.body 下,为了防止函数组件每一次执行都触发 createPortal, 所以通过 useMemo 做性能优化。

因为需要渐变的动画效果,所以需要两个变量 modelShow / modelShowAync 来控制显示/隐藏,modelShow 让元素显示/隐藏,modelShowAync 控制动画执行。

  • 当弹窗要显示的时候,要先设置 modelShow 让组件显示,然后用 setTimeout 调度让 modelShowAync 触发执行动画。
  • 当弹窗要隐藏的时候,需要先让动画执行,所以先控制 modelShowAync ,然后通过控制 modelShow 元素隐藏,和上述流程相反。
  • 用一个控制器 controlShow 来流畅执行更新任务。

2 静态属性方法

对于通过组件的静态方法来实现弹窗的显示与隐藏,流程在上述基础上,要更复杂有一些。

let ModalContainer = null
const modelSysbol = Symbol('$$__model__Container_hidden')

/* 静态属性show——控制 */
Modal.show = function(config){
    /* 如果modal已经存在了,那么就不需要第二次show */
   if(ModalContainer) return
   const props = { ...config , visible: true }
   const container = ModalContainer =  document.createElement('div')
   /* 创建一个管理者,管理moal状态 */
   const manager =  container[modelSysbol] = {
       setShow:null,
       mounted:false,
       hidden(){
          const { setShow } = manager
          setShow && setShow(false)
       },
       destory(){
           /* 卸载组件 */
           ReactDOM.unmountComponentAtNode(container)
          /* 移除节点 */
          document.body.removeChild(container)
          /* 置空元素 */
          ModalContainer = null
       }
   }
   const ModelApp = (props) => {
       const [ show , setShow ] = useState(false)
       manager.setShow = setShow
       const { visible,...trueProps } = props
       useEffect(()=>{
           /* 加载完成,设置状态 */
           manager.mounted = true
           setShow(visible)
        },[])
       return <Modal  {...trueProps} closeCb={() => manager.mounted &&  manager.destory()}  visible={show}  />
   }
   /* 插入到body中 */
   document.body.appendChild(container)
   /* 渲染React元素 */
   ReactDOM.render(<ModelApp  {...props}  />,container)
   return manager
}

/* 静态属性——hidden控制隐藏 */
Modal.hidden = function(){
   if(!ModalContainer) return
   /* 如果存在 ModalContainer 那么隐藏 ModalContainer  */
   ModalContainer[modelSysbol] && ModalContainer[modelSysbol].hidden()
}

export default Modal

接下来,描述一下流程和细节:

  • 第一点:因为要通过调用 Modal 的静态属性来实现组件的显示与隐藏。所以用 Modal.show 来控制显示,Modal.hidden来控制隐藏。但是两者要建立起关联,所以通过全局ModalContainer属性,能够隐藏掉Modal.show 产生的元素与组件。

  • 第二点:如果调用 Modal.show,首先会创建一个元素容器 container ,用来挂载 Modal 组件,通过 ReactDOM.render 挂载,这里需要把 contianer 插入到 document.body 上。

  • 第三点:因为 Modal 组件要动态混入 visible 属性,并且做一些初始化的工作,比如提供隐藏弹窗的方法,所以创建一个 ModelApp 容器组件包裹 Modal。

  • 第四点:因为要在弹窗消失的动画执行后,再统一卸载组件和元素,所以到了本模块难点,就是创建一个 modal manager 管理者,通过 Symbol('$$__model__Container_hidden') 把管理者和容器之间建立起关联。容器下有 hidden 只是隐藏组件,并没有销毁组件,当组件隐藏动画执行完毕,会执行 closeCb 回调函数,在回调函数中再统一卸载元素和组件。

  • 第五点:调用Modal.hidden 本质上调用的是 manager 上的 hidden 方法 ,然后执行动画,执行隐藏元素。然后再触发 destory ,用 unmountComponentAtNode 和 removeChild 做一些收尾工作。完成整个流程。

创建弹窗流程图:

3.jpg

关闭弹窗流程图:

4.jpg

四 验证环节

验证第一种——通过挂载组件方式

/* 挂载方式调用modal */
export default function Index() {
    const [ visible , setVisible ] = useState(false)
    const [ nameShow , setNameShow ] = useState(false)
    const handleClick = () => {
        console.log('点击')
        setVisible(!visible)
        setNameShow(!nameShow)
    }
    /* 防止 Model 的 PureComponent 失去作用 */
    const [ handleClose ,handleOk, handleCancel ] = useMemo(()=>{
        const Ok = () =>  console.log('点击确定按钮')
        const Close = () => setVisible(false)
        const Cancel = () => console.log('点击取消按钮')
        return [ Close , Ok , Cancel  ]
    },[])

    return <div>
        <Modal
            onCancel={handleCancel}
            onClose={handleClose}
            onOk={handleOk}
            title={'《React进阶实践指南》'}
            visible={visible}
            width={700}
        >
           <div className="feel" >
              小册阅读感受: <input placeholder="写下你的感受" />
              {nameShow && <p>作者: 我不是外星人</p>}
           </div>
        </Modal>
        <button onClick={() => {
            setVisible(!visible)
            setNameShow(false)
        }}
        > model show </button>
        <button onClick={handleClick} > model show ( 显示作者 ) </button>
    </div>
}
  • 如上就是挂载的方式使用 Modal,注意 Modal 用的是 PureComponent ,父组件是函数组件在给 PureComponent 绑定方法的时候 ,要用 useMemo 或 useCallback 处理。

效果:

1.gif

验证第二种——通过静态属性方式

export default function Index(){
    const handleClick =() => {
        Modal.show({
            content:<p>确定购买《React进阶指南小册》吗</p>,
            title:'《React进阶实践指南》',
            onOk:()=>console.log('点击确定'),
            onCancel:()=>console.log('点击取消'),
            onClose:()=> Modal.hidden()
        })
    }
    return <div>
        <button onClick={() => handleClick()} >静态方式调用,显示modal</button>
    </div>
}
  • 这种方式用起来比上一种要简单。流程我就不细说了。

效果:

2.gif

五 总结

本章节的知识点总结:

  • 自定义弹窗组件的编写——挂载组件/调用静态属性两种方式。
  • ReactDOM.createPortal 使用。
  • ReactDOM.unmountComponentAtNode 和 ReactDOM.render 实现自由挂载/卸载组件。
  • hooks 的使用与性能优化。
留言
Ctrl + Enter
全部评论(21)
很帅很愁人的头像
删除
全村工程师 @ 下王庄村委员会
大佬是antd的核心成员吗,这么6666
点赞
回复
桔子桔子的头像
删除
FE @ 某互联网公司
写的不错。
点赞
1
删除
感谢啦
点赞
回复
EEEEEEEEE的头像
删除
前端
不是很明白 这里 两个 useState是如何控制动画的
点赞
1
删除
一个控制显示,一个控制渐变效果
点赞
回复
lycpang的头像
删除
切图仔 @ 努力让这一格不为null
okTest => okText
点赞
1
删除
收到
点赞
回复
涛涛_江的头像
删除
其实还没有搞懂 useMemo 和 useCallBack 他们的区别在哪里
只是知道一个缓存结果的 logic,一个是缓存对象,
两个的特性如何对比?落在实践的代码上。
点赞
2
删除
useMemo 主要是一段逻辑,这里可以是一些计算的产物,而 callback 单纯是一个值。两个性能方面实际没什么可比性,看具体应用场景,比如通过 state 计算得到一些衍生的状态,就用 useMemo 很合理,而给 pure 或者 memo 子组件绑定回调函数,usecallback 就是首选
2
回复
删除
useMemo感觉更像是Vue里面的计算函数,将一种或几种状态通过计算转化为另一种状态
1
回复
文艺不起来的我的头像
删除
纠正一个拼写错误:okText
1
4
删除
到目前依然没有调整...
点赞
回复
删除
已经改了啊
到目前依然没有调整...
点赞
回复
查看更多回复
dcbryant18067的头像
删除
👍👍感觉作者是负责维护组件库的,看起来很不错
3
5
删除
你是按照顺序阅读的吗?阅读的这么快?
点赞
回复
删除
代码仓库在哪里呢?大佬
你是按照顺序阅读的吗?阅读的这么快?
点赞
回复
查看更多回复