写点什么

这可能是你需要的 React 实战技巧

  • 2022 年 10 月 04 日
    浙江
  • 本文字数:6010 字

    阅读完需:约 20 分钟

一、父组件通过 Ref 调用子组件中的方法

这里同时演示使用函数组件类组件的父子组件如何编写

子组件

  • React.forwardRef

  • React.useImperativeHandle

  • public、private、protected


/** * 声明一个 function component 作为子组件 * 通过 forwardRef 接收父组件传递的 ref * 通过 useImperativeHandle 改造原 ref * 同时定义类型 IFnChildInstance 明确返回的 ref 的类型(非 typescript 不用考虑这个) * 同时演示了组件的 props 应该写在哪里 */interface IFnChildInstance {  show: () => void}interface IFnChildProps {  testname: string}const CompFnChild = React.forwardRef<IFnChildInstance, IFnChildProps>(function (  props,  ref) {  const { testname } = props  React.useImperativeHandle(ref, () => ({    show: () => setVisible(true),  }))  const [visible, setVisible] = React.useState(false)
return ( <div> <p>演示下state:{visible ? "显示" : "隐藏"}</p> <p>演示下props:{testname}</p> <div onClick={() => setVisible(false)}>隐藏</div> </div> )})
/** * 声明一个 class component 作为子组件 * 通过 public 明确这就是我们希望父亲组件能调用的方法(public/private/protected) */interface IClassChildProps { testname: string}interface IClassChildState { visible: boolean}class CompClassChild extends React.Component< IClassChildProps, IClassChildState> { constructor(props: IClassChildProps) { super(props) this.state = { visible: false, } } public show = () => { this.setState({ visible: true, }) } private handleHide = () => { this.setState({ visible: false, }) } render() { const { visible } = this.state const { testname } = this.props return ( <div> <p>演示下state:{visible ? "显示" : "隐藏"}</p> <p>演示下props:{testname}</p> <div onClick={this.handleHide}>隐藏</div> </div> ) }}
复制代码

父组件

  • React.useRef

  • React.createRef


function CompFnParent() {  const RefFnChild = React.useRef<IFnChildInstance>(null)  const RefClassChild = React.useRef<CompClassChild>(null)
const myname = "tellyourmad"
return ( <> <div onClick={() => RefFnChild.current?.show()}> 调用 CompFnChild 的方法 </div> <CompFnChild ref={RefFnChild} testname={myname} />
<div onClick={() => RefClassChild.current?.show()}> 调用 CompClassChild 的方法 </div> <CompClassChild ref={RefClassChild} testname={myname} /> </> )}
class CompClassParent extends React.Component { private RefFnChild = React.createRef<IFnChildInstance>() private RefClassChild = React.createRef<CompClassChild>()
componentDidMount() { // TODO }
render() { const myname = "tellyourmad" return ( <> <div onClick={() => this.RefFnChild.current?.show()}> 调用 CompFnChild 的方法 </div> <CompFnChild ref={this.RefFnChild} testname={myname} />
<div onClick={() => this.RefClassChild.current?.show()}> 调用 CompClassChild 的方法 </div> <CompClassChild ref={this.RefClassChild} testname={myname} /> </> ) }}
复制代码


总结一下,其实使用 class 方式再配合上 typescript 编写的子组件其实是最能简洁明了的

参考 React 实战视频讲解:进入学习

二、memoize 的应用

get(computed)

平时我们有时候会用 get 对一些数据进行处理:


interface IMyTestProps {}interface IMyTestState {  firstname: string  lastname: string}class MyTest extends React.Component<IMyTestProps, IMyTestState> {  get name() {    const { firstname, lastname } = this.state    return firstname + lastname  }  render(){    return (      <div>我的名字是:{this.name}</div>    )  }}
复制代码


但是在每次触发 render 的时候都需要重新计算 get name,如果这里逻辑非常复杂,那将会消耗大量性能。其实很多时候我们只需要判断入参有没有发生变化即可判断是否需要重新计算。譬如例子中,如果 firstnamelastname 没有发生变化则不需要重新计算。

memoize

这里使用 memoize 包裹后:


import { memoize } from "lodash"
interface IMyTestProps {}interface IMyTestState { firstname: string lastname: string}class MyTest extends React.Component<IMyTestProps, IMyTestState> { get url() { const { firstname, lastname } = this.state return this.getUrl(firstname, lastname) } private getUrl = memoize(function (firstname, lastname) { return firstname + lastname }) render(){ return ( <div>我的名字是:{this.name}</div> ) }}
复制代码


