Redux原理分析以及使用详解(TS && JS)

2021-11-25 10:39:36 浏览数 (1)

Redux原理分析

一、Reudx基本介绍

1.1、什么时候使用Redux?

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

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

从组件角度看,如果你的应用有以下场景,可以考虑使用 Redux。

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

在React中,数据在组件中是单向流动的,这是react的一个特点,单向数据流动,会让开发者阅读代码以及数据流向时更清楚,数据从一个方向父组件流向子组件(通过props),但是这也伴随着一个问题,两个非父子组件之间通信就相对麻烦,例如A页面用到了B页面产生的数据,redux的出现就是方便解决了这类问题。

1.3、Redux设计理念

Redux是将整个应用状态存储到一个地方上称为 store ,里面保存着一个状态树 store tree ,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅 store 中的状态 state 来刷新自己的视图

1.4、Redux是什么?

很多人认为redux必须要结合React使用,其实并不是的,Redux 是 JavaScript 状态容器,只要你的项目中使用到了状态,并且状态十分复杂,那么你就可以使用Redux管理你的项目状态,它可以使用在react中,也可以使用中在Vue中,当然也适用其他的框架。

二、Redux的工作原理

1、首先我们找到最上面的state

2、在react中state决定了视图(UI),state的变化就会调用React的render()方法,从而改变视图

3、用户通过一些事件(如点击按钮,移动鼠标)就会向reducer派发一个action

4、reducer接受到action后就会去更新state

5、store是包含了所有的state,可以把它看作所有状态的集合

Redux三大原则
  • 1、唯一数据源
  • 2、保持只读状态
  • 3、数据改变只能通过纯函数来执行

1、唯一数据源

整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中

2、保持只读状态

state是只读的,唯一改变state的方法就是触发action,action会dispatch分发给reducer

3、数据改变只能通过纯函数来执行

使用纯函数来执行修改,也就是reducer

纯函数是什么 ,一个函数的返回结果只依赖其参数,并且执行过程中没有副作用。

返回结果只依赖其参数

代码语言:javascript复制
//  非纯函数 返回值与a相关,无法预料
const a = 1
const foo = (b) => a   b
foo(2)                    // => 3
​
// 纯函数 返回结果只依赖于它的参数 x 和 b
const a = 1
const foo = (x, b) => x   b
foo(1, 2) // => 3

函数执行过程中没有副作用

函数执行的过程中对外部产生了可观察的变化,我们就说函数产生了副作用。 例如修改外部的变量、调用DOM API修改页面,发送Ajax请求、调用window.reload刷新浏览器甚至是console.log打印数据,都是副作用。

代码语言:javascript复制
// 无副作用
const a = 1
const foo = (obj, b) => {
  return obj.x   b
}
const counter = { x: 1 }
foo(counter, 2)                       // => 3
counter.x                             // => 1
​
// 修改一下 ,再观察(修改了外部变量,产生了副作用。)
const a = 1
const foo = (obj, b) => {
  obj.x = 2;
  return obj.x   b
}
const counter = { x: 1 }
foo(counter, 2)                       // => 4
counter.x                             // => 2

为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。

2.1、Action

action本质上就是一个对象,它一定有一个名为type的key如 {type: 'add'} , {type: 'add'} 就是一个action , 但是我们只实际工作中并不是直接用action ,而是使用 action创建函数 (千万别弄混淆), 顾名思义action创建函数就是一个函数,它的作用就是返回一个action,如:

代码语言:javascript复制
function add() {    return {        type: 'add',        money : 1    }}
2.2、Reducer

reducer其实就是一个函数,它接收两个参数,第一个参数是需要管理的状态state,第二个是action。 reducer会根据传入的action的type值对state进行不同的操作,然后返回一个新的state,而不是在原有state的基础上进行修改,但是如果遇到了未知的(不匹配的)action,就会返回原有的state,不进行任何改变

代码语言:javascript复制
function reducer(state = {money: 0}, action) {
    //返回一个新的state可以使用es6提供的Object.assign()方法,或扩展运算符
    switch (action.type) {
        case ' ':
            return Object.assign({}, state, {money: action.money   1});
        case '-':
            return {...state, ...{money: action.money - 1}};
        default:
            return state;
    }
}
2.3、store

可以把store想成一个状态树,它包含了整个redeux应用的所有状态。我们使用redux提供的createStore方法生成store

代码语言:javascript复制
import {createStore} from 'redux';
const store = createStore(reducer);

store提供了几个方法供我们使用,下面是我们常用的3个:

代码语言:javascript复制
store.getState();//获取整个状态树
store.dispatch();//改变状态,改变state的唯一方法
store.subscribe();//订阅一个函数,每当state改变时,都会去调用这个函数

三、Redux中间件机制

Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能,以满足用户的开发需求。

