React从0到1基础总结

444 阅读42分钟

一、React简介

React的基础大体包括下面这些概念:

  1. 组件
  2. JSX
  3. Virtual DOM
  4. Data Flow

React.js不是一个框架,它只是一个库。它只提供 UI 层面的解决方案。在实际的项目当中,它并不能解决我们所有的问题,需要结合其它的库,例如 ReduxReact-router 等来协助提供完整的解决方案。

二、React浏览器开发环境

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <!--凡是使用 JSX 的地方,都要加上 type="text/babel"-->
    <script type="text/babel">
      // 1. 创建虚拟DOM
      const vDom = <h1>Hello,React</h1>
      // 2. 渲染虚拟DOM到页面
      ReactDOM.render(vDom, document.getElementById('root'))
    </script>
  </body>
</html>

搭建浏览器开发环境一共用了3个库:reactreact-dombabel,它们必须首先加载。

  1. react.jsReact的核心库。
  2. react-dom.js:负责Web页面的DOM操作。
  3. babel.js:将JSX语法转为JavaScript语法。

三、Virtual DOM介绍

一个真实页面对应一个DOM树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作DOM来进行更新。DOM操作非常昂贵。而且这些操作DOM的代码变得难以维护。

React把真实DOM树转换成JavaScript对象树,也就是Virtual DOM。如下图:

image.png

每次数据更新后,重新计算Virtual DOM,并和上一次生成的Virtual DOM做对比,对发生变化的部分做批量更新。VirtualDOM不仅提升了React的性能,而且它最大的好处还可以在其他平台集成(比如react-native是基于Virtual DOM渲染出的原生控件)。

因此在Virtual DOM输出的时候,是输出Web DOM,还是Android控件,还是iOS控件,由平台本身决定。

四、JSX介绍

手动编码创建虚拟DOM是非常是非常繁琐的,如果要创建<h1 id="title"><span>Hello,React</span></h1>html的结构:

 <body>
  <div id="root"></div>
  <script type="text/babel">
    // 1. 创建虚拟DOM
    const element = React.createElement('h1', { id: 'title' }, React.createElement('span', null, 'Hello,React!'))
    // 2. 渲染虚拟DOM到页面
    ReactDOM.render(element, document.getElementById('root'))
  </script>
</body>

React.createElement会构建一个JavaScript对象来描述HTML结构的信息,包括标签名、属性、还有子元素等。类似如下的对象结构:

{
    type: 'h1',
    props: {
      id: 'title',
      children: {
        type: 'span',
        props: {
          children: 'Hello,React'
        }
      }
    }
}

使用React.createElement创建虚拟DOM的时候在标签结构还不怎么复杂的结构时,书写就已经很难受了。没有使用HTML编写结构时的简洁。为了解决这个问题,所以JSX语法就诞生了。假如我们使用JSX语法来重新表达上述元素,只需下面这么写:

<body>
  <div id="root"></div>
  <script type="text/babel">
    // 1. 创建虚拟DOM
    const element = (
      <h1 id="title">
        <span>Hello,React</span>
      </h1>
    )
    // 2. 渲染虚拟DOM到页面
    ReactDOM.render(element, document.getElementById('root'))
  </script>
</body>

JSXHTML语法直接加入到JavaScript代码中,会让代码更加直观并易于维护。通过编译器转换到纯JavaScript后由浏览器执行。JSX在产品打包阶段都已经编译成了纯JavaScript

JSXJavaScript语言的一种语法扩展,长得像HTML,但并不是HTMLJSX是第三方标准,这套标准适用于任何一套框架。使用BabelJSX编译器可以实现对JSX语法的编译。

4.1、JSX中的HTML属性

ReactHTML之间有很多属性存在差异。比如说class要改写成className

 ReactDOM.render(<div className="foo">Hello</div>, document.getElementById('root'))

4.2、JSX中的JavaScript表达式

JSX遇到HTML标签(以<开头),就用HTML规则解析。遇到代码块(以 { 开头),就用 JavaScript规则解析。也就是说在JSX使用表达式要包裹在大括号{}中:

<body>
  <div id="root"></div>
  <script type="text/babel">
    const names = ['zhangsan', 'lisi', 'wangwu']
    const title = '标题'
    ReactDOM.render(
      <div>
        <h1>{title}</h1>
        <div>
          {names.map((name, key) => {
            return <div key={key}>Hello,{name}</div>
          })}
        </div>
      </div>,
      document.getElementById('root')
    )
  </script>
</body>

JSX可以直接在模板插入JavaScript变量。如果这个变量是一个数组,则会展开这个数组的所有成员。

const names = [<div key="1">zhangsan</div>, <div key="2">lisi</div>]

ReactDOM.render(
  <div>
    {names}
  </div>,
  document.getElementById('root')
)

JSX 的{}内可以嵌入任何表达式,{{}}就是在{}内部用对象字面量返回一个对象。比如说内联样式要用style={{key:value}}的形式去编写:

<span style={{ color: 'red' }}>Hello,React</span>

4.3、JSX中的注释

JSX里使用注释也很简单,就是沿用JavaScript,唯一要注意的是在一个组件的子元素位置使用注释要用 {} 包起来。

const element = (
/* 多行
注释 */

// 单行注释
<h1 id="title">
    {/* 节点注释 */}
    <span>Hello,React</span>
</h1>
)

ReactDOM.render(element, document.getElementById('root'))

4.4、JSX中的HTML转义

React会将所有要显示到DOM的字符串转义,防止XSS。所以任何的HTML格式都会被转义掉:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { title: '<h1>标题</h1>' }
  }
  render() {
    return <div>{this.state.title}</div>
  }
}

ReactDOM.render(<MyComponent />, document.getElementById('root'))

表达式插入并不会把一个<h1>渲染到页面,而是以文本形式渲染:

image.png

angerouslySetInnerHTML 是React为浏览器DOM提供 innerHTML 的替换方案,可以使dangerouslySetInnerHTML来实现,dangerouslySetInnerHTML传入一个对象,这个对象的 __html属性值就相当于元素的innerHTML

render() {
  return <div dangerouslySetInnerHTML={{ __html: this.state.title }}></div>
}

4.5、JSX中的自定义HTML属性

如果在JSX中使用的属性不存在于HTML的规范中,这个属性会被忽略。如果要使用自定义属性,可以用data-前缀。可访问性属性的前缀aria-也是支持的。

 ReactDOM.render(<div data-attr="abc">内容</div>, document.getElementById('root'))

4.6、Fragment 标签

React.Fragment代表空标签。它能够在不额外创建DOM元素的情况下,让render()方法中返回多个元素。Fragment只能接收key属性:

import React from 'react'

class App extends React.Component {
  render() {
    return (
      <React.Fragment key={1}>
        <p>App</p>
      </React.Fragment>
    )
  }
}

Fragment简写语法为 <></>,但是简写方式不能使用key

<>
  <p>App</p>
</>

五、组件化

React 允许将代码封装成组件(component)。Component(组件)可以是类组件(class component)、函数式组件(function component)。

组件有三个核心概念,React组件基本上由组件的外部属性状态(props),内部属性状态(state)和生命周期方法组成。如下图:

组件生成的 HTML 结构只能有一个单一的根节点。在React中,数据是自顶向下单向流动的,从父组件到子组件。

官方在React组件构建上提供了2种不同的方法:ES6 class 和无状态函数(stateless function)。

官方在React@15.5.0后不推荐用React.createClass创建组件了,这里就不做介绍了。

5.1、class方式编写组件

class Hello extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (<h1>Hello,React</h1>)
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'))

5.2、无状态组件

可以用纯函数来定义无状态的组件(stateless function),这种组件没有状态(state),没有生命周期,只是简单的接收props渲染生成DOM结构。无状态组件非常简单,开销很低。比如使用函数定义:

function Person(props) {
  return <h1>Hello, {props.name}</h1>
}
ReactDOM.render(<Person name="张三" />, document.getElementById('root'))

六、state

state是组件的当前状态(数据)。一旦状态(数据)更改,组件就会自动调用render重新渲染UI,这个更改的动作通过this.setState方法来触发。

不能直接修改state,要使用setState()方法进行修改。如果直接修改state,组件不会重新触发render方法:

// 错误,不会触发render()重新渲染
this.state.count = this.state.count + 1

6.1、setState()方法介绍

setState()方法,它接受一个对象或者函数作为参数,来更新state

  • 对象作为参数时只需要传入需要更新的部分,React会自动执行浅合并,而不需要传入整个对象:
