react 高阶组件

概述

高级组件到底能够解决什么问题?举一个特别简单的例子,话说小明负责开发一个 web 应用,应用的结构如下所示,而且这个功能小明已经开发完了。

但是,有一天老板突然提出了一个权限隔离的需求,就是部分模块组件受到权限控制,后台的数据交互的结果权限控制着模块展示与否,而且没有权限会默认展示无权限提示页面。(如下图,黄色部分是受到权限控制的组件模块)

那么小明面临的问题是,如何给需要权限隔离的模块,绑定权限呢?那第一种思路是把所有的需要权限隔离的模块重新绑定权限,通过权限来判断组件是否展示。

这样无疑会给小明带来很多的工作量,而且后续项目可能还有受权限控制的页面或者组件,都需要手动绑定权限。那么如何解决这个问题呢,思考一下,既然是判断权限,那么可以把逻辑都写在一个容器里,然后将每个需要权限的组件通过容器包装一层,这样不就不需要逐一手动绑定权限了吗?所以 HOC 可以合理的解决这个问题,通过 HOC 模式结构如下图所示:

综上所述,HOC的产生根本作用就是解决大量的代码复用,逻辑复用问题。既然说到了逻辑复用,那么具体复用了哪些逻辑呢?

  • 首先第一种就是像上述的拦截问题,本质上是对渲染的控制,对渲染的控制可不仅仅指是否渲染组件,还可以像 dva 中 dynamic 那样懒加载/动态加载组件。
  • 还有一种场景,比如项目中想让一个非 Route 组件,也能通过 props 获取路由实现跳转,但是不想通过父级路由组件层层绑定 props ,这个时候就需要一个 HOC 把改变路由的 history 对象混入 props 中,于是 withRoute 诞生了。所以 HOC 还有一个重要的作用就是让 props 中混入一些你需要的东西。
  • 还有一种情况,如果不想改变组件,只是监控组件的内部状态,对组件做一些赋能,HOC 也是一个不错的选择,比如对组件内的点击事件做一些监控,或者加一次额外的生命周期

高阶函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

JavaScript中比较常见的filter、map、reduce都是高阶函数

高阶组件:

  • HOC (higher order component)
  • 高阶组件是参数为组件,返回值为新组件的函数
  • 在有了hook函数之后就很少用高阶组件了

我们可以进行如下的解析:

  • 首先, 高阶组件 本身不是一个组件,而是一个函数;
  • 其次,这个函数的参数是一个组件,返回值也是一个组件

使用高级组件的目的:

  • 目的:实现状态逻辑复用 增强一个组件的能力
  • 采用 包装(装饰)模式 ,比如说:手机壳
  • 手机:获取保护功能
  • 手机壳 :提供保护功能
  • 高阶组件就相当于手机壳,通过包装组件,增强组件功能

思路分析

  • 高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件
  • 高阶组件的命名,通常: withMouse withRouter withXXX
  • 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给
    被包装组件

使用步骤

  • 创建一个函数,名称约定以 with 开头
  • 指定函数参数(作为要增强的组件) 传入的组件只能渲染基本的UI
  • 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
  • 在内部创建的组件的render中,需要渲染传入的基本组件,增强功能,通过props的方式给基本组件传值
  • 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中

两种不同的高阶组件

常用的高阶组件有属性代理和反向继承两种,两者之间有一些共性和区别。接下来分别介绍一下两种模式下的高阶组件。

属性代理

属性代理,就是用组件包裹一层代理组件,在代理组件上,可以做一些,对源组件的强化操作。这里注意属性代理返回的是一个新组件,被包裹的原始组件,将在新的组件里被挂载。

优点:

  • ① 属性代理可以和业务组件低耦合,零耦合,对于条件渲染和 props 属性增强,只负责控制子组件渲染和传递额外的 props 就可以了,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的 HOC ,目前开源的 HOC 基本都是通过这个模式实现的。
  • ② 同样适用于类组件和函数组件。
  • ③ 可以完全隔离业务组件的渲染,因为属性代理说白了是一个新的组件,相比反向继承,可以完全控制业务组件是否渲染。
  • ④ 可以嵌套使用,多个 HOC 是可以嵌套使用的,而且一般不会限制包装 HOC 的先后顺序。