上面是很典型的一次 redux 的数据流的过程,但在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。且由于业务场景的多样性,单纯的修改 dispatch 和 reduce 人显然不能满足大家的需要,因此对 redux middleware 的设计是可以自由组合,自由插拔的插件机制。也正是由于这个机制,我们在使用 middleware 时,我们可以通过串联不同的 middleware 来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联:

如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,值得注意的是这些中间件会按照指定的顺序一次处理传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action,同样的,每个中间件都有自己的“熔断”处理,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了。 换言之,中间件都是对store.dispatch()的增强

四、redux的异步流

在多种中间件中,处理 redux 异步事件的中间件,绝对占有举足轻重的地位。从简单的 react-thunk 到 redux-promise 再到 redux-saga等等,都代表这各自解决redux异步流管理问题的方案

4.1 、redux-thunk

redux-thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的next(action)执行。 正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作,就比如网络请求。

代码语言:javascript复制
export function addCount() {
  return {type: ADD_COUNT}
} 
export function addCountAsync() {
  return dispatch => {
    setTimeout( () => {
      dispatch(addCount())
    },2000)
  }
}

addCountAsync函数就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作。

尽管redux-thunk很简单,而且也很实用,但人总是有追求的,都追求着使用更加优雅的方法来实现redux异步流的控制,这就有了redux- promise。

4.2、redux-promise

使用redux-promise中间件,允许action是一个promise,在promise中,如果要触发action,则通过调用resolve来触发

4.3、redux-sage

redux-saga将react中的同步操作与异步操作区分开来,以便于后期的管理与维护 ,redux- saga相当于在Redux原有数据流中多了一层,通过对Action进行监听,从而捕获到监听的Action,然后可以派生一个新的任务对state进行维护,通过更改的state驱动View的变更。

4.4、总结

总的来讲Redux Saga适用于对事件操作有细粒度需求的场景,同时它也提供了更好的可测试性,与可维护性,比较适合对异步处理要求高的大型项目 。一般项目redux-thunk就足以满足自身需求了。毕竟react- thunk对于一个项目本身而言,毫无侵入,使用极其简单,只需引入这个中间件就行了。而react- saga则要求较高,难度较大,我现在也并没有掌握和实践这种异步流的管理方式。

五、使用redux-dev-tools插件调试redux

5.1、下载插件

首先在谷歌商店搜索redux-dev-tools,下载这个插件,然后重启浏览器

在redux中的store文件进行配置

若是JS则添加

代码语言:javascript复制
const store = createStore(
  reducers,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

若是TS则添加

代码语言:javascript复制
const store = createStore(reducer, compose(
    applyMiddleware(thunk),
    (window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()))

Tip :原来我使用JS Redux,添加这个插件配置,部署到服务器上用户访问以及别人启动我的项目,都没有报错,但是当我使用TS hooks Redux,没有测试部署到服务器会怎么样,但是当别人启动这个项目,若没有安装这个插件则会报错。若想避免这个问题,则可在webpack配置启动项目或者打包项目不同的环境。可使用 process.env.NODE_ENV === 'production' 判断不同环境,或者使用 window.location.host 获取url地址来进行判断是否开启这个插件。

下面则是工具的图,该工具,可以查看action的触发过程,以及state的变化。非常方便进行调试。

六、实际开发中使用redux

6.1、目录结构,在项目src里面创建即可

6.1.1、store

store则是配置redux总仓库,createStore()则需要把reducer传进来,以及上文介绍到的中间件,以及设置调试工具则都是在此文件进行配置

代码语言:javascript复制
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
​
const store = createStore(reducer, compose(
    applyMiddleware(thunk),
))
​
export default store

6.1.2、action

action则是view用来调用的,action通过dispatch来触发reducer,然后来更新state

6.1.3、reducer

store文件需要配置reducer,所以reducer文件夹中则需要一个index文件,来引入所有的reducer,并且暴露出去,供store文件使用。

代码语言:javascript复制
import {combineReducers} from 'redux'
import manage from './manage/manage'
import submit from './submit'
import saveName from './manage/saveName'
​
export default combineReducers({
    manage,
    submit,
    saveName
})

例如我现在需要存储上面action文件里面key为ALL_NAME的值,我reducer文件则需要这么写

代码语言:javascript复制
const init = {
    userNameData : []
}
​
export default (state = init, action : any) => {
    switch (action.type) {
        case 'ALL_NAME':
            return {...state,userNameData : action.allName}
        default:
            return state
    }
}

6.1.4、项目入口文件,index.ts

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';
import store from './redux/store'
import {Provider} from 'react-redux'
import App from './App';
代码语言:javascript复制
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
6.2、在组件中取出store仓库的值,和如果触发action(JS && TS hooks)

6.2.1、JS的用法(取值以及触发action)

代码语言:javascript复制
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {GetAllClass,SaveScroll} from '../../redux/action/product'
class Home extends Component {
 componentDidMount() {
       //取出值
       const {user,productAllClass,productScroll} = this.props
       //触发action
       this.props.getAllClass()
       const scroll = document.scrollingElement.scrollTop
       //触发action
       this.props.SaveScroll(scroll)
  }
}
​
//取值
//其实mapStateToProps接收了state,但是此处这么写,是使用了ES6的解构,会简化代码
const mapStateToProps = ({user, productAllClass,productScroll}) => ({
    user, productAllClass,productScroll
})
​
//调用action
const mapDispatchToProps = (dispatch) => ({
    getAllClass: () => dispatch(GetAllClass()),
    SaveScroll : (scroll) => dispatch(SaveScroll(scroll))
})
​
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Home))

