美文网首页redux前端开发那些事儿
Redux原理分析以及使用详解(TS && JS)

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

作者: Poppy11 | 来源:发表于2021-01-08 16:20 被阅读0次

    Redux原理分析

    image.png

    一、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的工作原理

    image.png

    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

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

    返回结果只依赖其参数

    //  非纯函数 返回值与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打印数据,都是副作用。

    // 无副作用
    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,如:

    function add() {    return {        type: 'add',        money : 1    }}
    

    2.2、Reducer

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

    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

    import {createStore} from 'redux';
    const store = createStore(reducer);
    

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

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

    三、Redux中间件机制

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

    image.png

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

    image.png

    如上图所示,派发给 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可以返回一个函数,那么就可以在这个函数中执行一些异步的操作,就比如网络请求。

    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,下载这个插件,然后重启浏览器

    image.png

    在redux中的store文件进行配置

    若是JS则添加

    const store = createStore(
      reducers,
      window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    );
    

    若是TS则添加

    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的变化。非常方便进行调试。

    image.png image.png

    六、实际开发中使用redux

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

    image.png

    6.1.1、store

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

    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

    image.png

    6.1.3、reducer

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

    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文件则需要这么写

    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

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

    6.2、在组件中取出store仓库的值,和如果触发action(JS && TS + hooks)

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

    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)

    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,页面就能获取到值。

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

    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
            })
        })
    }
    
     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里面这个值的变化,当有值的时候,才绑定到页面上

    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

    let test = async() => {
        let data = await test1()
        console.log(data)
        console.log(3)
    }
    
     
    
    let test1 = () => {
        return test2().then(data=>{ return data })
    }
    
     
    
    let test2 = async() => {
        return await test3()
    }
    
     
    
    let test3 = () => {
        return new Promise(reslove=>{
            setTimeout(()=>{
                reslove('hello')
            }, 1000)
        })
    }
    
     
    console.log(1)
    test()
    console.log(2)
    

    相关文章

      网友评论

        本文标题:Redux原理分析以及使用详解(TS && JS)

        本文链接:https://www.haomeiwen.com/subject/oqzzoktx.html