很多同学用react开发的时候,真正用到的Reactapi少之又少,基本停留在Component,React.memo等层面, 实际react源码中,暴露出来的方法并不少,只是我们平时很少用。但是React暴露出这么多api并非没有用,想要玩转react, 就要明白这些API究竟是干什么的,应用场景是什么,今天就让我们从reactreact-dom, 一次性把react生产环境的暴露api复习个遍 (涵盖 90%+)。

我们把react,API,分为组件类工具类,hooks,再加上 **react-dom** ,一共四大方向,分别加以探讨。

为了能让屏幕前的你,更明白api, 我是绞尽脑汁, 本文的每一个api基本都会出一个demo演示效果, 弥补一下天书般的react文档😂😂😂,还有就是我对api基本概念的理解。

老规矩,我们带着疑问开始今天的阅读 (自测掌握程度)?


  • 1 react暴露的api有哪些,该如何使用?

  • 2 react提供了哪些自测性能的手段?

  • 3 ref既然不能用在函数组件中,那么父组件如何控制函数子组件内的state和方法?

  • 4 createElementcloneElement有什么区别,应用场景是什么?

  • 5 react内置的children遍历方法,和数组方法, 有什么区别?

  • 6 react怎么将子元素渲染到父元素之外的指定容器中?

我相信读完这篇文章,这些问题全都会迎刃而解?

组件类

组件类,详细分的话有三种类,第一类说白了就是我平时用于继承的基类组件Component,PureComponent, 还有就是react提供的内置的组件,比如Fragment,StrictMode, 另一部分就是高阶组件forwardRef,memo等。

comp.jpg

Component

Componentclass组件的根基。类组件一切始于Component。对于React.Component使用,我们没有什么好讲的。我们这里重点研究一下reactComponent做了些什么。

react/src/ReactBaseClasses.js

  1. function Component(props, context, updater) {
  2. this.props = props;
  3. this.context = context;
  4. this.refs = emptyObject;
  5. this.updater = updater || ReactNoopUpdateQueue;
  6. }

这就是Component函数,其中updater对象上保存着更新组件的方法。

我们声明的类组件是什么时候以何种形式被实例化的呢?

react-reconciler/src/ReactFiberClassComponent.js

constructClassInstance

  1. function constructClassInstance(
  2. workInProgress,
  3. ctor,
  4. props
  5. ){
  6. const instance = new ctor(props, context);
  7. instance.updater = {
  8. isMounted,
  9. enqueueSetState(){
  10. /* setState 触发这里面的逻辑 */
  11. },
  12. enqueueReplaceState(){},
  13. enqueueForceUpdate(){
  14. /* forceUpdate 触发这里的逻辑 */
  15. }
  16. }
  17. }

对于Componentreact 处理逻辑还是很简单的,实例化我们类组件,然后赋值updater对象,负责组件的更新。然后在组件各个阶段,执行类组件的render函数,和对应的生命周期函数就可以了。

PureComponent

PureComponentComponent用法,差不多一样,唯一不同的是,纯组件PureComponent会浅比较,propsstate是否相同,来决定是否重新渲染组件。所以一般用于性能调优,减少 render 次数。

什么叫做浅比较,我这里举个列子:

  1. class Index extends React.PureComponent{
  2. constructor(props){
  3. super(props)
  4. this.state={
  5. data:{
  6. name:'alien',
  7. age:28
  8. }
  9. }
  10. }
  11. handerClick= () =>{
  12. const { data } = this.state
  13. data.age++
  14. this.setState({ data })
  15. }
  16. render(){
  17. const { data } = this.state
  18. return <div class >
  19. <div class >
  20. <div> 你的姓名是: { data.name } </div>
  21. <div> 年龄: { data.age }</div>
  22. <button onClick={ this.handerClick } >age++</button>
  23. </div>
  24. </div>
  25. }
  26. }

点击按钮,没有任何反应,因为PureComponent会比较两次data对象,都指向同一个data, 没有发生改变,所以不更新视图。

解决这个问题很简单,只需要在handerClick事件中这么写:

  1. this.setState({ data:{...data} })

浅拷贝就能根本解决问题。

memo

React.memoPureComponent作用类似,可以用作性能优化,React.memo 是高阶组件,函数组件和类组件都可以使用, 和区别PureComponentReact.memo只能对props的情况确定是否渲染,而PureComponent是针对propsstate

React.memo 接受两个参数,第一个参数原始组件本身,第二个参数,可以根据一次更新中props是否相同决定原始组件是否重新渲染。是一个返回布尔值,true 证明组件无须重新渲染,false证明组件需要重新渲染,这个和类组件中的shouldComponentUpdate()正好相反 。

