理解react-redux

181 阅读8分钟

学习react-redux之前需要有redux和react的基础,所以这篇文章需要建立在已经会redux和react基础上,react-redux源码较多,不扣细节,只看核心原理。

使用redux案例和react-redux案例对比 和 Provide与connect的核心实现看一下的react-redux作用

使用redux的案例

以下代码: 代码地址: redux-test-subscript-parent : 建议请打开 并点击 页面中的按钮 观察更新的输出

store.js: 使用countReducer和sumReducer创建一个store.

 import { createStore, combineReducers } from "redux";
 function countReducer(state = { count: 0 }, action) {
   switch (action.type) {
     case "countAdd":
       return { count: state.count + 1 };
     default:
       return state;
   }
 }
 function sumReducer(state = { sum: 1 }, action) {
   switch (action.type) {
     case "sumAdd":
       return { sum: state.sum + 1 };
     default:
       return state;
   }
 }
 const combinefn = combineReducers({ countReducer, sumReducer });
 const store = createStore(combinefn);
 export default store;
 ​

index.js: 将store传入App组件中,并订阅store.subscribe(render), 使得dispatch更新数据后,能重新渲染页面。

 import React from "react";
 import ReactDOM from "react-dom";
 import App from "./App";
 import store from "./store";
 const render = () =>
   ReactDOM.render(<App store={store} />, document.getElementById("root"));
 ​
 render();
 store.subscribe(render);

App.js: 使用A与B两个组件,并给A和B传入store,因为A与B中需要获取store的数据和使用store.dispatch更新数据

 import A from "./A";
 import B from "./B";
 function App(props) {
   console.log("App执行"); // 查看App的执行情况
   const { store } = props;
   return (
     <div className="App">
       <A store={store} />
       <B store={store} />
     </div>
   );
 }
 ​
 export default App;
 ​

A: 使用store中 countReducer.count的数据,并点击A+更新 countReducer.count

 function A(props) {
   console.log("A组件执行");  // 查看A的执行情况
   const { store } = props;
   const { dispatch, getState } = store;
   const count = getState().countReducer.count;
   return (
     <>
       <p> A(count): {count} </p>
       <button onClick={() => { dispatch({ type: "countAdd" }); }}  >
         A+
       </button>
     </>
   );
 }
 export default A;

B:使用store中sumReducer.sum的数据,并点击B+更新 sumReducer.sum

 function B(props) {
   console.log("B组件执行");  // 查看B的执行情况
   const { store } = props;
   const { dispatch, getState } = store;
   const sum = getState().sumReducer.sum;
   return (
     <>
       <p> B(sum): {sum} </p>
       <button
         onClick={() => {
           dispatch({ type: "sumAdd" });
         }}
       >
         B+
       </button>
     </>
   );
 }
 export default B;

最后展现效果:

当我们点击A+B+按钮时,比如点击任意一个按钮都会输出以下内容。

以上案例,存在两个问题

  1. 使用store.subscribe(render)订阅整个页面,导致不管更新哪个组件都重新渲染整个页面,正确的渲染应该是A组件依赖了count数据,那么只有当count改变时再重新渲染A组件,B组件没有依赖count就不重新渲染B

  2. 需要给每个子组件传递store, 组件嵌套过深时,会需要一直传递下去,导致非常复杂

先来解决第一个问题,将修改案例中的index.js , 不使用store.subscribe(render)订阅整个页面。

 import React from "react";
 import ReactDOM from "react-dom";
 import App from "./App";
 import store from "./store";
 const render = () =>
   ReactDOM.render(<App store={store} />, document.getElementById("root"));
 ​
 render();
//  store.subscribe(render);

修改完成后,执行dispatch更新数据后,就无法自动重新渲染组件了,所以需要我们在子组件中使用store.subscribe()使得组件可以重新渲染,如下:

A.js 改成以下形式,也就是当组件依赖的数据发生变化时,使用forceUpdate使得该组件重新,更新该组件需要的数据时,其他组件就不会更新,也就解决了第一个问题!!!

 import { useReducer, useEffect } from "react";
 function A(props) {
   console.log("A组件执行");
   const { store } = props;
   const { dispatch, getState } = store;
     // subscribe会利用到
   const count = getState().countReducer.count;
   // 模拟class 组件中的 forceUpdate
   // 实现方式参考react官网
   //https://zh-hans.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate
   const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);
     // 每次重新渲染重新订阅
   useEffect(() => {
       // subscribe订阅的函数会在dispatch数据更新后执行
     let unsubscribe = store.subscribe(() => {
       // 如果本次count与上次count不一致,才重新更新组件, count为上一轮的count,这里利用了闭
       // 包,
       if (getState().countReducer.count !== count) {
         forceUpdate();
       }
     });
       // 每次都卸载订阅
     return () => {
       unsubscribe();
     };
   }, [store, count, getState]);
   return (
     <>
       <p> {count} </p>
       <button
         onClick={() => {
           dispatch({ type: "countAdd" });
         }}
       >
         +
       </button>
     </>
   );
 }
 export default A;

B.js 同理上面改动后的源码地址 建议打开源码查看并点击 页面的 A+ 观察输出

此处点击9次A+进行测试,发现B组件并没有再次执行:

第一个问题虽然解决了,但是这又导致,我需要把forceUpdate 和store.subscribe,在所有用到store中的数据的组件,都需要写一遍,这里A与B都实现了一次,造成了大量的冗余(react-redux内部给我们实现了)

然后剩下第二个向子组件中传递数据的问题,在分析react-redux中的Provider时说明。我们继续往下看。

使用react-redux的案例

以下代码:源码地址--建议打开直接查看

store.js

使用countReduce 和 sumReducer创建一个store. 与redux案例中保持一致

 import { createStore, combineReducers } from "redux";
 ​
 function countReducer(state = { count: 0 }, action) {
   switch (action.type) {
     case "countAdd":
       return { count: state.count + 1 };
     default:
       return state;
   }
 }
 function sumReducer(state = { sum: 1 }, action) {
   switch (action.type) {
     case "sumAdd":
       return { sum: state.sum + 1 };
     default:
       return state;
   }
 }
 const combinefn = combineReducers({ countReducer, sumReducer });
 const store = createStore(combinefn);
 export default store;
 ​

index.js : 使用 传入store参数至Provider组件并包裹App组件

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

App.js: 不再需要向子组件中传递store

 import "./styles.css";
 import ReactReduxTest from "./react-redux-test";
 function App(props) {
   console.log("App执行");
   return (
     <div className="App">
       <A />
       <B />
     </div>
   );
 }
 export default App;

A.js: 使用connect将store中countReducer.count的注入到组件的props中

 import { connect } from "react-redux";
 function A(props) {
   console.log("A组件执行");
   const { count, countAdd } = props;
   return (
     <>
       <p> {count} </p>
       <button onClick={() => { countAdd();}}>
         A+
       </button>
     </>
   );
 }
 const mapStateToProps = (state, ownProps) => {
   const { count } = state.countReducer;
   return {
     count: count
   };
 };
 ​
 const mapDispatchToProps = (dispatch, ownProps) => {
   return {
     countAdd: () =>
       dispatch({
         type: "countAdd"
       })
   };
 };
 export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxTest);
 ​

B.js: 使用connect将store中sumReducer.sum的注入到组件的props中, 代码与A基本保持一致。

react-redux案例源码地址 -- 建议打开直接查看

点击页面A+B+ 按钮,比如点击7次A+按钮, 输出如下,

可以看到B也没有再次执行,与我上面修改过的redux案例一样的效果,但是不需要我手动在每个组件内部写forceUpdate和订阅组件更新的逻辑了。

相比redux案例中,有如下改变:通过 传入store参数至Provider组件并包裹App组件,子组件使用connect就能通过props获取数据了。接下来看看Provide和connect的核心逻辑。

Provider