缺点:

  • ① 一般无法直接获取原始组件的状态,如果想要获取,需要 ref 获取组件实例。
  • ② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
  • ③ 因为本质上是产生了一个新组件,所以需要配合 forwardRef 来转发 ref。

反向继承

反向继承和属性代理有一定的区别,在于包装后的组件继承了原始组件本身,所以此时无须再去挂载业务组件。

优点:

  • ① 方便获取组件内部状态,比如 state ,props ,生命周期,绑定的事件函数等。
  • ② es6继承可以良好继承静态属性。所以无须对静态属性和方法进行额外的处理。

缺点:

  • ① 函数组件无法使用。
  • ② 和被包装的组件耦合度高,需要知道被包装的原始组件的内部状态,具体做了些什么?
  • ③ 如果多个反向继承 HOC 嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个 componentDidMount ,当前 componentDidMount 会覆盖上一个 componentDidMount 。这样副作用串联起来,影响很大。

基本使用

接收一个组件作为参数,返回一个新组件

import React, { PureComponent } from 'react'
import Home from './Home'

// 定义高阶组件
function hoc(WrapperComponent) {
  // 1. 定义一个类组件
  class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent></WrapperComponent>
    }
  }

  return NewComponent

  // 2. 定义一个函数组件
  // function  NewComponent2(props) {}

  // return NewComponent2
}

const HomeHoc = hoc(Home)

export class classHello extends PureComponent {
  render() {
    return (
      <div>
        <HomeHoc />
      </div>
    )
  }
}

export default classHello

Home.jsx

import React, { PureComponent } from 'react';

class Home extends PureComponent {
  render() {
    return (
      <div>
        122
      </div>
    );
  }
}

export default Home;

高阶组件应用

应用场景

基础使用

为什么React引入高阶组件的概念?它到底有何威力?让我们先通过一个简单的例子说明一下。

假设有一个组件MyComponent,需要从LocalStorage中获取数据,然后渲染数据到界面。我们可以这样写组件代码:

代码很简单,但当有其他组件也需要从LocalStorage中获取同样的数据展示出来时,需要在每个组件都重复componentWillMount中的代码,这显然是很冗余的。下面让我们来看看使用高阶组件可以怎么改写这部分代码。

withPersistentData就是一个高阶组件,它返回一个新的组件,在新组件的componentWillMount中统一处理从LocalStorage中获取数据的逻辑,然后将获取到的数据以属性的方式传递给被包装的组件WrappedComponent,这样在WrappedComponent中就可以直接使用this.props.data获取需要展示的数据了,如MyComponent2所示。当有其他的组件也需要这段逻辑时,继续使用withPersistentData这个高阶组件包装这些组件就可以了。

通过这个例子,可以看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。高阶组件的这种实现方式,本质上是一个装饰者设计模式。

高阶组件的参数并非只能是一个组件,它还可以接收其他参数。例如,组件MyComponent3需要从LocalStorage中获取key等于name的数据,而不是上面例子中写死的key等于data的数据,withPersistentData这个高阶组件就不满足我们的需求了。我们可以让它接收额外的一个参数,来决定从LocalStorage中获取哪个数据:

新版本的withPersistentData就满足我们获取不同key的值的需求了。高阶组件中的参数当然也可以是函数,我们将在下一节进一步说明。

进阶用法

高阶组件最常见的函数签名形式是这样的:

HOC([param])([WrappedComponent])

用这种形式改写withPersistentData,如下:

实际上,此时的withPersistentData和我们最初对高阶组件的定义已经不同。它已经变成了一个高阶函数,但这个高阶函数的返回值是一个高阶组件。HOC([param])([WrappedComponent])这种形式中,HOC([param])才是真正的高阶组件,我们可以把它看成高阶组件的变种形式。这种形式的高阶组件因其特有的便利性------结构清晰(普通参数和被包裹组件分离)、易于组合,大量出现在第三方库中。如react-redux中的connect就是一个典型。connect的定义如下:

这个函数会将一个React组件连接到Redux 的 store。在连接的过程中,connect通过函数类型的参数mapStateToProps,从全局store中取出当前组件需要的state,并把state转化成当前组件的props;同时通过函数类型的参数mapDispatchToProps,把当前组件用到的Redux的action creators,以props的方式传递给当前组件。

例如,我们把组件ComponentA连接到Redux上的写法类似于:

我们可以把它拆分来看:

