重学React之组件化开发

538 阅读19分钟

组件化思想的应用

尽可能的将页面拆分成一个个小的、可复用的组件。这样让我们的代码更加方便组织和管理,并且扩展性也更强。

React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:

  • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component)。
  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)。
  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)。

这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:

  • 函数组件、无状态组件、展示型组件主要关注UI的展示。
  • 类组件、有状态组件、容器型组件主要关注数据逻辑。

类组件

类组件的定义有如下要求:

  • 组件的名称是大写字符开头(无论类组件还是函数组件)
  • 类组件需要继承自 React.Component
  • 类组件必须实现render函数

使用class定义一个组件:

  • constructor是可选的,我们通常在constructor中初始化一些数据。
  • this.state中维护的就是我们组件内部的数据。
  • render() 方法是 class 组件中唯一必须实现的方法。 其中render函数可以返回一下值:
  • React 元素。
  • 数组或 fragments:使得 render 方法可以返回多个元素。
  • Portals:可以渲染子节点到不同的 DOM 子树中。
  • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点。
  • 布尔类型或 null:什么都不渲染。

函数组件

函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。

普通的函数式组件(非hooks):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数。
  • 没有this(组件实例)。
  • 没有内部状态(state)。

生命周期

下面来介绍一下常用的生命周期函数。

Constructor

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

constructor中通常只做两件事情:

  • 通过给 this.state 赋值对象来初始化内部的state。
  • 为事件绑定实例(this)。

componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。

componentDidMount中通常进行哪里操作呢?

  • 依赖于DOM的操作可以在这里进行。
  • 在此处发送网络请求就最好的地方。
  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)。

componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

  • 当组件更新后,可以在此处对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。
  • componentDidUpdate() 中直接调用 setState() ,但请注意它必须被包裹在一个条件语句里。否则会导致死循环。

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。

  • 在此方法中执行必要的清理操作。 例如,清除 timer,取消网络请求或清除
  • 在 componentDidMount() 中创建的订阅等。
  • componentWillUnmount() 中不应调用 setState() ,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

组件通信

父组件向子组件传递信息

将数据当成子组件的属性,然后子组件的props接收。

  • 函数组件接收 函数的第一个参数就是props参数对象,可以拿到传递的属性数据。
   function MyButton(props) {
      console.log(props)
      return (
        <h1>{props.name}</h1>
      )
    }
  • 类组件接收 通过在构造函数中调用super(props),即可获取props。
    class MyButton extends React.Component {
      // 也可以不写,react自动传入
      // constructor(props) {
      //   super(props)
      // }
      render() {
        return (
            <h1>{this.props.name}</h1>
        )
      }
    }

我们在传递props时,可以给props指定默认值,并且指定props的类型。

  • 指定默认值 通过class属性defaultProps,设置props的默认值。
  static defaultProps = {
    name: "=====",
    age: 30
  }
  • 指定props的类型 安装
npm install prop-types --save

引入prop-types库

import PropTypes from 'prop-types';

函数组件

    import React from 'react';
    import propTypes from 'prop-types'

    export default function AppTest(props) {
      return (
        <div>
          <h1>{props.name}</h1>
        </div>
      )
    }



    AppTest.defaultProps = {
      name: 11, // 这里会报警告
    }

    // 他也会检查默认值的类型是否正确
    AppTest.propTypes = {
      name: propTypes.string
    }

类组件

    import React from 'react';
    import propTypes from 'prop-types'

    export default class AppTest extends React.Component{
      render() {
        return (
          <div>
            <h1>{this.props.name}</h1>
          </div>
        )
      }
    }

    AppTest.defaultProps = {
      name: 11, // 这里会报警告
    }

    // 他也会检查默认值的类型是否正确
    AppTest.propTypes = {
      name: propTypes.string
    }

子组件向父组件传递信息

其实就类似于回调函数,子组件通过this.props.事件调用函数,而父组件定义函数。然后将参数传递给父组件。

    //父组件
      state = {
        color: 'red'
      }

      changeColor = color => {
        this.setState({
          color: color
        })
      }

      render() {
        // console.log(this)
        return (
          <div>
            <h1 style={{ color: this.state.color }} >llm</h1>
            <Son changeColor={this.changeColor.bind(this)}></Son>
          </div>
        )
      }

      //子组件
     render() {
        return (
          <div>
            <button onClick={() => { this.props.changeColor('blue') }}>蓝色</button>
            <button onClick={() => { this.props.changeColor('red') }}>红色</button>
          </div>
        )
      }