class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0, name: '张三' }
  }
  handleClick() {
    // 只修改count
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
  • 函数作为参数时可以得到prevStateprops两个参数,state的值通过对象返回:
  1. prevState:上一个state
  2. props:更新被应用时的props
class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick(e) {
    this.setState((prevState, props) => {
      // 返回第一个state
      return { count: this.state.count + 1 }
    })
    this.setState((prevState, props) => {
      //第一个state,也就是上一个的state
      console.log(prevState) // 1
      return { count: this.state.count + 1 }
    })
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

6.2、setState()异步更新

setState()方法更新state是异步的。React并不会马上修改state。而是把这个对象放到一个更新队列里面,最后才会从队列当中把新的状态提取出来合并到state当中,然后再触发组件更新:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick(e) {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count) // 0
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

当触发handleClick事件的时候,this.state.count输出是0

一种解决方案是setState(updater, [callback])方法还可以传入一个回调函数,一旦setState()完成并且组件重绘之后,这个回调函数将会被调用。

handleClick(e) {
  this.setState({ count: this.state.count + 1 }, () => {
    console.log(this.state.count) // 1
  })
}

6.3、setState()浅合并

React.js出于性能原因,可能会将多次setState()的状态修改合并成一次状态修改。所以不要依赖当前的setState()计算下个State。如下的一个计数器:

class AddCount extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick() {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return (
      <div>
        {/* count 输出为1 */}
        <h2>{this.state.count}</h2>
        <button onClick={this.handleClick.bind(this)}>添加</button>
      </div>
    )
  }
}

当触发handleClick事件的时候,会发现页面输出的是1。虽然setState()方法调用了两次。是因为当调用setState()修改组件状态时,组件state的更新其实是一个浅合并的过程,相当于:

Object.assign(
  previousState,
  {count: state.count + 1},
  {count: state.count + 1},
  ...
)

所以如果后续操作要依赖前一个setState()的结果的情况下就要使用函数来作为setState()参数。React会把上一个setState()的结果传入这个函数,就可以使用上一个正确的结果进行操作,然后返回一个对象来更新state

handleClick () {
  this.setState({ count: this.state.count + 1 })
  this.setState((prevState) => {
    // 1
    console.log(prevState.count)
    return { count: prevState.count + 1 }
  })
}}

把上次操作setState()更新的值传入到下一个setState()里,就可以正确的显示count了。

6.4、state的Immutable(不可变性)

React官方建议把state当作是的Immutable(不可变性)对象,state中包含的所有状态都应该是不可变对象。当state中的某个状态发生变化,我们应该重新创建这个状态对象,而不是直接修改原来的状态。

假如有一个数组类型的状态names,当向name中添加一个名字时,使用数组的concat方法或ES6的扩展运算符:

const names = ['张三']
// 1
this.setState(prevState => ({
  names: prevState.names.concat(['李四'])
}))

// 2
this.setState(prevState => ({
  names: [...prevState.names,'李四']
}))

不要使用pushpopshiftunshiftsplice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concatslice等返回一个新的数组。

假如有一个对象类型的状态person,为了不改变原本的对象,我们可以使用Object.assign 方法或者对象扩展属性:

const person = { age: 30 }

// 1
function updatePerson (person) {
  return Object.assign({}, person, { age: 20 })
}

// 2
function updatePerson (person) {
  return {...person,age:20}
}

创建新的状态对象要避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。

6.5、修改深度嵌套对象

由于setState()只合并对象属性的第一级。如果想修改多层嵌套对象内的一个属性,就要像下面一层一层解构:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { person: { city: { cityName: '北京' } } }
  }
  handleClick() {
    this.setState((prevState) => {
      return {
        person: {
          ...prevState.person,
          city: {
            ...prevState.person.city,
            cityName: '上海'
          }
        }
      }
    })
  }
  render() {
    return (
      <div>
        <h2>{this.state.person.city.cityName}</h2>
        <button onClick={this.handleClick.bind(this)}>修改城市</button>
      </div>
    )
  }
}

上面代码可以看出当state对象结构的层级更深的时候,改动最深层的state子节点写起来会更麻烦。

我们可以想出一个简单的解决方案,先深拷贝出一个新的对象,然后直接更改新对象的属性,比如使用lodashcloneDeep

handleClick() {
  this.setState((prevState) => {
    const newState = _.cloneDeep(prevState)
    newState.person.city.cityName = '上海'
    return newState
  })
}

但是,这种方案有明显的性能问题。不管打算更新对象的哪一个属性,每次都要深拷贝整个对象。当对象特别大的时候,深拷贝会导致性能问题。

还有另一种解决方案就是可以使用一些Immutable的库,如immer.js来简化开发。它避免深拷贝所有属性,而只针对目标属性进行修改。

// 安装
npm i immer

当我们调用immerAPI produce时,immer将内部暂时存储着我们的目标对象。并暴露一个draft(草稿)我们就可以在draft上作修改,然后返回。这里只是简单使用:

import { produce } from 'immer'
handleClick() {
  this.setState((prevState) => {
    return produce(prevState, (draftState) => {
      draftState.person.city.cityName = '上海'
    })
  })
}

6.6、setState() 使用原则

  1. 如果新状态不依赖上一个状态可以使用对象方式。

  2. 如果新状态依赖上一个状态使用函数的方式。

  3. 如果需要在setState()获取最新的状态数据,可以在第二个callback函数中获取。

七、props

组件是相互独立、可复用的。一个组件可能在不同地方被用到。在不同的场景下对这个组件的需求可能会根据情况有所不同,所以要针对相同的组件传入不同的配置项。

Reactprops就可以达到这个效果。每个组件都可以接受一个props参数,它是一个对象,包含了所有对这个组件的配置:

class Person extends React.Component {
  render() {
    const { name, age } = this.props
    return (
      <ul>
        <li>{name}</li>
        <li>{age}</li>
      </ul>
    )
  }
}
ReactDOM.render(<Person name="张三" age={20} />, document.getElementById('root'))

组件内部是通过this.props的方式获取到组件的参数的。在使用一个组件的时候,所有的属性都会作为props对象的键值。我们还可以通过defaultProps静态属性来指定默认值:

class Person extends React.Component {
  // 指定默认值
  static defaultProps = {
    name: '李四',
    age: 30
  }
  render() {
    const { name, age } = this.props
    return (
      <ul>
        <li>{name}</li>
        <li>{age}</li>
      </ul>
    )
  }
}
ReactDOM.render(<Person age={20} />, document.getElementById('root'))

如果没有传入name属性,就会使用自定义默认的属性值:

image.png

7.1、props 标签属性类型检查

有时候我们需要对传入props的类型进行限制。从React16开始,类型检查被移除拆分为另一个库中。需要单独安装:

npm install prop-types

或者直接在浏览器引入:

<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>

下面是一个简单的小例子:

class Person extends React.Component {
  // 对属性进行检查
  static propTypes = {
    name: PropTypes.string.isRequired, // name必传,并且为字符串
    getName: PropTypes.func // getName为函数 因为`function`是关键字,所以使用`func`
  }
  render() {
    const { name, age } = this.props
    return (
      <ul>
        <li>{name}</li>
        <li>{age}</li>
      </ul>
    )
  }
}
ReactDOM.render(<Person name="张三" age={20} />, document.getElementById('root'))

7.2、props 不可变

props一旦传入进来就不可以在组件内部对它进行修改。但是可以通过父组件主动修改state重新渲染的方式来传入新的props,从而达到更新的效果。如下:

// 子组件
function Child(props) {
  return <div>{props.name}</div>
}
// 父组件
class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }

  // 修改name,重新渲染
  changeName() {
    this.setState({ name: '李四' })
  }
  render() {
    return (
      <div>
        <Child name={this.state.name} />
        <button onClick={this.changeName.bind(this)}>修改name</button>
      </div>
    )
  }
}
ReactDOM.render(<Parent />, document.getElementById('root'))

八、 Ref

React并不能完全满足所有DOM操作需求,有些时候我们还是需要和DOM打交道。比如进入页面以后自动 focus到某个输入框,需要调用input.focus()DOM APIReact提供几种方式来获取挂载后元素的DOM节点。

不要滥用refs。比如用它来按照传统的方式操作界面 UI:找到 DOM -> 更新 DOM

8.1、字符串形式

通过在DOM元素上面设置一个ref属性指定一个名称,然后通过 this.refs.name来访问对应的DOM元素。

