Redux - 状态管理

Redux - 状态管理

官方文档 redux.org.cn

1、传统MVC框架的缺陷

什么是MVC?

请添加图片描述

MVC的全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。

V即View视图是指用户看到并与之交互的界面。

M即Model模型是管理数据 ,很多业务逻辑都在模型中完成。在MVC的三个部件中,模型拥有最多的处理任务。

C即Controller控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。

MVC只是看起来很美

MVC框架的数据流很理想,请求先到Controller, 由Controller调用Model中的数据交给View进行渲染,但是在实际的项目中,又是允许Model和View直接通信的。然后就出现了这样的结果:

请添加图片描述

2、Flux

在2013年,Facebook让React亮相的同时推出了Flux框架,React的初衷实际上是用来替代jQuery的,Flux实际上就可以用来替代Backbone.jsEmber.js等一系列MVC架构的前端JS框架。

其实FluxReact里的应用就类似于Vue中的Vuex的作用,但是在Vue中,Vue是完整的mvvm框架,而Vuex只是一个全局的插件。

React只是一个MVC中的V(视图层),只管页面中的渲染,一旦有数据管理的时候,React本身的能力就不足以支撑复杂组件结构的项目,在传统的MVC中,就需要用到Model和Controller。Facebook对于当时世面上的MVC框架并不满意,于是就有了Flux, 但Flux并不是一个MVC框架,他是一种新的思想。

请添加图片描述

  • View: 视图层
  • ActionCreator(动作创造者):视图层发出的消息(比如mouseClick)
  • Dispatcher(派发器):用来接收Actions、执行回调函数
  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面

Flux的流程:

  1. 组件获取到store中保存的数据挂载在自己的状态上
  2. 用户产生了操作,调用actions的方法
  3. actions接收到了用户的操作,进行一系列的逻辑代码、异步操作
  4. 然后actions会创建出对应的action,action带有标识性的属性
  5. actions调用dispatcher的dispatch方法将action传递给dispatcher
  6. dispatcher接收到action并根据标识信息判断之后,调用store的更改数据的方法
  7. store的方法被调用后,更改状态,并触发自己的某一个事件
  8. store更改状态后事件被触发,该事件的处理程序会通知view去获取最新的数据

3、Redux

React 只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。有两个方面,它没涉及。

  • 代码结构
  • 组件之间的通信

2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。

如果你不知道是否需要 Redux,那就是不需要它

只有遇到 React 实在解决不了的问题,你才需要 Redux

简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。 *

  • 用户的使用方式非常简单
  • 用户之间没有协作
  • 不需要与服务器大量交互,也没有使用 WebSocket
  • 视图层(View)只从单一来源获取数据

需要使用Redux的项目: **

  • 用户的使用方式复杂
  • 不同身份的用户有不同的使用方式(比如普通用户和管理员)
  • 多个用户之间可以协作
  • 与服务器大量交互,或者使用了WebSocket
  • View要从多个来源获取数据

从组件层面考虑,什么样子的需要Redux: ***

  • 某个组件的状态,需要共享
  • 某个状态需要在任何地方都可以拿到
  • 一个组件需要改变全局状态
  • 一个组件需要改变另一个组件的状态

Redux的设计思想:

  1. Web 应用是一个状态机,视图与状态是一一对应的。
  2. 所有的状态,保存在一个对象里面(唯一数据源)。

注意:flux、redux都不是必须和react搭配使用的,因为flux和redux是完整的架构,在学习react的时候,只是将react的组件作为redux中的视图层去使用了。

redux react anguler …

redux vuex 的区别:

1.redux 是完整的架构、不仅使用在react中 ,还可以用于 angular … ;功能更强大;更复杂,学习成本更高

2.vuex 插件 只能使用于vue,vuex简单易学

Redux的使用的三大原则: ***

  • Single Source of Truth(唯一的数据源)
  • State is read-only(状态是只读的)
  • Changes are made with pure function(数据的改变必须通过纯函数完成)

(1) 自己实现Redux 中高阶的程序员

这个部分,不使用react,直接使用原生的html/js来写一个简易的的redux

基本的状态管理及数据渲染:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 01</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定义一个计数器的状态
    const countState = {
      count: 10
    }

    // 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
    const changeState = (action) => {
      switch(action.type) {
        // 处理减
        case 'COUNT_DECREMENT':
          countState.count -= action.number
          break;
        // 处理加        
        case 'COUNT_INCREMENT':
          countState.count += action.number
          break;
        default:
          break;
      }
    }

    // 定义一个方法用于渲染计数器的dom
    const renderCount = (state) => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = state.count
    }
  
    // 首次渲染数据
    renderCount(countState)

    // 定义一个dispatch的方法,接收到动作之后,自动调用
    const dispatch = (action) => {
      changeState(action)
      renderCount(countState)
    }

  </script>
</body>
</html>

