Redux及其相关库使用与源码学习

352 阅读9分钟

Redux

纯原生基本使用

redux源码的本质是发布订阅模式。subscribe方法接受函数(订阅函数),当dispatch(action)的时,内部会将上一次的state和本次的action传给reducer函数进行执行,将reducer返回的函数更新为新的state,然后再取出之前subscribe方法订阅好的函数进行执行。

仓库对象提供的核心方法:

  • getState
  • subscribe
  • dispatch

html

 <div>
     <p id="counter">0</p>
     <button id="add-button">+</button>
     <button id="minus-button">-</button>
 </div>

js

 import { createStore } from 'redux';
 ​
 const counter = document.getElementById('counter');
 const addButton = document.getElementById('add-button');
 const minusButton = document.getElementById('minus-button');
 ​
 // 动作类型
 const ADD = 'ADD';
 const MINUS = 'MINUS';
 ​
 //初始状态数据
 const initState = { number: 0 };
 ​
 function reducer(state = initState, action) {
   switch (action.type) {
     case ADD:
       return { number: state.number + 1 };
     case MINUS:
       return { number: state.number - 1 };
     default:
       return state;
   }
 }
 ​
 // 创建仓库
 const store = createStore(reducer);
 ​
 function render() {
   counter.innerText = store.getState().number;
 } 
 ​
 render();
 store.subscribe(()=>{
   render()
 });
 addButton.addEventListener('click', function () {
   store.dispatch({ type: ADD });
 });
 minusButton.addEventListener('click', function () {
   store.dispatch({ type: MINUS });
 });

核心实现

 function createStore(reducer){
     let state;
     const listeners = [];
     
     function getState(){
         return state;
     }
     
     function subscribe(listener){
         listeners.push(listener)
         // 返回卸载监听函数
         return ()=>{  
             const listenerIndex = listeners.indexOf(listener)
             listeners.splice(listenerIndex,1)
         }
     }
     
     function dispatch(action){
         state = reducer(state,action)
         listeners.forEach((l)=>{ l() })
     }
     
     dispatch({type:"@@redux/init"})
     
     return {
         getState,
         subscribe,
         dispatch
     }
 }
 ​
 export default createStore

结合react使用

 // 动作类型
 const ADD = 'ADD';
 const MINUS = 'MINUS';
 ​
 //初始状态数据
 const initState = { number: 0 };
 ​
 function reducer(state = initState, action) {
     switch (action.type) {
         case ADD:
             return { number: state.number + 1 };
         case MINUS:
             return { number: state.number - 1 };
         default:
             return state;
     }
 }
 ​
 const store = createStore(reducer);
 ​
 class Counter extends React.Component{
     constructor(props){
         super(props)
         // 将store中的数据映射为组件的state
         this.state = {number:store.getState().number}
     }
 ​
     componentDidMount(){
         // 注册一旦store数据更新就会执行的触发组件重新渲染的回调函数
         this.unsubscribe = store.subscribe(()=>{
             this.setState({number:store.getState().number})
         })
     }
 ​
     componentWillUnmount(){
         // 组件销毁时卸载之前注册的触发组件重新渲染的方法
         this.unsubscribe()
     }
 ​
     render(){
         <div>
             <p>{this.state.number}</p>
             <button onClick={()=>store.dispatch({type:ADD})}>+</button>
             <button onClick={()=>store.dispatch({type:MINUS})}>-</button>
         </div>
     }
 }

优化

  • 将action的创建交给函数调用返回
  • 将action生成函数传递一个函数bindActionCreators,该函数中将action生成器函数包装为store.dispatch(actionCreater)}
  function add (){
      return {type:ADD}
  }
 ​
  function minus (){
      return {type:MINUS}
  }
 ​
 const actionCreators = {add,minus}
 const boundActionCreators = bindActionCreators(actionCreators, store.dispatch)
 ​
 <button onClick={boundActionCreators.add}>+</button>