React.memo: 第二个参数 返回 **true** 组件不渲染 , 返回 **false** 组件重新渲染。shouldComponentUpdate: 返回 **true** 组件渲染 , 返回 **false** 组件不渲染。

解析来我们做一个场景,控制组件在仅此一个props数字变量,一定范围渲染。

例子🌰:

控制 props 中的 number


  • 1 只有 number 更改,组件渲染。

  • 2 只有 number 小于 5 ,组件渲染。
  1. function TextMemo(props){
  2. console.log('子组件渲染')
  3. if(props)
  4. return <div>hello,world</div>
  5. }
  6. const controlIsRender = (pre,next)=>{
  7. if(pre.number === next.number ){ // number 不改变 ,不渲染组件
  8. return true
  9. }else if(pre.number !== next.number && next.number > 5 ) { // number 改变 ,但值大于5 , 不渲染组件
  10. return true
  11. }else { // 否则渲染组件
  12. return false
  13. }
  14. }
  15. const NewTexMemo = memo(TextMemo,controlIsRender)
  16. class Index extends React.Component{
  17. constructor(props){
  18. super(props)
  19. this.state={
  20. number:1,
  21. num:1
  22. }
  23. }
  24. render(){
  25. const { num , number } = this.state
  26. return <div>
  27. <div>
  28. 改变num:当前值 { num }
  29. <button onClick={ ()=>this.setState({ num:num + 1 }) } >num++</button>
  30. <button onClick={ ()=>this.setState({ num:num - 1 }) } >num--</button>
  31. </div>
  32. <div>
  33. 改变number 当前值 { number }
  34. <button onClick={ ()=>this.setState({ number:number + 1 }) } > number ++</button>
  35. <button onClick={ ()=>this.setState({ number:number - 1 }) } > number -- </button>
  36. </div>
  37. <NewTexMemo num={ num } number={number} />
  38. </div>
  39. }
  40. }

完美达到了效果,React.memo一定程度上,可以等价于组件外部的shouldComponentUpdate ,用于拦截新老props,确定组件是否更新。

forwardRef

官网对forwardRef的概念和用法很笼统,也没有给定一个具体的案例。很多同学不知道 forwardRef具体怎么用,下面我结合具体例子给大家讲解forwardRef应用场景。

1 转发引入 Ref

这个场景实际很简单,比如父组件想获取孙组件,某一个dom元素。这种隔代ref获取引用,就需要forwardRef来助力。

  1. function Son (props){
  2. const { grandRef } = props
  3. return <div>
  4. <div> i am alien </div>
  5. <span ref={grandRef} >这个是想要获取元素</span>
  6. </div>
  7. }
  8. class Father extends React.Component{
  9. constructor(props){
  10. super(props)
  11. }
  12. render(){
  13. return <div>
  14. <Son grandRef={this.props.grandRef} />
  15. </div>
  16. }
  17. }
  18. const NewFather = React.forwardRef((props,ref)=><Father grandRef={ref} {...props} /> )
  19. class GrandFather extends React.Component{
  20. constructor(props){
  21. super(props)
  22. }
  23. node = null
  24. componentDidMount(){
  25. console.log(this.node)
  26. }
  27. render(){
  28. return <div>
  29. <NewFather ref={(node)=> this.node = node } />
  30. </div>
  31. }
  32. }

效果

forwaedRef.jpg

react不允许ref通过props传递,因为组件上已经有 ref 这个属性, 在组件调和过程中,已经被特殊处理,forwardRef出现就是解决这个问题,把ref转发到自定义的forwardRef定义的属性上,让ref,可以通过props传递。

2 高阶组件转发 Ref

一文吃透hoc文章中讲到,由于属性代理的hoc,被包裹一层,所以如果是类组件,是通过ref拿不到原始组件的实例的,不过我们可以通过forWardRef转发ref

  1. function HOC(Component){
  2. class Wrap extends React.Component{
  3. render(){
  4. const { forwardedRef ,...otherprops } = this.props
  5. return <Component ref={forwardedRef} {...otherprops} />
  6. }
  7. }
  8. return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
  9. }
  10. class Index extends React.Component{
  11. componentDidMount(){
  12. console.log(666)
  13. }
  14. render(){
  15. return <div>hello,world</div>
  16. }
  17. }
  18. const HocIndex = HOC(Index,true)
  19. export default ()=>{
  20. const node = useRef(null)
  21. useEffect(()=>{
  22. /* 就可以跨层级,捕获到 Index 组件的实例了 */
  23. console.log(node.current.componentDidMount)
  24. },[])
  25. return <div><HocIndex ref={node} /></div>
  26. }

如上,解决了高阶组件引入Ref的问题。

lazy

React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库

React.lazySuspense配合一起用,能够有动态加载组件效果。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise ,该 Promise 需要 resolve 一个 default exportReact 组件。