创建createStore方法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 02</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定义一个方法,用于集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用于获取状态
      const getState = () => state
      
      // 定义一个监听器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

       // 定义一个dispatch方法,让每次有action传入的时候返回render执行之后的结果
      const dispatch = (action) => {
        // 调用changeState来处理数据
        changeState(state, action)
        // 让监听器里的所以方法运行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定义一个计数器的状态
    const countState = {
      count: 10
    }
    // 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
    const changeState = (state, action) => {
      switch(action.type) {
        // 处理减
        case 'COUNT_DECREMENT':
          state.count -= action.number
          break;
        // 处理加        
        case 'COUNT_INCREMENT':
          state.count += action.number
          break;
        default:
          break;
      }
    }

    // 创建一个store
    const store = createStore(countState, changeState)
    // 定义一个方法用于渲染计数器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染数据
    renderCount()
    // 监听,只要有dispatch,这个方法就会自动运行
    store.subscribe(renderCount)
  </script>
</body>
</html>

让changeState方法变为一个纯函数

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 03</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定义一个方法,用于集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用于获取状态
      const getState = () => state
      
      // 定义一个监听器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

      // 定义一个dispatch方法,让每次有action传入的时候返回render执行之后的结果
      const dispatch = (action) => {
        // 调用changeState来处理数据
        state = changeState(state, action)
        // 让监听器里的所有方法运行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定义一个计数器的状态
    const countState = {
      count: 10
    }
    // 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
    const changeState = (state, action) => {
      switch(action.type) {
        // 处理减
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 处理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }

    // 创建一个store
    const store = createStore(countState, changeState)
    // 定义一个方法用于渲染计数器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染数据
    renderCount()
    // 监听,只要有dispatch,这个方法就会自动运行
    store.subscribe(renderCount)
  </script>
</body>
</html>

合并state和changeState(最终版)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 04</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定义一个方法,用于集中管理state和dispatch, changeState改名了,专业的叫法是reducer
    const createStore = (reducer) => {
      // 定义一个初始的state
      let state = null
      // getState用于获取状态
      const getState = () => state
      
      // 定义一个监听器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

      // 定义一个dispatch方法,让每次有action传入的时候返回reducer执行之后的结果
      const dispatch = (action) => {
        // 调用reducer来处理数据
        state = reducer(state, action)
        // 让监听器里的所有方法运行
        listeners.forEach(listener => listener())
      }
      //  初始化state
      dispatch({})
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定义一个计数器的状态
    const countState = {
      count: 10
    }
    // 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
    const reducer = (state, action) => {
      // 如果state是null, 就返回countState
      if (!state) return countState
      switch(action.type) {
        // 处理减
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 处理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }

    // 创建一个store
    const store = createStore(reducer)
    // 定义一个方法用于渲染计数器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染数据
    renderCount()
    // 监听,只要有dispatch,renderCount就会自动运行
    store.subscribe(renderCount)
  </script>
</body>
</html>

(2) 使用Redux框架

redux 实现集中式状态管理

a.Redux的流程:

请添加图片描述

  • redux 流程 ***

    1. store 是redux 数据仓库,在里面保存数据信息,
    2. 可以通过 reducer 定义初始状态
    3. 在 组件中 可以触发动作生产者,调用 actions
    4. 使用dispatch 将action动作发送到 store中的 reducers (纯函数)
    5. 在 reducers中 接收旧的状态值和 动作,返回新的状态值
    6. 当状态值发生改变时,会被 subscribe监听到 ,然后触发render函数,重新渲染页面
b. redux核心概念

action ,动作的对象,有两个属性,

​ type:标识属性,值为字符串、唯一,必选参数

​ data:数据属性,值类型任意,可选参数

reducer ,用于初始化状态和加工状态

加工时根据旧的state值和传过来的action,生成新的state的值,是一个纯函数

store,将state、action、reducer联系在一起的对象,相当于一个仓库,可以存放state的状态值。

​ 创建store对象 createStore(reducer);

​ 获取state的值 getState

​ 派发动作 dispatch(action)

​ 注册监听 subscribe() 当产生新的state时,自动调用

c.计数器实例

c1.安装redux库

yarn add redux

npm i -S redux

c2. 创建 store库

store.js

//该文件用于暴露一个store 对象,一个应用只有一个store对象
import { createStore } from 'redux';
import reducer from './reducer';
export default createStore(reducer);

reducer.js

//纯函数
//功能是:修改 store中的state的值
/*
* state = initState:旧的状态值
* action 传过来的动作
* */
//初始化
const initState = 10;
//形参默认值
export default (state = initState, action) => {
    console.log(state, action);
    const { type, data } = action;
    switch (type) {
        case 'INCREMENT':// 增加
            console.log(15);
            return state + data;
        case 'DECREMENT'://减少
            return state - data;
        default:
            return state;
    }
}

App.js

import store from './store';
//getState 获取 store 中的数据
function App() {
  return (
    <div >
      STORE中的数据是:{store.getState()}
      <button onClick={() => { store.dispatch({ type: 'INCREMENT', data: 1 }) }}>++</button>
    </div>
  );
}
export default App;

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';

//所有的页面如果 store 中的值发生变化时都需要重新渲染,所以 监听写在了 更节点上
let render = () => ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

render();
//监听store  是否发生变化,如果变化,自动调用 render
store.subscribe(render);

Reducer必须是一个纯函数:

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。Reducer不是只有Redux里才有,之前学的数组方法reduce, 它的第一个参数就是一个reducer

纯函数是函数式编程的概念,必须遵守以下一些约束。

  • 不得改写参数
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

在纯函数中 ,不得使用 js中 对原来的数组或对象进行修改的 函数 ,例如: push pop shift unshift …

由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。

// State 是一个对象
function reducer(state = defaultState, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state = defaultState, action) {
  return [...state, newItem];
}
d.action的使用

d1. 创建文件夹store

将src/store 和 src/reducer 拖进去

将 src/store/store.js 改名为 src/store/index.js

d2. 创建常量文件

store/contant.js

//定义常量,供reducer和action使用
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

d3. 创建动作文件

store/action.js

import { INCREMENT, DECREMENT } from './constant';
export const incrementAction = data => ({ type: INCREMENT, data })
//-
export function decrementAction(data) {
    return { type: DECREMENT, data }
}

修改 store/reducer

import { INCREMENT, DECREMENT } from './contant';
//初始化
const initState = 10;

//形参默认值
export default (state = initState, action) => {
    console.log(state, action);
    const { type, data } = action;
    switch (type) {
        case INCREMENT:// 增加
            console.log(15);
            return state + data;
        case DECREMENT://减少
            return state - data;
        default:
            return state;
    }
}

修改 App.js

import { incrementAction, decrementAction } from './store/action';

 <button onClick={() => { store.dispatch(incrementAction(5)) }}>++</button>

修改src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';
let render = () =>
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
render();
//检测 store是否有变化,如果变化,自动调用render
store.subscribe(render);

e.异步操作
  1. redux的异步操作 :Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
  2. 如果想要异步操作,需要中间件(middleware)

3.中间件的形成在哪一层比较正常:

请添加图片描述

Redux异步

通常情况下,action只是一个对象,不能包含异步操作,这导致了很多创建action的逻辑只能写在组件中,代码量较多也不便于复用,同时对该部分代码测试的时候也比较困难,组件的业务逻辑也不清晰,使用中间件了之后,可以通过actionCreator异步编写action,这样代码就会拆分到actionCreator中,可维护性大大提高,可以方便于测试、复用,同时actionCreator还集成了异步操作中不同的action派发机制,减少编码过程中的代码量

常见的异步库:

  • Redux-thunk
  • Redux-saga
  • Redux-effects
  • Redux-side-effects
  • Redux-loop
  • Redux-observable

基于Promise的异步库:

  • Redux-promise
  • Redux-promises
  • Redux-simple-promise
  • Redux-promise-middleware

建议使用 redux-logger 日志

[1].安装 redux-thunk yarn add redux-thunk 或 cnpm i -S redux-thunk

安装 redux-logger yarn add redux-logger 或 cnpm i -S redux-logger

[2].修改 store/index.js

import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';
import logger  from 'redux-logger';
export default createStore(reducer, applyMiddleware(thunk,logger));

[3].修改 store/action.js

//延时加操作,异步操作,必须是一个函数,会报错,缺异步中间件
//异步action中一般都会调用同步
export const incrementAsyncAction = (data) => {
    //store 会帮忙传过来一个dispatch
    return (dispatch) => {
        setTimeout(() => {
            dispatch(incrementAction(data));
            //store.dispatch(incrementAction(data));
        }, 1000)
    }
}

修改 App.js

//引入 action
import { incrementAction, decrementAction, incrementAsyncAction } from './store/count_action';

 <button onClick={() => { store.dispatch(incrementAsyncAction(5)) }}> 延时++</button>

安装 logger 中间件

yarn add redux-logger redux-devtools-extension

cnpm i -S redux-logger redux-devtools-extension

(5)安装开发者工具

redux-devtools-extension

在store引入

import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';

//创建仓库,需要纯函数作为 仓库的参数
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk, logger)));

logger 中间件需要添加在允许中间的最后面

函数式组件、类组件、受控组件、非受控组件、有状态组件、无状态组件、高阶组件、容器组件、UI组件

(3) 容器组件(Smart/Container Components)和展示组件 ui组件(Dumb/Presentational Components)

展示组件ui组件容器组件
作用描述如何展现(骨架、样式)描述如何运行(数据获取、状态更新、逻辑)
直接使用 Redux
数据来源props监听 Redux state
数据修改从 props 调用回调函数向 Redux 派发 actions
调用方式手动通常由 React Redux 生成

(4) 使用react-redux

请添加图片描述

  1. React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。
  2. UI 组件有以下几个特征。 ***
    只负责 UI 的呈现,不带有任何业务逻辑
    没有状态(即不使用this.state这个变量)
    所有数据都由参数(this.props)提供
    不使用任何 Redux 的 API
  3. 容器组件的特征 ***
    负责管理数据和业务逻辑,不负责 UI 的呈现
    带有内部状态
    使用 Redux 的 API
    总结:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑
    如果一个组件既有 UI 又有业务逻辑, 将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
  4. React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它

[0] 将 原来的 src重新保存一份

[1].安装 yarn add react-redux 或 cnpm i -S react-redux

使用 connect 将ui组件转容器组件

[2].

新建 容器组件 containers/Count.js

//容器组件
import React, { Component } from 'react'
//引入 react和redux关联的插件 ,可以将 ui组件转为容器组件
import { connect } from 'react-redux';
//引入 动作
import { incrementAction, decrementAction } from '../store/action';
class Count extends Component {
    render() {
        return (
            <div>
                <h2>计数器</h2>
                {this.props.state}
                <button onClick={() => { this.props.incrementAction(5) }}>++</button>
                <button onClick={() => { this.props.incrementAction(3) }}>--</button>
            </div>
        )
    }
}

//connect 的第一个参数:  state => ({ state })  将 容器组件的state  和  ui组件的 props  关联起来  
//connect 的第二各参数:  store中的动作 和 ui组件中的  props里面的函数进行关联
/*
(dispatch) {
    return {
        increment: number => dispatch(incrementAction(number)),
        incrementAsync: number => dispatch(incrementAsyncAction(number))
    }
}

*/
//可以写成是函数式的写法
// export default connect(state => {
//     console.log(state);
//     return { state: state }
// }, (dispatch) => {
//     return {
//         incrementAction: number => dispatch(incrementAction(number)),
//         decrementAction: number => dispatch(decrementAction(number)),
//     }
// })(Count);


// export default connect(state => ({ state: state }), {
//     incrementAction: incrementAction,
//     decrementAction: decrementAction,

// })(Count);

//简写模式
export default connect(state => ({ state }), {
    incrementAction,
    decrementAction,
})(Count);

[3].修改App.js

import React, { Component } from 'react';
import store from './store';
//引入 action
import Count from './containers/Count';
class App extends Component {
  render() {
    return (<div>
      <Count store={store} />
    </div >);
  }
}
//将store作为参数传递
export default App;

下一步,就需要使用Provider 完成store可以在项目的任何一个组件中调用

react-redux提供两个核心的api:

  • Provider: 提供store

  • 如果使用了 Provider , subscribe就可以删除不用了

  • connect: 用于连接容器组件和展示组件

    • Provider

      根据单一store原则 ,一般只会出现在整个应用程序的最顶层。

    • connect

      语法格式为

      connect(mapStateToProps?, mapDispatchToProps?)(component)

      一般来说只会用到前面两个,它的作用是:

      • store.getState()的状态转化为展示组件的props
      • actionCreators转化为展示组件props上的方法

修改 src/index.js

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


修改 src/App.jsx

//1.引入 store
// import store from './store';

//引入 容器组件
import Count from './containers/Count';
function App() {
  return (
    <div className="App">
      {/* <Count store={store} /> */}
      <Count />
    </div>
  );
}

export default App;

只要上层中有Provider组件并且提供了store, 那么,子孙级别的任何组件,要想使用store里的状态,都可以通过connect方法进行连接。如果只是想连接actionCreators,可以第一个参数传递为null

(6)todolist实例,

需要用到store树的合并

修改一下目录结构

store/actions/里面放动作

count.js 计数器的动作

todolist.js 列表数据

store/reducers/里面放纯函数

count.js 计数器纯函数

todolist.js 列表纯函数

index.js 用于store树对象合并

store/index.js 仓库

store/contant.js 常量

6.1 将 上个实例的 src/store/index.js 代码拷贝过来

//redux 的仓库
//redux的仓库,相当于redux的大脑
//引入 创建store的包
//createStore 创建 redux的仓库
//applyMiddleware 允许使用 中间件
import { createStore, applyMiddleware } from 'redux';
//引入 纯函数的文件
-+ import reducer from './reducers';
//引入日志的中间件
import logger from 'redux-logger';
//引入 执行异步操作的 中间件     
import thunk from 'redux-thunk';
//redux的开发者工具
import { composeWithDevTools } from 'redux-devtools-extension';
//创建 store  并输出,logger的中间件写在允许的中间件的最后面
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk, logger)));

