学习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+
按钮时,比如点击任意一个按钮都会输出以下内容。
以上案例,存在两个问题
-
使用store.subscribe(render)订阅整个页面,导致不管更新哪个组件都重新渲染整个页面,正确的渲染应该是A组件依赖了count数据,那么只有当count改变时再重新渲染A组件,B组件没有依赖count就不重新渲染B
-
需要给每个子组件传递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
- 使用React.createContext,解决store传递的问题,
- 对更新前后的值进行对比,避免重复更新的问题,重新渲染组件依旧使用的是redux的subscribe订阅组件,dispatch时遍历所有订阅的组件,重新渲染。
- 使用connect高阶组件,将组件需要store中的值,注入到组件的props中。
react-redux还提供了hooks的用法,相比connect高阶组件,使用时,对于代码理解性更好,这里就不再提及啦