插槽

react中没有插槽这一概念,默认自定义组件中不能有内容。

利用props.children来实现

  • 如果标签内只有一个元素,那么props.children代表就是该元素
  • 如果标签内有多个元素,那么props.children是这些元素组成的数组
  • 但是,我们可以在定义组件时,在内容中写上{props.children}来表示占位,当使用自定义标签时,标签里的内容会自动取代{props.children}。
  • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。
    import React from 'react'

    export default function SlotTest(props) {
      return (
        <div>
          <div>slotTest组件中的本身标签</div>
          {props.children}
        </div>
      )
    }
    
    import React from 'react';
    import ReactDOM from "react-dom"
    
    import SlotTest from './SlotTest'


    ReactDOM.render((
      <SlotTest>
        <p>p标签</p>
        <a href="#">a标签</a>
        <div>div标签</div>
      </SlotTest>
    ), document.getElementById("root"))

image.png 利用props传入插槽内容

  • 我们也可以不用children占位,可以自定义占位的,然后通过标签属性的形式传入内容。
    import React from 'react'

    export default function SlotTest(props) {
      return (
        <div>
          <div>slotTest组件中的本身标签</div>
          <div>{props.leftSlot}</div>
          {props.rightSlot}
        </div>
      )
    }
    
    
    import React from 'react';
    import ReactDOM from "react-dom"

    import SlotTest from './SlotTest'

    ReactDOM.render((
      <SlotTest leftSlot={<div>left标签</div>} rightSlot={<div>right标签</div>}  />
    ), document.getElementById("root"))

image.png

跨组件通信

通过逐个组件传递props。

通过context。

使用context传递数据要经过以下几步

  • 创建context实例。 注意: 这里的默认值仅会在Consumer在组件树中无法找到匹配的Provider才会使用,因此即使你给Providervalue传入undefined值时,Consumer也不会使用默认值。

注意:这里的默认值可以接受一个对象。

    let context = React.createContext('这里可以传入默认值')
  • 调用Provider内部组件将值进行传递,利用value属性来传递值。 如果没写这一步,则默认找默认值。这里是将接收共享数据的组件包裹着。
    <ThemeContext.Provider value="dark">
       <Toolbar />
    </ThemeContext.Provider>
  • 指定当前读取的是哪一个context值
  • React 会往上找到最近的 Provider,然后使用它的值。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
  • 如果没有定义Provider,则他会使用创建context实例时指定的默认值。
static contextType = context;

完整demo

    import React, { createContext } from 'react'

    const ParentContext = createContext({
      color: 'red',
      value: 'zh======================'
    })

    export default class Parent extends React.Component {
      render() {
        return (
          <div>
            <ParentContext.Provider
              value={{ color: 'skyblue', value: 'llm==================' }}
            >
              <Son />
            </ParentContext.Provider>
          </div>
        )
      }
    }

    class Son extends React.Component {
      render() {
        return (
          <div>
            {/* 方式一: */}
            {/* <p style={{ color: this.context.color }}>{this.context.value}</p> */}
            {/* 方式二: */}
            <ParentContext.Consumer>
              {(item) => <p style={{ color: item.color }}>{item.value}</p>}
            </ParentContext.Consumer>
          </div>
        )
      }
    }

    // 方式一
    // Son.contextType = ParentContext

那么我们就来看如何使用它吧?

  • 直接调用this.context,但是必须加上这个静态的属性 static contextType = context对象这种使用不可以用在函数式组件中。
render() {
    return <Button theme={this.context} />;
}
  • 直接通过context的Consumer属性结合函数使用。这种方式是可以在函数式组件中使用的。

这种方法需要一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

export default class MyContext extends Component {
  render() {

    return (
      <div>
        <ContextProp.Consumer>
          //利用函数获取该value值。
          {
            value => <div>{value}</div>
          }
        </ContextProp.Consumer>
      </div>
    )
  }
}
  • 消费多个context
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