6.2 将上个实例的 src/store/action.js 拷贝到 src/store/actions/count.js

//计算器的动作
//动作

-+ import { INCREMENT, DECREMENT } from '../contant';
//计数器的动作
//加加
export function incrementAction(data) {
    return { type: INCREMENT, data }
}

//减减
export function decrementAction(data) {
    return { type: DECREMENT, data }
}

//延时加加
export function incrementAsyncAction(data) {
    //重写dispatch
    return dispatch => {
        setTimeout(() => {
            dispatch(incrementAction(data));
        }, 2000)
    }
}

//延时减减
export function decrementAsyncAction(data) {
    //重写dispatch
    return dispatch => {
        setTimeout(() => {
            dispatch(decrementAction(data));
        }, 2000)
    }
}

6.3 将 src/store/contant.js 直接拷贝过来

//常量统一管理
export const INCREMENT = "INCREMENT";//加
export const DECREMENT = "DECREMENT";//减

6.4 将 src/store/reducer.js 拷贝到 src/store/reducers/count.js

//计算器的纯函数
//纯函数
//功能:给 state设置初始值,修改 state的状态值
//初始化
/*
参数:
第一个参数:旧的状态值
第二个参数:动作  action
action中有两个属性 ,1:type:动作类型;2:在组件中传过来的值
*/
//引入常量
-+ import { INCREMENT, DECREMENT } from '../contant';
const initState = 1;
const reducer = (state = initState, action) => {
    console.log(12, action);
    switch (action.type) {
        case INCREMENT: //增加  加一
            return state + action.data;
        case DECREMENT:
            return state - action.data;
        default:
            return state;
    }
}
export default reducer;
//纯函数: 没有输入输出语句、没有调用不纯的函数(比如: Math.random,  var data = new  Date()  data.now()  )  。都叫做纯函数
  1. 5添加了 src/store/reducers/index.js
//做状态数的合并
import { combineReducers } from "redux";
import count from './count';
export default combineReducers({
    count
})

6.6 修改了 src/containers/Count.js

//容器组件
import React, { Component } from 'react'
//引入 react和redux关联的插件 ,可以将 ui组件转为容器组件
import { connect } from 'react-redux';
//引入 动作
-+ import { incrementAction, decrementAction } from '../store/actions/count';
class Count extends Component {
    render() {
        return (
            <div>
                <h2>计数器</h2>
              -+  {this.props.count}
                <button onClick={() => { this.props.incrementAction(5) }}>++</button>
                <button onClick={() => { this.props.incrementAction(3) }}>--</button>
            </div>
        )
    }
}

//简写模式
-+ export default connect(state => { console.log(state); return { count: state.count } }, {
    incrementAction,
    decrementAction,
})(Count);

添加 列表的 store

6.7 添加常量 src/store/contant.js

//常量统一管理
export const INCREMENT = "INCREMENT";//加
export const DECREMENT = "DECREMENT";//减

//列表的常量
+ export const ADD_TODOLIST = "ADD_TODOLIST";

6.8 添加列表的纯函数

src/store/reducers/todolist.js

//列表的纯函数
//引入常量
import { ADD_TODOLIST } from '../contant';
const initTodolist = ["吃饭"];
/*
参数:1:旧的状态值
2:动作
*/
const todolist = (state = initTodolist, action) => {
    const { type, data } = action;
    switch (type) {
        case ADD_TODOLIST:
            //将data追加到 state 的后面
            return [...state, data];
        default:
            return state;
    }
}

export default todolist;

6.9 需要修改 src/store/reducers/index.js

//做状态数的合并
import { combineReducers } from "redux";
import count from './count';
+ import todolist from './todolist';
export default combineReducers({
    count,
+    todolist
})

6.10 添加 todolist的动作

src/store/actions/todolist.js

//列表的动作
import { ADD_TODOLIST } from "../contant";

export const todolistAction = data => {
    return { type: ADD_TODOLIST, data }
}

6.11 写 todolist列表的页面

//容器组件
import React, { Component } from 'react'
//引入 connect   将ui组件转容器组件
import { connect } from 'react-redux';
//引入动作
import { todolistAction } from '../store/actions/todolist';

class Todolist extends Component {
    constructor() {
        super();
        this.state = {
            todoText: ""
        }
    }
    changeText = (ev) => {
        this.setState({
            todoText: ev.target.value
        })
    }
    render() {
        const { todoText } = this.state;
        const { list, todolistAction } = this.props;
        return (
            <div>
                <h3>todolist 列表</h3>
                <input type="text" value={todoText} onChange={this.changeText} />
                <button onClick={() => { todolistAction(todoText) }}>添加</button>
                <ul>
                    {
                        list.map((item, index) => <li key={index}>{item}</li>)
                    }
                </ul>
            </div>
        )
    }
}

//参数  
//参数1:state和 props的映射
//参数2:state里面的 动作和  props里面的函数的映射 
export default connect(state => {
    return { list: state.todolist }
}, { todolistAction })(Todolist);

6.12 将页面添加到 App

//1.引入 store
// import store from './store';

//引入 容器组件
import Count from './containers/Count';
+import Todolist from './containers/Todolist';
function App() {
  return (
    <div className="App">
      {/* <Count store={store} /> */}
      <Count />

      <hr />
   +   <Todolist />
    </div>
  );
}

export default App;


(7)项目中redux的使用

如果仅将共享数据放在redux中管理,当页面出现问题时,定位问题时,需要全部检查,违背了代码出错的可调节性。

如果将数据统一放在redux中管理,数据的处理方式是一致的,action – store – reducer,可以快速定位问题。

考虑项目扩展,应该统一存放。

redux存储消耗内存? Chrome浏览器可以存储5个G的内存。

redux存储数据太多,会臃肿? redux 可以拆分成许多小的reducer。

1.安装并搭建redux

安装依赖

yarn add redux react-redux redux-devtools-extension redux-thunk redux-logger

2.使用redux完成购物车

创建常量文件 store/constants.js

export const ADD_CART = "ADD_CART";

创建 store /index.js

import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers/index';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk, logger)));

创建 reducers/cart.js

//购物车纯函数
import { ADD_CART} from '../constants';

const initCartlist = [];
const cartlist = (state = initCartlist, action) => {
    const { type, data } = action;
    console.log('reducer7', action);
    switch (type) {
        case ADD_CART://添加
            return [...state, data];
        default:
            return state;
    }
}
export default cartlist;

创建 actions/cart.js

//购物车的动作
import { ADD_CART } from '../constants';
//添加购物车动作
export const cartlistAction = data => {
    console.log('action5', data);
    return {
        type: ADD_CART, data
    }
}

创建了 store/reducers/index.js

//合并 reducer的
import { combineReducers } from "redux";
import cartlist from "./cart";
export default combineReducers({
    cartlist
})

修改 src/index.js

import App from './router';
import reportWebVitals from './reportWebVitals';
+import { Provider } from "react-redux";
+import store from './store';
//引入 antd   reset  flexible
import 'reset-css';
import 'react-flexible';
import 'antd/dist/antd.min.css';
import './index.css';

ReactDOM.render(
+  <Provider store={store}>
    <App />
+  </Provider>
  ,
  document.getElementById('root')
);

将 ui组件转容器组件 src/pages/item/index.jsx

import React, { PureComponent } from 'react';
import axios from 'axios';
import { Button } from 'antd';
import "./index.scss";

//引入动作cartlistAction
import { cartlistAction } from '../../store/actions/cart';
//将ui组件转容器组件
import { connect } from 'react-redux';

class Item extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            itemObj: {}
        }
    }

    componentDidMount() {
        axios.post("/appapi/root-ceo-commodity/commodityApi/commodityDetail", {
            channelId: null,
            isWholeSale: 0,
            memberKey: "",
            ptKey: this.props.match.params.id
        }).then(
            res => {
                console.log(res);
                this.setState({ itemObj: res.data.data.templateInfo })
            }
        )
    }
    addShop = () => {
        const token = localStorage.getItem("token");
        const { ptKey, name, listImages, retailPrice } = this.state.itemObj;
        if (token != null) {//判断登录状态
            //redux流程实现购物车
            console.log("实现购物车");
            //添加到购物车中的数据整理
            const data = {
                id: ptKey,
                name: name,
                img: listImages,
                price: retailPrice,
                num: 1
            };
            //调用 添加购物车的动作
            this.props.cartlistAction(data);
        } else {
            //跳转到登录页
            this.props.history.push("/login");
        }
    }
    render() {
        console.log(this.props.match.params.id);
        const { itemObj } = this.state;
        return (<div>内容页
            {/* 如果异步数据,导致有些图片会报错, */}
            {
                itemObj !== undefined ? <img src={itemObj.listImages} alt={itemObj.name} className="item_img" /> : ''
            }
            <Button type="primary" onClick={this.addShop}>加入购物车</Button>

            <div className="gohome" onClick={() => { this.props.history.push('/main') }}>首页</div>
        </div>);
    }
}