  • 这里用的是 lodash 库里面的方法,有兴趣可以自己去看源码

  • 这种优化方式其实跟 debounce、throttle 都是一个意思,都是根据一定条件来判断是否应该节约本次计算


p.s. 这里只是演示了一个简单的 firstname + lastname 例子,实际是不需要考虑优化的,因为本身 memoize 也是要执行比较逻辑,当入参数非常复杂时,这样优化其实是得不偿失的,所以具体情况要具体分析。

三、实现一个弹窗组件(toast/modal/dialog)

你可以看到不管 antd(react) 还是 element(vue) 中的弹窗组件都是渲染在 document.body 上的,而非当前组件所对应的 render 节点上


import { Modal } from "antd"
class Test extends React.Component { componentDidMount() { Modal.info({ title: "通过api", visible: true, }) } render() { return ( <div> <div>测是是的话i说的</div> <Modal title="通过组件" visible={true} /> </div> ) }}
ReactDOM.render(<Test />, document.getElementById("root"))
复制代码



上面例子演示了两种弹窗使用方式,分别是 通过 api 调用使用 react 组件,下面会逐个举例如何实现:

通过 api 调用

  1. document.createElement 创建 dom

  2. document.body.appendChild 插入 dom

  3. ReactDOM.render 渲染组件