对于Provider和connect并没有贴真正的源码,我们只需要关注核心逻辑原理即可,感兴趣的可以自己再去查看完整的源码。只看带有注释的地方

 import React, { useMemo } from 'react'import Subscription from '../utils/Subscription'
 import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'// 这句来自于源码中Contextjs, 使用 React.createContext(null),创建一个创建一个上下文的容器(组件),就是通过此Api,解决在子组件中获取 store
 const ReactReduxContext = /*#__PURE__*/ React.createContext(null)
 ​
 function Provider({ store, context, children }) {
     
   const contextValue = useMemo(() => {
       // new一个subscription实例,用于订阅组件
     const subscription = new Subscription(store)
     subscription.onStateChange = subscription.notifyNestedSubs
     return {
       store,
       subscription,
     }
   }, [store])
   const previousState = useMemo(() => store.getState(), [store])
 ​
   //发现没,不就是我在上面写的改版redux案例吗,建议参考上面的redux案例了解怎么优化即可。
   useIsomorphicLayoutEffect(() => {
     const { subscription } = contextValue
     subscription.trySubscribe()
     // 如果上一次数据与本次数据不同,则重新更新依赖的次数据的组件。。
     if (previousState !== store.getState()) {
       subscription.notifyNestedSubs()
     }
       // 卸载所有的订阅
     return () => {
       subscription.tryUnsubscribe()
       subscription.onStateChange = null
     }
   }, [contextValue, previousState])
    
   const Context = context || ReactReduxContext
   return <Context.Provider value={contextValue}>{children}</Context.Provider>
 }
 ​
 export default Provider

可以看到: 解决store向下传递的问题是通过react的Api, React.createContext,将store注入到react的上下文中,并且自动更新组件也是通过redux的subscribe原理实现的。

connect

react-redux 作者写的connect核心英文源码解析

下面的connect实现,不是真正的实现!!!!,而是一种心理模型。

它跳过了从哪里得到“store”的问题 (答案: 通过 把它放在React上下文中,Provider中已经提及过了)

它会跳过任何性能优化(真正的connect()会确保我们不会发生不必要的渲染),也就是串联Provider中useIsomorphicLayoutEffect的实现。建议参考上面的改版后的redux案例,了解怎么优化即可。

// connect() 是一个高阶函数 ,返回一个高阶组件,用于将redux中数据通过props注入到你的组件.
 // 你可以传入store中的state数据和用于修改state的dispatch(action) 函数。
 function connect(mapStateToProps, mapDispatchToProps) {
   // connect(mapStateToProps, mapDispatchToProps)(你的组件)
   // 返回一个高阶组件,此组件用于增强你原本的组件
   return function (WrappedComponent) {
     // It returns a component
     return class extends React.Component {
       render() {
         return (
           // connect(mapStateToProps, mapDispatchToProps)(你的组件) 传入的组件
           <WrappedComponent
             {/* 组件原本的props  */}
             {...this.props}
             {/* 添加store中的数据到原本组件的props中 */}
             {...mapStateToProps(store.getState(), this.props)}
             {/* 添加用于修改state的dispatch(action) 函数到原本组件的props中  */}
             {...mapDispatchToProps(store.dispatch, this.props)}
           />
         )
       }
       componentDidMount() {
         // 利用redux的原生能力,订阅handleChange
         this.unsubscribe = store.subscribe(this.handleChange.bind(this))
       }
       
       componentWillUnmount() {
         // 组件卸载的时候取消订阅
         this.unsubscribe()
       }
     
       handleChange() {
         // 强制重新更新此组件
         this.forceUpdate()
       }
     }
   }
 }
 ​
 // connect()的目的是您不必考虑
 // 订阅store或自己进行性能优化, and
 // 相反,你可以指定如何根据Redux存储状态获取道具:
 const mapStateToProps =  (state) => ({
     value: state.counter,
   })
 const mapDispatchToProps = (dispatch) => ({
     onIncrement() {
       dispatch({ type: 'INCREMENT' })
     }
   })    
 // mapStateToProps 与 mapDispatchToProps 这两个函数,
 // 在connect内部执行,如{...mapStateToProps(store.getState(), this.props)},
 // mapStateToProps 执行后的返回值就是 { value: state.counter},然后通过...解构传入组件中
 // 在组件中就能直接获取了,如:class组件中 props.value。
 // mapDispatchToProps 同理,不再阐述。
 const ConnectedCounter = cnnect(
   mapStateToProps,
   mapDispatchToProps
 )(Counter)

总结

react-redux相比redux

  1. 使用React.createContext,解决store传递的问题,
  2. 对更新前后的值进行对比,避免重复更新的问题,重新渲染组件依旧使用的是redux的subscribe订阅组件,dispatch时遍历所有订阅的组件,重新渲染。
  3. 使用connect高阶组件,将组件需要store中的值,注入到组件的props中。

react-redux还提供了hooks的用法,相比connect高阶组件,使用时,对于代码理解性更好,这里就不再提及啦

参考链接

为什么要使用 React Redux?

react-redux 作者写的connect核心英文源码解析