当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一起使用的。例如,有f,g,h三个高阶组件,都只接受一个组件作为参数,于是我们可以很方便的嵌套使用它们:f( g( h(WrappedComponent) ) )。这里可以有一个例外,即最内层的高阶组件h可以有多个参数,但其他高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。

例如我们将connect和另一个打印日志的高阶组件withLog联合使用:

这里我们定义一个工具函数:compose(...functions),调用compose(f, g, h)等价于 (...args) => f(g(h(...args)))。用compose函数我们可以把高阶组件嵌套的写法打平:

像Redux等很多第三方库都提供了compose的实现,compose结合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。

与父组件的区别

有些同学可能会觉得高阶组件有些类似父组件的使用。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。

注意事项

不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。

如果需要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。因为高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics可以帮助我们方便的拷贝组件所有的自定义静态方法。有兴趣的同学可以自行了解。

Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref并不会传递给被包装组件。如果你在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果你希望获取被包装组件的引用,你可以把ref的回调函数定义成一个普通属性(给它一个ref以外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:

props增强

定义组件:给一些需要特殊数据的组件,注入props

hoc/enhanced_props.js

import { PureComponent } from 'react'

// 定义高阶组件
// 也可以将这里单独提取出去
function enhancedUserInfo(OriginComponent){
  class NewComponent extends PureComponent {
    constructor (props) {
      super(props)
      this.state = {
        userInfo: {
          username: 'zs',
          age: 14
        }
      }
    }
    render(){
      /*{针对本身就有数据}----{...this.props}*/
      return <OriginComponent {...this.props} {...this.state.userInfo}></OriginComponent>
    }
  }
  return NewComponent
}

export default enhancedUserInfo

About.jsx

import React, { PureComponent } from 'react'
import enhancedUserInfo from '../hoc/enhanced_props'

export class About extends PureComponent {
  render() {
    return (
      <div>About--{this.props.username}</div>
    )
  }
}

export default enhancedUserInfo(About)

class.jsx

import React, { PureComponent } from 'react'

import enhancedUserInfo from '../hoc/enhanced_props.js'
import About from '../components/About'

// 普通组件
const Home = enhancedUserInfo((props) => {
  return <h1>Home--{props.username}--{props.age}--{props.banners}</h1>
})

// 普通组件
const Profile = enhancedUserInfo((props) => {
  return <h1>Profile</h1>
})

// 普通组件
const HelloFriend = enhancedUserInfo((props) => {
  return <h1>HelloFriend</h1>
})

export class classHello extends PureComponent {
  render() {
    return (
      <div>
        /*{本身就有数据}*/
        <Home banners={['轮播图1', '轮播图2']}></Home>
        <Profile></Profile>
        <HelloFriend></HelloFriend>
        <About></About>
      </div>
    )
  }
}

export default classHello

Context共享

components/context/theme_context

import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext

hoc/with_theme.js

import ThemeContext from '../components/context/theme_context'

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme

class.jsx

import React, { PureComponent } from 'react'
import ThemeContext from '../components/context/theme_context'
import Product from '../components/Product'

export class classHello extends PureComponent {
  render() {
    return (
      <div>
        /*{提供数据}*/
        <ThemeContext.Provider value={{color: "red", size: 16}}>
          <Product></Product>
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default classHello

product.jsx

import React, { PureComponent } from 'react'
import ThemeContext from '../components/context/theme_context.js'

import withTheme from '../hoc/with_theme'

// 方式1--原始方式
// export class Product extends PureComponent {
//   render() {
//     return (
//       <div>Product: 
//         <ThemeContext.Consumer>
//           {
//             value => {
//               return <h2>theme: {value.color} - {value.size}</h2>
//             }
//           }
//         </ThemeContext.Consumer>
//       </div>
//     )
//   }
// }
// export default Product


// 方式2--利用高阶组件
export class Product extends PureComponent {
  render() {
    const { color, size } = this.props

    return (
      <div>
        <h2>Product: {color}-{size}</h2>
      </div>
    )
  }
}

export default withTheme(Product)

登录鉴权

登录之后才会展示购物车页面,反之展示另外页面

hoc/login_auth.js

function loginAuth(OriginComponent) {
  return props => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token")

    if (token) {
      return <OriginComponent {...props}/>
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>
    }
  }
}

export default loginAuth

Cart.jsx

购物车页面

import React, { PureComponent } from 'react'

import loginAuth from '../hoc/login_auth'

export class Cart extends PureComponent {
  render() {
    return (
      <div>Cart</div>
    )
  }
}

export default loginAuth(Cart)

class.jsx

其他页面

import React, { PureComponent } from 'react'

import Cart from '../components/Cart'

export class classHello extends PureComponent {
  constructor() {
    super()

    this.state = {
      isLogin: false
    }
  }
  loginClick() {
    localStorage.setItem("token", "coderwhy")

    this.setState({ isLogin: true })

    // 强制刷新页面--方式2(不推荐)
    // this.forceUpdate()
  }

  render() {
    return (
      <div>
        <button onClick={e => this.loginClick()}>登录</button>
        <Cart />
      </div>
    )
  }
}

export default classHello

生命周期

利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑

例如计算render函数渲染完成需要花费多长时间

hoc/log_render_time.js

import { PureComponent } from "react";

function logRenderTime(OriginComponent) {
  return class extends PureComponent {
    // 即将被挂载
    UNSAFE_componentWillMount() {
      this.beginTime = new Date().getTime()
    }
  
    // 挂载完成
    componentDidMount() {
      this.endTime = new Date().getTime()
      const interval = this.endTime - this.beginTime
      console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`)
    }

    render() {
      return <OriginComponent {...this.props}/>
    }
  }
}

export default logRenderTime

Detail.jsx

import React, { PureComponent } from "react";

import logRenderTime from "../hoc/log_render_time.js";

export class Detail extends PureComponent {
  render() {
    return (
      <div>
        <h2>Detail Page</h2>
        <ul>
          <li>数据列表1</li>
          <li>数据列表2</li>
          <li>数据列表3</li>
          <li>数据列表4</li>
          <li>数据列表5</li>
          <li>数据列表6</li>
          <li>数据列表7</li>
          <li>数据列表8</li>
          <li>数据列表9</li>
          <li>数据列表10</li>
        </ul>
      </div>
    );
  }
}

export default logRenderTime(Detail);

class.jsx

import React, { PureComponent } from 'react'

import Detail from '../components/Detail'

export class classHello extends PureComponent {
  render() {
    return (
      <div>
        <Detail />
      </div>
    )
  }
}

export default classHello

页面权限控制

  • 利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别和 页面元素级别

    // HOC.js
    function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
    state = {
    isAdmin: false,
    }
    async UNSAFE_componentWillMount() {
    const currentRole = await getCurrentUserRole();
    this.setState({
    isAdmin: currentRole === 'Admin',
    });
    }
    render() {
    if (this.state.isAdmin) {
    return <WrappedComponent {...this.props} />;
    } else {
    return (

    您没有权限查看该页面,请联系管理员!
    );
    }
    }
    };
    }

    // pages/page-a.js
    class PageA extends React.Component {
    constructor(props) {
    super(props);
    // something here...
    }
    UNSAFE_componentWillMount() {
    // fetching data
    }
    render() {
    // render page with data
    }
    }
    export default withAdminAuth(PageA);

    // pages/page-b.js
    class PageB extends React.Component {
    constructor(props) {
    super(props);
    // something here...
    }
    UNSAFE_componentWillMount() {
    // fetching data
    }
    render() {
    // render page with data
    }
    }
    export default withAdminAuth(PageB);

组件渲染性能追踪

  • 借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录

    class Home extends React.Component {
    render() {
    return (

    Hello World.

    );
    }
    }
    function withTiming(WrappedComponent) {
    return class extends WrappedComponent {
    constructor(props) {
    super(props);
    this.start = 0;
    this.end = 0;
    }
    UNSAFE_componentWillMount() {
    super.componentWillMount && super.componentWillMount();
    this.start = Date.now();
    }
    componentDidMount() {
    super.componentDidMount && super.componentDidMount();
    this.end = Date.now();
    console.log(${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms);
    }
    render() {
    return super.render();
    }
    };
    }

    export default withTiming(Home);

页面复用

const withFetching = fetching => WrappedComponent => {
    return class extends React.Component {
        state = {
            data: [],
        }
    async UNSAFE_componentWillMount() {
        const data = await fetching();
        this.setState({
            data,
        });
    }
    render() {
        return <WrappedComponent data={this.state.data} {...this.props} />;
    }
}
}

// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);

控制渲染

渲染劫持

HOC 反向继承模式,可以通过 super.render() 得到 render 之后的内容,利用这一点,可以做渲染劫持 ,更有甚者可以修改 render 之后的 React element 对象。

修改渲染树

动态加载

dva 中 dynamic 就是配合 import ,实现组件的动态加载的,而且每次切换路由,都会有 Loading 效果,接下来看看大致的实现思路。

组件赋能

ref 获取实例

对于属性代理虽然不能直接获取组件内的状态,但是可以通过 ref 获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:类组件才存在实例,函数组件不存在实例。

事件监控

HOC 不一定非要对组件本身做些什么?也可以单纯增加一些事件监听,错误监控。接下来,接下来做一个

注意事项

谨慎修改原型链

如上 HOC 作用仅仅是修改了原来组件原型链上的 componentDidMount 生命周期。但是这样有一个弊端就是如果再用另外一个 HOC 修改原型链上的 componentDidMount ,那么这个HOC的功能即将失效。

不要在函数组件内部或类组件render函数中使用HOC

类组件中🙅错误写法:

函数组件中🙅错误写法:

这么写的话每一次类组件触发 render 或者函数组件执行都会产生一个新的WrapHome,react diff会判定两次不是同一个组件,那么就会卸载老组件,重新挂载新组件,老组件内部的真实 DOM 节点,都不会合理的复用,从而造成了性能的浪费,而且原始组件会被初始化多次。

ref 的处理

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 ref 并不适用。那是因为 ref 实际上并不是一个 prop , 就像 key 一样,对于 ref 属性它是由 React 专门处理的。那么如何通过 ref 正常获取到原始组件的实例呢?我们可以用forwardRef做 ref 的转发处理。

注意多个HOC嵌套顺序问题

多个HOC嵌套,应该留意一下HOC的顺序,还要分析出要各个 HOC 之间是否有依赖关系。

对于 class 声明的类组件,可以用装饰器模式,对类组件进行包装:

对于函数组件:

HOC1 -> HOC2 -> HOC3 -> Index

要注意一下包装顺序,越靠近Index组件的,也就是越内层的HOC,离组件Index也就越近

还有一些其他的小细节:

  • 如果2个 HOC 相互之间有依赖。比如 HOC1 依赖 HOC2 ,那么 HOC1 应该在 HOC2 内部。
  • 如果想通过 HOC 方式给原始组件添加一些额外生命周期,因为涉及到获取原始组件的实例 instance ,那么当前的 HOC 要离原始组件最近。

继承静态属性

上述讲到在属性代理 HOC 本质上返回了一个新的 component ,那么如果给原来的 component 绑定一些静态属性方法,如果不处理,新的 component 上就会丢失这些静态属性方法。那么如何解决这个问题呢。

手动继承

当然可以手动将原始组件的静态方法 copy 到 HOC 组件上来,但前提是必须准确知道应该拷贝哪些方法。

引入第三方库

每个静态属性方法都手动绑定会很累,尤其对于开源的 HOC ,对原生组件的静态方法是未知 ,为了解决这个问题可以使用hoist-non-react-statics自动拷贝所有的静态方法:

意义

利用高阶组件可以针对某些React代码进行更加优雅的处理

在早期的React有提供组件之间的一种复用方式--mixin:

  • mixin可能会相互依赖,相互耦合,不利于代码的维护
  • 不同的mixin中的方法可能会相互冲突
  • mixin非常多时,组件处理起来比较麻烦,甚至还要为其做相关处理,这样会对代码造成滚雪球式的复杂性

当然,HOC也有自己的一些缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突
  • Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
  • 比如this指向问题、比如hoc的嵌套复杂度问题等等;

Portals的使用

基本概念

◼ 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:

  •  第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
  •  第二个参数(container)是一个 DOM 元素;

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

Modal组件案例

比如说,我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置:

  1. ◼ 步骤一:修改index.html添加新的节点
  1. ◼ 步骤二:编写这个节点的样式
  1. ◼ 步骤三:编写组件代码

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
    <style>
      #modal {
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
    </style>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="why"></div>
    <div id="modal"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

modal.jsx

import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"

export class Modal extends PureComponent {
  render() {
    return createPortal(this.props.children, document.querySelector("#modal"))
  }
}

export default Modal

App.jsx

import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"
import Modal from './Modal'

export class App extends PureComponent {
  render() {
    return (
      <div className='app'>
        <h1>App H1</h1>
        /*{渲染到 why 上,而不是 root}*/
        {
          createPortal(<h2>App H2</h2>, document.querySelector("#why"))
        }

        {/* 2.Modal组件 */}
        <Modal>
          <h2>我是标题</h2>
          <p>我是内容, 哈哈哈</p>
        </Modal>
      </div>
    )
  }
}

export default App

fragment

基本概念

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:

我们又希望可以不渲染这样一个div应该如何操作呢?

  •  使用Fragment
  •  Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;

React还提供了Fragment的短语法:

  •  它看起来像空标签 <> </>;
  •  但是,如果我们需要在Fragment中添加key,那么就不能使用短语法

使用示例

<> ===> 等价于 Fragment,是Fragment的一种语法糖形式

import React, { PureComponent, Fragment } from 'react'

export class App extends PureComponent {
  constructor() {
    super() 

    this.state = {
      sections: [
        { title: "哈哈哈", content: "我是内容, 哈哈哈" },
        { title: "呵呵呵", content: "我是内容, 呵呵呵" },
        { title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
        { title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
      ]
    }
  }

  render() {
    const { sections } = this.state

    return (
      <>
        <h2>我是App的标题</h2>
        <p>我是App的内容, 哈哈哈哈</p>
        <hr />

        {
          sections.map(item => {
            return (
              <Fragment key={item.title}>
                <h2>{item.title}</h2>
                <p>{item.content}</p>
              </Fragment>
            )
          })
        }
      </>
    )
  }
}

export default App

StrictMode

基本概念

StrictMode 是一个用来突出显示应用程序中潜在问题的工具:

  •  与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  •  它为其后代元素触发额外的检查和警告;
  •  严格模式检查仅在开发模式下运行;它们不会影响生产构建

可以为应用程序的任何部分启用严格模式:

  •  不会对 Header 和 Footer 组件运行严格模式检查;
  •  但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;

使用示例

Profile.jsx

import React, { PureComponent } from 'react'

export class Profile extends PureComponent {
  UNSAFE_componentWillMount() {
    console.log("UNSAFE_componentWillMount")
  }

  componentDidMount() {
    // console.log(this.refs.title)
    console.log("Profile componentDidMount")
  }

  render() {
    console.log("Profile render")
    return (
      <div>
        <h2 ref="title">Profile Title</h2>
      </div>
    )
  }
}

export default Profile

Home.jsx

import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  // UNSAFE_componentWillMount() {
  //   console.log("Home UNSAFE_componentWillMount")
  // }

  constructor(props) {
    super(props)

    console.log("Home Constructor")
  }

  componentDidMount() {
    console.log("Home componentDidMount")
  }

  render() {
    console.log("Home Render")

    return (
      <div>
        {/* <h2 ref="title">Home Title</h2> */}

        <h2>Home</h2>
      </div>
    )
  }
}

export default Home

App.jsx

import React, { PureComponent, StrictMode } from 'react'
// import { findDOMNode } from "react-dom"
import Home from './pages/Home'
import Profile from './pages/Profile'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <StrictMode>
          <Home/>
        </StrictMode>
        <Profile/>
      </div>
    )
  }
}

export default App
相关推荐
m0_748239838 分钟前
基于web的音乐网站(Java+SpringBoot+Mysql)
java·前端·spring boot
时雨h12 分钟前
RuoYi-ue前端分离版部署流程
java·开发语言·前端
爱喝奶茶的企鹅16 分钟前
Next.js 14 性能优化:从首屏加载到运行时优化的最佳实践
react.js
Cachel wood25 分钟前
Django REST framework (DRF)中的api_view和APIView权限控制
javascript·vue.js·后端·python·ui·django·前端框架
m0_7482340832 分钟前
Spring Boot教程之三十一:入门 Web
前端·spring boot·后端
Domain-zhuo41 分钟前
如何提高webpack的构建速度?
前端·webpack·前端框架·node.js·ecmascript
放逐者-保持本心,方可放逐1 小时前
SSE 流式场景应用 及 方案总结
javascript·axios·fetch·eventsource
还是大剑师兰特1 小时前
面试题:ES6模块与CommonJS模块有什么异同?
前端·es6·大剑师
胡西风_foxww1 小时前
【ES6复习笔记】数值扩展(16)
前端·笔记·es6·扩展·数值
mosen8681 小时前
uniapp中uni.scss如何引入页面内或生效
前端·uni-app·scss