  4. 调用实例中的方法 或者 直接给实例传递


import React from "react"import ReactDOM from "react-dom"
const show = function (props: IModalProps) { const node = document.createElement("div") document.body.appendChild(node)
// 顺便说下,其实不必在这里写样式,没意义的 // node.style.zIndex = "999";
const handleClose = function () { /** * 在 modal 关闭后会触发销毁 * 目前这里是 setState({visible: false}) 之后就立马触发销毁的 * 如果想 antd 那样还有消失过渡效果(transition)的话,还得加个延迟哦~ * * p.s. 不销毁会导致性能等问题 */ ReactDOM.unmountComponentAtNode(node) // 卸载 react 组件 document.body.removeChild(node) // 删除 dom 节点 }
/** ReactDOM.render<IModalProps, Modal>( <Modal {...props} onClose={() => { props.onClose && props.onClose() handleClose() }} />, node ).show() // render 之后调用实例中的 show 方法 **/ // 因为在未来的 react 中,组件的渲染又可能是异步的,所以不建议直接使用 render 返回的实例,应该用下面方式 ReactDOM.render<IModalProps, Modal>( <Modal {...props} onClose={() => { props.onClose && props.onClose() handleClose() }} />, node, function(this: Modal) { this.show() // 在 callback 之后调用实例的方法 } )}
interface IModalProps { title: string onClose?: () => void}interface IModalState { visible: boolean}class Modal extends React.Component<IModalProps, IModalState> { constructor(props: IModalProps) { super(props) this.state = { visible: false, } }
// 暴露 show 方法 public show = () => { this.setState({ visible: true, }) }
private handleClose = () => { this.setState({ visible: false, }) this.props.onClose && this.props.onClose() } render() { return ( <div style={{ // 其他样式就应该不用演示了吧,就不写了 position: "absolute", zIndex: 999, left: 0, right: 0, top: 0, bottom: 0, visibility: this.state.visible ? "visible" : "hidden", }} > <div>{this.props.title}</div> <span onClick={this.handleClose}>关闭</span> </div> ) }}
function Test() { return ( <div onClick={() => show({ title: "标题" })}> <p>通过调用 show 方法即可显示弹窗</p> </div> )}
复制代码

使用 react 组件

  • ReactDOM.createPortal 建立传送门

  • 通过 props 控制


interface IModalProps {  visible: boolean  title: string  onClose: () => void}class Modal extends React.Component<IModalProps> {  private el: HTMLDivElement  private readonly container = document.body
constructor(props: IModalProps) { super(props) this.el = document.createElement("div") } componentDidMount() { this.container.appendChild(this.el) } componentWillUnmount() { this.container.removeChild(this.el) }
private handleClose = () => this.props.onClose() render() { return ReactDOM.createPortal( <div style={{ // 其他样式就应该不用演示了吧,就不写了 position: "absolute", zIndex: 999, left: 0, right: 0, top: 0, bottom: 0, visibility: this.props.visible ? "visible" : "hidden", }} > <div>{this.props.title}</div> <span onClick={this.handleClose}>关闭</span> </div>, this.el ) }}
function Test() { const [visible, setVisible] = React.useState(false) return ( <div onClick={() => setVisible(true)}> <p>使用 react 组件</p> <Modal title="标题" visible={visible} onClose={() => setVisible(false)} /> </div> )}
复制代码

合体

我们的期望是这个 Modal 组件像 antd 的一样,既能通过 Modal.show() 方式使用,也是通过 <Modal/> 方式使用,这里就将上面两个例子进行合并


const show = function (props: Omit<IModalProps, "visible">) {  const node = document.createElement("div")  document.body.appendChild(node)
const handleClose = function () { ReactDOM.unmountComponentAtNode(node) document.body.removeChild(node) } return ReactDOM.render<IModalProps, Modal>( <Modal {...props} onClose={() => { props.onClose && props.onClose() handleClose() }} />, node ).show()}
interface IModalProps { visible?: boolean title: string onClose?: () => void}interface IModalState { visible: boolean}class Modal extends React.Component<IModalProps, IModalState> { /** * 将 show 方法放到 class component 的静态方法中 */ static show = show
private el: HTMLDivElement private readonly container = document.body constructor(props: IModalProps) { super(props) this.el = document.createElement("div") this.state = { visible: false, } } componentDidMount() { this.container.appendChild(this.el) } componentWillUnmount() { this.container.removeChild(this.el) }
get visible() { /** * props 优先级比 state 高 * 保证如果外部在控制弹窗状态,则根据外部的来 */ if (typeof this.props.visible !== "undefined") { return this.props.visible } return this.state.visible }
public show = () => { this.setState({ visible: true, }) /** * return 当前实例,提供链式调用的操作方式 * 譬如: Modal.show().hide() */ return this } public hide = () => { this.setState({ visible: false, }) return this }
private handleClose = () => { this.setState({ visible: false, }) this.props.onClose && this.props.onClose() } render() { return ReactDOM.createPortal( <div style={{ // 其他样式就应该不用演示了吧,就不写了 position: "absolute", zIndex: 999, left: 0, right: 0, top: 0, bottom: 0, visibility: this.visible ? "visible" : "hidden", }} > <div>{this.props.title}</div> <span onClick={this.handleClose}>关闭</span> </div>, this.el ) }}
function Test() { const [visible, setVisible] = React.useState(false)
return ( <> <div onClick={() => Modal.show({ title: "标题", }) } > <p>使用 api 调用</p> </div> <div onClick={() => setVisible(true)}> <p>使用 react 组件</p> <Modal title="标题" visible={visible} onClose={() => setVisible(false)} /> </div> </> )}
复制代码


总结一下,最后的这个组件有多种实现方式,这里只是很简单的演示一下,关键点在你要掌握 ReactDOM.renderReactDOM.createPortal 的使用,当你掌握了这两者,诸如 Toast、Dialog、Dropdown 大体都是一个实现原理。

用户头像

还未添加个人签名 2022.09.05 加入

还未添加个人简介

评论

发布
暂无评论
这可能是你需要的React实战技巧_React_夏天的味道123_InfoQ写作社区