class MyComponent extends React.Component {
  // 点击获取焦点
  handleClick() {
    this.refs.textInput.focus()
  }
  render() {
    return (
      <div>
        <input ref="textInput" />
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

官方已经不建议使用它,因为string类型的refs存在一些问题。它已过时并可能会在未来的版本被移除。

8.2、回调形式

通过ref属性可以设置为一个回调函数,回调函数会接收到当前DOM元素:

class MyComponent extends React.Component {
  handleClick() {
    this.textInput.focus()
  }
  render() {
    return (
      <div>
        <input ref={(element) => (this.textInput = element)} />
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
}

ReactDOM.render(<MyComponent />, document.getElementById('root'))

如果ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数DOM元素。这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的。通过将ref的回调函数定义成class的绑定函数的方式可以避免上述问题,但是大多数情况下是无关紧要的。

8.3、createRef()形式

React 16.3版本中引入的React.createRef() API也可以用来获取DOM元素,通过调用createRef()后返回一个容器,该容器可以存储被ref所标识的节点,并且该容器只能存一个元素节点:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  handleClick() {
    this.myRef.current.focus()
  }
  render() {
    return (
      <div>
        <input ref={this.myRef} />
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

8.4、ref获取组件实例

Ref不仅可以获取DOM元素,还可以获取子组件的实例。下面用三种不同的方式获取子组件实例:

import React, { createRef } from 'react'

// 子组件
class Child extends React.Component {
  handleChild() {
    console.log('Child Component')
  }
  render() {
    return <div>Child组件</div>
  }
}

// 父组件
class Father extends React.Component {
  constructor(props) {
    super(props)
    this.childRef3 = createRef()
  }
  componentDidMount() {
    this.refs.childRef1.handleChild()     //Child Component
    this.childRef2.handleChild()          //Child Component
    this.childRef3.current.handleChild()  //Child Component
  }
  render() {
    return (
      <div>
        <Child ref="childRef1" />
        <Child ref={(element) => (this.childRef2 = element)} />
        <Child ref={this.childRef3} />
      </div>
    )
  }
}

九、事件处理

React里面绑定事件的方式和在HTML中绑定事件类似,但是要使用驼峰式命名的方式。下面是一个数字累加的小例子:

class AddCount extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick(e) {
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
}
ReactDOM.render(<AddCount />, document.getElementById('root'))

需要注意的是要显式调用bind(this)将事件函数上下文绑定组件实例上。

十、收集表单数据

React处理表单可以通过受控组件和非受控组件来管理。下面分别介绍下面这两种方式。

10.1、受控组件

React中,表单元素通过组件的state属性来维护。并根据用户输入调用setState()来进行数据更新。被React以这种方式控制取值的表单输入元素就叫做受控组件。下面是一个修改用户名的一个例子:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { userName: 'init userName' }
  }
  // 修改用户名
  changeUsername(e) {
    this.setState({ userName: e.target.value })
  }
  // 提交
  handleSubmit(e) {
    e.preventDefault()
    const { userName } = this.state
    console.log(`您的用户名是:${userName}`)
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <input type="text" value={this.state.userName} name="username" onChange={this.changeUsername.bind(this)} />
        <button type="submit">提交</button>
      </form>
    )
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

React中,数据是单向流动的。表单的数据源于组件的state,并通过props传入,这也称为单向数据绑定。然后,我们又通过onChange事件处理器将新的表单数据写回到组件的state,完成了双向数据绑定。

React受控组件更新state的流程:

  1. 通过在初始state中设置表单的默认值。
  2. 每当表单的值发生变化时,调用onChange事件处理器。
  3. 通过事件处理器获取最新的值,并使用setState()更新state
  4. setState()触发视图的重新渲染,完成表单组件值的更新。

大多数情况下,我们还是使用受控组件来处理表单数据。

10.2、非受控组件

非受控组件就是输入类元素不通过state来维护数据,使用refDOM节点中获取数据:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.userNameNode = React.createRef()
  }
  handleSubmit(e) {
    e.preventDefault()
    console.log(`您的用户名是:${this.userNameNode.current.value}`)
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <input type="text" name="username" ref={this.userNameNode} />
        <button type="submit">提交</button>
      </form>
    )
  }
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))

十一、组件生命周期

React中的每个组件都包含组件生命周期方法,在运行过程中特定的阶段执行这些方法。

组件的生命周期分为四类:

  1. 挂载(初始化)阶段。
  2. 更新阶段。
  3. 销毁阶段。
  4. 错误阶段。

因为React版本问题导致一些生命周期方法被弃用或者被修改,下面会根据版本的不同来介绍组件的生命周期。

11.1、React 16.3前的生命周期

先通过一张图片总览一下旧的生命周期:

image.png

挂载阶段

当组件实例被创建并插入DOM中时,组件生命周期调用顺序如下:

  1. constructor():构造器调用。
  2. componentWillMount():组件即将挂载之前调用。
  3. render():初始化渲染。
  4. componentDidMount():在组件挂载后(插入DOM树中)立即调用。
class LifeCycle extends React.Component {
  constructor(props) {
    super(props)
    console.log('1. construct')
  }
  componentWillMount() {
    console.log('2. componentWillMount')
  }
  render() {
    console.log('3. render')
    return <div>React 旧生命周期</div>
  }
  componentDidMount() {
    console.log('4. componentDidMount')
  }
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))

组件挂载阶段会按照下面依次输出:

1. construct
2. componentWillMount
3. render
4. componentDidMount

更新阶段

组件的更新阶段就是setState()使React重新渲染组件并且把组件的变化应用到DOM元素上的过程。React也提供了一系列的生命周期函数可以让我们在这个组件更新的过程执行一些操作。当组件的state发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  1. shouldComponentUpdate(): 当propsstate发生变化时会调用。
  2. componentWillUpdate():组件开始重新渲染之前调用。
  3. render():更新后重新渲染。
  4. componentDidUpdate():组件重新渲染并且变更到真实的DOM以后调用。

需要注意shouldComponentUpdate()可以通过这个方法控制组件是否重新渲染。默认值返回true,就是每次发生变化组件都会重新渲染。如果编写这个函数后返回false组件就不会重新渲染。这个生命周期在React性能优化上非常有用(后面会说)。

class LifeCycle extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick() {
    this.setState({ count: this.state.count + 1 })
  }
  shouldComponentUpdate() {
    console.log('1. shouldComponentUpdate')
    return true
  }
  componentWillUpdate() {
    console.log('2. componentWillUpdate')
  }
  render() {
    console.log('3. render')
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
  componentDidUpdate() {
    console.log('4. componentDidUpdate')
  }
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))

当触发handleClick点击事件时会按照下面依次输出:

1. shouldComponentUpdate
2. componentWillUpdate
3. render
4. componentDidUpdate

forceUpdate()

我们还可以使用强制更新forceUpdate()让组件重新渲染。强制更新会跳过shouldComponentUpdate()。一般情况下应该避免使用 forceUpdate()

props变化的更新

如果有一个父子组件,通过props传递参数,子组件的生命周期更新会增加一个componentWillReceiveProps()生命周期。它的作用是子组件从父组件接收到新的props之前调用。

componentWillReceiveProps(nextProps)接收一个参数,这个参数是更新后的props对象:

// 父组件
class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }
  changeName() {
    this.setState({ name: '李四' })
  }
  render() {
    return (
      <div>
        <Child name={this.state.name} />
        <button onClick={this.changeName.bind(this)}>修改名字</button>
      </div>
    )
  }
}
// 子组件
class Child extends React.Component {
  componentWillReceiveProps() {
    console.log('1. componentWillReceiveProps')
  }
  shouldComponentUpdate() {
    console.log('2. shouldComponentUpdate')
    return true
  }
  componentWillUpdate() {
    console.log('3. componentWillUpdate')
  }
  render() {
    console.log('4. render')
    return <div>我是子组件,接收到的名字是{this.props.name}</div>
  }
  componentDidUpdate() {
    console.log('5. componentDidUpdate')
  }
}
ReactDOM.render(<Parent />, document.getElementById('root'))

当触发点击事件时会按照下面依次输出:

1. componentWillReceiveProps
2. shouldComponentUpdate
3. componentWillUpdate
4. render
5. componentDidUpdate

初始化传递props的时候不会执行componentWillReceiveProps()。如果父组件导致组件重新渲染,即使props没有更改,也会调用此方法。

卸载阶段

当组件从DOM中移除时会调用下面组件生命周期:

  1. componentWillUnmount():在组件卸载及销毁之前直接调用。
class LifeCycle extends React.Component {
  handleClick() {
    // 卸载
    ReactDOM.unmountComponentAtNode(document.getElementById('root'))
  }
  render() {
    return <button onClick={this.handleClick.bind(this)}>组件卸载</button>
  }
  componentWillUnmount() {
    console.log('1. componentWillUnmount')
  }
}

const App = () => <LifeCycle />
export default App

当触发点击事件handleClick时会输出:

1. componentWillUnmount

11.2、React v16.3后的新的生命周期

先通过一张图片总览一下新的生命周期:

image.png

新版本的生命周期有三个方法改了名字:

  1. componentWillMount()改为了UNSAFE_componentWillMount()
  2. componentWillReceiveProps()改为了UNSAFE_componentWillReceiveProps()
  3. componentWillUpdate改为了UNSAFE_componentWillUpdate()

新版本后必须要添加UNSAFE_前缀,否则可能没办法使用这三个生命钩子。

新版本新增了两个静态方法生命周期:

  1. static getDerivedStateFromProps()
  2. static getSnapshotBeforeUpdate()

static getDerivedStateFromProps()

getDerivedStateFromProps()的意思是从props中获取state,将传入的props映射到state上面。

getDerivedStateFromProps()在初始挂载及后续更新时都会被调用。

在使用getDerivedStateFromProps()的时候必须要初始化state。它应返回一个对象来更新state,如果返回null则不更新任何内容。

