一、父组件通过 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 编写的子组件其实是最能简洁明了的
二、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
,如果这里逻辑非常复杂,那将会消耗大量性能。其实很多时候我们只需要判断入参有没有发生变化即可判断是否需要重新计算。譬如例子中,如果 firstname
和 lastname
没有发生变化则不需要重新计算。
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 调用
document.createElement
创建 domdocument.body.appendChild
插入 domReactDOM.render
渲染组件- 调用实例中的方法 或者 直接给实例传递
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.render
和 ReactDOM.createPortal
的使用,当你掌握了这两者,诸如 Toast、Dialog、Dropdown 大体都是一个实现原理。