redux工具方法实现

 function bindActionCreators(actionCreators,dispatch){
     const boundActionCreators ={}
     
     for(const key in actionCreators){
         boundActionCreators[key] = function(...args){
             dispatch(actionCreators[key](...args))
         }
     }
     
     return boundActionCreators
 }
 ​
 export default bindActionCreators
  • 合并多个reducer

    当UI派发一个动作action之后,redux源码中不知道这个动作具体会命中哪个reducer中的type,所以会全部循环一边所有的reducer并将state和action传递过去,所以如果多个reducer中都有同一个type类型,都会被触发修改(这是redux库这么设计的)。

     function combineReducers(reducers){
         // 这个combination函数就会被传递给下面的createStore
         return function combination(state={},action){
             let nextState ={}
             for(let key in reducers){
                 nextState[key]  = reducers[key](state[key],action)
                 // 看似没有state[key] = nextState[key]这行代码,但是实际更新后的state被存放在了外层函数中了
             }
             return nextState
         }
     }
     ​
     export default combineReducers
     ​
     ​
     function createStore(reducer){
         let state;
         const listeners = [];
         
         function getState(){
             return state;
         }
         
         function subscribe(listener){
             listeners.push(listener)
             return ()=>{
                 const listenerIndex = listeners.indexOf(listener)
                 listeners.splice(listenerIndex,1)
             }
         }
         
         function dispatch(action){
             state = reducer(state,action)
             listeners.forEach((l)=>{ l() })
         }
         
         dispatch({type:"@@redux/init"})
         
         return {
             getState,
             subscribe,
             dispatch
         }
     }
     ​
     export default createStore
    
 import React from 'react';
 import { bindActionCreators } from 'redux';
 import store from '@/store';
 import actionCreators from '@/store/actionCreators/counter1';
 const boundActionCreators = bindActionCreators(actionCreators, store.dispatch);
 //boundActionCreators={add:()=>dispatch({ type: ADD }),minus}
 class Counter extends React.Component {
   constructor(props) {
     super(props);
     this.state = { number: store.getState().counter1.number };
   }
     
   componentDidMount() {
     this.unsubscribe = store.subscribe(() => this.setState({
       number: store.getState().counter1.number
     }));
   }
     
   componentWillUnmount() {
     this.unsubscribe();
   }
     
   render() {
     return (
       <div>
         <p>{this.state.number}</p>
         <button onClick={boundActionCreators.add1}>+</button>
         <button onClick={boundActionCreators.minus1}>-</button>
         <button onClick={() => store.dispatch({ type: 'DOUBLE' })}>DOUBLE</button>
       </div >
     )
   }
 }
 export default Counter;

上面的只使用redux 的代码还是存在冗余:

  1. 每个组件都需自己去订阅触发组件重新渲染的方法和当组件卸载时注销组件重新渲染的方法
  2. 组件需要引入仓库并将仓库中的状态数据映射为组件的state中

真实项目中redux仓库的目录结构的一种安排方式:

  • src

    • store

      • reducers

        1. index.jsx
        2. reducerName1.jsx
        3. reducerName2.jsx
        4. ...
      • index.jsx

      • actionTypes.jsx

      • actionCreators

        • actionCreaters1.jsx
        • actionCreaters2.jsx

React-Redux

react-redux提供的核心方法:

  • Provider组件
  • connect
  • useSelector
  • useDispatch

基本使用

创建仓库

 import React from 'react'
 import ReactDOM from 'react-dom/client'
 import {Provider} from 'react-redux'
 import store from './store'
 import Counter from './components/client'
 ​
 const root  = ReactDOM.createRoot(document.getElementById('root'))
 root.render(
     <Provider store={store}>
         <Counter>
     </Provider>
 )

类组件中使用

 import React from 'react';
 import actionCreators from '../store/actionCreators/counter1';
 import { connect } from 'react-redux';
 class Counter1 extends React.Component {
   render() {
     return (
       <div>
         <p>{this.props.number}</p>
         <button onClick={this.props.add1}>+</button>
         <button onClick={this.props.minus1}>-</button>
       </div >
     )
   }
 }
 //把仓库中的状态映射为组件的属性props对象 仓库到组件的输出
 const mapStateToProps = state => state.counter1;
 //const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch)
 export default connect(
   mapStateToProps,
   actionCreators //组件的输出,在组件里派发动作,修改仓库  ,这个参数可以是对象也可以是函数,函数则会接受store.dispatch作为参数
 )(Counter1);

函数组件中使用

 import React from 'react';
 import actionCreators from '../store/actionCreators/counter2';
 import { useSelector, useDispatch, useBoundDispatch } from 'react-redux';
 function Counter2() {
   //1.从状态树中获取某一部分状态,进行渲染 2.当仓库中的状态发生改变后会重新渲染组件
   const counter2 = useSelector(state => state.counter2);
   const dispatch = useDispatch();//store.dispatch
   const { add2, minus2 } = useBoundDispatch(actionCreators);  // useBoundDispatch原生库中并没有实现,自己实现的
   return (
     <div>
       <p>{counter2.number}</p>
       <button onClick={()=>dispatch(actionCreators.add2())}>+</button>
       <button onClick={add2}>+</button>
       <button onClick={minus2}>-</button>
     </div >
   )
 }
 export default Counter2;

基本实现

react-redux内部是借助了react原生提供的createContext API实现的,所以需要先了解createContext 的基本使用才行。

Provider组件实现

 import React from 'react'
 import ReactReduxContext from './ReactReduxContext'  // 创建的一个在redux源码中共享的上下文
 ​
 export default function Provider(props){
     return (
         <ReactReduxContext.Provider value={{store:props.store}}>
             {props.children}
         </ReactReduxContext.Provider>
     )
 }