通过以上事例,我们可以看出,在对于多个context共享时,代码非常难写,难看。所以一般共享多个数据不会去使用它,都是使用redux。

setState

将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

为什么setState更新是异步的

  • setState设计为异步,可以显著的提升性能。
    • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的。
    • 最好的办法应该是获取到多个更新,之后进行批量更新。
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题。

如何获取异步更新后的state

可以使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

    this.setState({
      pp: "++++++++++++"
    }, () => {
      console.log(this.state.pp) // "++++++++++++"
    })
  componentDidUpdate(prevProps, prevState, prev3) { // 这上面的参数都获取的是以前的值
    console.log(this.state.pp) //  "++++++++++++"
  }

那么setState修改数据,如何让其同步获取呢

  • 将setState放在定时器中更新数据
  handleClick = () => {
    setTimeout(() => {
        this.setState({
          name: 'llm'
        })
    }, 0)
    console.log(this.state.name) // "llm"
  }
  • 将setState放在原生的dom事件中更新数据
  componentDidMount() {
    const btn = document.getElementById('btn')
    btn.addEventListener('click', () => {
      this.setState({
        name: 'llm'
      })
      console.log(this.state.name) // "llm"
    })
  }

setState参数介绍

  • 可以直接传入一个对象,来修改state的值。
    this.state = {
      name: "kkk",
      age: 20
    }
    handleClick = () => {
        this.setState({
          name: "==="
        })

        // 不会立刻获取到更新后的值。
        console.log(this.state.name) // kkk
    }
    
  <div>
    <h1>{this.state.name}</h1>
    <button onClick={this.handleClick}>按钮</button>
  </div>
  • 也可以传入一个updater。 updater 函数中接收的 state 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。
    this.state = {
      name: "kkk",
      age: 20
    }

     handleClick = () => {
        this.setState((state, props) => {
          return {
            name: "===="
          }
        })
      }
  
  <div>
    <h1>{this.state.name}</h1>
    <button onClick={this.handleClick}>按钮</button>
  </div>

setState两种参数的区别

下面通过一个小案例,来介绍一下他两的区别。点击按钮,多次调用setState方法,然后,看其num增加几。

  • 传入一个对象
    import React from 'react'

    export default class BtnTest extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          num: 0
        }
      }

      handleClick = () => {
        this.setState({
          num: this.state.num + 1
        })
        this.setState({
          num: this.state.num + 1
        })
        this.setState({
          num: this.state.num + 1
        })
      }
      render() {
        return (
          <div>
            <h1>{this.state.num}</h1>
            <button onClick={this.handleClick}>按钮</button>
          </div>
        )
      }
    }

btn1.gif 由上面可以看出,每次点击只会增加1。

  • 传入一个updater函数。
    import React from 'react'

export default class BtnTest extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      num: 0
    }
  }

  handleClick = () => {
    console.log('点击按钮')
    this.setState((state, props) => {
      return {
        num: state.num + 1
      }
    })
    this.setState((state, props) => {
      return {
        num: state.num + 1
      }
    })
    this.setState((state, props) => {
      return {
        num: state.num + 1
      }
    })
  }
  render() {
    return (
      <div>
        <h1>{this.state.num}</h1>
        <button onClick={this.handleClick}>按钮</button>
      </div>
    )
  }
}

btn2.gif 从上面的结果可以看出,点击一次按钮,你会增加3。

所以传入对象作为参数,他的更新不会依据上一次的结果。但是传入一个函数作为参数,他会依据上一次结果计算。

React更新流程

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。然后会对比旧的DOM树。

对比不同类型的元素

当节点为不同的元素,React会拆卸原有的树,并且建立起新的树。

  • 当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment> 都会触发一个完整的重建流程。
  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执componentWillUnmount() 方法。
  • 建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法。

对比同一类型的元素

  • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对更新有改变的属性。

对子节点进行递归

  • 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。
  • 当然如果自在最后插入一个元素,不会影响性能。但是如果在中间或者开始,或者修改元素,那么将严重影响性能。 对于上面的性能问题,我们就需要通过设置key值来避免。

render函数何时被调用