getDerivedStateFromProps(nextProps,nextState)接收两个参数:

  1. nextProps:最新的props
  2. nextState: 最新的state

当返回null的时候功能不会受影响:

class LifeCycle extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  static getDerivedStateFromProps(props, state) {
    return null
  }
  render() {
    return <div>{this.state.count}</div>
  }
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))

props当作状态对象返回的时候,组件内的状态值在任何时候都取决于props的值:

class LifeCycle extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  //组件内的状态值取决于props,设置值无效
  handleClick() {
    this.setState({ count: this.state.count + 1 }) 
  }
  static getDerivedStateFromProps(props, state) {
    return props // 返回props:{count: 100}
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>count:{this.state.count}</div>
  }
}

ReactDOM.render(<LifeCycle count={100} />, document.getElementById('root'))

传入propscount={100},通过getDerivedStateFromProps返回,代替了原始的this.state = { count: 0 }

image.png

static getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate()在最近一次渲染输出(提交到DOM节点)之前调用,在更新之前获取快照。

必须返回一个Snapshot value(任意值)或返回null。返回的值将作为参数传递给 componentDidUpdate()钩子函数。

使用getSnapshotBeforeUpdate()的时候也必须要定义componentDidUpdate()钩子函数。

componentDidUpdate(prevProps, prevState, snapShotValue)函数接收三个参数:

  1. prevProps:上一个props
  2. prevState:上一个state
  3. snapShotValuegetSnapshotBeforeUpdate()返回的快照值。

getSnapshotBeforeUpdate()返回的值Snapshot value通过componentDidUpdate()方法接收:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick(e) {
    this.setState({ count: this.state.count + 1 })
  }
  // 在更新之前获取快照
  getSnapshotBeforeUpdate() {
    return '张三'
  }
  componentDidUpdate(prevProps, prevState, snapShotValue) {
    console.log(snapShotValue) // 张三
  }
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
}
ReactDOM.render(<MyComponent count={100} />, document.getElementById('root'))

可以使用getSnapshotBeforeUpdate()实现一个列表高度持续增加的时候始终定位当前位置的案例:

屏幕录制 2021-08-27 上午1.gif

class News extends React.Component {
  constructor(props) {
    super(props)
    this.state = { news: [] }
    this.newWrapRef = React.createRef()
  }
  componentDidMount() {
    // 每秒生成一条新闻
    setInterval(() => {
      const { news } = this.state
      const oneNew = '新闻' + news.length
      this.setState({ news: [oneNew, ...news] })
    }, 1000)
  }
  render() {
    return (
      <ul ref={this.newWrapRef} style={{ height: '180px', border: '1px solid red', overflow: 'auto' }}>
        {this.state.news.map((item, index) => {
          return (
            <li key={index} style={{ height: '30px' }}>
              {item}
            </li>
          )
        })}
      </ul>
    )
  }
  getSnapshotBeforeUpdate() {
    // 获取重新渲染之前容器的的scrollHeight
    return this.newWrapRef.current.scrollHeight
  }
  componentDidUpdate(prevProps, prevState, snapshotValue) {
    // 新的scrollHeight减去上一次的scrollHeight
    const newScrollHeight = this.newWrapRef.current.scrollHeight - snapshotValue
    // 重新设置最新的scrollTop
    this.newWrapRef.current.scrollTop += newScrollHeight
  }
}

ReactDOM.render(<News count={100} />, document.getElementById('root'))

挂载阶段

当组件实例被创建并插入DOM中时,组件生命周期调用顺序如下:

  1. constructor():构造器调用。
  2. getDerivedStateFromProps():在初始挂载及后续更新时调用。
  3. render():初始化渲染。
  4. componentDidMount():在组件挂载后(插入DOM树中)立即调用。
class LifeCyCle extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
    console.log('1. constructor')
  }
  static getDerivedStateFromProps() {
    console.log('2. getDerivedStateFromProps')
    return null // 必须有返回值
  }

  render() {
    console.log('3. render')
    return <div>React 新生命周期</div>
  }

  componentDidMount() {
    console.log('4. componentDidMount')
  }
}
ReactDOM.render(<LifeCyCle />, document.getElementById('root'))

组件挂载阶段会按照下面依次输出:

1. constructor
2. getDerivedStateFromProps
3. render
4. componentDidMount

更新阶段

组件更新的生命周期调用顺序如下:

  1. getDerivedStateFromProps():初始挂载及后续更新时都会被调用。
  2. shouldComponentUpdate(): 当propsstate发生变化时会调用。
  3. render():状态更新后渲染。
  4. getSnapshotBeforeUpdate():在最近一次渲染输出(提交到 DOM 节点)之前调用。
  5. componentDidUpdate():组件重新渲染并且变更到真实的DOM以后调用。
class LifeCycle extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick() {
    this.setState({ count: this.state.count + 1 })
  }
  static getDerivedStateFromProps() {
    console.log('1. getDerivedStateFromProps')
    return null
  }
  shouldComponentUpdate() {
    console.log('2. shouldComponentUpdate')
    return true
  }
  render() {
    console.log('3. render')
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      </div>
    )
  }
  getSnapshotBeforeUpdate() {
    console.log('4. getSnapshotBeforeUpdate')
    return null
  }
  componentDidUpdate() {
    console.log('5. componentDidUpdate')
  }
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))

当触发点击事件handleClick时会按照下面依次输出:

1. getDerivedStateFromProps
2. shouldComponentUpdate
3. render
4. getSnapshotBeforeUpdate
5. componentDidUpdate

卸载阶段

新老版本的销毁阶段的生命周期没有发生变化。

十二、错误边界

部分UIJavaScript错误不应该导致整个应用崩溃,为了解决这个问题,React 16引入了一个新的概念——错误边界(Error Boundaries)。错误边界是一种React组件,它可以用来捕获后代组件错误,渲染备用页面。它只能捕获后代组件生命周期产生的错误。

如果子组件发生错误,父组件需要通过getDerivedStateFromError()渲染备用UI,使用 componentDidCatch() 打印错误信息。下面是一个父子组件的案例:

import React from 'react'

// 子组件
class Child extends React.Component {
  constructor(props) {
    super(props)
    this.state = { userList: 'abc' }
  }
  render() {
    return (
      <div>
        <h2>我是Child组件</h2>
        {/* Child组件会报错 */}
        {this.state.userList.map((item) => {
          return <span key={item.id}>{item.name}</span>
        })}
      </div>
    )
  }
}

// 父组件
class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }
  // 如果子组件出现错误,会触发getDerivedStateFromError的调用,并在参数里携带错误信息
  static getDerivedStateFromError(error) {
    console.log(error)
    return { hasError: error } // 需要返回错误信息
  }
  componentDidCatch() {
    // 可以将错误日志上报给服务器
  }
  render() {
    return (
      <div>
        <h2>我是Parent组件</h2>
        {/* 开发环境下同样会抛出错误,生产环境会友好提示 */}
        {this.state.hasError ? <span>出现错误</span> : <Child />}
      </div>
    )
  }
}

十三、PureComponent组件优化

当父组件数据修改重新渲染的时候,子组件没有用到父组件的任何数据时也会重新渲染:

import React from 'react'

class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }
  handleChangeName(e) {
    this.setState({ name: '李四' })
  }
  render() {
    console.log('Parent render')
    return (
      <div>
        <button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
        <Child />
      </div>
    )
  }
}

class Child extends React.Component {
  render() {
    console.log('Child render')
    return <div>我是Child组件</div>
  }
}

当点击按钮触发handleChangeName()方法时,会输出Child render。说明子组件没有用到父组件的任何数据会重新渲染。

还有一点需要注意的是:当调用this.setState({})什么值都不传的时候,组件也会重新渲染。

我们可以使用shouldComponentUpdate()来进行优化,父组件比较state,子组件比较props来确定是否重新渲染:

import React from 'react'

// 父组件
class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }
  handleChangeName(e) {
    this.setState({ name: '李四' })
  }

  /**
   * @param {*} nextProps 最新的props
   * @param {*} nextState 最新的state
   */
  shouldComponentUpdate(nextProps, nextState) {
    // state的name属性如果没有变化则不需要重新渲染
    return !(this.state.name === nextState.name)
  }

  render() {
    console.log('Parent render')
    return (
      <div>
        <button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
        <Child name={this.state.name} />
      </div>
    )
  }
}

// 子组件
class Child extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // props的name属性如果没有变化则不需要重新渲染
    return !(this.props.name === nextProps.name)
  }
  render() {
    console.log('Child render')
    return <div>我是Child组件:{this.props.name}</div>
  }
}

上面代码可以看出来,如果对象有多个属性,还需要每个属性对比。项目开发的时候这种编写方式是非常麻烦的。React提供了React.PureComponentReact.PureComponent中以浅层次对比propstate的方式来实现了shouldComponentUpdate函数。

import React from 'react'

class Parent extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }
  handleChangeName(e) {
    this.setState({ name: '李四' })
  }

  render() {
    console.log('Parent render')
    return (
      <div>
        <button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
        <Child name={this.state.name} />
      </div>
    )
  }
}