大家可能看到这就有疑问,mapStateToProps和mapDispatchToProps是干嘛的?有什么作用?

首先我们在组件当中使用redux,就需要使用react- redux中的connect将该组件与store连接起来,而connect又可以接受两个参数,分别是mapStateToProps和mapDispatchToProps,前者则是获取store里面的状态,用于建立组件跟store的state的映射关系,后者则是用于建立组件跟store.dispatch的映射关系。 TS的用法(取值以及触发action)

代码语言:javascript复制
import { useDispatch, useSelector } from 'react-redux'
​
const ManageTable: React.FC<{}> = () => {
    const dispatch = useDispatch()
    const userNameRedux = useSelector((state: any) => state.saveName.userNmae);
    return (
        useEffect(() => {
            //调用action,传一个值name
            dispatch(saveSearchUserName(name))
            //获取store的值
            console.log(userNameRedux)
        },[])
    )
}
BUG分享

需求:一个接口,需要在多个页面调用,而且多个页面互相没有关联,我在每个页面都去调用这个接口,显然这是浪费性能的,我就想在react入口文件去调用action,然后分发给reducer,存储到store,页面就能获取到值。

大家可以先观察观察这份代码。大家觉得我能如愿在第一次加载的时候能拿到数据吗?

代码语言:javascript复制
export const test = () => {
    console.log("1")
    return  async (dispatch: any) => {
        console.log("2")
        const data = await getAllNameApi()
        console.log("3")
        dispatch({
           type: 'ALL_NAME',
           allName: data
        })
    }
}
​
//拆分一下上面的代码
export const test = () => {
    console.log("1")
    return new Promise(async (resolve) => {
        console.log("2")
        resolve(await getAllNameApi())
    }).then(() => {
        console.log("3")
        dispatch({
           type: 'ALL_NAME',
           allName: data
        })
    })
}
代码语言:javascript复制
 useEffect(() => {
      const manage: any = useSelector((state: any) => state.manage);
      console.log(manage.userNameData)
  },[])

最终正确打印顺序应该是1,2,数据,4。

最后经过反复研究,并且请教各路大神,最终总结了两个原因。

从同步异步的角度来说这个问题:想让异步变成类似同步的操作我们应该怎么办,大家想到的肯定是async/await,阻塞代码,我开始一直陷入一个误区,我内部的确造成了阻塞,等到data有值了,才会dispatch,但是,这整个Action方法,返回的是一个async,async其实本质也就是promise对象,那么又是一个异步对象,所以它的外部不会等待,当代码执行到await这块, 因为需要时间来调用接口,所以会跳出去,页面第一次会渲染,而不会说等待这个数据成功存入redux里面才会渲染页面。

从React页面渲染来说:页面肯定是先渲染,不会关心dispatch,也不会关心action,只会关心我store里面数据的变化,其实也就是我第一次useEffect的时候,数据取得其实是初始值。

对于这个问题,在我这份代码里面,目前我想到了三个解决方法:

1、定义初始值loading为true,当我们dispatch成功把数据存入的时候,才将loading改为false,写一个加载动画,用这个loading来控制。

2、在useEffect监听store里面这个值的变化,当有值的时候,才绑定到页面上

代码语言:javascript复制
const [autoData,setAutoData] = useState<Array[item]>([])   //此处item是我写的定义类型的接口
useEffect(() => {
    if(manage.userNameData !== []){
        setAutoData(manage.userNameData)
    }
},[manage.userNameData])
  • 3、因为我这个组件可以直接绑定数据源,其实我直接数据源头,写上这个store里面的值就好 <Auto dataSource={manage.userNameData} allowClear={true} style={{ width: 250 }} filterOption={(inputValue, option: any) => option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 } notFoundContent={<Empty />} placeholder="Please input or select" onChange={(e) => setUserNameValue(e)} value={autoValue} />

大家可以再看看下面这个小demo

代码语言:javascript复制
let test = async() => {
    let data = await test1()
    console.log(data)
    console.log(3)
}
代码语言:javascript复制
let test1 = () => {
    return test2().then(data=>{ return data })
}
代码语言:javascript复制
let test2 = async() => {
    return await test3()
}
代码语言:javascript复制
let test3 = () => {
    return new Promise(reslove=>{
        setTimeout(()=>{
            reslove('hello')
        }, 1000)
    })
}
代码语言:javascript复制
console.log(1)
test()
console.log(2)

0 人点赞