React 入门学习(十四)– redux 基本使用

引言

在了解了 Antd 组件库之后,我们现在开始学习了 Redux ,在我们之前写的案例当中,例如:todolist 案例,GitHub 搜索案例当中,我们对于状态的管理,都是通过 state 来实现的,比如,我们在给兄弟组件传递数据时,需要先将数据传递给父组件,再由父组件转发 给它的子组件。这个过程十分的复杂,后来我们又学习了消息的发布订阅,我们通过 pubsub 库,实现了消息的转发,直接将数据发布,由兄弟组件订阅,实现了兄弟组件间的数据传递。但是,随着我们的需求不断地提升,我们需要进行更加复杂的数据传递,更多层次的数据交换。因此我们为何不可以将所有的数据交给一个中转站,这个中转站独立于所有的组件之外,由这个中转站来进行数据的分发,这样不管哪个组件需要数据,我们都可以很轻易的给他派发。

而有这么一个库就可以帮助我们来实现,那就是 Redux ,它可以帮助我们实现集中式状态管理

1. 什么情况使用 Redux ?

首先,我们先明晰 Redux 的作用 ,实现集中式状态管理。

Redux 适用于多交互、多数据源的场景。简单理解就是复杂

从组件角度去考虑的话,当我们有以下的应用场景时,我们可以尝试采用 Redux 来实现

  1. 某个组件的状态需要共享时
  2. 一个组件需要改变其他组件的状态时
  3. 一个组件需要改变全局的状态时

除此之外,还有很多情况都需要使用 Redux 来实现(还没有学 hook,或许还有更好的方法)

image-20210909194446988

(从掘友的文章里截的图)

这张图,非常形象的将纯 React 和 采用 Redux 的区别体现了出来

2. Redux 的工作流程

image-20210909194900532

首先组件会在 Redux 中派发一个 action 方法,通过调用 store.dispatch 方法,将 action 对象派发给 store ,当 store 接收到 action 对象时,会将先前的 state 与传来的 action 一同发送给 reducerreducer 在接收到数据后,进行数据的更改,返回一个新的状态给 store ,最后由 store 更改 state

img

(图来自掘金社区,侵删)

3. Redux 三个核心概念

1. store

store 是 Redux 的核心,可以理解为是 Redux 的数据中台,我们可以将任何我们想要存放的数据放在 store 中,在我们需要使用这些数据时,我们可以从中取出相应的数据。因此我们需要先创建一个 store ,在 Redux 中可以使用 createStore API 来创建一个 store

在生产中,我们需要在 src 目录下的 redux 文件夹中新增一个 store.js 文件,在这个文件中,创建一个 store 对象,并暴露它

因此我们需要从 redux 中暴露两个方法

import {
    createStore,
    applyMiddleware
} from 'redux'

并引入为 count 组件服务的 reducer

import countReducer from './count_reducer'

最后调用 createStore 方法来暴露 store

export default createStore(countReducer, applyMiddleware(thunk))

这里采用了中间件,本文应该不会写到~

store 对象下有一些常用的内置方法

获取当前时刻的 store ,我们可以采用 getStore 方法

const state = store.getState();

在前面我们的流程图中,我们需要通过 store 中的 dispatch 方法来派生一个 action 对象给 store

store.dispatch(`action对象`)

最后还有一个 subscribe 方法,这个方法可以帮助我们订阅 store 的改变,只要 store 发生改变,这个方法的回调就会执行

为了监听数据的更新,我们可以将 subscribe 方法绑定在组件挂载完毕生命周期函数上,但是这样,当我们的组件数量很多时,会比较的麻烦,因此我们可以直接将 subscribe 函数用来监听整个 App组件的变化