class Child extends React.PureComponent {
  render() {
    console.log('Child render')
    return <div>我是Child组件:{this.props.name}</div>
  }
}

React.PureComponent 中的 shouldComponentUpdate()仅作对象的浅层比较。

如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的对比结果。

仅在propsstate较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用forceUpdate()来确保组件被正确地更新。

总之不要直接修改数据,要重新生成新数据。

十四、Render Props

具有render prop的组件接受一个返回React元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑。

首先先看一个需求,如果组件标签里面写入一些内容,标签体内容需要通过组件的props.children来获取。

import React from 'react'

// 父组件
class Parent extends React.Component {
  render() {
    return (
      <div>
        <p>我是Parent组件</p>
        <Child>Hello!</Child>
      </div>
    )
  }
}

// 子组件
class Child extends React.Component {
  render() {
    return (
      <div>
        <p>我是Child组件</p>
        {this.props.children}
      </div>
    )
  }
}

Child组件就会输出Hello:

image.png

假如说还有一个需求就是像下面一样,组件里嵌套组件形成父子关系:

class App extends React.Component {
  render() {
    return (
      <B>
        <C></C>
      </B>
    )
  }
}

要把B组件的state数据要传递给C组件,就可以使用Render Props的方式来编写,在B组件里使用this.props.render来调用我们编写的render的函数:

import React from 'react'

class App extends React.Component {
  render() {
    return (
      <div>
        <p>我是App组件</p>
        {/* 编写render的回调函数,并接收数据 */}
        <B render={(name) => <C name={name} />} />
      </div>
    )
  }
}

// 子组件
class B extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: '张三' }
  }
  render() {
    return (
      <div>
        <p>我是B组件</p>
        {/* 调用props的 render() 方法,并传递数据 */}
        {this.props.render(this.state.name)}
      </div>
    )
  }
}

// 孙组件
class C extends React.Component {
  render() {
    return (
      <div>
        <p>我是C组件,我从B组件获取的name是:{this.props.name}</p>
      </div>
    )
  }
}

通过Render Props的方式,即使是嵌套关系,C组件里通过props也能获取B组件的数据:

image.png

Render Props类似于Vue的插槽技术。

十五、React脚手架

全局安装官方推荐脚手架工具create-react-app,类似于Vuevue-cli

npm install -g create-react-app
// 使用create-react-app新建项目
create-react-app my-react-app

安装成功之后,npm start 或者 yarn start就可以启动项目了。

package.json安装的React依赖如下:

image.png

package.jsondependencies可以看出来脚手架工具默认安装了React需要的依赖。下面就介绍这些主要核心依赖的作用:

  1. react:是React的核心库。
  2. react-dom:负责Web页面的DOM操作。
  3. react-scripts:生成项目所有的依赖。例如babelcss-loader,webpack等从开发到打包前端工程化所需要的react-scripts都帮我们做好了。

十六、todoList功能

下面我们可以使用create-react-app脚手架新建一个项目来做一个todoList的功能:

屏幕录制 2021-08-20 下午4.gif

由于还没有接触到状态管理,我们可以兄弟组件互相传递数据,通过父组件来中转实现功能。首先先定义下面四个组件:

  1. Header组件。
  2. Item组件,代表列表中的每一项。
  3. List列表组件。
  4. Footer组件。

App.js

/* app.css */
#root {
  display: flex;
  justify-content: center;
}

.todo-container {
  margin-top: 20px;
  min-height: 240px;
  min-width: 420px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 20px;
  box-sizing: border-box;
}
// App.js
import React from 'react'
import Header from './components/Header'
import List from './components/List'
import './app.css'
import Footer from './components/footer'
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      list: [{ id: 1, name: 'JS', isChecked: false }]
    }
  }
  // 添加todo
  addTodo(todo) {
    const newList = [todo, ...this.state.list]
    this.setState({ list: newList })
    console.log(todo)
  }
  // 修改todo
  editTodo(item, isChecked) {
    const newTodo = this.state.list.map((el) => {
      return el.id === item.id ? { ...el, isChecked } : el
    })
    this.setState({ list: newTodo })
  }
  // 删除todo
  deleteTodo(item) {
    const newTodo = this.state.list.filter((el) => el.id !== item.id)
    this.setState({ list: newTodo })
  }
  // 删除所有选中
  deleteChecked() {
    const newTodos = this.state.list.filter((el) => !el.isChecked)
    this.setState({ list: newTodos })
  }
  // 删除所有元素
  deleteCheckedAll() {
    this.setState({ list: [] })
  }
  // 全选和反选
  changeTodoAll(isChecked) {
    const newTodos = this.state.list.map((el) => {
      return { ...el, isChecked }
    })
    this.setState({ list: newTodos })
  }
  render() {
    return (
      <div className="todo-container">
        <Header addTodo={this.addTodo.bind(this)} />
        <List list={this.state.list} editTodo={this.editTodo.bind(this)} deleteTodo={this.deleteTodo.bind(this)} />
        <Footer list={this.state.list} deleteChecked={this.deleteChecked.bind(this)} deleteCheckedAll={this.deleteCheckedAll.bind(this)} changeTodoAll={this.changeTodoAll.bind(this)} />
      </div>
    )
  }
}

export default App

Header组件

/* components/Header/index.css */
.header {
  margin-bottom: 20px;
}

.header input {
  box-sizing: border-box;
  padding: 8px;
  width: 100%;
  border: 1px solid #ccc;
}
.header input:focus {
  outline: none;
  border-color: #409eff;
}
// components/Header/index.jsx

import React from 'react'
import './index.css'

export default class Header extends React.Component {
  // 回车添加元素
  handleKeyUp(e) {
    if (e.keyCode !== 13) return
    const val = e.target.value
    if (val.trim() === '') return
    const todo = {
      id: Math.random(), // 这里简单使用随机数,实际开发中不推荐
      name: val,
      isChecked: false
    }
    // 调用父组件props传入的函数添加数据
    this.props.addTodo(todo)
    e.target.value = ''
  }

  render() {
    return (
      <div className="header">
        {/* 输入框 */}
        <input onKeyUp={this.handleKeyUp.bind(this)} type="text" autoComplete="off" name="myInput" placeholder="请输入任务名称,按回车确认" />
      </div>
    )
  }
}

List组件

/* components/List/index.css */
.list-container {
  border: 1px solid #ccc;
  padding: 20px;
}
// components/List/index.jsx
import React from 'react'
import Item from '../Item'
import './index.css'
export default class List extends React.Component {
  render() {
    const { list, editTodo, deleteTodo } = this.props
    return (
      <div className="list-container">
        {list.map((item) => {
          /* 使用Item组件 */
          return <Item key={item.id} item={item} editTodo={editTodo} deleteTodo={deleteTodo} />
        })}
      </div>
    )
  }
}

Item组件

// components/Item/index.jsx
import React from 'react'
export default class Item extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isShowBtn: false
    }
  }
  // 鼠标移入显示
  handleMouseEnter(e) {
    this.setState({ isShowBtn: true })
  }

  // 鼠标移除隐藏
  handleMouseLeave(e) {
    this.setState({ isShowBtn: false })
  }

  handleChange(e, item) {
    // 通知父组件修改状态
    this.props.editTodo(item, e.target.checked)
  }
  // 删除
  handleDelete(e, item) {
    this.props.deleteTodo(item)
  }
  render() {
    const { item } = this.props
    const { isShowBtn } = this.state
    return (
      <div
        onMouseEnter={this.handleMouseEnter.bind(this)}
        onMouseLeave={this.handleMouseLeave.bind(this)}
        style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0' }}
      >
        <div>
          <input type="checkbox" id={item.id} checked={item.isChecked} onChange={(e) => this.handleChange.call(this, e, item)} />
          <label style={{ paddingLeft: '10px' }} htmlFor={item.id}>
            {item.name}
          </label>
        </div>
        <button onClick={(e) => this.handleDelete.call(this, e, item)} style={{ display: isShowBtn ? 'block' : 'none' }}>
          删除
        </button>
      </div>
    )
  }
}

Footer组件

// components/Footer/index.jsx

import React from 'react'
export default class Footer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
  handleChange(e) {
    // 通知父元素修改
    this.props.changeTodoAll(e.target.checked)
  }
  render() {
    const { list, deleteChecked, deleteCheckedAll } = this.props
    // 已经选中的列表
    const checkeds = list.filter((item) => item.isChecked)
    return (
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '20px' }}>
        <div>
          <input type="checkbox" checked={checkeds.length === list.length && list.length !== 0} onChange={this.handleChange.bind(this)} name="all" />
          <span>
            已完成 {checkeds.length} / 全部 {list.length}
          </span>
        </div>
        <div>
          <button onClick={deleteChecked} style={{ marginRight: '5px' }}>
            删除所有选中
          </button>
          <button onClick={deleteCheckedAll}>删除所有</button>
        </div>
      </div>
    )
  }
}

