写在前边
从Vue
转到React
差不多快三个月,这两种框架其实在设计哲学上完全是不一样的道路但是同时又那么相似。
最近在开发组件时遇到了一些需要关于Dom
的操作,所以写下这边文章记录下自己对于react-dom
核心Api
的理解,希望可以帮助到大家。
-
ReactDOM.render(element, container[, callback])
-
ReactDOM.unmountComponentAtNode(container)
-
ReactDOM.findDOMNode(component)
-
ReactDOM.createPortal(child, container)
文章会重点讲述上述四个API
,因为官网提供的描述是在是过于简陋,所以这里我会结合例子(通过人话)来重点讲述他们的用法和适应场景,以及对比他们之间的差异性和相似性。
这篇文章的内容主要就是围绕上边四个
API
,比较基础。如果你已经能够完全熟练掌握他们的用法,那么到这里就可以啦!
Ok! Let's do it!
ReactDOM.render(element, container[, callback])
在提供的
container
里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回null
)。如果 React 元素之前已经在
container
里渲染过,这将会对其执行更新操作,并仅会在必要时改变 DOM 以映射最新的 React 元素。如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
单独的ReactDom.render
方法的确没有什么可讲的,它的作用就是将我们传入的JSX
对象通过React.createElement(VDom)
生成虚拟VDom
,然后将生成的Vdom
对象挂载真实Dom
元素container
元素上。
我们会在之后重点对比它和React.createPortal
的区别。
同时,我们可以通过ReactDOM.unmountComponentAtNode(container)
卸载对应的React.render(VNode,container)
对应的节点和事件处理程序。
ReactDOM.unmountComponentAtNode(container)
从 DOM 中移除一个挂载的 React 组件并清理它的事件处理程序和状态。如果容器中没有安装任何组件,则调用此函数什么也不做。返回
true
是否已卸载组件以及false
是否没有要卸载的组件。
我们来看看他的类型定义,所谓的container
就是Element | DocumentFragment
:
针对unmountComponentAtNode
,我们来用一个例子来稍微解释一下它吧:
比如我们通过js
创建一个div
,它的内容是这样的:
const div = document.createElement('div');
复制代码
HTML中的内容如下:
代码语言:javascript复制<div></div>
复制代码
接下来让我们在React
代码中执行下一句:
ReactDOM.render(<Components />, div);
复制代码
此时我们通过Components
组件中的元素创建了一个VDom
元素,通过render
方法将它渲染到上边的div
中去,我们得到这样的html
内容:
<div>
(whatever HTML is created by Components)
<div>
复制代码
此时让我们来执行最后一行代码:
代码语言:javascript复制ReactDOM.unmountComponentAtNode(div);
复制代码
删除Components
渲染到 div 中的组件,并清除与Components
组件关联的所有处理程序和 React 状态(如果有的话)。
HTML 现在又是这个样子:
代码语言:javascript复制<div></div>
复制代码
通过这个简单的例子我相信你已经能明白unmountComponentAtNode
实现的作用了。
需要额外注意的是:
-
unmountComponentAtNode
仅仅只能针对通过ReactDom.render
顶层挂载的元素进行卸载。针对其他不相关Render方法元素是无效的(永远返回false) - 之所以只能通过
unmountComponentAtNode
卸载顶层组件,这是React
团队刻意为之的。React
希望子组件的卸载/渲染是通过父组件的状态来控制,而不是直接通过操纵子组件。你可以查看这个回答来理解它。
ReactDOM.findDOMNode(component)
如果组件已经被挂载到 DOM 上,此方法会返回浏览器中相应的原生 DOM 元素。此方法对于从 DOM 中读取值很有用,例如获取表单字段的值或者执行 DOM 检测(performing DOM measurements)。大多数情况下,你可以绑定一个 ref 到 DOM 节点上,可以完全避免使用 findDOMNode。
简单来说findDOMNode
这个方法会返回传入组件对应渲染真实的DOM节点,简而言之也就是在React
中获取Dom
的一种方式。
大多数情况下,你可以绑定一个 ref 到 DOM 节点上,可以完全避免使用 findDOMNode。
findDOMNode
在严格模式下已经被React
废弃掉了。
ReactDOM.createPortal(child, container)
createPortal
能为我们带来什么
关于Portal
,我们先来考虑这样一种业务场景。
假设我需要为我的项目定制化一款自己的dialog
,你可能需要这么一个组件去承载:
当然这样去开发组件的话会存在一些问题。
- 首先在组件结构层面,我们开发的Dialog组件和当前页面上的结构是无关的,通常它是直接“盖”在页面之上的某个位置的。
比如,这样:
所以在结构上,我们希望它是可以独立于页面直接挂载在body
元素上。但是目前我们这种写法Dialog
组件的结构会跟随它的父元素嵌套在层级内。
当然我们可以通过position:fixed
达到我们想让dialog
在页面中呈现的效果。但是这会引来另一个另一个致命的问题。
- 如果使用Fixed布局让
Dialog
定位后,会和业务强耦合。
Dialog设置为Fixed后,它的层级是基于定位父元素而决定的。这也就导致了如果我们想要调整Dialog的层级的话它还依赖于嵌套父元素的定位。这无疑是一种噩梦,作为一个通用组件来说它具有太多和业务的耦合,甚至还会被业务影响。
关于如何解决上述的问题就要引出来我们的主角了:ReactDOM.createPortal(child, container)
。
这里也许有的同学会疑惑,
ReactDom.render
和ReactDom.createPortal
的区别。这里我会在下边给大家分析他们的区别。
createPortal
简介
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
我们可以通过createPortal(vNode,dom)
在React
中跳过层级关系将我们的vNode
任何React元素渲染到指定的真实Dom
元素上去。
熟悉Vue3
的同学可能第一时间就想到Teleport
,没错。你完全可以使用Teleport
的思想来理解createPortal
。
关于createPortal你可以点击它来查看更加详尽的解释。
这里其实我想给大家重点讲述的是
ReactDOM.createPortal(child, container)
作用
简单来说就一句话:createPortal
提供一种将React
元素子节点渲染到真实DOM
节点的方式。
这个节点可以脱离于React
中的DOM
结构层次。
ReactDOM.createPortal(child, container)
和ReactDom.render(vNode,dom)
区别于联系
你可以在codeopen中点击这个例子查看代码对比。
同时也可以在点击官网查看
简单来说createPortal渲染的元素尽管可以出现在DOM
结构中的任何地方,但是同时通过 Portal 仍然可以进行事件冒泡/context 传递之类的特性。 你可以将它简单的理解成为Portal
元素仅仅是渲染时在脱离固定的结构而已,本质上它仍然是React Tree
中固定位置的普通节点,所以它仍然可以进行context
传递以及React
事件冒泡等。
而针对于ReactDom.render()
方法。这个方法根据传入的VDom
元素重新渲染了一个React Tree
从而渲染挂载在对应的元素上。它已经脱离了原本的React Tree
,自然而言就无法通过React
事件冒泡机制触发父元素的事件以及接受父元素的Context
。(因为它本身就属于原本的React Tree
中,只是在你的代码结构中看起来他们一致而已)。
本质上造成这种原因的还是
React
中的render
方法原理和合成事件,有兴趣了解这部分源码的朋友可以移步这里。(代码中有详细的注释,是我自己实现的一个简单版本的React
,目前已经完成合成事件章节)。
ReactDOM.createPortal
拓展
关于ReactDom.render
的使用方式这里我不打算累赘,它已经出现很久了。众所周知,它可以:
- 在我们的业务页面上直接通过
ReactDom.render(<App />, document.getElementById('root'))
去控制页面渲染。 - 在函数式
API
调用方式中大展身手,比如antd
中的message.success(config)
相关APi
。
这里,我想和大家重点聊聊createPortal
。
我们先来看一看关于它的类型定义:
这里我们可以看到,这个方法接受的参数类型和它返回的参数类型。
注意:它返回的是ReactPortal
,继承于ReactElement
。
嗯,这里我们了解了Portal
的返回值本质上就可以当作ReactElement
去使用,说白了它也就是VDom
。
我们来打印它看看:
代码语言:javascript复制import React from 'react'
import { createPortal } from 'react-dom'
function Dialog() {
const div = document.createElement('div')
console.log(createPortal(<p></p>, div), '打印')
return createPortal(<p></p>, div)
}
export default Dialog;
复制代码
我们发现它比平常的VDom
对象多出了一个containerInfo
属性,而这个属性指向的节点正是我们上边创建的div
。
看到这里我相信有部分同学已经明白了,没错React
内部正是通过containerInfo
来选择当前VNode
挂载的节点,当不存在containerInfo
时他会遵循规则挂载,而当存在containerInfo
时它会将传入的React
元素挂载在containerInfo
对应的节点中去。
结尾
其实如果要深挖ReactDom
的API
还是能挖出不少知识点的,这里我给大家带来的仅仅是抛砖引玉,仅仅达到使用层面的讲解。
感谢每一位看到结尾的同学,希望文中的知识可以带给大家帮助。