默认情况下只要修改了props,state数据,他都会被调用。如何才能有条件的使render被调用呢?

  • 可以通过shouldComponentUpdate生命周期函数返回false。并且也可以根据最新的newProps,newState的值来做出对应的需求。
  shouldComponentUpdate(newProps, newState) {
    // 这里都是new值。
    console.log(newProps, newState)
    return false
  }
  • 可以让class继承PureComponent,而不是Component。他可以避免我们每次手动在class中处理大量的props,state比对。PureComponent内部是通过浅比较比较新旧props, state来决定是否重新执行render函数。
  • 如果想要优化函数组件,我们可以将函数组件传入memo函数。而且memo还可以传入第二个参数,表示根据什么来保证是否再次渲染该组件,如果没有传递,他依旧调用PureComponent中调用的浅层比较函数。 image.png 浅层比较函数
  • 先比较新旧props或者state是否是同一个对象。
  • 比较新旧props或者state其中之一是否为null。
  • 比较新旧props或者state第一层属性个数是否相同。
  • 比较新旧props或者state第一层属性是否相同。 image.png 所以说,我们一定不要直接修改state中的数据,如果类组件是继承PureComponent或者函数组件被memo包裹,那么如果传入的state中的数据是引用类型,将会出现不会更新界面的bug。
class ListOfWords extends React.PureComponent {
  render() {
    console.log('子组件')
    return <div>{this.props.words.join(',')}</div>
  }
}

// 如果继承Component,只要props,state改变,render就会重新渲染。
export default class PureComTest extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      words: ['marklar']
    }
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug // 这里的一个bug是使用了同一个数组。
    const words = this.state.words
    words.push('marklar')
    this.setState({ words: words })
  }

  render() {
    console.log('父组件')
    return (
      <div>
        <button onClick={this.handleClick}>按钮</button>
        <ListOfWords words={this.state.words} />
      </div>
    )
  }
}

上面这个例子是不会渲染界面的。

事件总线

第三方库events。 安装

npm install events

使用

    import React from 'react'
    import EventEmitter from 'events'

    const eventEmitter = new EventEmitter()

    class Com extends React.Component {
      getName(...args) {
        console.log(args)
      }
      componentDidMount() {
        // eventEmitter.addListener('name', this.getName)
        eventEmitter.on('name', this.getName)
      }
      componentWillUnmount() {}
        eventEmitter.removeListener('name')
      }
      render() {
        return <div>子元素</div>
      }
    }

    export default class EventBusTest extends React.Component {
      handleEmit() {
        eventEmitter.emit('name', 'zh', 'llm')
      }
      render() {
        return (
          <div>
            <Com />
            <button onClick={this.handleEmit}>传递事件</button>
          </div>
        )
      }
    }

主要的作用是实现非父子组件的通信。

ref

通过createRef来创建一个ref实例

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    //这里创建一个ref实例
    this.domRef = React.createRef();
  }
  componentDidMount() {
    console.log('======', this.refs.stringRef) // 
    console.log('======', this.domRef.current) // <div>createRef获取dom</div>
  }
  render() {
    //使ref实例绑定该元素
    return <div ref={this.domRef}>createRef获取dom</div>
  }
}

直接获取到该组件this.refs["ref的属性值"]


  componentDidMount() {
    console.log('======', this.refs.stringRef) //<div>字符串获取dom</div>
  }
  render() {
    return (
        <div ref="stringRef">字符串获取dom</div>
    )
  }

直接给ref传递一个函数

    constructor(props) {
    this.fnDom = null
  }

  componentDidMount() {
    console.log('======', this.fnDom) // <div>通过函数获取dom</div>
  }
  render() {
    return (
      <div>
        <div
          ref={(dom) => {
            this.fnDom = dom
          }}
        >
          通过函数获取dom
        </div>
      </div>
    )
  }

注意我们可以在类组件中定义ref属性(上述三种方法都可以),不能在函数式组件中定义ref属性。这样会报错误。

    // 当Com是类组件时,将不会出现错误。但是当Com是函数式组件时,将会出现错误。
    <Com ref={this.componentDom} />
    <Com ref="componentDom" />
    <Com
      ref={(el) => {
        this.c = el
      }}
    />