十七、Redux

Redux专门用来做状态管理的库(不是React的官方库)。它将整个应用状态存储在store中。组件可以派发(dispatch)行为(action)给store。其它组件可以通过订阅store中的状态(state)来刷新自己的视图:

image.png

可以把整个Redux当做一个饭店,React Components是一桌客人,Action Creators是服务员,Store是大堂经理,Reducers是后厨负责做饭。客人通过点餐给服务员,服务员通知大堂经理,大唐经理通知后厨来做饭,做好饭之后通过大堂经理送到客人餐桌上:

image.png

  • action

action表示动作对象。它包含两个属性:

  1. type:标识属性。值为字符串并且唯一。
  2. data:传递的数据。
  • store

store存储state(数据集合)。并将state,action,reducer联系在一起的对象。整个应用只有一个store对象。

  • reducer

reducer用于初始化状态和加工状态。加工状态时,根据旧的stateaction,产生新的state纯函数。

安装:

npm install --save redux

下面是一个加减的小案例:

image.png

首先创建redux/store.js

// redux/store.js
// 引入createStore,用于创建redux中最核心的store对象
import { createStore } from 'redux'

// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1

/**
 * 创建action
 */
export const actions = {
  increment(data) {
    return { type: INCREMENT, data }
  },
  decrement(data) {
    return { type: DECREMENT, data }
  }
}

/**
 * 创建reducer
 * @param {*} preState 上一个状态
 * @param {*} action 行为对象
 * @returns
 */
function countReducer(preState = 0, action) {
  /**
   * 从action对象中获取type和data
   * type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
   */
  const { type, data } = action
  switch (type) {
    case INCREMENT:
      return preState + data
    case DECREMENT:
      return preState - data
    default:
      return preState
  }
}

// 创建store,传入对应的countReducer
const store = createStore(countReducer)

export default store

然后在App.js里使用:

import React from 'react'
// 引入store和Action
import store, { actions } from './redux/store'
class App extends React.Component {
  /**
   * 通过dispatch调用action使状态改变后redux默认不会去渲染页面,需要通过store.subscribe()监听状态变化
   */
  componentDidMount() {
    // 监听redux中状态的变化,只要变化,就调用setState()重新渲染页面
    store.subscribe(() => {
      this.setState({})
    })
  }
  increment() {
    store.dispatch(actions.increment(1))
  }

  decrement() {
    store.dispatch(actions.decrement(1))
  }
  render() {
    return (
      <div>
        <h1>{store.getState()}</h1>
        <button onClick={this.increment.bind(this)}>加1</button>
        <button onClick={this.decrement.bind(this)}>减1</button>
      </div>
    )
  }
}

export default App

17.1、异步action

action不仅可以返回对象,还可以返回函数。如果是对象就是同步action,如果是函数就是异步action

如果要在Action中使用异步方法.需要使用到redux-thunk中间件:

// 安装
npm install -S redux-thunk

安装完成之后,要使用使用reduxapplymiddleware()中间件作为createStore()的第二个参数:

// 引入react-thunk,用于支持异步action
import thunk from 'redux-thunk'
// 传入对应的countReducer和redux-thunk
const store = createStore(countReducer, applyMiddleware(thunk))

下面是一个异步加减的小案例:

image.png

首先创建redux/store.js

// 引入createStore,用于创建redux中最核心的store对象
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1

/**
 * 创建action
 */
export const actions = {
  increment(data) {
    return { type: INCREMENT, data }
  },
  decrement(data) {
    return { type: DECREMENT, data }
  },
  incrementAsync(data) {
    return (dispatch) => {
      setTimeout(() => {
        dispatch(this.increment(data)) // 异步action中一般都会调用同步action
      }, 500)
    }
  },
  decrementAsync(data) {
    return (dispatch) => {
      setTimeout(() => {
        dispatch(this.decrement(data)) // 异步action中一般都会调用同步action
      }, 500)
    }
  }
}

/**
 * 创建reducer
 * @param {*} preState 上一个状态
 * @param {*} action 行为对象
 * @returns
 */
function countReducer(preState = 0, action) {
  /**
   * 从action对象中获取type和data
   * type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
   */
  const { type, data } = action
  switch (type) {
    case INCREMENT:
      return preState + data
    case DECREMENT:
      return preState - data
    default:
      return preState
  }
}

// 创建store,传入对应的countReducer和applyMiddleware
const store = createStore(countReducer, applyMiddleware(thunk))

export default store

然后在App.js里使用:

import React from 'react'
// 引入store和Action
import store, { actions } from './redux/store'
class App extends React.Component {
  /**
   * 通过dispatch调用action使状态改变后redux默认不会去渲染页面,需要通过store.subscribe()监听状态变化
   */
  componentDidMount() {
    // 监听redux中状态的变化,只要变化,就调用setState()重新渲染页面
    store.subscribe(() => {
      this.setState({})
    })
  }
  incrementAsync() {
    store.dispatch(actions.incrementAsync(1))
  }

  decrementAsync() {
    store.dispatch(actions.decrementAsync(1))
  }

  render() {
    return (
      <div>
        <h1>{store.getState()}</h1>
        <button onClick={this.incrementAsync.bind(this)}>异步加1</button>
        <button onClick={this.decrementAsync.bind(this)}>异步减1</button>
      </div>
    )
  }
}

export default App

十八、react-redux

React官方出品的react-redux,可以在React更加简单和方便的使用redux

react-redux内部已经自动实现监听数据变化重新渲染页面的功能,不用在使用store.subscribe()来监听数据的变化来手动编码实现渲染。首先进行安装:

# If you use npm:
npm install react-redux

# Or if you use Yarn:
yarn add react-redux
  1. 所有的UI组件都应该包裹一个容器组件,他们是父子关系。
  2. 容器组件是真正和redux交互的,可以随意的使用reduxapi
  3. UI组件中不能使用任何reduxapi
  4. 容器组件会传给组件redux中所保存的状态和用于操作状态的方法,通过props传递。

image.png

connect()方法用于连接UI组件和redux

connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)接收4个参数,这里只介绍前两个参数:

  1. mapStateToProps:值为Function,表示redux中保存的状态,返回一个对象,对象中的key就是传递给UI组件propskeyvalue就是传递给UI组件propsvalue

  2. mapDispatchToProps:值为Function | Object,表示redux中操作状态的方法,返回一个对象,对象中的key就是传递给UI组件propskeyvalue就是传递给UI组件propsvalue

  3. mergeProps:值为Function

  4. options:值为Object

下面是一个使用react-redux加减的小案例:

image.png

首先首先创建redux/store.js

// 引入createStore,用于创建redux中最核心的store对象
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1

/**
 * 创建action
 */
export const actions = {
  increment(data) {
    return { type: INCREMENT, data }
  },
  decrement(data) {
    return { type: DECREMENT, data }
  }
}

/**
 * 创建reducer
 * @param {*} preState 上一个状态
 * @param {*} action 行为对象
 * @returns
 */
function countReducer(preState = 0, action) {
  /**
   * 从action对象中获取type和data
   * type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
   */
  const { type, data } = action
  switch (type) {
    case INCREMENT:
      return preState + data
    case DECREMENT:
      return preState - data
    default:
      return preState
  }
}
const store = createStore(countReducer, applyMiddleware(thunk))

export default store

新建一个Count容器组件containers/Count.jsx

// 引入Count的UI组件
import CountUI from '../components/Count'

// 引入actions
import { actions } from '../redux/store'

// 引入connect连接UI组件和redux
import { connect } from 'react-redux'

/**
 * @param {*} state: redux保存的状态,由react-redux自动传入
 */
const mapStateToProps = function (state) {
  return { count: state }
}

/**
 * @param {*} dispatch redux的dispatch方法,由react-redux自动传入
 */
const mapDispatchToProps = function (dispatch) {
  return {
    incr: (data) => dispatch(actions.increment(data)),
    decr: (data) => dispatch(actions.decrement(data))
  }
}

// 使用connect()()创建并暴露一个Count容器组件
export default connect(mapStateToProps, mapDispatchToProps)(CountUI)

需要注意的一点是mapDispatchToProps的简写方式还可以传入一个对象,react-redux会自动帮我们调用dispatch()

const mapDispatchToProps = {
  incr: actions.increment,
  decr: actions.decrement
}

然后新建UI组件components/Count.jsx

import React from 'react'
// 引入store和Action
class Count extends React.Component {
  componentDidMount() {
    console.log(this.props) // {store: {…}, count: 0, incr: ƒ, decr: ƒ}
  }
  increment() {
    console.log(this.props)
    this.props.incr(1)
  }
  decrement() {
    this.props.decr(1)
  }
  render() {
    return (
      <div>
        <h1>{this.props.count}</h1>
        <button onClick={this.increment.bind(this)}>加1</button>
        <button onClick={this.decrement.bind(this)}>减1</button>
      </div>
    )
  }
}
export default Count