我们模拟一个动态加载的场景。

父组件

  1. import Test from './comTest'
  2. const LazyComponent = React.lazy(()=> new Promise((resolve)=>{
  3. setTimeout(()=>{
  4. resolve({
  5. default: ()=> <Test />
  6. })
  7. },2000)
  8. }))
  9. class index extends React.Component{
  10. render(){
  11. return <div class style={ { marginTop :'50px' } } >
  12. <React.Suspense fallback={ <div class ><SyncOutlined spin /></div> } >
  13. <LazyComponent />
  14. </React.Suspense>
  15. </div>
  16. }
  17. }

我们用setTimeout来模拟import异步引入效果。Test

  1. class Test extends React.Component{
  2. constructor(props){
  3. super(props)
  4. }
  5. componentDidMount(){
  6. console.log('--componentDidMount--')
  7. }
  8. render(){
  9. return <div>
  10. <img src={alien} class />
  11. </div>
  12. }
  13. }

效果

lazy.gif

Suspense

何为Suspense, Suspense 让组件 “等待” 某个异步操作,直到该异步操作结束即可渲染。

用于数据获取的 Suspense 是一个新特性,你可以使用 <Suspense> 以声明的方式来 “等待” 任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。

上面讲到高阶组件lazy时候,已经用 lazy + Suspense模式,构建了异步渲染组件。我们看一下官网文档中的案例:

  1. const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
  2. <Suspense fallback={<Spinner />}>
  3. <ProfilePage />
  4. </Suspense>

Fragment

react不允许一个组件返回多个节点元素,比如说如下情况

  1. render(){
  2. return <li> 🍎🍎🍎 </li>
  3. <li> 🍌🍌🍌 </li>
  4. <li> 🍇🍇🍇 </li>
  5. }

如果我们想解决这个情况,很简单,只需要在外层套一个容器元素。

  1. render(){
  2. return <div>
  3. <li> 🍎🍎🍎 </li>
  4. <li> 🍌🍌🍌 </li>
  5. <li> 🍇🍇🍇 </li>
  6. </div>
  7. }

但是我们不期望,增加额外的dom节点,所以react提供Fragment碎片概念,能够让一个组件返回多个元素。所以我们可以这么写

  1. <React.Fragment>
  2. <li> 🍎🍎🍎 </li>
  3. <li> 🍌🍌🍌 </li>
  4. <li> 🍇🍇🍇 </li>
  5. </React.Fragment>

还可以简写成:

  1. <>
  2. <li> 🍎🍎🍎 </li>
  3. <li> 🍌🍌🍌 </li>
  4. <li> 🍇🍇🍇 </li>
  5. </>

Fragment区别是,Fragment可以支持key属性。<></>不支持key属性。

Profiler

Profiler这个api一般用于开发阶段,性能检测,检测一次react组件渲染用时,性能开销。

Profiler 需要两个参数:

第一个参数:是 id,用于表识唯一性的Profiler

第二个参数:onRender回调函数,用于渲染完成,接受渲染参数。

实践:

  1. const index = () => {
  2. const callback = (...arg) => console.log(arg)
  3. return <div >
  4. <div >
  5. <Profiler id="root" onRender={ callback } >
  6. <Router >
  7. <Meuns/>
  8. <KeepaliveRouterSwitch withoutRoute >
  9. { renderRoutes(menusList) }
  10. </KeepaliveRouterSwitch>
  11. </Router>
  12. </Profiler>
  13. </div>
  14. </div>
  15. }

结果

Profiler.jpg

onRender

0 -id: root -> Profiler 树的 id 。1 -phase: mount -> mount 挂载 , update 渲染了。2 -actualDuration: 6.685000262223184 -> 更新 committed 花费的渲染时间。3 -baseDuration: 4.430000321008265 -> 渲染整颗子树需要的时间 4 -startTime : 689.7299999836832 -> 本次更新开始渲染的时间 5 -commitTime : 698.5799999674782 -> 本次更新 committed 的时间 6 -interactions: set{} -> 本次更新的 interactions 的集合

尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。

StrictMode

StrictMode见名知意,严格模式,用于检测react项目中的潜在的问题,。与 Fragment 一样, StrictMode 不会渲染任何可见的 UI 。它为其后代元素触发额外的检查和警告。

严格模式检查仅在开发模式下运行;它们不会影响生产构建。

StrictMode目前有助于:


  • ①识别不安全的生命周期。

  • ②关于使用过时字符串 ref API 的警告

  • ③关于使用废弃的 findDOMNode 方法的警告

  • ④检测意外的副作用

  • ⑤检测过时的 context API

实践: 识别不安全的生命周期

对于不安全的生命周期,指的是UNSAFE_componentWillMountUNSAFE_componentWillReceiveProps , UNSAFE_componentWillUpdate