image.png 上面我们可以看出,想要在函数式组件中使用ref属性,我们可以使用React.forwardRef()

ref转发

一般我们在自定义组件中定义ref属性,那么我们也可以指定自定义组件中html元素。

  • 可以将ref对象当做一个props属性传递到子组件(需要自定义ref传递的props名),然后将该props属性子组件中赋值到ref属性。注意类组件和函数组件都可以。
    import React from 'react'

    class Son extends React.Component {
      render() {
        return <div ref={this.props.refDom}>子组件</div>
      }
    }
    // function Son(props) {
    //   return <div ref={props.refDom}>子组件</div>
    // }

    export default class Parent extends React.Component {
      constructor(props) {
        super(props)
        this.refDom = React.createRef()
      }
      componentDidMount() {
        console.log('==========', this.refDom.current) //  <div>子组件</div>
      }
      render() {
        return (
          <div>
            <Son refDom={this.refDom}></Son>
          </div>
        )
      }
    }
  • 通过内置的forward函数来转发ref。这里我们可以直接设置ref作为自定义组件的props,不需要自定义ref属性名。
    const ConvertComponent = React.forwardRef((props, ref) => (
      <div ref={ref}>子组件</div>
    ))

    export default class Parent extends React.Component {
      constructor(props) {
        super(props)
        this.refDom = React.createRef()
      }
      componentDidMount() {
        console.log('==========', this.refDom.current) //  <div>子组件</div>
      }
      render() {
        return (
          <div>
            <ConvertComponent ref={this.refDom} />
          </div>
        )
      }
    }

受控组件

组件的状态通过React 的状态值 state 或者 props 控制。

HTML 中,表单元素(如、 和 )之类的表单元素通常自己维护 state,并根据用户输入进行更新。

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

  • 将两者结合起来,使React的state成为“唯一数据源”。
  • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。
  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

select标签

单选

  • value属性可以设置option的默认值。绑定state中对应的值。
  • option的value属性是设置触发事件的targe.value。并更新到对应的state中。 多选
  • value属性可以设置option的默认值。绑定state中对应的值。为一个数组。
  • option的value属性是设置触发事件的targe.value。并更新到对应的state中。

input:text, textarea

  • value属性将被作为target.value值。并更新到对应的state中。

input:checkbox, input:radio

  • checked属性可以设置input的默认值,绑定state中对应的值。
  • checked属性是设置触发事件的target.checked,并更新到对应的state中。 注意当使用多个input标签时,我们可以给input标签设置name属性,让我们可以通过e.target.name来复用事件处理。
 handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value    
    });
  }

非受控组件

组件不被 React的状态值控制。通过 dom 的特性或者React 的ref 来控制。

export default class AppTest extends React.Component {
  constructor(props) {
    super(props)
    this.inputDom = React.createRef()
  }
  componentDidMount() {
    this.inputDom.current.addEventListener('change', (e) => {
      console.log('========', e.target.value)
    })
  }
  render() {
    return (
      <div>
        <form action="#">
          <input type="text" ref={this.inputDom} />
        </form>
      </div>
    )
  }
}

高阶组件

高阶组件是参数为组件,返回值为新组件的函数。

  • 我们可以通过displayName来给组件命名,以便React developer tools区分各个组件。
  • 他可以返回函数式组件或者类组件。
  • 它主要的目的是将传入的组件统一处理,然后返回。简化单一的枯燥的操作。
  • 了解过express, koa等框架,可以给他理解为一个中间件。

增强props

当我们一个组件使用多次,或者多个组件都需要增加相同的props,那么我们就可以设置一个高阶组件统一做处理。

下面一个例子就是给App, App2增加age这个props。

    import React from 'react'

    function enhanceComponent(WrapeComponent) {
      class newComponent extends React.Component {
        render() {
          return <WrapeComponent {...this.props} age="30" />
        }
      }
      // newComponent.displayName = 'pp'
      return newComponent
    }

    class App extends React.Component {
      render() {
        return (
          <div>
            App1: {this.props.name} - {this.props.age}
          </div>
        )
      }
    }

    function App2(props) {
      return (
        <div>
          App2: {props.name} - {props.age}
        </div>
      )
    }

    const EnhanceApp = enhanceComponent(App)
    const EnhanceApp2 = enhanceComponent(App2)

    class ParentCom extends React.Component {
      render() {
        return (
          <div>
            <EnhanceApp name="zh" />
            <EnhanceApp2 name="gl" />
          </div>
        )
      }
    }