App.js里使用,需要注意的是store需要通过props传递给容器组件:

import React from 'react'
// 引入store
import store from './redux/store'

// 引入Count容器
import Count from './containers/Count'
class App extends React.Component {
  render() {
    // 给容器组件传递store
    return <Count store={store} />
  }
}
export default App

通过使用react-redux,我们把与redux状态交互的代码全部写到了容器组件内,通过connect()关联他们,最后UI组件只需要通过props来调用。

18.1、Provider

Provider可以让所有的组件都能收到store并作为props绑定到组件上。

假如说我们要使用组件很多次,每个组件都要传递store,就像下面一样:

import store from './redux/store'
class App extends React.Component {
  render() {
    return (
      <div>
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
        <Count store={store} />
      </div>
    )
  }
}

可以把store传递给Providerstore会自动传递给容器组件:

import { Provider } from 'react-redux'
import store from './redux/store'
import App from './App'
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
)

容器组件就不用手动传入store

import store from './redux/store'
class App extends React.Component {
  render() {
    return (
      <div>
        <Count />
        <Count />
        <Count />
        <Count />
        <Count />
        <Count />
        <Count />
        <Count />
      </div>
    )
  }
}

18.2、整合UI组件和容器组件

如果说项目里有100UI组件需要用到容器组件,那么我们还要新建100个容器组件。这样会使文件成倍增长。我们可以把UI组件和容器组件整合在一个文件内:

// Container/Count.jsx

import { actions } from '../redux/store'
import { connect } from 'react-redux'
import React from 'react'

// UI组件
class Count extends React.Component {
  componentDidMount() {
    console.log(this.props) // {store: {…}, count: 0, incr: ƒ, decr: ƒ}
  }
  increment() {
    console.log(this.props)
    this.props.incr(1)
  }
  decrement() {
    this.props.decr(1)
  }
  render() {
    return (
      <div>
        <h1>{this.props.count}</h1>
        <button onClick={this.increment.bind(this)}>加1</button>
        <button onClick={this.decrement.bind(this)}>减1</button>
      </div>
    )
  }
}

// 导出容器组件
export default connect(
  // mapStateToProps
  (state) => {
    return { count: state }
  },
  // mapDispatchToProps
  {
    incr: actions.increment,
    decr: actions.decrement
  }
)(Count)

18.3、组件间数据共享

之前介绍的redux功能都比较简单。redux里的数据只是一个单一的基本数据类型。实际开发的时候会使用到很多数据,我们需要合并redux的数据,需要用对象{}存储。可以使用combineReducers()方法来实现合并:

// 引入combineReducers
import { combineReducers } from 'redux'

/**
 *
 * 通过combineReducers()方法汇总所有的reducer,变为一个reducer
 * 返回的结果合并成一个 state 对象。如:{a: aReducer,b: bReducer}
 */
const allReducer = combineReducers({
  a: aReducer,
  b: bReducer
})

我们还需要更清晰的功能目录划分,如下:

├── redux                       
    ├── actions                 // 所有的action
        └── personAction.js           // 自定义person action
        └── bookAction.js             // 自定义book action
    ├── reducers                // 所有的reducers
        └── index.js                   // 汇总所有的reducer
        └── countReducer.js            // 自定义person reducer
        └── bookReducer.js             // 自定义book reducer
      
    ├── constant.js             // 常量
    ├── store.js                // store

下面是一个PersonBook组件实现一个人可以有多本书的案例:

image.png

编写constant

/**
 * type类型常量值
 */
export const ADD_PERSON = 'addPerson' // 添加一个人
export const ADD_BOOK = 'addBook'     // 添加一本书

编写Action

// redux/actions/bookAction

import { ADD_BOOK } from '../constant'

export const addBookAction = (data) => {
  return { type: ADD_BOOK, data }
}
// redux/actions/PersonAction

import { ADD_PERSON } from '../constant'

export const addPersonAction = (data) => {
  return { type: ADD_PERSON, data }
}

编写reducer

// redux/reducers/bookReducer

import { ADD_BOOK } from '../constant.js'
export default function bookReducer(preState = [], action) {
  const { type, data } = action
  switch (type) {
    case ADD_BOOK:
      return [data, ...preState]
    default:
      return preState
  }
}
// redux/reducers/personReducer

import { ADD_PERSON } from '../constant'

// 初始化state
const initState = [{ id: '001', name: '张三' }]

function personReducer(preState = initState, { type, data }) {
  switch (type) {
    case ADD_PERSON:
      return [data, ...preState]

    default:
      return preState
  }
}

export default personReducer
// redux/reducers/index.js

/* 汇总所有的reducer */

import { combineReducers } from 'redux'

// 引入bookReducer
import bookReducer from './bookReducer'
// 引入personReducer
import personReducer from './personReducer'

/**
 *
 * 通过combineReducers()方法汇总所有的reducer,变为一个reducer
 * 返回的结果合并成一个 state 对象。 如: {a: xxx,b: xxx}
 */
const allReducer = combineReducers({
  storeBook: bookReducer,
  storePerson: personReducer
})

export default allReducer

编写store

import { createStore, applyMiddleware } from 'redux'

import thunk from 'redux-thunk'
import allReducer from '../redux/reducers'
import { composeWithDevTools } from 'redux-devtools-extension'

const store = createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))

export default store

编写PersonBook组件:

import React from 'react'
import { connect } from 'react-redux'
import { addBookAction } from '../redux/actions/bookAction'
class Book extends React.Component {
  handleKeyUp(e) {
    if (e.keyCode !== 13) return
    const val = e.target.value
    if (!val.trim() === '') return
    this.props.addBookAction({ id: Date.now(), name: val })
    e.target.value = ''
  }
  render() {
    return (
      <div>
        <h1>我是Book组件</h1>
        <input type="text" name="book" placeholder="添加一本书" onKeyUp={this.handleKeyUp.bind(this)} />
      </div>
    )
  }
}
export default connect(
  (state) => {
    return { storeBook: state.storeBook }
  },
  {
    addBookAction: addBookAction
  }
)(Book)
import React from 'react'
import { connect } from 'react-redux'
import { addPersonAction } from '../redux/actions/personAction'
class Person extends React.Component {
  render() {
    return (
      <div style={{ borderBottom: '1px solid #ccc' }}>
        <h1>我是Person组件</h1>
        {this.props.storePerson[0].name}的技术书有:
        {this.props.storeBook.map(({ id, name }) => (
          <span key={id}>{name},</span>
        ))}
      </div>
    )
  }
}
export default connect(
  (state) => {
    return { storeBook: state.storeBook, storePerson: state.storePerson }
  },
  {
    addPersonAction: addPersonAction
  }
)(Person)

最后在App.js里使用:

import React from 'react'
import Book from './components/Book'
import Person from './components/Person'
class App extends React.Component {
  render() {
    return (
      <div>
        <Person />
        <Book />
      </div>
    )
  }
}
export default App

18.4、reducer纯函数

reduxreducer函数必须是一个纯函数。只要是同样的输入(实参),必须得到同样的输出(返回)。

  1. 不能改写参数数据。
  2. 不会产生任何副作用,例如网络请求。
  3. 不能使用Date.now()或者Math.random()等不纯的方法。

因为比较两个javascript对象中所有的属性是否完全相同,唯一的办法就是深比较。然而,深比较在真实的应用中非常耗性能的,需要比较的次数特别多,所以一个有效的解决方案就是做一个规定,当无论发生任何变化时,开发者都要返回一个新的对象,没有变化时,开发者返回旧的对象,这也就是redux为什么要把reducer设计成纯函数的原因。

18.5、redux开发者工具

image.png

redux开发者工具不仅需要安装Chrome插件,还要在项目里配置:

  1. 安装Chrome插件:Redux DevTools

  2. 项目里需要安装redux-devtools-extension

// 安装
npm install redux-devtools-extension
  1. 然后在项目里store.js里配置:
import { composeWithDevTools } from 'redux-devtools-extension'

const store = createStore(allReducer,composeWithDevTools( applyMiddleware(thunk) ))

十九、Hook

HookReact 16.8的新增特性。它可以在不编写class的情况下使用state以及其他的 React特性。下面会介绍一下比较常用的Hook

19.1、useState()

State hook让函数组件也有state状态,并进行状态数据的读写操作。语法:

const [xxx, setXXX] = React.useState(initValue)

initValue在第一次初始化的值在内部做缓存。返回值包含两个元素的数组,第一个为内部状态当前值,第二个为更新状态值的函数。

setXXX()有两种写法:

  1. setxxx(newValue):参数为非函效值,直接指定新的状态值,内部覆盖原来的状态值。
  2. setxxx(value=> newvalue):参数为函数,接收之前的状态值,返回新的状态值,内部覆盖原来的状态值。
import React, { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  function addCount() {
    // 第一种写法
    setCount(count + 1)
    // 第二种写法
    setCount((count) => {
      return count + 1
    })
  }

  return (
    <div>
      <p>count值为:{count}</p>
      <button onClick={addCount}>点击+2</button>
    </div>
  )
}