外层开启严格模式:

  1. <React.StrictMode>
  2. <Router >
  3. <Meuns/>
  4. <KeepaliveRouterSwitch withoutRoute >
  5. { renderRoutes(menusList) }
  6. </KeepaliveRouterSwitch>
  7. </Router>
  8. </React.StrictMode>

我们在内层组件中,使用不安全的生命周期:

  1. class Index extends React.Component{
  2. UNSAFE_componentWillReceiveProps(){
  3. }
  4. render(){
  5. return <div class />
  6. }
  7. }

效果:

strictMode.jpg

工具类

接下来我们一起来探究一下react工具类函数的用法。

utils.jpg

createElement

一提到createElement,就不由得和JSX联系一起。我们写的jsx,最终会被 babel,用createElement编译成react元素形式。我写一个组件,我们看一下会被编译成什么样子,

如果我们在render里面这么写:

  1. render(){
  2. return <div class >
  3. <div class >生命周期</div>
  4. <Text mes="hello,world" />
  5. <React.Fragment> Flagment </React.Fragment>
  6. { /* */ }
  7. text文本
  8. </div>
  9. }

会被编译成这样:

  1. render() {
  2. return React.createElement("div", { className: "box" },
  3. React.createElement("div", { className: "item" }, "\u751F\u547D\u5468\u671F"),
  4. React.createElement(Text, { mes: "hello,world" }),
  5. React.createElement(React.Fragment, null, " Flagment "),
  6. "text\u6587\u672C");
  7. }

当然我们可以不用jsx模式,而是直接通过createElement进行开发。

**createElement**模型:

  1. React.createElement(
  2. type,
  3. [props],
  4. [...children]
  5. )

createElement参数:

第一个参数: 如果是组件类型,会传入组件,如果是dom元素类型,传入div或者span之类的字符串。

第二个参数:: 第二个参数为一个对象,在dom类型中为属性,在组件类型中为 props

其他参数:,依次为children,根据顺序排列。

createElement 做了些什么?

经过createElement处理,最终会形成 $$typeof = Symbol(react.element)对象。对象上保存了该react.element的信息。

cloneElement

可能有的同学还傻傻的分不清楚cloneElementcreateElement区别和作用。

createElement把我们写的jsx,变成element对象; 而cloneElement的作用是以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。

那么cloneElement感觉在我们实际业务组件中,可能没什么用,但是在一些开源项目,或者是公共插槽组件中用处还是蛮大的,比如说,我们可以在组件中,劫持children element,然后通过cloneElement克隆element,混入props。经典的案例就是 react-router中的Swtich组件,通过这种方式,来匹配唯一的 Route并加以渲染。

我们设置一个场景,在组件中,去劫持children,然后给children赋能一些额外的props:

  1. function FatherComponent({ children }){
  2. const newChildren = React.cloneElement(children, { age: 18})
  3. return <div> { newChildren } </div>
  4. }
  5. function SonComponent(props){
  6. console.log(props)
  7. return <div>hello,world</div>
  8. }
  9. class Index extends React.Component{
  10. render(){
  11. return <div class >
  12. <FatherComponent>
  13. <SonComponent />
  14. </FatherComponent>
  15. </div>
  16. }
  17. }

打印:

cloneElment.jpg

完美达到了效果!

createContext

createContext用于创建一个Context对象,createContext对象中,包括用于传递 Context 对象值 valueProvider,和接受value变化订阅的Consumer

  1. const MyContext = React.createContext(defaultValue)

createContext接受一个参数defaultValue,如果Consumer上一级一直没有Provider, 则会应用defaultValue作为value只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

我们来模拟一个 Context.ProviderContext.Consumer的例子:

  1. function ComponentB(){
  2. /* 用 Consumer 订阅, 来自 Provider 中 value 的改变 */
  3. return <MyContext.Consumer>
  4. { (value) => <ComponentA {...value} /> }
  5. </MyContext.Consumer>
  6. }
  7. function ComponentA(props){
  8. const { name , mes } = props
  9. return <div>
  10. <div> 姓名: { name } </div>
  11. <div> 想对大家说: { mes } </div>
  12. </div>
  13. }
  14. function index(){
  15. const [ value , ] = React.useState({
  16. name:'alien',
  17. mes:'let us learn React '
  18. })
  19. return <div style={{ marginTop:'50px' }} >
  20. <MyContext.Provider value={value} >
  21. <ComponentB />
  22. </MyContext.Provider>
  23. </div>
  24. }

打印结果:

createContent.jpg

ProviderConsumer的良好的特性,可以做数据的Consumer一方面传递value, 另一方面可以订阅value的改变。

Provider还有一个特性可以层层传递value,这种特性在react-redux中表现的淋漓尽致。