通过context增强props

通过context包裹高阶组件,共享context数据,来增强props。

    import React from 'react'

    const UserContext = React.createContext({
      name: 'zh',
      age: 20
    })

    function enhanceComponent(WrapeComponent) {
      class newComponent extends React.Component {
        render() {
          return (
            <UserContext.Consumer>
              {(user) => {
                return <WrapeComponent {...this.props} {...user} />
              }}
            </UserContext.Consumer>
          )
        }
      }
      // newComponent.displayName = 'pp'
      return newComponent
    }

    class App extends React.Component {
      render() {
        return (
          <div>
            App1: {this.props.name} - {this.props.age}
          </div>
        )
      }
    }

    function App2(props) {
      return (
        <div>
          App2: {props.name} - {props.age}
        </div>
      )
    }

    const EnhanceApp = enhanceComponent(App)
    const EnhanceApp2 = enhanceComponent(App2)

    class ParentCom extends React.Component {
      render() {
        return (
          <div>
            <UserContext.Provider value={{ name: '===', age: 30 }}>
              <EnhanceApp />
              <EnhanceApp2 />
            </UserContext.Provider>
          </div>
        )
      }
    }

    export default ParentCom

渲染判断鉴权

就是根据props,来控制显示不同的组件。 image.png

生命周期劫持

就是把生命周期处理相同事情的内容,当在高阶组件中统一处理。 image.png

高阶组件的意义

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

HOC也有自己的一些缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。

Portals转移节点

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

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

  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。
  • 第二个参数(container)是一个 DOM 元素。参数一挂载的根节点。
    import React from 'react'
    import ReactDOM from 'react-dom'

    function Modal(props) {
      // 这里将modal组件中传入的内容拿过来。
      return ReactDOM.createPortal(props.children, document.getElementById('modal'))
    }

    export default class Parent extends React.Component {
      constructor(props) {
        super(props)
        this.refDom = React.createRef()
      }
      render() {
        return (
          <div>
            <Modal>
              <h1>modal标题</h1>
            </Modal>
          </div>
        )
      }
    }

image.png

fragment 空标签

由于render函数或者函数组件必须只能放回单标签组件,所以我们必须用一个标签包裹众多内容,如果不想渲染这个标签,我们可以使用Fragment标签。

  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
  • React还提供了Fragment的短语法:<> </>。 但是,如果我们需要在Fragment中添加key,那么就不能使用短语法。

严格模式

具体请访问react官网

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。严格模式检查仅在开发模式下运行;它们不会影响生产构建

通过React.StrictMode标签包括的react元素及其子元素都会被检查。

那么严格模式到底检查什么呢?

  • 识别不安全的生命周期。
  • 使用过时的ref API。
  • 使用废弃的findDOMNode方法
    • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了。
  • 检查意外的副作用
    • 这个组件的constructor会被调用两次。
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用。
    • 在生产环境中,是不会被调用两次的。
  • 检测过时的context API
    • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的。

添加className的方式

  • 字符串拼接 注意: 需要在每个class后面或者前面加一个空格。
  <p className={'pp ' + (true ? 'active' : '')}>iiii</p>
  • 数组拼接
  {/* 这里的class会用,链接 */}
  <p className={['title', 'active']}>oooo</p>
  <p className={['title', 'active'].join(' ')}>ppppp</p>
  • 通过第三方库classnames 我们知道vue中添加class属性是非常方便的。所以这个库可以让我们很方便的给dom元素添加class属性。注意undefined, null, 0, false, NaN, true传入classNames函数不会被加入className中。
  <div
    className={classNames(
      { active: true },
      'title',
      undefined,
      null,
      0,
      NaN,
      '',
      true
    )}
  >
    classnams格式
  </div>

image.png xdm,学习任何一个框架时,一定要去仔细去看官网。react官网还是挺容易搞懂的。但是对于第一次学习react的程序员来说,就比较困难了。一起加油,一起冲。