store.subscribe(() => {
    ReactDOM.render( < App /> , document.getElementById('root'))
&#125;)

2. action

actionstore 中唯一的数据来源,一般来说,我们会通过调用 store.dispatch 将 action 传到 store

我们需要传递的 action 是一个对象,它必须要有一个 type

例如,这里我们暴露了一个用于返回一个 action 对象的方法

export const createIncrementAction = data => (&#123;
    type: INCREMENT,
    data
&#125;)

我们调用它时,会返回一个 action 对象

3. reducer

在 Reducer 中,我们需要指定状态的操作类型,要做怎样的数据更新,因此这个类型是必要的。

reducer 会根据 action 的指示,对 state 进行对应的操作,然后返回操作后的 state

如下,我们对接收的 action 中传来的 type 进行判断

export default function countReducer(preState = initState, action) &#123;
    const &#123;
        type,
        data
    &#125; = action;
    switch (type) &#123;
        case INCREMENT:
            return preState + data
        case DECREMENT:
            return preState - data
        default:
            return preState
    &#125;
&#125;

更改数据,返回新的状态

4. 创建 constant 文件

在我们正常的编码中,有可能会出现拼写错误的情况,但是我们会发现,拼写错误了不一定会报错,因此就会比较难搞。

我们可以在 redux 目录下,创建一个 constant 文件,这个文件用于定义我们代码中常用的一些变量,例如

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

将这两个单词写在 constant 文件中,并对外暴露,当我们需要使用时,我们可以引入这个文件,并直接使用它的名称即可

直接使用 INCREMENT 即可

5. 实现异步 action

一开始,我们直接调用一个异步函数,这虽然没有什么问题,但是难道 redux 就不可以实现了吗?

incrementAsync = () => &#123;
    const &#123; value &#125; = this.selectNumber
    const &#123; count &#125; = this.state;
    setTimeout(() => &#123;
        this.setState(&#123; count: count + value * 1 &#125;)
    &#125;, 500);
&#125;

我们可以先尝试将它封装到 action 对象中调用

export const createIncrementAsyncAction = (data, time) => &#123;
    // 无需引入 store ,在调用的时候是由 store 调用的
    return (dispatch) => &#123;
        setTimeout(() => &#123;
            dispatch(createIncrementAction(data))
        &#125;, time)
    &#125;
&#125;

当我们点击异步加操作时,我们会调用这个函数,在这个函数里接收一个延时加的时间,还有action所需的数据,和原先的区别只在于返回的时一个定时器函数

但是如果仅仅这样,很显然是会报错的,它默认需要接收一个对象

如果我们需要实现传入函数,那我们就需要告诉:你只需要默默的帮我执行以下这个函数就好!

这时我们就需要引入中间件,在原生的 redux 中暴露出 applyMiddleware 中间件执行函数,并引入 redux-thunk 中间件(需要手动下载)

import thunk from 'redux-thunk'

通过第二个参数传递下去就可以了

export default createStore(countReducer, applyMiddleware(thunk))

注意:异步 action 不是必须要写的,完全可以自己等待异步任务的结果后再去分发同步action

采用 react-thunk 能让异步代码像同步代码一样执行,在 redux 中我们也是可以实现异步的,但是这样我们的代码中会有很多异步的细节,这不是我们想看到的,利用 react-thunk 之类的库,就能让我们只关心我们的业务

6. Redux 三大原则

理解好 Redux 有助于我们更好的理解接下来的 React -Redux

第一个原则

单向数据流:整个 Redux 中,数据流向是单向的

UI 组件 —> action —> store —> reducer —> store

第二个原则

state 只读:在 Redux 中不能通过直接改变 state ,来控制状态的改变,如果想要改变 state ,则需要触发一次 action。通过 action 执行 reducer

第三个原则

纯函数执行:每一个reducer 都是一个纯函数,不会有任何副作用,返回是一个新的 state,state 改变会触发 store 中的 subscribe

参考资料

Redux + React-router 的入门📖和配置👩🏾‍💻教程

小册:React 进阶实践指南


非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈

React 入门学习(十五)– React-Redux 基本使用

引言

在前面我们学习了 Redux ,我们在写案例的时候,也发现了它存在着一些问题,例如组件无法状态无法公用,每一个状态组件都需要通过订阅来监视,状态更新会影响到全部组件更新,面对着这些问题,React 官方在 redux 基础上提出了 React-Redux 库

在前面的案例中,我们如果把 store 直接写在了 React 应用的顶层 props 中,各个子组件,就能访问到顶层 props

<顶层组件 store=&#123;store&#125;>
  <App />
</顶层组件/>

这就类似于 React-Redux

容器组件和 UI 组件

  1. 所有的 UI 组件都需要有一个容器组件包裹
  2. 容器组件来负责和 Redux 打交道,可以随意使用 Redux 的API
  3. UI 组件无任何 Redux API
  4. 容器组件用于处理逻辑,UI 组件只会负责渲染和交互,不处理逻辑

image-20210910094426268

在我们的生产当中,我们可以直接将 UI 组件写在容器组件的代码文件当中,这样就无需多个文件

首先,我们在 src 目录下,创建一个 containers 文件夹,用于存放各种容器组件,在该文件夹内创建 Count 文件夹,即表示即将创建 Count 容器组件,再创建 index.jsx 编写代码

要实现容器组件和 UI 组件的连接,我们需要通过 connect 来实现

// 引入UI组件
import CountUI from '../../components/Count'
// 引入 connect 连接UI组件
import &#123;connect&#125; from 'react-redux'
// 建立连接
export default connect()(CountUI)

后面还会详细讲到

Provider

由于我们的状态可能会被很多组件使用,所以 React-Redux 给我们提供了一个 Provider 组件,可以全局注入 redux 中的 store ,只需要把 Provider 注册在根部组件即可

例如,当以下组件都需要使用 store 时,我们需要这么做,但是这样徒增了工作量,很不便利

<Count store=&#123;store&#125;/>
&#123;/* 示例 */&#125;
<Demo1 store=&#123;store&#125;/>
<Demo1 store=&#123;store&#125;/>
<Demo1 store=&#123;store&#125;/>
<Demo1 store=&#123;store&#125;/>
<Demo1 store=&#123;store&#125;/>

我们可以这么做:在 src 目录下的 index.js 文件中,引入 Provider ,直接用 Provider 标签包裹 App 组件,将 store 写在 Provider 中即可

ReactDOM.render(
  <Provider store=&#123;store&#125;>
    <App />
  </Provider>,
  document.getElementById("root")
);

这样我们在 App.jsx 文件中,组件无需手写指定 store ,即可使用 store

connect

在前面我们看到的 react-redux 原理图时,我们会发现容器组件需要给 UI 组件传递状态和方法,并且是通过 props 来传递,看起来很简单。但是,我们会发现容器组件中似乎没有我们平常传递 props 的情形

这时候就需要继续研究一下容器组件中的唯一一个函数 connect

connect 方法是一个连接器,用于连接容器组件和 UI 组件,它第一次执行时,接收4个参数,这些参数都是可选的,它执行的执行的结果还是一个函数,第二次执行接收一个 UI 组件

第一次执行时的四个参数:mapStateToPropsmapDispatchToPropsmergePropsoptions

mapStateToProps

const mapStateToProps = state => (&#123; count: state &#125;)

它接收 state 作为参数,并且返回一个对象,这个对象标识着 UI 组件的同名参数,

返回的对象中的 key 就作为传递给 UI 组件 props 的 key,value 就作为 props 的 value

如上面的代码,我们可以在 UI 组件中直接通过 props 来读取 count

<h1>当前求和为:&#123;this.props.count&#125;</h1>

这样我们就打通了 UI 组件和容器组件间的状态传递,那如何传递方法呢?

mapDispatchToProps

connect 接受的第二个参数是 mapDispatchToProps 它是用于建立 UI 组件的参数到 store.dispacth 方法的映射

我们可以把参数写成对象形式,在这里面定义 action 执行的方法,例如 jia 执行什么函数,jian 执行什么函数?

我们都可以在这个参数中定义,如下定义了几个方法对应的操作函数

&#123;
    jia: createIncrementAction,
    jian: createDecrementAction,
    jiaAsync: createIncrementAsyncAction
&#125;

写到这里其实 connect 已经比较完善了,但是你可以仔细想想 redux 的工作流程

image-20210909194900532

似乎少了点什么,我们在这里调用了函数,创建了 action 对象,但是好像 store 并没有执行 dispatch ,那是不是断了呢?执行不了呢?

其实这里 react-redux 已经帮我们做了优化,当调用 actionCreator 的时候,会立即发送 actionstore 而不用手动的 dispatch

  • 自动调用 dispatch

完整开发

首先我们在 containers 文件夹中,直接编写我们的容器组件,无需编写 UI 组件

先打 rcc 打出指定代码段,然后暴露出 connect 方法

import &#123; connect &#125; from 'react-redux'

action 文件中暴露创建 action 的方法

import &#123;createIncrementAction&#125; from '../../redux/count_action'

编写 UI 组件,简单写个 demo,绑定 props 和方法

return (
    <div>
        <h2>当前求和为:&#123;this.props.count&#125;</h2>
        <button onClick=&#123;this.add&#125;>点我加1</button>
    </div>
);

调用 connect 包装暴露 UI 组件

export default connect(
    state => (&#123; count: state &#125;),// 状态
    &#123; jia: createIncrementAction &#125; // 方法
)(Count);

第一次执行的参数就直接传递 state 和一个指定 action 的对象


非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈

React 入门学习(十六)– 数据共享

引言

在写完了基本的 Redux 案例之后,我们可以尝试一些更实战性的操作,比如我们可以试试多组件间的状态传递,相互之间的交互

react-redux-demo

如上动图所示,我们想要实现上面的案例,采用纯 React 来实现是比较困难的,我们需要很多层的数据交换才能实现,但是我们如果采用 Redux 来实现会变得非常简单

因为 Redux 打通了组件间的隔阂,我们可以自由的进行数据交换,所有存放在 store 中的数据都可以实现共享,那我们接下来看看如何实现的吧~

1. 编写 Person 组件

上面的 Count 组件,已经在前面几篇写过了,但是我没有记录详细的实现过程,只是做了一些小小的总结(我摸鱼了)

不管如何,我们先来实现一个 Person 组件吧

首先我们需要在 containers 文件夹下编写 Person 组件的容器组件

如何编写一个容器组件呢?(上一篇也讲过了)

首先我们需要编写 index.jsx 文件,在这个文件里面编写 Person 组件的 UI 组件,并使用 connect 函数将它包装,映射它的状态和方法

编写 UI 组件架构

<div>
    <h2>我是 Person 组件,上方组件求和为:&#123;this.props.countAll&#125;</h2>
    <input ref=&#123;c => this.nameNode = c&#125; type="text" placeholder="输入名字" />
    <input ref=&#123;c => this.ageNode = c&#125; type="text" placeholder="输入年龄" />
    <button onClick=&#123;this.addPerson&#125;>添加</button>
    <ul>
        &#123;
            this.props.persons.map((p) => &#123;
                return <li key=&#123;p.id&#125;> &#123;p.name&#125;--&#123;p.age&#125;</li>
            &#125;)
        &#125;
    </ul>
</div>

我们可以看到这里采用了 ref 来获取到当前事件触发的节点,并通过 this.addPerson 的方式给按钮绑定了一个点击事件

编写点击事件回调

addPerson = () => &#123;
    const name = this.nameNode.value
    const age = this.ageNode.value
    const personObj = &#123; id: nanoid(), name, age &#125;
    this.props.add(personObj)
    this.nameNode.value = ''
    this.ageNode.value = ''
&#125;

在这里我们需要处理输入框中的数据,并且将这些数据用于创建一个 action 对象,传递给 store 进行状态的更新

在这里我们需要回顾的是,这里我们使用了一个 nanoid 库,这个库我们之前也有使用过

下载,引入,暴露
import &#123; nanoid &#125; from 'nanoid'

暴露的 nanoid 是一个函数,我们每一次调用时,都会返回一个不重复的数,用于确保 id 的唯一性,同时在后面的 map 遍历的过程中,我们将 id 作为了 key 值,这样也确保了 key 的唯一性,关于 key 的作用,可以看看 diffing 算法的文章

状态管理

在这里我们需要非常熟练的采用 this.props.add 的方式来更新状态

那么它是如何实现状态更新的呢?我们来看看

在我们调用 connect 函数时,我们第一次调用时传入的第二个参数,就是用于传递方法的,我们传递了一个 add 方法

export default connect(
    state => (&#123; persons: state.person, countAll: state.count &#125;),//映射状态
    &#123; add: createAddPersonAction &#125;
)(Person);

它的原词是:mapDispatchToProps

我的理解是,传入的东西会被映射映射成 props 对象下的方法,这也是我们能够在 props 下访问到 add 方法的原因

对于这一块 connect ,我们必须要能够形成自己的理解,这里非常的重要,它实现了数据的交互,不至于一个组件,而是全部组件

我是如何理解的呢?

想象一个 store 仓库,在我们这个案例当中,Count 组件需要存放 count 值在 store 中,Person 组件需要存放新增用户对象在 store 中,我们要把这两个数据存放在一个对象当中。当某个组件需要使用 store 中的值时,可以通过 connect 中的两个参数来获取,例如这里我们需要使用到 Count 组件的值,可以通过 .count 来从 store 中取值。

也就是说,所有的值都存放在 store 当中,通过点运算符来获取,所有的操作 store 的方法都需要通过 action 来实现。当前组件需要使用的数据都需要在 connect 中暴露

2. 编写 reducer

首先,我们需要明确 reducer 的作用,它是用来干什么的?

根据操作类型来指定状态的更新

也就是说当我们点击了添加按钮后,会将输入框中的数据整合成一个对象,作为当前 action 对象的 data 传递给 reducer

我们可以看看我们编写的 action 文件,和我们想的一样

import &#123; ADD_PERSON &#125; from "../constant";
// 创建一个人的action 对象
export const createAddPersonAction = (personObj) => (&#123;
  type: ADD_PERSON,
  data: personObj,
&#125;);

当 reducer 接收到 action 对象时,会对 type 进行判断

export default function personReducer(preState = initState, action) &#123;
  const &#123; type, data &#125; = action;
  switch (type) &#123;
    case ADD_PERSON:
      return [data,...preState]
    default:
      return preState
  &#125;
&#125;

一般都采用 switch 来编写

这里有个值得注意的地方是,这个 personReducer 函数是一个纯函数,什么是纯函数呢?这个是高阶函数部分的知识了,纯函数是一个不改变参数的函数,也就是说,传入的参数是不能被改变的。

为什么要提这个呢?在我们 return 时,有时候会想通过数组的 API 来在数组前面塞一个值,不也可以吗?

但是我们要采用 unshirt 方法,这个方法是会改变原数组的,也就是我们传入的参数会被改变,因此这样的方法是不可行的!

3. 打通数据共享

写到这里,或许已经写完了,但是有些细节还是需要注意一下

采用 Redux 来进行组件的数据交互真的挺方便。

我们可以在 Count 组件中引入 Person 组件存在 store 中的状态。

export default connect(state => (&#123; count: state.count, personNum: state.person.length &#125;),
    &#123;
       ...
    &#125;
)(Count)

在这里我们将 store 中的 person 数组的长度暴露出来这样 Count 组件就可以直接通过 props 来使用了

同样的我们也可以在 Person 组件中使用 Count 组件的值

从而实现了我们的这个 Demo

4. 最终优化

  1. 利用对象的简写方法,将键名和键值同名,从而只写一个名即可
  2. 合并 reducer ,我们可以将多个 reducer文件 写在一个 index 文件当中,需要采用 combineReducers 来合并

5. 项目打包

执行 npm run build 命令,即可打包项目,打包完成后,会生成一个 build 文件,这个文件我们需要部署到服务器上才能运行

我们可以放在自己的服务器上即可

但是我遇到了一个问题

打包后的文件路径少了一个 . 导致文件无法找到,报错无法执行,我通过手动添加的方式解决了,不知道还有没有什么其他方法解决

react-redux-demo

也可以采用 npm i serve -g 安装,如何通过 serve ‘指定文件夹’ 来执行


非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈

React 入门学习(十七)– React 扩展

引言

学到这里 React 已经学的差不多了,接下来就学习一些 React 扩展内容,可以帮助我们更好的开发和理解,这部分的知识还有很多的东西可以探寻,比如:网红 React-Hook,就是我们需要注意的地方,打了 100 多集的类式组件,出来一个 hooks ,现在用函数式组件偏多了………….

所以 Hooks 就需要我们深入的学习一下了,下面我们就一起来看看扩展部分有哪些内容吧

1. setState

对象式 setState

首先在我们以前的认知中,setState 是用来更新状态的,我们一般给它传递一个对象,就像这样

this.setState(&#123;
    count: count + 1
&#125;)

这样每次更新都会让 count 的值加 1。这也是我们最常做的东西

这里我们做一个案例,点我加 1,一个按钮一个值,我要在控制台输出每次的 count 的值

react-extension-demo1

那我们需要在控制台输出,要如何实现呢?

我们会考虑在 setState 更新之后 log 一下

add = () => &#123;
    const &#123; count &#125; = this.state
    this.setState(&#123;
        count: count + 1
    &#125;)
    console.log(this.state.count);
&#125;

因此可能会写出这样的代码,看起来很合理,在调用完 setState 之后,输出 count

react-extension-demo1-2

我们发现显示的 count 和我们控制台输出的 count 值是不一样的

这是因为,我们调用的 setState 是同步事件,但是它的作用是让 React 去更新数据,而 React 不会立即的去更新数据,这是一个异步的任务,因此我们输出的 count 值会是状态更新之前的数据。“React 状态更新是异步的

那我们要如何实现同步呢?

其实在 setState 调用的第二个参数,我们可以接收一个函数,这个函数会在状态更新完毕并且界面更新之后调用,我们可以试试

add = () => &#123;
    const &#123; count &#125; = this.state
    this.setState(&#123;
        count: count + 1
    &#125;, () => &#123;
        console.log(this.state.count)
    &#125;)
&#125;

我们将 setState 填上第二个参数,输出更新后的 count

react-extension-demo1-3

这样我们就能成功的获取到最新的数据了,如果有这个需求我们可以在第二个参数输出噢~

函数式 setState

这种用法我也是第一次见,函数式的 setState 也是接收两个参数

第一个参数是 updater ,它是一个能够返回 stateChange 对象的函数

第二个参数是一个回调函数,用于在状态更新完毕,界面也更新之后调用

与对象式 setState 不同的是,我们传递的第一个参数 updater 可以接收到2个参数 stateprops

我们尝试一下

add = () => &#123;
    this.setState((state) => (&#123; count: state.count + 1 &#125;))
&#125;

react-extension-demo2-1

我们也成功的实现了

我们在第一个参数中传入了一个函数,这个函数可以接收到 state ,我们通过更新 state 中的 count 值,来驱动页面的更新

利用函数式 setState 的优势还是很不错的,可以直接获得 stateprops

可以理解为对象式的 setState 是函数式 setState 的语法糖

2. LazyLoad

懒加载在 React 中用的最多的就是路由组件了,页面刷新时,所有的页面都会重新加载,这并不是我们想要的,我们想要实现点击哪个路由链接再加载即可,这样避免了不必要的加载

react-extension-demo2-2

我们可以发现,我们页面一加载时,所有的路由组件都会被加载

如果我们有 100 个路由组件,但是用户只点击了几个,这就会有很大的消耗,因此我们需要做懒加载处理,我们点击哪个时,才去加载哪一个

首先我们需要从 react 库中暴露一个 lazy 函数

import React, &#123; Component ,lazy&#125; from 'react';

然后我们需要更改引入组件的方式

const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))

采用 lazy 函数包裹

image-20210911114307684

我们会遇到这样的错误,提示我们用一个标签包裹

这里是因为,当我们网速慢的时候,路由组件就会有可能加载不出来,页面就会白屏,它需要我们来指定一个路由组件加载的东西,相对于 loading

<Suspense fallback=&#123;<h1>loading</h1>&#125;>
    <Route path="/home" component=&#123;Home&#125;></Route>
    <Route path="/about" component=&#123;About&#125;></Route>
</Suspense>

在做这个案例的时候,一定不要设置重定向的东西,所有的路由我们要点击再加载

初次登录页面的时候

image-20210911115542647

注意噢,这些文件都不是路由组件,当我们点击了对应组件之后才会加载

react-extension-lazyload-2

从上图我们可以看出,每次点击时,才会去请求 chunk 文件

那我们更改写的 fallback 有什么用呢?它会在页面还没有加载出来的时候显示

react-extension-lazyload-3

注意:因为 loading 是作为一个兜底的存在,因此 loading 是 必须提前引入的,不能懒加载

3. Hooks

useState

hooks 解决了函数式组件和类式组件的差异,让函数式组件拥有了类式组件所拥有的 state ,同时新增了一些 API ,让函数式组件,变得更加的灵活

首先我们需要明确一点,函数式组件没有自己的 this

function Demo() &#123;
    const [count, setCount] = React.useState(0)
    console.log(count, setCount);
    function add() &#123;
        setCount(count + 1)
    &#125;
    return (
        <div>
            <h2>当前求和为:&#123;count&#125;</h2>
            <button onClick=&#123;add&#125;>点我加1</button>
        </div>
    )
&#125;
export default Demo

利用函数式组件完成的 点我加1 案例

这里利用了一个 Hook :useState

它让函数式组件能够维护自己的 state ,它接收一个参数,作为初始化 state 的值,赋值给 count,因此 useState 的初始值只有第一次有效,它所映射出的两个变量 countsetCount 我们可以理解为 setState 来使用

useState 能够返回一个数组,第一个元素是 state ,第二个是更新 state 的函数

我们先看看控制台输出的什么

image-20210911123011304

count 是初始化的值,而 setCount 就像是一个 action 对象驱动状态更新

我们可以通过 setCount 来更新 count 的值

setCount(count + 1)

useEffect

在类式组件中,提供了一些声明周期钩子给我们使用,我们可以在组件的特殊时期执行特定的事情,例如 componentDidMount ,能够在组件挂载完成后执行一些东西

在函数式组件中也可以实现,它采用的是 effectHook ,它的语法更加的简单,同时融合了 componentDidUpdata 生命周期,极大的方便了我们的开发

React.useEffect(() => &#123;
    console.log('被调用了');
&#125;)

由于函数的特性,我们可以在函数中随意的编写函数,这里我们调用了 useEffect 函数,这个函数有多个功能

当我们像上面代码那样使用时,它相当于 componentDidUpdatacomponentDidMount 一同使用,也就是在组件挂载和组件更新的时候都会调用这个函数

react-extension-hook-1

它还可以接收第二个参数,这个参数表示它要监测的数据,也就是他要监视哪个数据的变化

当我们不需要监听任何状态变化的时候,我们可以就传递一个空数组,这样它就能当作 componentMidMount 来使用

React.useEffect(() => &#123;
    console.log('被调用了');
&#125;, [])

这样我们只有在组件第一次挂载的时候触发

当然当页面中有多个数据源时,我们也可以选择个别的数据进行监测以达到我们想要的效果

React.useEffect(() => &#123;
    console.log('被调用了');
&#125;, [count])

这样,我们就只监视 count 数据的变化

当我们想要在卸载一个组件之前进行一些清除定时器的操作,在类式组件中,我们会调用生命周期钩子 componentDidUnmount 来实现,在函数式组件中,我们的写法更为简单,我们直接在 useEffect 的第一个参数的返回值中实现即可
也就是说,第一个参数的函数体相当于 componentDidMount 返回体相当于 componentDidUnmount ,这样我们就能实现在组件即将被卸载时输出一些东西了

实现卸载

function unmount() &#123;
    ReactDOM.unmountComponentAtNode(document.getElementById("root"))
&#125;

卸载前输出

React.useEffect(() => &#123;
    console.log('被调用了');
    return () => &#123;
        console.log('我要被卸载了');
    &#125;
&#125;, [count])

react-extension-hook-2

实现了在组件即将被卸载的时候输出

因此 useEffect 相当于三个生命周期钩子,componentDidMountcomponentDidUpdatacomponentDidUnmount

useRef

当我们想要获取组件内的信息时,在类式组件中,我们会采用 ref 的方式来获取。在函数式组件中,我们可以采用也可以采用 ref 但是,我们需要采用 useRef 函数来创建一个 ref 容器,这和 createRef 很类似。

<input type="text" ref=&#123;myRef&#125; />

获取 ref 值

function show() &#123;
    alert(myRef.current.value)
&#125;

即可成功的获取到 input 框中的值

4. Fragment

我们编写组件的时候每次都需要采用一个 div 标签包裹,才能让它正常的编译,但是这样会引发什么问题呢?我们打开控制台看看它的层级

image-20210911151643934

它包裹了几层无意义的 div 标签,我们可以采用 Fragment 来解决这个问题

首先,我们需要从 react 中暴露出 Fragment ,将我们所写的内容采用 Fragment 标签进行包裹,当它解析到 Fragment 标签的时候,就会把它去掉

image-20210911152037120

这样我们的内容就直接挂在了 root 标签下

同时采用空标签,也能实现,但是它不能接收任何值,而 Fragment 能够接收 1 个值key

5. Context

仅适用于类式组件

当我们想要给子类的子类传递数据时,前面我们讲过了 redux 的做法,这里介绍的 Context 我觉得也类似于 Redux

首先我们需要引入一个 MyContext 组件,我们需要引用MyContext 下的 Provider

const MyContext = React.createContext();
const &#123; Provider &#125; = MyContext;

Provider 标签包裹 A组件内的 B 组件,并通过 value 值,将数据传递给子组件,这样以 A 组件为父代组件的所有子组件都能够接受到数据

<Provider value=&#123;&#123; username, age &#125;&#125;>
    <B />
</Provider>

但是我们需要在使用数据的组件中引入 MyContext

static contextType = MyContext;

在使用时,直接从 this.context 上取值即可

const &#123;username,age&#125; = this.context

适用于函数和类式组件

由于函数式组件没有自己 this ,所以我们不能通过 this.context 来获取数据

这里我们需要从 Context 身上暴露出一个 Consumer

const &#123; Provider ,Consumer&#125; = MyContext;

然后通过 value 取值即可

function C() &#123;
  return (
    <div>
      <h3>我是C组件,我从A接收到的数据 </h3>
      <Consumer>
        &#123;(value) => &#123;
          return `$&#123;value.username&#125;,年龄是$&#123;value.age&#125;`;
        &#125;&#125;
      </Consumer>
    </div>
  );
&#125;

image-20210911161103300

因此想要在函数式组件中使用,需要引入 Consumer

6. PureComponent

在我们之前一直写的代码中,我们一直使用的Component 是有问题存在的

  1. 只要执行 setState ,即使不改变状态数据,组件也会调用 render
  2. 当前组件状态更新,也会引起子组件 render

而我们想要的是只有组件的 state 或者 props 数据发生改变的时候,再调用 render

我们可以采用重写 shouldComponentUpdate 的方法,但是这个方法不能根治这个问题,当状态很多时,我们没有办法增加判断

我们可以采用 PureComponent

我们可以从 react 身上暴露出 PureComponent 而不使用 Component

import React, &#123; PureComponent &#125; from 'react'

就这~听了半天结果就只一个 PureComponent

PureComponent 会对比当前对象和下一个状态的 propstate ,而这个比较属于浅比较,比较基本数据类型是否相同,而对于引用数据类型,比较的是它的引用地址是否相同,这个比较与内容无关

7. render props

采用 render props 技术,我们可以像组件内部动态传入带有内容的结构

当我们在一个组件标签中填写内容时,这个内容会被定义为 children props,我们可以通过 this.props.children 来获取

例如:

<A>hello</A>

这个 hello 我们就可以通过 children 来获取

而我们所说的 render props 就是在组件标签中传入一个 render 方法,又因为属于 props ,因而被叫做了 render props

<A render=&#123;(name) => <C name=&#123;name&#125; />&#125; />

你可以把 render 看作是 props,只是它有特殊作用,当然它也可以用其他名字来命名

在上面的代码中,我们需要在 A 组件中预留出 C 组件渲染的位置 在需要的位置上加上{this.props.render(name)}

那我们在 C 组件中,如何接收 A 组件传递的 name 值呢?通过 this.props.name 的方式

8. ErrorBoundary

当不可控因素导致数据不正常时,我们不能直接将报错页面呈现在用户的面前,由于我们没有办法给每一个组件、每一个文件添加判断,来确保正常运行,这样很不现实,因此我们要用到错误边界技术

错误边界就是让这块组件报错的影响降到最小,不要影响到其他组件或者全局的正常运行

例如 A 组件报错了,我们可以在 A 组件内添加一小段的提示,并把错误控制在 A 组件内,不影响其他组件

  • 我们要对容易出错的组件的父组件做手脚,而不是组件本身

我们在父组件中通过 getDerivedStateFromError 来配置子组件出错时的处理函数

static getDerivedStateFromError(error) &#123;
    console.log(error);
    return &#123; hasError: error &#125;
&#125;

我们可以将 hasError 配置到状态当中,当 hasError 状态改变成 error 时,表明有错误发生,我们需要在组件中通过判断 hasError 值,来指定是否显示子组件

&#123;this.state.hasError ? <h2>出错啦</h2> : <Child />&#125;

在服务器中启动,才能正常看到效果

可以在 componentDidCatch 中统计错误次数,通知编码人员进行 bug 解决

9. 组件通信方式总结

  1. props
    • children props
    • render props
  2. 消息发布订阅
    • 利用 pubsub 库来实现
  3. 集中式状态管理
    • redux
  4. conText
    • 生成者-消费者

选择方式

父子组件采用:props

兄弟组件采用:消息的发布订阅、redux

祖孙组件:消息发布订阅、redux、context

React核心 – React-Hooks

hooks 存在的意义

  1. hooks 之间的状态是独立的,有自己独立的上下文,不会出现混淆状态的情况

  2. 让函数有了状态管理

  3. 解决了 组件树不直观、类组件难维护、逻辑不易复用的问题

  4. 避免函数重复执行的副作用

应用场景

  1. 利用 hooks 取代生命周期函数
  2. 让组件有了状态
  3. 组件辅助函数
  4. 处理发送请求
  5. 存取数据
  6. 做好性能优化

hooks API

react 中引入

1. useState

给函数组件添加状态

  • 初始化以及更新组件状态
const [count, setCount] = React.useState(0)

接收一个参数作为初始值,返回一个数组:第一个是状态变量,第二个是修改变量的函数

2. useEffect

副作用 hooks

  • 给没有生命周期的组件,添加结束渲染的信号

注意:

  • render 之后执行的 hooks

第一个参数接收一个函数,在组件更新的时候执行

第二个参数接收一个数组,用来表示需要追踪的变量,依赖列表,只有依赖更新的时候才会更新内容

第一个参数的返回值,返回一个函数,在 useEffect 执行之前,都会先执行里面返回的函数

一般用于添加销毁事件,这样就能保证只添加一个

React.useEffect(() => &#123;
    console.log('被调用了');
    return () => &#123;
        console.log('我要被卸载了');
    &#125;
&#125;, [count])

打印

image-20210914172936843

3. useLayoutEffect

useEffect 很类似

它的作用是:在 DOM 更新完成之后执行某个操作

注意:

  • 有 DOM 操作的副作用 hooks
  • 在 DOM 更新之后执行

执行时机在 useEffect 之前,其他都和 useEffect 都相同

useEffect 执行时机在 render 之后

useLayoutEffect 执行时机在 DOM 更新之后

4. useMemo

作用:让组件中的函数跟随状态更新

注意:优化函数组件中的功能函数

为了避免由于其他状态更新导致的当前函数的被迫执行

第一个参数接收一个函数,第二个参数为数组的依赖列表,返回一个值

const getDoubleNum = useMemo(() => &#123;
    console.log('ddd')
    return 2 * num
&#125;, [num])

5. useCallback

作用:跟随状态更新执行

注意:

  • 只有依赖项改变时才执行
  • useMemo( () => fn, deps) 相当于 useCallback(fn, deps)

不同点:

  1. useCallback 返回的是一个函数,不再是值
  2. useCallback 缓存的是一个函数,useMemo 缓存的是一个值,如果依赖不更新,返回的永远是缓存的那个函数
  3. 给子组件中传递 props 的时候,如果当前组件不更新,不会触发子组件的重新渲染

6. useRef

作用:长久保存数据

注意事项:

  • 返回一个子元素索引,这个索引在整个生命周期中保持不变
  • 对象发生改变时,不通知,属性变更不重新渲染
  1. 保存一个值,在整个生命周期中维持不变
  2. 重新赋值 ref.current 不会触发重新渲染
  3. 相当于创建一个额外的容器来存储数据,我们可以在外部拿到这个值

当我们通过正常的方式去获取计时器的 id 是无法获取的,需要通过 ref

useEffect(() => &#123;
    ref.current = setInterval(() => &#123;
        setNum(num => num + 1)
    &#125;, 400)
&#125;, [])
useEffect(() => &#123;
    if (num > 10) &#123;
        console.log('到十了');
        clearInterval(ref.current)
    &#125;
&#125;, [num])

7. useContext

作用:带着子组件渲染

注意:

  • 上层数据发生改变,肯定会触发重新渲染
  1. 我们需要引入 useContextcreateContext 两个内容
  2. 通过 createContext 创建一个 Context 句柄
  3. 通过 Provider 确定数据共享范围
  4. 通过 value 来分发数据
  5. 在子组件中,通过 useContext 来获取数据
import React, &#123; useContext, createContext &#125; from 'react'
const Context = createContext(null)
export default function Hook() &#123;
    const [num, setNum] = React.useState(1)

    return (
        <h1>
            这是一个函数组件 - &#123;num&#125;
        // 确定范围
            <Context.Provider value=&#123;num&#125;>
                <Item1 num=&#123;num&#125; />
                <Item2 num=&#123;num&#125; />
            </Context.Provider>
        </h1>
    )
&#125;
function Item1() &#123;
    const num = useContext(Context)
    return <div>子组件1  &#123;num&#125;</div>
&#125;
function Item2() &#123;
    const num = useContext(Context)
    return <div>子组件2 &#123;num&#125;</div>
&#125;

8. useReducer

作用:去其他地方借资源

注意:函数组件的 Redux 的操作

  1. 创建数据仓库 store 和管理者 reducer
  2. 通过 useReducer(store,dispatch) 来获取 statedispatch
const store = &#123;
    num: 10
&#125;
const reducer = (state, action) => &#123;
    switch (action.type) &#123;
        case "":
            return
        default:
            return
    &#125;
&#125;
    const [state, dispatch] = useReducer(reducer, store)

通过 dispatch 去派发 action

9. 自定义 hooks

放在 utils 文件夹中,以 use 开头命名

例如:模拟数据请求的 Hooks

import React, &#123; useState, useEffect &#125; from "react";
function useLoadData() &#123;
  const [num, setNum] = useState(1);
  useEffect(() => &#123;
    setTimeout(() => &#123;
      setNum(2);
    &#125;, 1000);
  &#125;, []);
  return [num, setNum];
&#125;
export default useLoadData;

减少代码耦合

我们希望 reducer 能让每个组件来使用,我们自己写一个 hooks

自定义一个自己的 LocalReducer

import React, &#123; useReducer &#125; from "react";
const store = &#123; num: 1210 &#125;;
const reducer = (state, action) => &#123;
  switch (action.type) &#123;
    case "num":
      return &#123; ...state, num: action.num &#125;;
    default:
      return &#123; ...state &#125;;
  &#125;
&#125;;
function useLocalReducer() &#123;
  const [state, dispatch] = useReducer(reducer, store);
  return [state, dispatch];
&#125;
export default useLocalReducer;
  1. 引入 react 和自己需要的 hook
  2. 创建自己的hook函数
  3. 返回一个数组,数组中第一个内容是数据,第二个是修改数据的函数
  4. 暴露自定义 hook 函数出去
  5. 引入自己的业务组件