19.2、Effect()

Effect Hook可以在函数组件中使用生命周期钩子函数。可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。

Effect()传入一个回调函数,它在第一次渲染之后和每次更新之后都会执行:

import { useEffect } from 'react'
// 类似于componentDidMount和componentDidUpdate
useEffect(() => {
  console.log('success')
})

Effect()还可以传入第二个参数。它是一个数组指定需要监听的依赖项。有依赖项发生变化,才会重新渲染。默认不传表示监听全部。空数组[]表示都不监听。

const [count, setCount] = useState(0)
const [name, setName] = useState('张三')

// 初始化时执行和状态改变时执行,类似于componentDidMount()和componentDidUpdate()
useEffect(() => {
// TODO
}) // 不传表示监听全部

// 初始化时执行,类似于componentDidMount()
useEffect(() => {
// TODO
}, []) // []表示都不监听

// 初始化时和count改变的时候会执行
useEffect(() => {
// TODO
}, [count]) // 只监听count

Effect()可以返回一个函数,在组件卸载之前执行。相当于componentWillUnmount钩子函数:

import { useEffect } from 'react'
import ReactDOM from 'react-dom'
function Demo() {
  useEffect(() => {
    return function () {
      console.log('组件卸载前执行')
    }
  })

  // 卸载
  function handleUnmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById('root'))
  }

  return (
    <div>
      <button onClick={handleUnmount}>卸载组件</button>
    </div>
  )
}

当点击卸载组件时,就会执行Effect()返回的函数。

下面是一个定时器的案例:

import { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
// 定时器组件
function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    let timer = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)

    return () => {
      clearInterval(timer) // 卸载组件前清除定时器
    }
  }, [])

  // 卸载
  function handleUnmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById('root'))
  }

  return (
    <div>
      <p>count的值为:{count}</p>
      <button onClick={handleUnmount}>卸载组件</button>
    </div>
  )
}

使用Effect()需要注意⚠的一点是:如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起:

import { useEffect, useState } from 'react'

function App() {
  const [timerA, setTimerA] = useState(0)
  const [timerB, setTimerB] = useState(0)
  useEffect(() => {
    setInterval(() => setTimerA((timerA) => timerA + 1), 1000)
  }, [])

  useEffect(() => {
    setInterval(() => setTimerB((timerB) => timerB + 1), 2000)
  }, [])

  return (
    <div>
      {timerA},{timerB}
    </div>
  )
}

19.3、useRef()

在函数组件中使用String Ref、Callback Ref、Create Ref会抛出错误。因为函数组件没有实例,所以函数组件中无法使用,需要使用useRef()

useRef的作用:

  1. 获取DOM元素的节点。
  2. 获取子组件的实例。
  3. 渲染周期之间共享数据的存储。

获取DOM元素的节点:

import { useEffect, useRef } from 'react'

function App() {
  let inputRef = useRef()
  useEffect(() => {
    console.log(inputRef.current) // <input type="text">
  }, [])

  return <input type="text" ref={inputRef} />
}

因为函数组件没有实例,如果想用ref获取子组件的实例,子组件组要写成类组件。获取子组件的实例:

import React, { useEffect, useRef } from 'react'

function App() {
  let childRef = useRef()
  useEffect(() => {
    childRef.current.handleChild() // Child Component
  }, [])

  return <Child ref={childRef} />
}

class Child extends React.Component {
  handleChild() {
    console.log('Child Component')
  }

  render() {
    return <div>Child组件</div>
  }
}

渲染周期之间共享数据的存储:

import React, { useEffect, useRef } from 'react'

function App() {
  const timerRef = useRef()
  useEffect(() => {
    let timerId = setInterval(() => {
      // TODO
    })
    timerRef.current = timerId
    return () => {
      clearInterval(timerRef.current)
    }
  }, [])

  return <div>App.js</div>
}

把定时器的ID存入到useRef中,定时器ID不仅在useEffect可以拿到,而且可以在整个组件函数中都可以获取到。

19.4、useContext()

createContext()是一种组件间的通信方式,常用于祖孙组件通信。

假设有A,B,C三个组件,它们是组孙关系。A代表父组件,B代表子组件,C代表孙组件,A组件的数据要传给C组件:

image.png

使用createContext()来传递跨级组件数据:

import React from 'react'

// 在组件外部建立一个 Context 容器对象
const MyContext = React.createContext()

// 父组件
class A extends React.Component {
  constructor(props) {
    super(props)
    this.state = { username: '张三' }
  }
  render() {
    return (
      <>
        <div>我是A组件</div>
        {/* 使用MyContext.Provider 包裹子组件,并通过value传递数据 */}
        <MyContext.Provider value={{ username: this.state.username }}>
          <B />
        </MyContext.Provider>
      </>
    )
  }
}

// 子组件
class B extends React.Component {
  render() {
    return (
      <>
        <div>我是B组件</div>
        <C />
      </>
    )
  }
}

// 孙组件
class C extends React.Component {
  static contextType = MyContext // 声明接收context
  render() {
    const { username } = this.context // 获取context
    return <div>我是C组件,我从A组件得到的姓名是:{username}</div>
  }
}

如果C组件是函数组件,就需要使用Consume来获取值,回调里的value值就是contextvalue值:

function C() {
  return <MyContext.Consumer>{(value) => <div>我是C组件,我从A组件得到的姓名是:{value.username}</div>}</MyContext.Consumer>
}

C组件还可以使用useContext()来获取值:

function C() {
  const { username } = useContext(MyContext)
  return <div>我是C组件,我从A组件得到的姓名是:{username}</div>
}

useContext(MyContext)相当于class组件中的static contextType = MyContext 或者 <MyContext.Consumer>

19.5、useReducer()

useReducer()useState的替代方案。它的使用方式与Redux相似:

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer() 可以传入三个参数:

  1. 第一个参数 reducer 它的使用方式跟Redux中的Reducer函数是非常相似的: (state, action) => newState

  2. 第二个参数 initialArg 就是状态的初始值。

  3. 第三个参数 init 是一个可选的用于懒初始化(Lazy Initialization)的函数,这个函数返回初始化后的状态。

下面是一个计数器的案例:

import { useReducer } from 'react'

// Reducer 函数
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  )
}
export default Counter

使用useReducer()的场景还是很多的,下面举几个例子:

  • state是一个数组或者对象。

  • state变化很复杂,经常一个操作需要修改很多state

  • 复杂的业务逻辑,UI和业务能够分离。

  • 不同的属性被捆绑在了一起必须使用一个state object对象进行统一管理。

假如说有个登录的功能,分别使用useState()useReduer()的方式:

// useState() 方式

import { useState, useReducer } from 'react'

// 登录组件
function LoginUI() {
  const [name, setName] = useState('') // 用户名
  const [pwd, setPwd] = useState('') // 密码
  const [isLoading, setIsLoading] = useState(false) // 是否展示loading,发送请求中
  const [error, setError] = useState('') // 错误信息
  const [isLoggedIn, setIsLoggedIn] = useState(false) // 是否登录

  async function LoginPage() {
    setError('')
    setIsLoading(true)
    try {
      await loginService({ name, pwd })
      setIsLoggedIn(true)
      setIsLoading(false)
    } catch (err) {
      // 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
      setError(error.message)
      setName('')
      setPwd('')
      setIsLoading(false)
    }
  }
  
   // 登录接口
  function loginService() {
    // ...
  }

  return <h2>Login Page</h2>
}

上面代码使用useState()可以看到随着需求越来越复杂的时候,更多的state被定义,更多的setState()调用,很容易设置错误或者遗漏,可维护性也很差。下面来看一下使用useReducer()的方式:

// useReducer() 方式

import { useReducer } from 'react'

const initState = { name: '', pwd: '', error: '', isLoading: false, isLoggedIn: false }

function loginReducer(state, action) {
  switch (action.type) {
    case 'login':
      return { ...state, isLoading: true, error: '' }
    case 'success':
      return { ...state, isLoggedIn: true, isLoading: false }
    case 'error':
      return { ...state, error: action.payload.error, name: '', pwd: '', isLoading: false }
    default:
      return state
  }
}

// 登录组件
function LoginPage() {
  const [state, dispatch] = useReducer(loginReducer, initState)
  const { name, pwd, error } = state

  async function Login() {
    dispatch({ type: 'login' })
    try {
      await loginService({ name, pwd })
      dispatch({ type: 'success' })
    } catch (err) {
      // 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
      dispatch({
        type: 'error',
        payload: { error: error.message }
      })
    }
  }
  // 登录接口
  function loginService() {
    // ...
  }

  return <h2>Login Page</h2>
}
export default Login

参考

github.com/xzlaptt/Rea…

zhuanlan.zhihu.com/p/146773995

segmentfault.com/a/119000002…

www.bilibili.com/video/BV1wy…

www.ruanyifeng.com/blog/2020/0…

juejin.cn/post/684490…