Redux
简述
Redux 最本质的东西是一个 store 对象,store向外暴露以下方法,用来获取/更新的 state,以及订阅 state 的更新,本质上就是一个发布/订阅。
下面三行伪代码来简单描述出来 redux 的工作原理:
scss
// 创建一个 store
const store = createStore(reducer);
// 订阅 store 中 state 变化,state 更新后执行回调 `console.log(store.getState()`
store.subscribe(() => console.log(store.getState()));
// 派发一个更新行为,用来更新 store 内部的 state
store.dispatch(action);
下图为 redux 官方展示的 Redux 内部的工作原理图,但是我认为并不是很清晰,没有表明如何 dispatch 之后如何更新 UI,那就是 subscribe 操作:
个人理解如下图:
解释:
- UI 订阅了 store 内部 state 变化
- UI 事件触发了dispatch({ type: 'xxx', payload: yyy })
- 执行 dispatch 时,内部先执行 reducer
- reducer 通过 dispatch 过来的 action.type 和 store 内部上一次的state,来更新state,注意这个更新 state 是全量替换的,如何 state 是一个结构复杂的对象,也是重新构造一个对象,而不是直接修改原来 state 的属性
- 更新 state 之后,通知 UI,Store 发生变化,更新 UI
源码解析
store 对象是由 Redux 中的 createStore 方法创建(工厂模式?)点击看源码
可以看到 store 对象中其实只有少量的几个属性,我们这里先关注最核心的三个属性:
注意这里并没有直接导出 state 属性,通过 createStore 时在内部维护 currentState,然后通过 getState 返回,形成了闭包,防止直接修改。
React-Redux
在上文中,简单描述了工作原理:发布订阅
但是在 react 使用 redux 时,是如何做到 dispatch 之后可以重新渲染组件呢
个人的思路就是在组件挂载时,订阅 store 更新,更新之后 render 组件(通过 useState 来实现 rerender)
纯Redux版本
- 在函数组件内部直接使用 useState(store.getState().count) 来初始化一个组件内部的 state
- 在一个 useEffect 中订阅 store 中 state 的变化,回调函数是 setCouter 然后重新渲染 react
typescript
import { useEffect, useState } from "react";
import { createStore } from "redux";
const store = createStore<
{ count: number },
{ type: string; payload?: number }
>((state = { count: 0 }, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
});
function App() {
const [count, setCount] = useState(store.getState().count);
useEffect(() => {
const rerender = () => {
setCount(store.getState().count);
}
const unsubscribe = store.subscribe(rerender);
return unsubscribe
}, [store.subscribe]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => store.dispatch({ type: "INCREMENT" })}>
Increment
</button>
<button onClick={() => store.dispatch({ type: "DECREMENT" })}>
Decrement
</button>
</div>
);
}
export default App;
React-Redux
最简单的理解:
- react-redux 通过在 Provider 组件中提供了store
- 在 Provider 组件的后代组件中使用 useSelector中,封装了对子状态的选择,以及自动处理订阅等一系列操作
javascript
import { Provider } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import store from "./store";
const SubComponent = () => {
const dispatch = useDispatch();
const { todos } = useSelector(
(state: RootState) => state.todos
);
return todos.map(item => (
<div key={item.id}>{item.name}</div>
))
}
const App = () => {
return (
<Provider store={store}>
</Provider>
);
};
export default App;
可以看一下 react-redux 源码中 useSelector 的 hook 实现,实现原理也蛮简单的,关键步骤用到了 react18 中的useSyncExternalStore 来实现,点击看源码
useSelector 的伪代码如下
scss
function useSelector(seletor) {
// 从react-redux中的provider中获取context
const reduxContext = useReduxContext()
// 从 上下文中获取store和subscription,subscription 是 react-redux 优化过的发布订阅
const { store, subscription } = reduxContext
// 对 seletor 函数进行包装,这里先不关注,其实就是选择 redux 内部 state 的 subState
// 比如 seletor = (state) => state.subState
const wrappedSelector = wrapper(seletor)
// 使用 useSyncExternalStoreWithSelector 完成自动订阅
const selectedState = useSyncExternalStoreWithSelector(
subscription.addNestedSub,
store.getState,
getServerState || store.getState,
wrappedSelector,
equalityFn,
)
React.useDebugValue(selectedState)
// 返回改状态
return selectedState
}
顺便可以看一下 React 内部关于 useSyncExternalStore 的源码实现,也很简单,下面是 useSyncExternalStore 的执行过程:
简化之后,可以理解为:
-
在 useEffect 中执行 subscribeToStore
-
subscribeToStore 内部其实就是订阅了handleStoreChange
-
handleStoreChange 首先检查状态有没有更新(checkIfSnapshotChanged),决定是否要执行强制更新forceStoreRerender
- checkIfSnapshotChanged 通过 getSnapshot 和 value进行浅比较
最后,看了源码实现,useSelector 的伪代码实现可以简化为
scss
function useSelector(seletor) {
// 从react-redux中的provider中获取context
const reduxContext = useReduxContext()
// 从 上下文中获取store和subscription,subscription 是 react-redux 优化过的发布订阅
const { store, subscription } = reduxContext
// 对 seletor 函数进行包装,这里先不关注,其实就是选择 redux 内部 state 的 subState
// 比如 seletor = (state) => state.subState
const wrappedSelector = wrapper(seletor)
// 使用 useSyncExternalStoreWithSelector 完成自动订阅
// const selectedState = useSyncExternalStoreWithSelector(
// subscription.addNestedSub,
// store.getState,
// getServerState || store.getState,
// wrappedSelector,
// equalityFn,
// )
const selectedState = wrappedSelector();
useEffect(() => {
const unsub = subscription.scribe(() => {
if(selectedState !== preState) {
forceRender()
}
})
return unsub;
}, [subscription.scribe])
React.useDebugValue(selectedState)
// 返回改状态
return selectedState
}
所以简化之后的代码是不是和最开始的代码非常类似
ini
useEffect(() => {
const rerender = () => {
setCount(store.getState().count);
}
const unsubscribe = store.subscribe(rerender);
return unsubscribe
}, [store.subscribe]);