export default connect(state => ({ cartlist: state.cartlist }), { cartlistAction })(Item);

做购物车更新操作第二次以上的加一

//购物车纯函数
import { ADD_CART, EDIT_CART } from '../constants';

const initCartlist = [];
const cartlist = (state = initCartlist, action) => {
    const { type, data } = action;
    console.log('reducer7', action);
    switch (type) {
        case ADD_CART://添加
            let { item_id } = data;
            let index = state.findIndex((value) => value.item_id === item_id);
            if (index !== -1) {
                state[index].num++;
                return [...state];
            } else {
                return [...state, data]
            }
        default:
            return state;
    }
}
export default cartlist;

购物车页面获取数据列表

pages/cart/index.jsx

import React, { PureComponent } from 'react';
import { Redirect } from 'react-router';
import { connect } from 'react-redux';

class Cart extends PureComponent {

    render() {
        const { cartlist } = this.props;
        return (
            localStorage.getItem("token") !== null ? <div>购物车
            <ul>
                    {cartlist.map(item => <li key={item.id}>{item.name}-{item.price}-{item.num}</li>)}
                </ul>
            </div> : <Redirect to="/login" />
        );
    }
}

export default connect(state => ({ cartlist: state.cartlist }))(Cart);

3.使用redux-thunk完成各种数据的获取及统一保存管理

添加常量 store/constants.js


//获取data数据的常量
export const GET_CATE_NAV_LIST = "GET_CATE_NAV_LIST";

添加纯函数 store/reducers/wzdata.js

//旺仔数据 纯函数
import { GET_CATE_NAV_LIST } from '../constants';
//初始化
const initdata = {};
//纯函数
const wzdata = (state = initdata, action) => {
    const { type, data } = action;
    switch (type) {
        case GET_CATE_NAV_LIST://分类的菜单数据
            //assign 对象合并
            return Object.assign({}, state, { cateNavList: data });
        default:
            return state;
    }
}

export default wzdata;

添加动作 store/actions/wzdata.js

//获取数据的 动作
import { GET_CATE_NAV_LIST } from '../constants';
import axios from 'axios';

//获取分类菜单列表的数据
export function fetchCateNavListAction() {
    return dispatch => {
        return axios.post("/appapi/root-ceo-commodity/categoryApi/categoryList", {
            channelId: null,
            isWholeSale: 0
        }).then(res => {
            console.log(res);
            //将数据提交给reducer
            dispatch({ type: GET_CATE_NAV_LIST, data: res.data.data.categoryInfo })
        })
    }
}

修改 store/reducers/index.js

//合并 reducer的
import { combineReducers } from "redux";
import cartlist from "./cart";
import wzdata from "./wzdata";
export default combineReducers({
    cartlist,
    wzdata
})

修改首页的ui组件为容器组件 src/pages/category/index.jsx

import React, { PureComponent } from 'react';
//ui转容器
import { connect } from 'react-redux';
//引入动作
import { fetchCateNavListAction } from "../../store/actions/wzdata";

import "./cate.scss";
import axios from 'axios';
class Category extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            catkey: "1",//获取数据是的id号
            cateList: []
        }
    }

    componentDidMount() {
        //调用 redux 中的动作
        this.props.fetchCateNavListAction();

        //获取右侧列表数据
        this.getListData(1);
    }
    //改变catkey值
    changeCodeKey = (code) => {
        this.setState({ catkey: code });
        //console.log(27, this.state.catkey);
        this.getListData(code);
    }
    getListData = (code) => {
        console.log(code);
        axios.post("/appapi/root-ceo-commodity/categoryApi/commoditiesByCategory", {
            catKey: code,
            ceoStatus: 0,
            channelId: null,
            page: 1,
            pageSize: 1000
        }).then(res => {
            console.log(32, res);
            this.setState({
                cateList: res.data.data.commodityInfo
            })
        })
    }

    render() {
        const { catkey, cateList } = this.state;
        const { cateNavList } = this.props;
        return (<div className="cate_con">
            <div className="cate_left">
                <ul>
                    {
                        cateNavList.map((item) => <li key={item.code} onClick={() => { this.changeCodeKey(item.code) }} className={catkey === item.code ? "active" : ""}>
                            {item.code}{item.displayName}</li>)
                    }
                </ul>

            </div>
            <div className="cate_right">
                <ul>
                    {
                        cateList.map((item) => <li key={item.key}>
                            {item.name}

                        </li>)
                    }
                </ul>

            </div>
        </div>);
    }
}

export default connect(state => ({ cateNavList: state.wzdata.cateNavList || [] }), { fetchCateNavListAction })(Category);

对 分类页面的分类数据进行 redux 统一管理

添加 常量 store/constants.js

//获取某一类别下面的商品列表
export const GET_CATE_LIST = "GET_CATE_LIST";

添加 store/reducers/wzdata.js

//旺仔数据 纯函数
-+ import { GET_CATE_NAV_LIST, GET_CATE_LIST } from '../constants';
//初始化
const initdata = {};
//纯函数
const wzdata = (state = initdata, action) => {
    const { type, data } = action;
    switch (type) {
        case GET_CATE_NAV_LIST://分类的菜单数据
            //assign 对象合并
            return Object.assign({}, state, { cateNavList: data });
 +       case GET_CATE_LIST://分类的数据
 +           return Object.assign({}, state, { cateList: data });
        default:
            return state;
    }
}

export default wzdata;

添加了动作 src/store/actions/wzdata.js

-+ import { GET_CATE_NAV_LIST, GET_CATE_LIST } from '../constants';

//添加下面的函数
//获取分类列表的数据
export function fetchCateListAction(code) {
    return dispatch => {
        return axios.post("/appapi/root-ceo-commodity/categoryApi/commoditiesByCategory", {
            catKey: code,
            ceoStatus: 0,
            channelId: null,
            page: 1,
            pageSize: 1000
        }).then(res => {
            console.log(res);
            //将数据提交给reducer
            dispatch({ type: GET_CATE_LIST, data: res.data.data.commodityInfo })
        })
    }
}

修改 页面 src/pages/category/index.jsx

import React, { PureComponent } from 'react';
//ui转容器
import { connect } from 'react-redux';
//引入动作
import { fetchCateNavListAction, fetchCateListAction } from "../../store/actions/wzdata";

import "./cate.scss";
import axios from 'axios';
class Category extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            catkey: "1",//获取数据是的id号
        }
    }
    componentDidMount() {
        //调用 redux 中的动作
        this.props.fetchCateNavListAction();
        //获取右侧列表数据
        this.props.fetchCateListAction(1);
    }
    //改变catkey值
    changeCodeKey = (code) => {
        this.setState({ catkey: code });
        this.props.fetchCateListAction(code);
    }
    render() {
        const { catkey } = this.state;
        const { cateNavList, cateList } = this.props;
        return (<div className="cate_con">
            <div className="cate_left">
                <ul>
                    {
                        cateNavList.map((item) => <li key={item.code} onClick={() => { this.changeCodeKey(item.code) }} className={catkey === item.code ? "active" : ""}>
                            {item.code}{item.displayName}</li>)
                    }
                </ul>
            </div>
            <div className="cate_right">
                <ul>
                    {
                        cateList.map((item) => <li key={item.key}>
                            {item.name}

                        </li>)
                    }
                </ul>
            </div>
        </div>);
    }
}

export default connect(state => ({
    cateNavList: state.wzdata.cateNavList || [],
    cateList: state.wzdata.cateList || [],
}), { fetchCateNavListAction, fetchCateListAction })(Category);

数据持久化处理 redux-persist

先安装

yarn add redux-persist

修改 src/store/store.js代码:

import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers/index';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import { composeWithDevTools } from 'redux-devtools-extension';
const persistConfig = {
    key: 'root',
    storage: storage,
    stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况
};
const myPersistReducer = persistReducer(persistConfig, reducers)
const store = createStore(myPersistReducer, composeWithDevTools(applyMiddleware(thunk, logger)))
export const persistor = persistStore(store)
export default store

修改 src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './router';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './store/store';
//引入 css样式
import "reset-css";
import "react-flexible";
import "antd/dist/antd.min.css";
import { persistor } from './store/store'
import { PersistGate } from 'redux-persist/lib/integration/react';
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate >
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
reportWebVitals();

以上实例,redux-thunk 异步操作和同步操作混在一起,redux-saga会将异步操作放在一起统一管理。把异步操作抽出来放在同一个地方统一管理。

saga解决的是异步action的问题