ReactReduxContext

 import React from 'react'
 const ReactReduxContext = React.createContext(null)
 export default ReactReduxContext

connect

connect方法本质是一个高阶组件:

 import React from 'react'
 import ReactReduxContext from './ReactReduxContext'
 import {bindActionCreators} from 'redux'
 ​
 function connect(mapStateToProps,mapDispatchToProps){
     return function(Component){
         return class extends React.Component{
             static contextType = ReactReduxContext
             constructor(props,context){
                 super(props)
                 const {store} = context 
                 const {getState, dispatch, subscribe} = store
                 
                 this.state = mapStateToProps(getState())
                 
                 this.unsubscribe = subscribe(()=>{
                     this.setState(mapStateToProps(getState()))
                 })
                 
                 let dispatchProps
                 if(typeof mapDispatchToProps === 'function'){
                     dispatchProps = mapDispatchToProps(dispatch)
                 }else{
                     dispatchProps = bindActionCreators(mapDispatchToProps,dispatch)
                 }
                 this.dispatchProps = dispatchProps
             }
             
             componentWillUnmount(){
                 this.unsubscribe()
             }
             
             render(){
                 return (
                     <Component {...this.props} {...this.state} {...this.dispatchProps}></Component>
                 )
             }
         }
     }
 }

useSelector

 import  { useContext, useState, useLayoutEffect, useReducer, useRef } from 'react';
 import ReactReduxContext from '../ReactReduxContext'
 function useSelector(selector){
     const { store } = useContext(ReactReduxContext);
 ​
     const lastSelectedState = useRef(null)
 ​
     // const [state,setState] = useState(0)
     const [,forceUpdate] = useReducer(x=>x+1,0)
 ​
     useLayoutEffect(()=>{
         store.subscribe(()=>{
             // 优化,避免不必要的更新
             let slectedState = selector(store.getState())
             if(shallowEqual(lastSelectedState.current,slectedState)){
                 return 
             }
             lastSelectedState.current = slectedState
             // setState(state+1)
             
             forceUpdate()
         })
     },[])
 ​
     return selector(store.getState())
 }
 ​
 function shallowEqual(obj1, obj2) {
     if (obj1 === obj2) {
         return true;
     }
     if (typeof obj1 != "object" || obj1 === null || typeof obj2 != "object" || obj2 === null) {
         return false;
     }
     let keys1 = Object.keys(obj1);
     let keys2 = Object.keys(obj2);
     if (keys1.length !== keys2.length) {
         return false;
     }
     for (let key of keys1) {
         if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
             return false;
         }
     }
     return true;
 }
 export default useSelector

优化useSelector

useSyncExternalStore 是 React 18 新增的一个 Hook,它提供了一种更加规范和优化的方式来同步外部数据源的状态,以及在 React 组件中使用这些状态。这个 Hook 旨在替代过去的一些不太规范的模式,如使用 useEffectuseLayoutEffectuseState/useReducer 来订阅外部数据源,并在数据源更新时强制组件重新渲染。

useSyncExternalStore 的工作原理

useSyncExternalStore 允许订阅外部数据源,并在该数据源更新时确保组件能够同步更新。它接收三个参数:

  1. subscribe:一个函数,用于订阅外部数据源的更新。当外部数据源更新时,这个函数应该触发提供的监听器(listener)。
  2. getSnapshot:一个函数,返回外部数据源的最新状态。
  3. getServerSnapshot(可选):在服务端渲染(SSR)中使用,返回外部数据源的最新状态。

应用场景

useSyncExternalStore 主要用于下列场景:

  1. 集成非 React 状态管理库:当使用 Redux、MobX 或其他非 React 状态管理库时,useSyncExternalStore 提供了一种标准化的方式来确保 React 组件能够及时响应状态库中状态的更新。
  2. 订阅外部数据源:对于任何外部数据源,如 WebSocket 数据流、浏览器 API(例如 localStorage)的变化,或者是自定义的外部事件系统,useSyncExternalStore 都提供了一种有效的方式来同步这些数据的变化到 React 组件。
  3. 性能优化:相比于 useStateuseReducer 配合 useEffect/useLayoutEffect 的方式,useSyncExternalStore 通过减少不必要的组件重渲染和提供更精准的订阅控制,可以帮助提升应用性能。

示例代码

假设有一个外部的数据源 externalStore,你想在 React 组件中使用它的数据,并在数据更新时同步更新组件:

import { useSyncExternalStore } from 'react';

// 假设的外部数据源
const externalStore = {
  subscribe(callback) {
    // 订阅逻辑
  },
  unsubscribe(callback) {
    // 取消订阅逻辑
  },
  getState() {
    // 获取当前状态
    return {/* 状态数据 */};
  }
};

