为什么会出现redux
因为在很多的系统中,是需要管理一些全局的数据的,比如用户的名字,电话等信息是全局共享的,并且需要数据是响应式的
设计理念
因为数据会在全局使用,也就会存在于全局可以更改,所以为了保证数据修改的溯源与可预测性,便参照了flux的单向数据流的理念,流程大概就是下面的流程图
手搓一个Redux
我们有了需求.跟大体的设计理念,那我们可以开始尝试手搓一个redux
数据需要共享,所以我们创建一个store
我们可以思考一下react的数据共享,是父组件上提供数据,然后所有的子组件都可以拿到数据,所以我们应该想到了可以用react的createContext来创建一个共享的数据
1,所以我们在根目录下创建一个my-redux 的文件夹,然后创建一个 index.js 的文件
2,创建一个Provider 的函数组件,接受一个store 的参数,并且把这个参数使用createContext这个Api来创建一个子组件可获取到的数据,
3,返回storeContext.Provider 的这么一个组件,并且把数据赋值给value,不太明白createContext的同学可以看一下这个Api的用,这个不多加赘述
javascript
import {createContext, useContext} from "react"
let StoreContext;
const Provider = (props)=>{
StoreContext = createContext(props.store)
return <StoreContext.Provider value={props.store}>{props.children}</StoreContext.Provider>
}
const useStoreContext = () => {
return useContext(StoreContext);
};
export {
useStoreContext
}
export default Provider
我们尝试一下,在components中创建一个ReduxTest的组件,然后引用useStoreContext,使用div来展示,会发现数据是能正常拿到的
javascript
import Provider from "./redux/index"
import ReduxTest from "./components/ReduxTest";
function App() {
return (
<Provider store={{count:3}}>
<ReduxTest></ReduxTest>
</Provider>
);
}
export default App;
到这里,只要我们把根组件使用Provider这个组件进行包裹,那我们所有的后代组件就是可以数据共享
界面更新
上面我们实现了跨组件的数据共享,但是也同时发现了一个问题,数据更新时,界面并不会刷新,接下来我们就来解决这个问题
如何刷新?
界面刷新,我们可以想到利用react的useState 这个hooks,让共享的数据更新的时候,我们都去给他set一个新的对象呗,不就可以引发界面刷新了吗?那我们可以得出下面的代码
scss
// useContext的写法可以获取到createContext创建出来的数据,都是可以用于跨组件的通信
const [,updateState] = useState()
// 这个方法是直接给updateState一个新的对象来触发强制渲染
const forceUpdate = useCallback(()=>{updateState({})},[])
const handleStoreChange = ()=>{
// 强制刷新
forceUpdate();
}
怎么触发刷新呢?
刷新的逻辑我们实现了伪代码,但是我们怎么知道在什么时候刷新呢?我们想到使用订阅者这个设计模式来实现
我们创建一个createStore的js文件,并且导出一个createStore的函数,函数返回两个函数,一个是订阅,一个是触发
javascript
export const createStore = (reducer)=>{
const obervers = [];
// 触发某个动作
function dispatch(){
obervers.forEach(fn => fn())
}
// 订阅回调函数
function subscrible(fn){
obervers.push(fn)
}
return{
dispatch,
subscrible
}
}
然后在TestRedux这个组件中去创建实例,并且订阅一下界面刷新的函数,在需要的地方调用dispatch的触发
javascript
import {useStoreContext} from '../redux'
import {createStore} from "../redux/createStore";
import {useCallback, useEffect, useState} from "react";
const reduxStore = createStore()
const ReduxTest = ()=>{
const store = useStoreContext()
const [,updateState] = useState()
// 这个方法是直接给updateState一个新的对象来触发强制渲染
const forceUpdate = useCallback(()=>{updateState({})},[])
const handleStoreChange = ()=>{
// 强制刷新
forceUpdate();
}
useEffect(() => {
// 订阅一下强制刷新的函数
reduxStore.subscrible(handleStoreChange)
}, []);
return <>
<div onClick={()=>{
store.count++;
// 调用一下刷新
reduxStore.dispatch()
}}>
{store.count}
</div>
</>
}
export default ReduxTest
这个时候界面会根据数值的变化刷新了,但是现在修改数据的方式并不符合我们flux单向数据流的设计理想,所以我们需要进行优化修改
flux设计理念的单向数据流
我们完善一下之前的状态流向图
action 是由组件的触发事件,update 这个我们已经封装到了装饰器函数中,后面会说到,所以我们应该再提供3个东西
1, store共享的数据
2, dispatch触发器
3, subscrible用于收集回调函数的订阅器
需要的东西我们前面的createStore.js 文件中已经实现了两个了,所以我们直接修改该文件,添加上store 需要共享的数据,并且在dispatch的时候把新的状态替换掉
javascript
export const createStore = ()=>{
const obervers = [];
let currentState = undefined;
// 获取当前状态
function getState(){
return currentState
}
// 触发某个动作
function dispatch(newState){
currentState = newState;
obervers.forEach(fn => fn())
}
// 订阅回调函数
function subscrible(fn){
obervers.push(fn)
}
return{
dispatch,
subscrible,
getState
}
}
然后我们修改一下我们提供给Provider 组件的数据,改为createStore 的实例,这样所有的后代组件都可以通过useContext 拿到dispatch,subscrible,getState三个方法
javascript
import Provider from "./redux/index"
import ReduxTest from "./components/ReduxTest";
import {createStore} from "./redux/createStore";
function App() {
return (
<Provider store={createStore()}>
<ReduxTest></ReduxTest>
</Provider>
);
}
export default App;
装饰器设计模式优化界面刷新逻辑
上面界面刷新的伪代码,我们思考一下不可能在所有需要数据共享的子组件当中去写这一段代码,非常不友好,使用自定义hooks进行封装?可以,但是还是需要手动的调用,还是不够好,我们可以捋一下思路,store 的数据共享和界面刷新,其实相当于是给这个组件增加了某些功能 ,所以这个时候我们可以使用装饰器设计模式来实现
我们在原本的index.js文件中,导出一个connect的函数,接受一个组件作为参数,然后给这个组件添加一些功能并且最后返回这个组件,其实也就是高阶组件HOC的写法
scss
export function connect(Component){
// 返回一个高阶组件,是为了可以让Component组件能够拿到原本的props参数,不影响原本组件的数据的传递跟功能
return function ConnextComponent(props){
// useContext的写法可以获取到createContext创建出来的数据,都是可以用于跨组件的通信
const store = useContext(StoreContext)
const [,updateState] = useState()
// 这个方法是直接给updateState一个新的对象来触发强制渲染
const forceUpdate = useCallback(()=>{updateState({})},[])
const handleStoreChange = ()=>{
// 强制刷新
forceUpdate();
}
useEffect(() => {
store.subscrible(handleStoreChange)
}, []);
return <Component
{...props}
{...store.getState()}
dispatch={store.dispatch}
>
</Component>
}
}
我们在改变一下我们子组件导出的方式,使用connect 装饰器函数装饰一下,并且可以把之前界面刷新的逻辑给去掉了,因为connect这个函数已经帮我们做了
javascript
import {useStoreContext} from '../redux'
import {createStore} from "../redux/createStore";
import {useCallback, useEffect, useState} from "react";
import {connect} from "../redux/index"
const reduxStore = createStore()
const ReduxTest = (props)=>{
return <>
<div onClick={()=>{
props.dispatch({count:2})
}}>
{props.count}
</div>
</>
}
// 使用connect装饰一下,就有了界面更新的能力
export default connect(ReduxTest)
小结一下
- 通过react自带的createContextApi实现了跨组件的数据共享
- 通过useState 这个hooks,每次set一个新的对象来触发界面的刷新
- 为了实现flux 的单向数据流的设计理念,我们把state,subscrible,dispatch都进行了函数封装并且统一返回
- 使用装饰器的设计模式 来给组件增加界面刷新的功能,这样使用connect 函数装饰过的组件直接可以通过props 来拿到共享的数据和触发dispatch
优化单向数据流
上面我们看似实现了单向数据流,但是很明显并不够严谨,因为如果通过直接的dispatch来给一个新的数据的话,那使用起来还是会很乱,而且数据没有办法预知,还可能会有覆盖别的组件数据的可能,所以我们要优化一下
使用reducer来进行数据与触发的统一管理
我们新建一个reducer.js 的文件,把初始化数据 ,改变数据的事件都在这个文件中进行管理
javascript
// 初始化的数据
const initalState = {
count:0,
}
// reducer函数用于数据的改变操作
export function reducer(state=initalState,action){
switch (action.type){
case 'add':
return {
...state,
count:state.count + 1
}
case 'reduce':
return {
...state,
count:state.count - 1
}
default:
return initalState
}
}
这个时候我们就需要修改一下createStore.js 的初始化方法 ,接收reducer 放饭作为参数,dispatch 方法,接受一个action 参数,并且调用reducer 函数将action传入 ,这样要改变共享的数据,只能通过在reducer 中定义对应的action.type来实现,并且数据需要在上面进行初始化,这样数据的流向就更加的严谨了
javascript
export const createStore = (reducer)=>{
const obervers = [];
let currentState = undefined;
// 获取当前状态
function getState(){
return currentState
}
// 触发某个动作
function dispatch(action){
currentState = reducer(currentState,action)
obervers.forEach(fn => fn())
}
// 订阅回调函数
function subscrible(fn){
obervers.push(fn)
}
// 初始化state数据
dispatch({type:'@@REDUX/INIT'})
return{
dispatch,
subscrible,
getState
}
}
上面需要注意dispatch({type:'@@REDUX/INIT'}) , 是因为我们把state的数值单独进行抽取,所以需要进行初始化
我们修改一下App.js创建store的时候把reducer作为参数传入
javascript
import Provider from "./redux/index"
import ReduxTest from "./components/ReduxTest";
import {createStore} from "./redux/createStore";
import {reducer} from "./redux/reducer";
function App() {
return (
<Provider store={createStore(reducer)}>
<ReduxTest></ReduxTest>
</Provider>
);
}
export default App;
这个时候,我们调用diaptch时候就要根据reducer中使用的action.type来传入了
javascript
import {connect} from "../redux/index"
const ReduxTest = (props)=>{
return <>
<div onClick={()=>{
props.dispatch({type:'add'})
}}>
{props.count}
click Me
</div>
</>
}
export default connect(ReduxTest)
总结:
- 通过react自带的createContextApi实现了跨组件的数据共享
- 通过useState 这个hooks,每次set一个新的对象来触发界面的刷新
- 为了实现flux 的单向数据流的设计理念,我们把state,subscrible,dispatch都进行了函数封装并且统一返回
- 使用装饰器的设计模式 来给组件增加界面刷新的功能,这样使用connect 函数装饰过的组件直接可以通过props 来拿到共享的数据和触发dispatch
- 为了为了全局数据的可预测性 ,我们使用reducer 来把初始化状态 ,action 都单独抽取放到一起方便管理,这样使用的时候就只能通过action 来触发dispatch ,在reducer 也可以限制用户随意的修改全局的共享数据
文章到这里就已经写完了,手搓的redux是完全不如框架的逻辑完善的,框架自身也有很优秀的一些优化,推荐大家有兴趣可以去研究一下源码~