redux-thunk的缺点

thunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action。

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

action不易维护的原因:

I)action的形式不统一

II)就是异步操作太为分散,分散在了各个action中

redux-saga的使用

在redux-saga中,action是plain object(原始对象),并且集中处理了所有的异步操作。

redux-saga的优缺点

优点:

(1)集中处理了所有的异步操作,异步接口部分一目了然

(2)action是普通对象,这跟redux同步的action一模一样

(3)通过Effect,方便异步接口的测试

(4)通过worker 和watcher可以实现非阻塞异步调用,并且同时可以实

现非阻塞调用下的事件监听

(5) 异步操作的流程是可以控制的,可以随时取消相应的异步操作。

缺点:太复杂,学习成本较高
————————————————

(8) redux-saga

a.介绍redux-saga

学习资料推荐:

https://redux-saga-in-chinese.js.org/

https://segmentfault.com/a/1190000007248878

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。

redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。 通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。(有点像 async/await,但 Generator 还有一些更棒而且我们也需要的功能)。

你可能已经用了 redux-thunk 来处理数据的读取。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。

b.generator语法

https://jsonplaceholder.typicode.com 提供模拟数据

/*generator  实际应用,在 redux上 使用的非常多*/
/*
generator(生成器),是属于es6的扩展语法。解决了两方面的问题:1.迭代器(循环) 2.解决异步编程的回调地狱问题
generator 可以认为是一个状态机,内部封装了多种状态,等着用户去调用
语法:1.函数名前面需要加星号;2.函数内部需要使用 yield关键字返回值
    generator函数的调用,必须使用 next方法
*/
//一.generator基础
//1.实例
// function * hello (){
//  console.log("这是第一个生成器实例");
// }
// //调用
// //hello();//必须进一步调用 next,才能出发方法
// hello().next();

// //2.实例2
// function * hello2(){
//  yield "hello";
//  yield "hi";
//  return;
// }
// var h = hello2();
// console.log(h.next());//?{value: "hello", done: false}
// console.log(h.next());//?{value: "hi", done: false}
// console.log(h.next());//?{value: undefined, done: true}
//总结:每次执行.next();都会弹出一个最新的状态值;如果弹出的状态值为undefined时,就表示迭代器已经完成

//二.generator 和迭代器
//1.实例
// function * hi(){
//  yield "hello";
//  yield "hi";
//  yield "11111";
//  yield "aaaa";
//  return;
// }
// var h = hi();
// for(var value of h){
//  console.log(value); //hello  hi  11111 aaaa
// }
//迭代器,每次输出 value的值,如果迭代结束就不再输出了,不会输出   undefined

//2.实例
console.log(111);
function* gen(x, y) {
    let z = yield x + y;
    console.log("z:" + z);
    let result = yield z * x;
    return result;
}
var g = gen(5, 6);
console.log(g.next());//?{value: 11, done: false}
console.log(g.next());//?{value: NaN, done: false}  第一次的x+y的值没有保存,第二次调用时z的值是 undefined


//function * gen(x,y){
//			   let z = yield x+y;
//			   console.log("z:"+z);
//			   let result = yield z*x;
//			   return result;
//}
//var g = gen(5,6);
//console.log(g.next());//?{value: 11, done: false}
//console.log(g.next(11));//next()里面可以传参数

// function * gen(x,y){
// 			   let z = yield x+y;
// 			   console.log("z:"+z);
// 			   let result = yield z*x;
// 			   return result;
// }
// var g = gen(10,20);
// let nn = g.next();
// console.log(nn);//?{value: 11, done: false}
// console.log(g.next(nn.value));//next()里面可以传参数
//总结:nn.value 就解决了无论上次的运行结果是什么,都可以作为下一次的值传入

//3.return 的使用 
//function * hi(){
//	   yield "hello";
//	   yield "hi";
//	   yield "11111";
//	   yield "aaaa";
//	   return;
//}

//var h = hi();
//console.log(h.next());//hello
//console.log(h.next());//hi
//console.log(h.return());//{value: undefined, done: true}
//console.log(h.next());//{value: undefined, done: true}
//console.log(h.next());//{value: undefined, done: true}
//如果 vlaue是return 传入的,那么后面的yield都不再执行了

//三.异步编程 解决回调地狱问题
//举例:吃鸡
//模拟的回调地狱的问题
// setTimeout(function(){
//  console.log("准备 鸡肉,洗好、切块");
//  setTimeout(function(){
//  		console.log("炒鸡");
// setTimeout(function(){
// 		console.log("炖鸡");
// 		setTimeout(function(){
// 				console.log("加调料");
// 				setTimeout(function(){
// 						console.log("吃鸡");
// 				},5000);
// 		},1000);
// },3000);
//  },2000);
// },1000);

//使用generator改写以上程序
//准备工作
function zhunbei(success) {
    setTimeout(function () {
        console.log("1.准备 鸡肉,洗好、切块");
        success();
    }, 2000);
}
//炒鸡
function chaoji(success) {
    setTimeout(function () {
        console.log("2.炒鸡");
        success();
    }, 2000);
}
//炖鸡
function dunji(success) {
    setTimeout(function () {
        console.log("3.炖鸡");
        success();
    }, 2000);
}
//加调料
function tiaoliao(success) {
    setTimeout(function () {
        console.log("4.加调料");
        success();
    }, 2000);
}
//吃鸡
function eat(success) {
    setTimeout(function () {
        console.log("5.吃鸡");
        success();
    }, 2000);
}

//流程控制
//递归完成
function run(fn) {
    const gen = fn();
    function next() {
        const result = gen.next();
        if (result.done) return;//吃鸡的流程结束了
        result.value(next);//next作为入参,在执行下一个工序
    }
    next();
}

//工序
function* task() {
    yield zhunbei;
    yield chaoji;
    yield dunji;
    yield tiaoliao;
    yield eat;
}

//调用
run(task);
//总结: generator 迭代器,解决回调地狱  redux (dva)

c.redux-saga 的辅助函数

put:对应的是middleware中的dispatch方法,可以发送一个动作

call: 表示异步调用,其中call表示的是阻塞调用,

fork:表示异步调用,表示的是非阻塞调用。

take:监听action,返回监听到的action对象

delay:延时

takeEvery:予以相当于 on ,允许并发action

all:创建effect的描述信息,用来命令中间件,并行多个effect,并等待他们全部完成

d.redux-saga的使用计数器

安装redux-saga

yarn add redux-saga

目录结构:

src/index.js

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

src/App.js

import React, { Component } from 'react';
//引入 action
import Count from './containers/Count';
class App extends Component {
  render() {
    return (<div>
      <Count />
    </div >);
  }
}
export default App;

src/containers/Count.jsx

import React, { Component } from 'react';
import { incrementAction, incrementAsync } from '../store/actions/count';
import { connect } from 'react-redux';
//count ui组件
class Count extends Component {
    constructor(props) {
        super(props);
        this.state = {}
    }
    render() {
        console.log(this.props);
        return (
            <div>
                <h1>计数器</h1>
                <button onClick={() => { this.props.increment(5) }}>++</button>
                <button onClick={() => { this.props.incrementAsync(3) }}> 延时++</button>
                {this.props.count}
            </div>
        );
    }
}
export default connect(
    state => ({ count: state.count }),
    {
        increment: incrementAction,//简写方式,可以是一个对象,react-redux可以自动分发|api层级的优化
        incrementAsync: incrementAsync
    }
)(Count);

src/store/store.js

//该文件用于暴露一个store 对象,一个应用只有一个store对象
import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers/index';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
//引入saga
import createSagaMiddleware from 'redux-saga';
import rootSaga from './saga';
import { composeWithDevTools } from 'redux-devtools-extension'
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunk, sagaMiddleware, logger)));
sagaMiddleware.run(rootSaga);
export default store;

src/store/constant.js

//定义常量,供reducer和action使用
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const INCREMENT_ASYNC = "INCREMENT_ASYNC";
export const DECREMENT_ASYNC = "DECREMENT_ASYNC";

src/store/saga.js

//所有的异步操作,我们写在这里
//计数器的延时处理
//引入saga的工具方法
import { put, call, take, fork, delay, takeEvery, all } from 'redux-saga/effects';
import {
    INCREMENT, DECREMENT, INCREMENT_ASYNC, DECREMENT_ASYNC
} from './constant';

function* incrementAsync(data) {
    yield delay(2000);//首先延时2秒钟
    yield put({ type: INCREMENT, data: data.data });//发送加一的动作给 纯函数
}
function* decrementAsync() {
    yield delay(2000);//首先延时2秒钟
    yield put({ type: DECREMENT });//发送加一的动作给 纯函数
}
//监听延时加一
export function* watchCountAsync() {
    //延时加一
    yield takeEvery(INCREMENT_ASYNC, incrementAsync);
    yield takeEvery(DECREMENT_ASYNC, decrementAsync);
}