createFactory

  1. React.createFactory(type)

返回用于生成指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 ‘div‘或’span‘),也可以是 React 组件 类型 ( class 组件或函数组件),或是 React fragment 类型。

使用:

  1. const Text = React.createFactory(()=><div>hello,world</div>)
  2. function Index(){
  3. return <div style={{ marginTop:'50px' }} >
  4. <Text/>
  5. </div>
  6. }

效果

createFactory.jpg

报出警告,这个api将要被废弃,我们这里就不多讲了,如果想要达到同样的效果,请用React.createElement

createRef

createRef可以创建一个 ref 元素,附加在react元素上。

用法:

  1. class Index extends React.Component{
  2. constructor(props){
  3. super(props)
  4. this.node = React.createRef()
  5. }
  6. componentDidMount(){
  7. console.log(this.node)
  8. }
  9. render(){
  10. return <div ref={this.node} > my name is alien </div>
  11. }
  12. }

个人觉得createRef这个方法,很鸡肋,我们完全可以class类组件中这么写,来捕获ref

  1. class Index extends React.Component{
  2. node = null
  3. componentDidMount(){
  4. console.log(this.node)
  5. }
  6. render(){
  7. return <div ref={(node)=> this.node } > my name is alien </div>
  8. }
  9. }

或者在function组件中这么写:

  1. function Index(){
  2. const node = React.useRef(null)
  3. useEffect(()=>{
  4. console.log(node.current)
  5. },[])
  6. return <div ref={node} > my name is alien </div>
  7. }

isValidElement

这个方法可以用来检测是否为react element元素, 接受待验证对象,返回true或者false。这个 api 可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是react element 元素。但是,对于一起公共组件或是开源库,isValidElement就很有作用了。

实践

我们做一个场景,验证容器组件的所有子组件,过滤到非react element类型。

没有用isValidElement验证之前:

  1. const Text = () => <div>hello,world</div>
  2. class WarpComponent extends React.Component{
  3. constructor(props){
  4. super(props)
  5. }
  6. render(){
  7. return this.props.children
  8. }
  9. }
  10. function Index(){
  11. return <div style={{ marginTop:'50px' }} >
  12. <WarpComponent>
  13. <Text/>
  14. <div> my name is alien </div>
  15. Let's learn react together!
  16. </WarpComponent>
  17. </div>
  18. }

过滤之前的效果

isValidElement.jpg

我们用**isValidElement**进行**react element**验证:

  1. class WarpComponent extends React.Component{
  2. constructor(props){
  3. super(props)
  4. this.newChidren = this.props.children.filter(item => React.isValidElement(item) )
  5. }
  6. render(){
  7. return this.newChidren
  8. }
  9. }

过滤之后效果

isValidElement111.jpg

过滤掉了非react elementLet's learn react together!

Children.map

接下来的五个api都是和react.Chidren相关的,我们来分别介绍一下,我们先来看看官网的描述,React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

有的同学会问遍历 children用数组方法,mapforEach 不就可以了吗?请我们注意一下不透明数据结构, 什么叫做不透明结构?

我们先看一下透明的结构:

  1. class Text extends React.Component{
  2. render(){
  3. return <div>hello,world</div>
  4. }
  5. }
  6. function WarpComponent(props){
  7. console.log(props.children)
  8. return props.children
  9. }
  10. function Index(){
  11. return <div style={{ marginTop:'50px' }} >
  12. <WarpComponent>
  13. <Text/>
  14. <Text/>
  15. <Text/>
  16. <span>hello,world</span>
  17. </WarpComponent>
  18. </div>
  19. }

打印

chidrenmap.jpg

但是我们把Index结构改变一下:

  1. function Index(){
  2. return <div style={{ marginTop:'50px' }} >
  3. <WarpComponent>
  4. { new Array(3).fill(0).map(()=><Text/>) }
  5. <span>hello,world</span>
  6. </WarpComponent>
  7. </div>
  8. }

打印

chidrenmap2.jpg

这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren 来帮忙了。

但是我们把WarpComponent组件用react.Chidren处理children:

  1. function WarpComponent(props){
  2. const newChildren = React.Children.map(props.children,(item)=>item)
  3. console.log(newChildren)
  4. return newChildren
  5. }

此时就能正常遍历了,达到了预期效果。

C71364B2-25E8-4F7D-A26D-50CA36AF4E33.jpg

注意如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

Children.forEach

Children.forEachChildren.map 用法类似,Children.map可以返回新的数组,Children.forEach仅停留在遍历阶段。

我们将上面的WarpComponent方法,用Children.forEach改一下。

  1. function WarpComponent(props){
  2. React.Children.forEach(props.children,(item)=>console.log(item))
  3. return props.children
  4. }