function MyComponent() {
  const state = useSyncExternalStore(
    // 订阅和取消订阅
    (callback) => {
      externalStore.subscribe(callback);
      return () => externalStore.unsubscribe(callback);
    },
    // 获取最新状态
    () => externalStore.getState()
  );

  // 使用 state 渲染组件
  return <div>{/* 渲染逻辑 */}</div>;
}

useSyncExternalStore 提供了一种更安全和高效的方式来处理 React 组件与外部数据源之间的交互,使得状态同步更加简洁和可靠。

import React, { useReducer, useLayoutEffect, useState, useRef } from 'react';
import ReactReduxContext from '../ReactReduxContext';
function useSelector(selector) {
    const { store } = React.useContext(ReactReduxContext);
    //React18新添加的自定义Hooks   二个参数 1 外部仓库订阅的方法 2 获取快照的方法 获取最新的状态
    return useSyncExternalStore(
        store.subscribe,
        () => selector(store.getState())
    );
}

function useSyncExternalStore(subscribe, getSnapShot) {
    let [state, setState] = useState(getSnapShot());
    //因为订阅只要一次就可以了,写在外面每次重新组件都要订阅
    useLayoutEffect(() => {
        subscribe(() => {
            setState(getSnapShot())
        });
    }, []);
    return state;
}

useDispatch

import ReactReduxContext from '../ReactReduxContext'
function useDispatch(){
    const { store } = useContext(ReactReduxContext);
    return store.dispatch
}

export default useDispatch

useBoundDispatch

工具方法,react-redux中并没有,自己实现的。

import  { useContext } from 'react';
import {bindActionCreators} from 'redux'

function useBoundDispatch(actionCreators){
    const { store } = useContext(ReactReduxContext);
    return bindActionCreators(actionCreators, store.dispatch)
}

export default useBoundDispatch

Redux中间件

  • 没有中间件redux 的工作流程是 action -> reducer,这是相当于同步操作,由dispatch 触发action后,直接去reducer执行相应的动作
  • 但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决问题。比如有一个这样的需求,点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时候就需要引入中间件改变redux同步执行的流程,形成异步流程来实现所要的逻辑,有了中间件,redux 的工作流程就变成这样 action -> middlewares -> reducer,点击按钮就相当于dispatch 触发action,接下去获取服务器数据 middlewares 的执行,当 middlewares 成功获取到服务器就去触发reducer对应的动作,更新需要渲染视图的数据
  • 中间件的机制可以改变数据流,实现如异步 action ,action 过滤,日志输出,异常报告等功能

image-20240220162332937

redux中间件的实现本质就是面向切片编程。将store自带的dispatch方法先保存下来。然后自己重新写一个方法赋值给store.dispatch,然后在自己实现的方法中再去调用原来的那个dispatch方法。

只是redux 的中间件机制设计为了考虑扩展性和可维护性,对如何注册中间件进行设计。

import { createStore } from 'redux';
import combinedReducer from './reducers';

const oldDispatch = store.dispatch;
let store = createStore(combinedReducer);
//实现异步操作
/*
 store.dispatch = function (action) {
    setTimeout(() => {
        oldDispatch(action);
    }, 1000);
    return action;
} */

//实现打印日志
/* store.dispatch = function (action) {
    console.log('prev stat', store.getState());
    oldDispatch(action);
    console.log('next state', store.getState());
} */
export default store;

基本使用

import { createStore } from '../redux';
import combinedReducer from './reducers';
import promise from './redux-promise';
import thunk from './redux-thunk';
import logger from './redux-logger';

function applyMiddleware(middleware) {
    return function (createStore) {
        return function (reducer, preloadedState) {
            const store = createStore(reducer);
            let dispatch;
            let middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
            };
            dispatch = middleware(middlewareAPI)(store.dispatch)
            return {
                ...store,
                dispatch
            }
        }
    }
}
const store = applyMiddleware(promise, thunk, logger)(createStore)(combinedReducer);

export default store;

核心实现

function applyMiddleware() {
  var middlewares = Array.prototype.slice.call(arguments);
  return function(createStore) {
    return function(reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function(action) {
          return dispatch(action);
        }
      };
      
      chain = middlewares.map(function(middleware) {
        return middleware(middlewareAPI);
      });
      dispatch = compose.apply(undefined, chain)(store.dispatch);

      return {
        ...store,
        dispatch: dispatch
      };
    };
  };
}

function compose() {
  var funcs = Array.prototype.slice.call(arguments);
  
  if (funcs.length === 0) {
    return function(arg) {
      return arg;
    };
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce(function(a, b) {
    return function() {
      return a(b.apply(undefined, arguments));
    };
  });
}