export default function* rootSaga() {
    yield all([
        watchCountAsync()
    ])
}
/*
put:对应的是middleware中的dispatch方法,可以发送一个动作
call: 表示异步调用,其中call表示的是阻塞调用,
fork:表示异步调用,表示的是非阻塞调用。
take:监听action,返回监听到的action对象
delay:延时
takeEvery:予以相当于 on ,允许并发action
all:创建effect的描述信息,用来命令中间件,并行多个effect,并等待他们全部完成
*/
/*
   *   yield  是generator的语法结构
*/

src/store/reducers/count.js

//纯函数
//功能是:修改 store中的state的值
/*
* state = initState:旧的状态值
* action 传过来的动作
* */
import { INCREMENT, DECREMENT, INCREMENT_ASYNC, DECREMENT_ASYNC } from '../constant';
//初始化
const initState = 10;
//形参默认值
const count = (state = initState, action) => {
    console.log(state, action);
    const { type, data } = action;
    switch (type) {
        case INCREMENT:// 增加
            console.log(15);
            return state + data;
        case DECREMENT://减少
            return state - data;
        default:
            return state;
    }
}
export default count;

src/store/reducers/index.js

//合并状态树
import { combineReducers } from 'redux';
import count from './count';
export default combineReducers({
    count
})

src/store/actions/count.js

import { INCREMENT, DECREMENT, INCREMENT_ASYNC } from '../constant';
export const incrementAction = data => ({ type: INCREMENT, data })
export function decrementAction(data) {
    return { type: DECREMENT, data }
}
export function incrementAsync(data) {
    return { type: INCREMENT_ASYNC, data }
}

e.redux-sage的使用获取数据

安装依赖 yarn add axios

配置跨域 “proxy”: “https://h5.watsons.com.cn”,

修改 store/constant.js

export const FETCH_BAOKUAN_DATA = "FETCH_BAOKUAN_DATA";//保存到reducer中的type
export const BAOKUAN_DATA = "BAOKUAN_DATA";//执行获取数据异步操作的type

修改saga.js,分成三个文件

store/saga/index.js

import {  all } from 'redux-saga/effects';
import { watchCountAsync } from './count';
import { watchQcsData } from './qcsdata';
export default function* rootSaga() {
    yield all([
        watchCountAsync(),
        watchQcsData()
    ])
}

store/saga/count.js

import { put, delay, takeEvery } from 'redux-saga/effects';
import {
    INCREMENT, DECREMENT, INCREMENT_ASYNC, DECREMENT_ASYNC
} from '../constant';
function* incrementAsync(data) {
    yield delay(2000);//首先延时2秒钟
    yield put({ type: INCREMENT, data: data.data });//发送加一的动作给 纯函数
}
function* decrementAsync() {
    yield delay(2000);//首先延时2秒钟
    yield put({ type: DECREMENT });//发送加一的动作给 纯函数
}
//监听延时加一
export function* watchCountAsync() {
    //延时加一
    yield takeEvery(INCREMENT_ASYNC, incrementAsync);
    yield takeEvery(DECREMENT_ASYNC, decrementAsync);
}

store/saga/qcsdata.js

import { put, call, takeEvery } from 'redux-saga/effects';
import { FETCH_BAOKUAN_DATA, BAOKUAN_DATA } from '../constant';
import axios from 'axios';
//异步获取轮播数据
function* getBaokuanList(data) {
    console.log(data);
    try {
        const res = yield call(axios.get, "/item/ws/group_list?current_page=1&page_size=9&group_id=" + data.data + "&device_id=2d1ca050-aa92-11eb-b11b-4d9b0d3c4728")
        // console.log(res);
        //将数据发送到 纯函数
        yield put({
            type: FETCH_BAOKUAN_DATA,
            baokuanList: res.data.data.item_list
        })
    } catch (e) {
        console.log("请求失败,错误是" + e);
    }
}
//监听屈臣氏获取数据
export function* watchQcsData() {
    //异步获取屈臣氏数据的动作
    yield takeEvery(BAOKUAN_DATA, getBaokuanList);
}

新建actions/qcsdata.js

import { BAOKUAN_DATA } from '../constant';

export function fetchBaokuanData(data) {
    console.log('baokuan-action4');
    return { type: BAOKUAN_DATA, data }
}

新建reducers/qcsdata.js

//屈臣氏数据纯函数子树
import { FETCH_BAOKUAN_DATA, BAOKUAN_DATA } from '../constant';
const qcsdata = (state = {}, action) => {
    switch (action.type) {
        case FETCH_BAOKUAN_DATA://获取轮播数据
            return Object.assign({}, state, { baokuanList: action.baokuanList });
        default:
            return state;
    }
}
export default qcsdata;

新建 containers/Baokuan.js

import React, { Component } from 'react'
import { connect } from 'react-redux';
import { fetchBaokuanData } from '../store/actions/qcsdata ';
class Baokuan extends Component {
    componentDidMount() {
        this.props.fetchBaokuanData(40313);
    }
    render() {
        console.log(this.props);
        const { baokuanList } = this.props;
        return (
            <div>
                <div>爆款
                    {baokuanList.map((item, index) => <div key={index}>{item.item_name}</div>)}
                </div>
            </div>
        )
    }
}
export default connect(state => ({ baokuanList: state.qcsdata.baokuanList || [] }), { fetchBaokuanData })(Baokuan);

umi+dva有待完善

十八、React Hooks ***

在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染。在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处:

  1. 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护
  2. 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks 中,这些功能都可以通过强大的自定义的 Hooks 来实现

React 在 v16.8 的版本中推出了 React Hooks 新特性,虽然社区还没有最佳实践如何基于 React Hooks 来打造复杂应用(至少我还没有),凭借着阅读社区中大量的关于这方面的文章,下面我将通过十个案例来帮助你认识理解并可以熟练运用 React Hooks 大部分特性。

1、useState 保存组件状态 ***

在类组件中,我们使用 this.state 来保存组件状态,并对其修改触发组件重新渲染。比如下面这个简单的计数器组件,很好诠释了类组件如何运行:

import React, { Component } from 'react';
class App extends Component {
  constructor() {
    super();
    this.state = { count: 0 }
  }
  changeCount = () => {
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return (<div>
      <button onClick={this.changeCount}>++{this.state.count}</button>
    </div >);
  }
}
export default App;

一个简单的计数器组件就完成了,而在函数组件中,由于没有 this 这个黑魔法,React 通过 useState 来帮我们保存组件的状态。

import React, { useState } from 'react';

function App() {
  //参数是state的初始值
  //返回值是一个数组,数组的第一个元素是默认状态,相当于 state的key值
  //返回值得第二个元素是改变状态的函数,相当于自定义的setState
  const [count, setCount] = useState(0)
  const changeCount = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <h2>计数器</h2>
      count:{count}
      <button onClick={changeCount}>++</button>
    </div>
  )
}
export default App;

通过传入 useState 参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑。(代码如上)

似乎有个 useState 后,函数组件也可以拥有自己的状态了,但仅仅是这样完全不够。

useState使用对象形式

import React, { useState } from 'react';

function App() {
  //参数是state的初始值
  //返回值是一个数组,数组的第一个元素是默认状态,相当于 state的key值
  //返回值得第二个元素是改变状态的函数,相当于自定义的setState
  const [obj, setObj] = useState({
    count: 0,
    name: '1000phone'
  })
  const changeCount = () => {
    setObj({ ...obj, count: obj.count + 1 })
  }
  return (
    <div>
      <h2>计数器</h2>
      count:{obj.count}
      <button onClick={changeCount}>++</button>
    </div>
  )
}
export default App;

2、useEffect 处理副作用 ***

函数组件能保存状态,但是对于异步请求,副作用的操作还是无能为力,所以 React 提供了 useEffect 来帮助开发者处理函数组件的副作用,在介绍新 API 之前,我们先来看看类组件是怎么做的:

import React, { Component } from "react";
class App extends Component {
  state = {
    count: 1
  };
  componentDidMount() {
    const { count } = this.state;
    document.title = "componentDidMount" + count;
    this.timer = setInterval(() => {
      this.setState(({ count }) => ({
        count: count + 1
      }));
    }, 1000);
  }
  componentDidUpdate() {
    const { count } = this.state;
    document.title = "componentDidMount" + count;
  }
  componentWillUnmount() {
    document.title = "componentWillUnmount";
    clearInterval(this.timer);
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        Count:{count}
        <button onClick={() => clearInterval(this.timer)}>clear</button>
      </div>
    );
  }
}

卸载组件写法:

在 index.js 中

setTimeout(()=>{
ReactDOM.unmountComponentAtNode(document.querySelector('#root'))
},2000)

在例子中,组件每隔一秒更新组件状态,并且每次触发更新都会触发 document.title 的更新(副作用),而在组件卸载时修改 document.title(类似于清除)

从例子中可以看到,一些重复的功能开发者需要在 componentDidMount 和 componentDidUpdate 重复编写,而如果使用 useEffect 则完全不一样。