Children.count

children 中的组件总数量,等同于通过 mapforEach 调用回调函数的次数。对于更复杂的结果,Children.count可以返回同一级别子组件的数量。

我们还是把上述例子进行改造:

  1. function WarpComponent(props){
  2. const childrenCount = React.Children.count(props.children)
  3. console.log(childrenCount,'childrenCount')
  4. return props.children
  5. }
  6. function Index(){
  7. return <div style={{ marginTop:'50px' }} >
  8. <WarpComponent>
  9. { new Array(3).fill(0).map((item,index) => new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
  10. <span>hello,world</span>
  11. </WarpComponent>
  12. </div>
  13. }

效果:

chidrencunt.jpg

Children.toArray

Children.toArray返回,props.children扁平化后结果。

  1. function WarpComponent(props){
  2. const newChidrenArray = React.Children.toArray(props.children)
  3. console.log(newChidrenArray,'newChidrenArray')
  4. return newChidrenArray
  5. }
  6. function Index(){
  7. return <div style={{ marginTop:'50px' }} >
  8. <WarpComponent>
  9. { new Array(3).fill(0).map((item,index)=>new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
  10. <span>hello,world</span>
  11. </WarpComponent>
  12. </div>
  13. }

效果:

chuldeanarrgy.jpg

newChidrenArray , 就是扁平化的数组结构。React.Children.toArray() 在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说, toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

Children.only

验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

不唯一

  1. function WarpComponent(props){
  2. console.log(React.Children.only(props.children))
  3. return props.children
  4. }
  5. function Index(){
  6. return <div style={{ marginTop:'50px' }} >
  7. <WarpComponent>
  8. { new Array(3).fill(0).map((item,index)=><Text key={index} />) }
  9. <span>hello,world</span>
  10. </WarpComponent>
  11. </div>
  12. }

效果

falseonly.jpg

唯一

  1. function WarpComponent(props){
  2. console.log(React.Children.only(props.children))
  3. return props.children
  4. }
  5. function Index(){
  6. return <div style={{ marginTop:'50px' }} >
  7. <WarpComponent>
  8. <Text/>
  9. </WarpComponent>
  10. </div>
  11. }

效果

only.jpg

React.Children.only() 不接受 React.Children.map() 的返回值,因为它是一个数组而并不是 React 元素。

react-hooks

对于react-hooks, 我已经写了三部曲,对于常见的hooks,我这里就不多讲了,还没看过的同学建议看一下react-hooks三部曲

三部曲

「react 进阶」一文吃透 react-hooks 原理

玩转 react-hooks, 自定义 hooks 设计模式及其实战

react-hooks 如何使用?

我们今天重点说一下,几个少用的api

hooks.jpg

useImperativeHandle

useImperativeHandle 可以配合 forwardRef自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的,那么此时useImperativeHandleforwardRef配合就能达到效果。

useImperativeHandle接受三个参数:


  • 第一个参数 ref: 接受 forWardRef 传递过来的 ref

  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的ref对象。

  • 第三个参数 deps: 依赖项 deps,依赖项更改形成新的ref对象。

我们来模拟给场景,用**useImperativeHandle**,使得父组件能让子组件中的**input**自动赋值并聚焦。

  1. function Son (props,ref) {
  2. console.log(props)
  3. const inputRef = useRef(null)
  4. const [ inputValue , setInputValue ] = useState('')
  5. useImperativeHandle(ref,()=>{
  6. const handleRefs = {
  7. /* 声明方法用于聚焦input框 */
  8. onFocus(){
  9. inputRef.current.focus()
  10. },
  11. /* 声明方法用于改变input的值 */
  12. onChangeValue(value){
  13. setInputValue(value)
  14. }
  15. }
  16. return handleRefs
  17. },[])
  18. return <div>
  19. <input
  20. placeholder="请输入内容"
  21. ref={inputRef}
  22. value={inputValue}
  23. />
  24. </div>
  25. }
  26. const ForwarSon = forwardRef(Son)
  27. class Index extends React.Component{
  28. cur = null
  29. handerClick(){
  30. const { onFocus , onChangeValue } =this.cur
  31. onFocus()
  32. onChangeValue('let us learn React!')
  33. }
  34. render(){
  35. return <div style={{ marginTop:'50px' }} >
  36. <ForwarSon ref={cur => (this.cur = cur)} />
  37. <button onClick={this.handerClick.bind(this)} >操控子组件</button>
  38. </div>
  39. }
  40. }

效果:

useImperativeHandle.gif

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。这个hooks目的就是检查自定义hooks

  1. function useFriendStatus(friendID) {
  2. const [isOnline, setIsOnline] = useState(null);
  3. // ...
  4. // 在开发者工具中的这个 Hook 旁边显示标签
  5. // e.g. "FriendStatus: Online"
  6. useDebugValue(isOnline ? 'Online' : 'Offline');
  7. return isOnline;
  8. }

我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

useTransition

useTransition允许延时由state改变而带来的视图渲染。避免不必要的渲染。它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新。

  1. const TIMEOUT_MS = { timeoutMs: 2000 }
  2. const [startTransition, isPending] = useTransition(TIMEOUT_MS)

  • useTransition 接受一个对象, timeoutMs代码需要延时的时间。

  • 返回一个数组。第一个参数: 是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state第二个参数: 一个布尔值。表示是否正在等待,过度状态的完成 (延时state的更新)。

下面我们引入官网的列子,来了解useTransition的使用。

  1. const SUSPENSE_CONFIG = { timeoutMs: 2000 };
  2. function App() {
  3. const [resource, setResource] = useState(initialResource);
  4. const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
  5. return (
  6. <>
  7. <button
  8. disabled={isPending}
  9. onClick={() => {
  10. startTransition(() => {
  11. const nextUserId = getNextId(resource.userId);
  12. setResource(fetchProfileData(nextUserId));
  13. });
  14. }}
  15. >
  16. Next
  17. </button>
  18. {isPending ? " 加载中..." : null}
  19. <Suspense fallback={<Spinner />}>
  20. <ProfilePage resource={resource} />
  21. </Suspense>
  22. </>
  23. );
  24. }

在这段代码中,我们使用 startTransition 包装了我们的数据获取。这使我们可以立即开始获取用户资料的数据,同时推迟下一个用户资料页面以及其关联的 Spinner 的渲染 2 秒钟( timeoutMs 中显示的时间)。

这个api目前处于实验阶段,没有被完全开放出来。

react-dom

接下来,我们来一起研究react-dom中比较重要的api

react-dom.jpg

render

render 是我们最常用的react-domapi,用于渲染一个react元素,一般react项目我们都用它,渲染根部容器app

  1. ReactDOM.render(element, container[, callback])

使用

  1. ReactDOM.render(
  2. < App / >,
  3. document.getElementById('app')
  4. )

ReactDOM.render会控制container容器节点里的内容,但是不会修改容器节点本身。

hydrate

服务端渲染用hydrate。用法与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。

  1. ReactDOM.hydrate(element, container[, callback])

createPortal

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

那么具体应用到什么场景呢?

比如一些全局的弹窗组件model,<Model/>组件一般都写在我们的组件内部,倒是真正挂载的dom,都是在外层容器,比如body上。此时就很适合createPortalAPI。

createPortal接受两个参数:

  1. ReactDOM.createPortal(child, container)

第一个:child 是任何可渲染的 React 子元素第二个:container是一个 DOM 元素。

接下来我们实践一下:

  1. function WrapComponent({ children }){
  2. const domRef = useRef(null)
  3. const [ PortalComponent, setPortalComponent ] = useState(null)
  4. React.useEffect(()=>{
  5. setPortalComponent( ReactDOM.createPortal(children,domRef.current) )
  6. },[])
  7. return <div>
  8. <div class ref={ domRef } ></div>
  9. { PortalComponent }
  10. </div>
  11. }
  12. class Index extends React.Component{
  13. render(){
  14. return <div style={{ marginTop:'50px' }} >
  15. <WrapComponent>
  16. <div >hello,world</div>
  17. </WrapComponent>
  18. </div>
  19. }
  20. }

效果

createPortal.jpg

我们可以看到,我们children实际在container 之外挂载的,但是已经被createPortal渲染到container中。

unstable_batchedUpdates

react-legacy模式下,对于事件,react事件有批量更新来处理功能, 但是这一些非常规的事件中,批量更新功能会被打破。所以我们可以用react-dom中提供的unstable_batchedUpdates 来进行批量更新。

一次点击实现的批量更新

  1. class Index extends React.Component{
  2. constructor(props){
  3. super(props)
  4. this.state={
  5. numer:1,
  6. }
  7. }
  8. handerClick=()=>{
  9. this.setState({ numer : this.state.numer + 1 })
  10. console.log(this.state.numer)
  11. this.setState({ numer : this.state.numer + 1 })
  12. console.log(this.state.numer)
  13. this.setState({ numer : this.state.numer + 1 })
  14. console.log(this.state.numer)
  15. }
  16. render(){
  17. return <div style={{ marginTop:'50px' }} >
  18. <button onClick={ this.handerClick } >click me</button>
  19. </div>
  20. }
  21. }

效果

batch1.jpg

渲染次数一次。

批量更新条件被打破

  1. handerClick=()=>{
  2. Promise.resolve().then(()=>{
  3. this.setState({ numer : this.state.numer + 1 })
  4. console.log(this.state.numer)
  5. this.setState({ numer : this.state.numer + 1 })
  6. console.log(this.state.numer)
  7. this.setState({ numer : this.state.numer + 1 })
  8. console.log(this.state.numer)
  9. })
  10. }

效果

batch2.jpg

渲染次数三次。

unstable_batchedUpdate 助力

  1. handerClick=()=>{
  2. Promise.resolve().then(()=>{
  3. ReactDOM.unstable_batchedUpdates(()=>{
  4. this.setState({ numer : this.state.numer + 1 })
  5. console.log(this.state.numer)
  6. this.setState({ numer : this.state.numer + 1 })
  7. console.log(this.state.numer)
  8. this.setState({ numer : this.state.numer + 1 })
  9. console.log(this.state.numer)
  10. })
  11. })
  12. }

渲染次数一次, 完美解决批量更新问题。

flushSync

flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。我们知道react设定了很多不同优先级的更新任务。如果一次更新任务在flushSync回调函数内部,那么将获得一个较高优先级的更新。比如

  1. ReactDOM.flushSync(()=>{
  2. /* 此次更新将设置一个较高优先级的更新 */
  3. this.setState({ name: 'alien' })
  4. })

为了让大家理解flushSync,我这里做一个demo奉上,

  1. /* flushSync */
  2. import ReactDOM from 'react-dom'
  3. class Index extends React.Component{
  4. state={ number:0 }
  5. handerClick=()=>{
  6. setTimeout(()=>{
  7. this.setState({ number: 1 })
  8. })
  9. this.setState({ number: 2 })
  10. ReactDOM.flushSync(()=>{
  11. this.setState({ number: 3 })
  12. })
  13. this.setState({ number: 4 })
  14. }
  15. render(){
  16. const { number } = this.state
  17. console.log(number) // 打印什么??
  18. return <div>
  19. <div>{ number }</div>
  20. <button onClick={this.handerClick} >测试flushSync</button>
  21. </div>
  22. }
  23. }

先不看答案,点击一下按钮,打印什么呢?

我们来点击一下看看

flushSync.gif

打印 0 3 4 1 ,相信不难理解为什么这么打印了。


  • 首先 flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以 3 先被打印

  • 2 4 被批量更新为 4

相信这个demo让我们更深入了解了flushSync

findDOMNode

findDOMNode用于访问组件DOM元素节点,react推荐使用ref模式,不期望使用findDOMNode

  1. ReactDOM.findDOMNode(component)

注意的是:


  • 1 findDOMNode只能用在已经挂载的组件上。

  • 2 如果组件渲染内容为 null 或者是 false,那么 findDOMNode返回值也是 null

  • 3 findDOMNode 不能用于函数组件。

接下来让我们看一下,findDOMNode具体怎么使用的:

  1. class Index extends React.Component{
  2. handerFindDom=()=>{
  3. console.log(ReactDOM.findDOMNode(this))
  4. }
  5. render(){
  6. return <div style={{ marginTop:'100px' }} >
  7. <div>hello,world</div>
  8. <button onClick={ this.handerFindDom } >获取容器dom</button>
  9. </div>
  10. }
  11. }

效果:

findNodedom.gif

我们完全可以将外层容器用ref来标记,获取捕获原生的dom节点。

unmountComponentAtNode

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

我们来简单举例看看unmountComponentAtNode如何使用?

  1. function Text(){
  2. return <div>hello,world</div>
  3. }
  4. class Index extends React.Component{
  5. node = null
  6. constructor(props){
  7. super(props)
  8. this.state={
  9. numer:1,
  10. }
  11. }
  12. componentDidMount(){
  13. /* 组件初始化的时候,创建一个 container 容器 */
  14. ReactDOM.render(<Text/> , this.node )
  15. }
  16. handerClick=()=>{
  17. /* 点击卸载容器 */
  18. const state = ReactDOM.unmountComponentAtNode(this.node)
  19. console.log(state)
  20. }
  21. render(){
  22. return <div style={{ marginTop:'50px' }} >
  23. <div ref={ ( node ) => this.node = node } ></div>
  24. <button onClick={ this.handerClick } >click me</button>
  25. </div>
  26. }
  27. }

效果

unmounted.gif

总结

本文通过react组件层面,工具层面,hooks层面,react-dom了解了api的用法,希望看完的同学,能够对着文章中的demo自己敲一遍,到头来会发现自己成长不少。

  • EOF -

推荐阅读 点击标题可跳转

1、React 之道:软件设计、架构和最佳实践

2、10 个 React 安全最佳实践

3、第一个 React 项目做完了,谈谈自己对 hooks 的思考

觉得本文对你有帮助?请分享给更多人

推荐关注「前端大全」,提升前端技能

公众号

点赞和在看就是最大的支持❤️