import React, { useState, useEffect } from "react";
let timer = null;
function App() {
  const [count, setCount] = useState(0);
  //相当于类组件的钩子函数,
  //无参数,相当于componentDidMount  componentDidUpdate
  // useEffect(() => {
  //   console.log('useEffect钩子函数运行了');
  // })
  //第二个参数为空数组时。相当于componentDidMount 
  // useEffect(() => {
  //   document.title = "componentDidMount" + count;
  //   console.log('componentDidMount钩子函数运行了' + count);
  // }, [])
  //第二个参数有值,相当于componentDidUpdate,可以定义依赖于谁发生变化,可以写多个
  //下面这个例子,如果count发生变化则该钩子会被调用
  // useEffect(() => {
  //   console.log('componentDidUpdate钩子函数运行了' + count);
  //   document.title = "componentDidUpdate" + count;
  // }, [count])
  //写在return中的相当于卸载钩子 componentWillUnmount
  // useEffect(() => {
  //   return () => {
  //     console.log('componentWillUnmount钩子函数运行了');
  //     document.title = "componentWillUnmount";
  //     clearInterval(timer);
  //   }
  // }, [])
  //可以将挂载和卸载写在一起
  useEffect(() => {
    document.title = "componentDidMount" + count;
    console.log('componentDidMount钩子函数运行了' + count);
    return () => {
      console.log('componentWillUnmount钩子函数运行了');
      document.title = "componentWillUnmount";
      clearInterval(timer);
    }
  }, [])
  const changeCount = () => {
    setCount(count + 1);
  }
  return (
    <div>Count:{count}
      <button onClick={changeCount}>++</button>
    </div>
  )
}
export default App;

useEffect 第一个参数接收一个函数,可以用来做一些副作用比如异步请求,修改外部参数等行为,而第二个参数称之为dependencies,是一个数组,如果数组中的值变化才会触发 执行useEffect 第一个参数中的函数。返回值(如果有)则在组件销毁或者调用函数前调用

  • 1.比如第一个 useEffect 中,理解起来就是一旦 count 值发生改变,则修改 document.title 值;
  • 2.而第二个 useEffect 中传递了一个空数组[],这种情况下只有在组件初始化或销毁的时候才会触发,用来代替 componentDidMount 和 componentWillUnmount,慎用;
  • 3.还有另外一个情况,就是不传递第二个参数,也就是useEffect只接收了第一个函数参数,代表不监听任何参数变化。每次渲染DOM之后,都会执行useEffect中的函数。

在useEffect中获取数据

注意:需要安装 axios

import React, { useState, useEffect } from "react";
import axios from 'axios';
function App() {
  const [count, setCount] = useState(0);
  function getData() {
    return axios.get("http://www.51houniao.com/product/product/guessYouLike").then(res => {
      console.log(res);
      return res;
    })
  }
  useEffect(async () => {
    //做ajax操作
    const data = await getData();
    console.log(data);
  }, [])

  const changeCount = () => {
    setCount(count + 1);
  }
  return (
    <div>Count:{count}
      <button onClick={changeCount}>++</button>
    </div>
  )
}
export default App;

基于这个强大 Hooks,我们可以模拟封装出其他生命周期函数,比如 componentDidUpdate 代码十分简单

function useUpdate(fn) {
    // useRef 创建一个引用
    const mounting = useRef(true);
    useEffect(() => {
      if (mounting.current) {
        mounting.current = false;
      } else {
        fn();
      }
    });
}

现在我们有了 useState 管理状态,useEffect 处理副作用,异步逻辑,学会这两招足以应对大部分类组件的使用场景。

3、useContext 减少组件层级 了解

上面介绍了 useState、useEffect 这两个最基本的 API,接下来介绍的 useContext 是 React 帮你封装好的,用来处理多层级传递数据的方式,在以前组件树种,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,我们还可以使用 React Context API 来帮我们做这件事,举个简单的例子:

const { Provider, Consumer } = React.createContext(null);
function Bar() {
  return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Foo() {
  return <Bar />;
}
function App() {
  return (
    <Provider value={"grey"}>
      <Foo />
    </Provider>
  );
}

通过 React createContext 的语法,在 APP 组件中可以跨过 Foo 组件给 Bar 传递数据。而在 React Hooks 中,我们可以使用 useContext 进行改造。

import React, { useState, useContext } from "react";

const colorContext = React.createContext("blue");
function Bar() {
  const color = useContext(colorContext);
  return <div>Bar组件{color}</div>
}
function Foo() {
  return <div>
    Foo
    <Bar />
  </div>
}
function App() {
  return (
    <colorContext.Provider value={"green"} ><Foo /></colorContext.Provider>
  )
}
export default App;

传递给 useContext 的是 context 而不是 consumer,返回值即是想要透传的数据了。用法很简单,使用 useContext 可以解决 Consumer 多状态嵌套的问题。

function HeaderBar() {
  return (
    <CurrentUser.Consumer>
      {user =>
        <Notifications.Consumer>
          {notifications =>
            <header>
              Welcome back, {user.name}!
              You have {notifications.length} notifications.
            </header>
          }
      }
    </CurrentUser.Consumer>
  );
}

而使用 useContext 则变得十分简洁,可读性更强且不会增加组件树深度。

import React, { useState, useContext } from "react";
const colorContext = React.createContext("blue");
const userContext = React.createContext({ "username": "lili" });
function Bar() {
  const color = useContext(colorContext);
  const user = useContext(userContext);
  console.log(color);
  return <div>Bar组件
    名字:{user.username}<br />
    颜色:{color}</div>
}
function Foo() {
  return <div>
    Foo
    <Bar />
  </div>
}
function App() {
  return (
    <Foo />
  )
}
export default App;

4、useReducer

useReducer 这个 Hooks 在使用上几乎跟 Redux/React-Redux 一模一样,唯一缺少的就是无法使用 redux 提供的中间件。我们将上述的计时器组件改写为 useReducer,

import React, { useReducer } from "react";
const initState = { count: 0 }
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
    default:
      return state;
  }
}
function App() {
  const [state, dispatch] = useReducer(reducer, initState);
  return (
    <div>
      count:{state.count}
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>+</button>
      <button onClick={() => dispatch({ type: "decrement", payload: 3 })}>-</button>
    </div>
  )
}
export default App;
}

用法跟 Redux 基本上是一致的,用法也很简单,算是提供一个 mini 的 Redux 版本。

5、useCallback 记忆函数 **

在类组件中,我们经常犯下面这样的错误:

class App {
    render() {
        return <div>
            <SomeComponent style={{ fontSize: 14 }} doSomething={ () => { console.log('do something'); }}  />
        </div>;
    }
}

这样写有什么坏处呢?一旦 App 组件的 props 或者状态改变了就会触发重渲染,即使跟 SomeComponent 组件不相关,由于每次 render 都会产生新的 style 和 doSomething(因为重新render前后, style 和 doSomething分别指向了不同的引用),所以会导致 SomeComponent 重新渲染,倘若 SomeComponent 是一个大型的组件树,这样的 Virtual Dom 的比较显然是很浪费的,解决的办法也很简单,将参数抽离成变量。

const fontSizeStyle = { fontSize: 14 };
class App {
    doSomething = () => {
        console.log('do something');
    }
    render() {
        return <div>
            <SomeComponent style={fontSizeStyle} doSomething={ this.doSomething }  />
        </div>;
    }
}

在类组件中,我们还可以通过 this 这个对象来存储函数,而在函数组件中没办法进行挂载了。所以函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。

function App() {
  const handleClick = () => {
    console.log('Click happened');
  }
  return <SomeComponent onClick={handleClick}>Click Me</SomeComponent>;
}

这里多说一句,把函数式组件理解为class组件render函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。所以上述代码中每次render,handleClick都会是一个新的引用,所以也就是说传递给SomeComponent组件的props.onClick一直在变(因为每次都是一个新的引用),所以才会说这种情况下,函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。

而有了 useCallback 就不一样了,你可以通过 useCallback 获得一个记忆后的函数(函数缓存了,不会变)。

想要演示这个,必须先讲memo,相当于我们类组件中的 shouldComponentUpdate 获取 PureComponent

import React, { useState, useCallback, useMemo } from 'react';
//使用了memo的子组件
//使用了memo子组件在没有变化时是不会重新渲染的
const Child = React.memo(() => {
  console.log("子组件渲染了");
  return (<div>我是子组件</div>)
})
const Child2 = () => {
  console.log("render");
  return (
    <div>child2</div>
  )
}
function App() {
  let [count, setCount] = useState(0);
  const changeCount = () => {
    setCount(count + 1);
  }
  return (
    <div>App组件
      count:{count}
      <button onClick={changeCount}>+</button>
      <Child />
      <Child2 />
    </div>
  )
}
export default App

使用useCallback


import React, { useState, useCallback } from 'react';
//使用了memo的子组件
//使用了memo子组件在没有变化时是不会重新渲染的
const Child = React.memo(() => {
  console.log("子组件渲染了");
  return (<div>我是子组件</div>)
})

function App() {
  let [count, setCount] = useState(0);
  const changeCount = () => {
    setCount(count + 1);
  }
  //
  const clickHandle = useCallback(() => {
    console.log("我是父组件的函数");
  }, [])
  return (
    <div>App组件
      count:{count}
      <button onClick={changeCount}>+</button>
      <Child onClick={clickHandle} />
    </div>
  )
}

export default App

老规矩,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。

这样只要子组件继承了 PureComponent 或者使用 React.memo 就可以有效避免不必要的 VDOM 渲染。

6、useMemo 记忆组件 **

useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。

useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs).

所以前面使用 useCallback 的例子可以使用 useMemo 进行改写:


import React, { useState, useMemo } from 'react';
//使用了memo的子组件
//使用了memo子组件在没有变化时是不会重新渲染的
const Child = React.memo(() => {
  console.log("子组件渲染了");
  return (<div>我是子组件</div>)
})

function App() {
  let [count, setCount] = useState(0);
  const changeCount = () => {
    setCount(count + 1);
  }
  //
  // const clickHandle = useCallback(() => {
  //   console.log("我是父组件的函数");
  // }, [])
  // const clickHandle = useMemo(() => () => {
  //   console.log("我是父组件的函数");
  // }, [])
  //少调用一层函数也是可以的
  const clickHandle = useMemo(() => {
    console.log("我是父组件的函数");
  }, [])
  return (
    <div>App组件
      count:{count}
      <button onClick={changeCount}>+</button>
      <Child onClick={clickHandle} />
    </div>
  )
}

export default App

唯一的区别是:**useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你。**所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。

所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。

7、useRef 保存引用值 **

useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:

import React, { useState, useRef } from "react";
function App() {
  let [name, setName] = useState("Nate");
  let nameRef = useRef();
  const submitButton = () => {
    setName(nameRef.current.value);
  };
  return (
    <div className="App">
      <p>{name}</p>

      <div>
        <input ref={nameRef} type="text" />
        <button type="button" onClick={submitButton}>
          Submit
        </button>
      </div>
    </div>
  );
}

useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。

当然 useRef 远比你想象中的功能更加强大,useRef 的功能有点像类属性,或者说您想要在组件中记录一些值,并且这些值在稍后可以更改。

利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。

React Hooks 中存在 Capture Value 的特性:

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

  useEffect(() => {
    setTimeout(() => {
      alert("count: " + count);
    }, 3000);
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>增加 count</button>
      <button onClick={() => setCount(count - 1)}>减少 count</button>
    </div>
  );
}

先点击增加button,后点击减少button,3秒后先alert 1,后alert 0,而不是alert两次0。这就是所谓的 capture value 的特性。而在类组件中 3 秒后输出的就是修改后的值,因为这时候 message 是挂载在 this 变量上,它保留的是一个引用值,对 this 属性的访问都会获取到最新的值。讲到这里你应该就明白了,useRef 创建一个引用,就可以有效规避 React Hooks 中 Capture Value 特性。

function App() {
  const count = useRef(0);

  const showCount = () => {
    alert("count: " + count.current);
  };

  const handleClick = number => {
    count.current = count.current + number;
    setTimeout(showCount, 3000);
  };

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button onClick={() => handleClick(1)}>增加 count</button>
      <button onClick={() => handleClick(-1)}>减少 count</button>
    </div>
  );
}

只要将赋值与取值的对象变成 useRef,而不是 useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。

8、useImperativeHandle 透传 Ref

通过 useImperativeHandle 用于让父组件获取子组件内的索引

在组件外部获取组件内部的input

import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => inputRef.current);
  return <input type="text" name="child input" ref={inputRef} />;
}
//定义的ref在子组件拿不到,需要使用forwardRef高阶组件
const ChildInput = forwardRef(ChildInputComponent);
function App() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <div>
      <ChildInput ref={inputRef} />
    </div>
  );
}

通过这种方式,App 组件可以获得子组件的 input 的 DOM 节点。

9、useLayoutEffect 同步执行副作用 **

大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。

function App() {
  const [width, setWidth] = useState(0);
  useLayoutEffect(() => {
    const title = document.querySelector("#title");
    const titleWidth = title.getBoundingClientRect().width;
    console.log("useLayoutEffect");
    if (width !== titleWidth) {
      setWidth(titleWidth);
    }
  });
  useEffect(() => {
    console.log("useEffect");
  });
  return (
    <div>
      <h1 id="title">hello</h1>
      <h2>{width}</h2>
    </div>
  );
}

在上面的例子中,useLayoutEffect 会在 render,DOM 更新之后同步触发函数,会优于 useEffect 异步触发函数。

useEffect和useLayoutEffect有什么区别?

简单来说就是调用时机不同,useLayoutEffect和原来componentDidMount&componentDidUpdate一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而useEffect是会在整个页面渲染完才会调用的代码。

官方建议优先使用useEffect

However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.

在实际使用时如果想避免页面抖动(在useEffect里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect里。关于使用useEffect导致页面抖动。

不过useLayoutEffect在服务端渲染时会出现一个warning,要消除的话得用useEffect代替或者推迟渲染时机。

先安装 gsap动画插件

css

.animate{position: relative;top:0;left:0;    width:800px;height:600px; bordeR:5px solid pink;}
.square{position: absolute;top:0;left:0;   width:100px;height:100px; bordeR:1px solid blue;}

App.jsx

import React, { useEffect, useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import './index.css';
const App = () => {
  const REl = useRef(null);
  // useEffect(() => {
  //   /*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置,会闪动*/
  //   gsap.to(REl.current, 0, { x: 600 })
  // }, []);
  useLayoutEffect(() => {
    /*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
    gsap.to(REl.current, 0, { x: 600 })
  }, []);
  return (
    <div className='animate'>
      <div ref={REl} className="square">square</div>
    </div>
  );
};

export default App

10、自定义Hooks ***

程序员自己写的hook,相当于 类组件的 高阶组件

定义hooks

完成加一减一的自定义钩子

App.js

// 在需要的页面调用该hook函数
import React, { useState } from 'react'
import useCount from './count';
export default function App() {
  const [count, addCount, minusCount] = useCount(0);
  return (
    <div>
      <h5>自定义hook使用</h5>
      <div>count:{count}</div>
      <button onClick={addCount}>+</button>
      <button onClick={minusCount}>-</button>
    </div>
  )
}

count.js

//计数器hooks
import React, { useState } from 'react';
function useCount(initNum) {
    const [count, setCount] = useState(initNum);
    const addCount = () => setCount(count + 1);
    const minusCount = () => setCount(count - 1);
    return [
        count, addCount, minusCount
    ]
}
export default useCount;

完成 url地址参数解析的hook

useUrlHandler.js

import url from 'url'
import qs from 'querystring'

// use就是hook标识
// 封装了一个将string的url中的search处理成obj的自定义hook
export function useUrlHandler(str) {
    const { search } = url.parse(str)
    return qs.parse(search.slice(1))
}

App.jsx

// 在需要的页面调用该hook函数
import React, { useState } from 'react'
import { useUrlHandler } from './useUrlHandler'

export default function App() {
  const urlObj = useUrlHandler('https://jd.com/phone/18?a=2&b=3#hash')
  const [url] = useState(urlObj)
  console.log(url, 77)
  return (
    <div>
      <h5>自定义hook使用</h5>
    </div>
  )
}

日期处理hooks

export function useTimeHandler(date,type='/'){
  const d = new Date(date)
  const arr = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
  return d.getFullYear() + type + useNumber(d.getMonth() + 1) + type + useNumber(d.getDate()) + '' + arr[d.getDay()]
}

// 判断月日是否需要加0
export function useNumber(n){
  if(isNaN(n)) return 'n格式不正确'
  if(n<10) return '0' + n
  return n
}

// 调用
const t = useTimeHandler(Date.now(),'-')
const [time] = useState(t)

实现自定义useTitle

import { useEffect } from 'react'
const useTitle = (title) => {
    useEffect(() => {
      document.title = title
    }, [])
  
    return
  }
const Home = () => {
    // ...
    useTitle('趣谈前端')
    return <div>home</div>
}

实现自定义的useUpdate
我们都知道如果想让组件重新渲染,我们不得不更新state,但是有时候业务需要的state是没必要更新的,我们不能仅仅为了让组件会重新渲染而强制让一个state做无意义的更新,所以这个时候我们就可以自定义一个更新的hooks来优雅的实现组件的强制更新,实现代码如下:

import { useState } from 'react'
const useUpdate = () => {
    const [, setFlag] = useState()
    const update = () => {
        setFlag(Date.now())
    }
    return update
  }
export default useUpdate

const Home = (props) => {
  // ...
  const update = useUpdate()
  return <div>
    {Date.now()}
    <div><button onClick={update}>update</button></div>
  </div>
}

附加

一、create-react-app 支持decorators

yarn add @babel/core @babel/plugin-proposal-decorators @babel/preset-env

创建 .babelrc

{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "legacy": true
            }
        ]
    ]
}

创建config-overrides.js

const path = require('path')
const { override, addDecoratorsLegacy } = require('customize-cra')

function resolve(dir) {
    return path.join(__dirname, dir)
}

const customize = () => (config, env) => {
    config.resolve.alias['@'] = resolve('src')
    if (env === 'production') {
        config.externals = {
            'react': 'React',
            'react-dom': 'ReactDOM'
        }
    }

    return config
};


module.exports = override(addDecoratorsLegacy(), customize())

安装依赖

yarn add customize-cra react-app-rewired

修改package.json

...
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
...

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值