百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

「干货满满」从零实现 react-redux

toyiye 2024-06-21 12:06 12 浏览 0 评论


作者: sheen

转发连接:https://mp.weixin.qq.com/s/QNpsRZFVYFFl_A8WXVW9Yw

1. 前言

在 React 诞生之初,Facebook 宣传这是一个用于前端开发的界面库,仅仅是一个 View 层。前面我们也介绍过 React 的组件通信,在大型应用中,处理好 React 组件通信和状态管理就显得非常重要。为了解决这一问题,Facebook 最先提出了单向数据流的 Flux 架构,弥补了使用 React 开发大型网站的不足。

Flux:

随后,Dan Abramov 受到 Flux 和函数式编程语言 Elm 启发,开发了 Redux 这个状态管理库。Redux 源码非常精简,实现也很巧妙,这篇文章将带你从零手写一个 Redux 和 react-redux 库,以及告诉你该如何设计 Redux 中的 store。在开始前,我已经将这篇文章的完整代码都整理到 GitHub 上,大家可以参考一下。

Redux:simple-redux

React-redux:simple-react-redux

2. 状态管理

2.1 理解数据驱动

在开始讲解状态管理前,我们先来了解一下现代前端框架都做了些什么。以 Vue 为例子,在刚开始的时候,Vue 官网首页写的卖点是数据驱动、组件化、MVVM 等等(现在首页已经改版了)。那么数据驱动的意思是什么呢?不管是原生 JS 还是 jQuery,他们都是通过直接修改 DOM 的形式来实现页面刷新的。而 Vue/React 之类的框架不是粗暴地直接修改 DOM,而是通过修改 data/state 中的数据,实现了组件的重新渲染。也就是说,他们封装了从数据变化到组件渲染这一个过程。

原本我们用 jQuery 开发应用,除了要实现业务逻辑,还要操作 DOM 来手动实现页面的更新。尤其是涉及到渲染列表的时候,更新起来非常麻烦。

function logger(middlewareAPI) {
  return function (next) { // next 即 dispatch
    return function (action) {
      console.log('dispatch 前:', middlewareAPI.getState());
      var returnValue = next(action);
      console.log('dispatch 后:', middlewareAPI.getState(), '\n');
      return returnValue;
    };
  };
}

所以后来出现了 jQuery.tpl 和 Underscore.template 之类的模板,这些让操作 DOM 变得容易起来,有了数据驱动和组件化的雏形,可惜我们还是要手动去渲染一遍。

<script type="text/template" id="tpl">
    <ul id="todo-list">
        <% _.each(todos, function(todo){ %>
            <li data-id="<%=todo.id%>" class="todo-item">
                <%= todo.content %>
            </li>
        <% }); %>
    </ul>
</script>

如果说用纯原生 JS 或者 jQuery 开发页面是原始农耕时代,那么 React/Vue 等现代化框架则是自动化的时代。有了前端框架之后,我们不需要再去关注怎么生成和修改 DOM,只需要关心页面上的这些数据以及流动。所以如何管理好这些数据流动就成了重中之重,这也是我们常说的“状态管理”。

2.2 什么状态需要管理?

前面讲了很多例子,可状态管理到底要管理什么呢?在我看来,状态管理的一般就是这两种数据。

  1. Domain State Domain State 就是服务端的状态,这个一般是指通过网络请求来从服务端获取到的数据,比如列表数据,通常是为了和服务端数据保持一致。
{
    "data": {
        "hotels": [
            {
                "id": "31231231",
                "name": "希尔顿",
                "price": "1300"
            }
        ]
    }
}
  1. UI State UI State 常常和交互相关。例如模态框的开关状态、页面的 loading 状态、单(多)选项的选中状态等等,这些状态常常分散在不同的组件里面。
{
    "isLoading": true,
    "isShowModal": false,
    "isSelected": false
}

2.3 全局状态管理

我们用 React 写组件的时候,如果需要涉及到兄弟组件通信,经常需要将状态提升到两者父组件里面。一旦这种组件通信多了起来,数据管理就是个问题。结合上面的例子,如果想要对应用的数据流进行管理,那是不是可以将所有的状态放到顶层组件中呢?将数据按照功能或者组件来划分,将多个组件共享的数据单独放置,这样就形成了一个大的树形 store。这里更建议按照功能来划分。

这个大的 store 可以放到顶层组件中维护,也可以放到顶层组件之外来维护,这个顶层组件我们一般称之为“容器组件”。容器组件可以将组件依赖的数据以及修改数据的方法一层层传给子组件。我们可以将容器组件的 state 按照组件来划分,现在这个 state 就是整个应用的 store。将修改 state 的方法放到 actions 里面,按照和 state 一样的结构来组织,最后将其传入各自对应的子组件中。

class App extends Component {
    constructor(props) {
        this.state = {
            common: {},
            headerProps: {},
            bodyProps: {
                sidebarProps: {},
                cardProps: {},
                tableProps: {},
                modalProps: {}
            },
            footerProps: {}
        }
        this.actions = {
            header: {
                changeHeaderProps: this.changeHeaderProps
            },
            footer: {
                changeFooterProps: this.changeFooterProps
            },
            body: {
                sidebar: {
                    changeSiderbarProps: this.changeSiderbarProps
                }
            }
        }
    }
    
    changeHeaderProps(props) {
        this.setState({
            headerProps: props
        })
    }
    changeFooterProps() {}
    changeSiderbarProps() {}
    ...
    
    render() {
        const {
            headerProps,
            bodyProps,
            footerProps
        } = this.state;
        const {
            header,
            body,
            footer
        } = this.actions;
        return (
            <div className="main">
                <Header {...headerProps} {...header} />
                <Body {...bodyProps} {...body} />
                <Footer {...footerProps} {...footer} />
            </div>
        )
    }
}

我们可以看到,这种方式可以很完美地解决子组件之间的通信问题。只需要修改对应的 state 就行了,App 组件会在 state 变化后重新渲染,子组件接收新的 props 后也跟着渲染。

这种模式还可以继续做一些优化,比如结合 Context 来实现向深层的组件传递数据。

const Context = createContext(null);
class App extends Component {
    ...
    render() {
        return (
            <div className="main">
                <Context.Provider value={...this.state, ...this.events}>
                    <Header />
                    <Body />
                    <Footer />
                </Context.Provider>
            </div>
        )
    }
}
const Header = () => {
    // 获取到 Context 数据
    const context = useContext(Context);
}

如果你已经接触过 Redux 这个状态管理库,你会惊奇地发现,如果我们把 App 组件中的 state 移到外面,这不就是 Redux 了吗?没错,Redux 的核心原理也是这样,在组件外部维护一个 store,在 store 修改的时候会通知所有被 connect 包裹的组件进行更新。这个例子可以看做 Redux 的一个雏形。

3. 实现一个 Redux

根据前面的介绍我们已经知道了,Redux 是一个状态管理库,它并非绑定于 React 使用,你还可以将其和其他框架甚至原生 JS 一起使用,比如这篇文章:如何在非 React 项目中使用 Redux (https://segmentfault.com/a/1190000009963395)

Redux 工作原理:

在学习 Redux 之前需要先理解其工作原理,一般来说流程是这样的:

  1. 用户触发页面上的某种操作,通过 dispatch 发送一个 action。
  2. Redux 接收到这个 action 后通过 reducer 函数获取到下一个状态。
  3. 将新状态更新进 store,store 更新后通知页面重新渲染。

从这个流程中不难看出,Redux 的核心就是一个 发布-订阅 模式。一旦 store 发生了变化就会通知所有的订阅者,view 接收到通知之后会进行重新渲染。

Redux 有三大原则:

  • 单一数据源前面的那个例子,最终将所有的状态放到了顶层组件的 state 中,这个 state 形成了一棵状态树。在 Redux 中,这个 state 则是 store,一个应用中一般只有一个 store。
  • State 是只读的在 Redux 中,唯一改变 state 的方法是触发 action,action 描述了这次修改行为的相关信息。只允许通过 action 修改可以使应用中的每个状态修改都很清晰,便于后期的调试和回放。
  • 通过纯函数来修改为了描述 action 使状态如何修改,需要你编写 reducer 函数来修改状态。reducer 函数接收前一次的 state 和 action,返回新的 state。无论被调用多少次,只要传入相同的 state 和 action,那么就一定返回同样的结果。

关于 Redux 的用法,这里不做详细讲解,建议参考阮一峰老师的《Redux 入门》系列的教程:Redux 入门教程

3.1 实现 store

在 Redux 中,store 一般通过 createStore 来创建。

import { createStore } from 'redux'; 
const store = createStore(rootReducer, initalStore, middleware);

先看一下 Redux 中暴露出来的几个方法。

其中 createStore 返回的方法主要有 subscribe、dispatch、replaceReducer、getState。

createStore 接收三个参数,分别是 reducers 函数、初始值 initalStore、中间件 middleware。

store 上挂载了 getState、dispatch、subscribe 三个方法。

getState 是获取到 store 的方法,可以通过 store.getState() 获取到 store。

dispatch 是发送 action 的方法,它接收一个 action 对象,通知 store 去执行 reducer 函数。

subscribe 则是一个监听方法,它可以监听到 store 的变化,所以可以通过 subscribe 将 Redux 和其他框架结合起来。

replaceReducer 用来异步注入 reducer 的方法,可以传入新的 reducer 来代替当前的 reducer。

3.2 实现 getState

store 的实现原理比较简单,就是根据传入的初始值来创建一个对象。利用闭包的特性来保留这个 store,允许通过 getState 来获取到 store。之所以通过 getState 来获取 store 是为了获取到当前 store 的快照,这样便于打印日志以对比前后两次 store 变化,方便调试。

const createStore = (reducers, initialState, enhancer) => {
    let store = initialState;
    const getState = () => store;
    return {
        getState
    }
}

当然,现在这个 store 实现的比较简单,毕竟 createStore 还有两个参数没用到呢。先别急,这俩参数后面会用到的。

3.3 实现 subscribe && unsubscribe

既然 Redux 本质上是一个 发布-订阅 模式,那么就一定会有一个监听方法,类似 jQuery 中的 $.on,在 Redux 中提供了监听和解除监听的两个方法。实现方式也比较简单,使用一个数组来保存所有监听的方法。

const createStore = (...) => {
    ...
    let listeners = [];
    const subscribe = (listener) => {
        listeners.push(listener);
    }
    const unsubscribe = (listener) => {
        const index = listeners.indexOf(listener)
        listeners.splice(index, 1)
    }
}

3.4 实现 dispatch

dispatch 和 action 是息息相关的,只有通过 dispatch 才能发送 action。而发送 action 之后才会执行 subscribe 监听到的那些方法。所以 dispatch 做的事情就是将 action 传给 reducer 函数,将执行后的结果设置为新的 store,然后执行 listeners 中的方法。

const createStore = (reducers, initialState) => {
    ...
    let store = initialState;
    const dispatch = (action) => {
        store = reducers(store, action);
        listeners.forEach(listener => listener())
    }
}

这样就行了吗?当然还不够。如果有多个 action 同时发送,这样很难说清楚最后的 store 到底是什么样的,所以需要加锁。在 Redux 中 dispatch 执行后的返回值也是当前的 action。

const createStore = (reducers, initialState) => {
    ...
    let store = initialState;
    let isDispatch = false;
    const dispatch = (action) => {
        if (isDispatch) return action
        // dispatch必须一个个来
        isDispatch = true
        store = reducers(store, action);
        isDispatch = false
        listeners.forEach(listener => listener())
        return action;
    }
}

至此为止,Redux 工作流程的原理就已经实现了。但你可能还会有很多疑问,如果没有传 initialState,那么 store 的默认值是什么呢?如果传入了中间件,那么又是什么工作原理呢?

3.5 实现 combineReducers

在刚开始接触 Redux 的 store 的时候,我们都会有一种疑问,store 的结构究竟是怎么定的?combineReducers 会揭开这个谜底。现在来分析 createStore 接收的第一个参数,这个参数有两种形式,一种直接是一个 reducer 函数,另一个是用 combineReducers 把多个 reducer 函数合并到一起。

可以猜测 combineReducers 是一个高阶函数,接收一个对象作为参数,返回了一个新的函数。这个新的函数应当和普通的 reducer 函数传参保持一致。

const combineReducers = (reducers) => {
    return function combination(state = {}, action) {
    }
}

那么 combineReducers 做了什么工作呢?主要是下面几步:

  1. 收集所有传入的 reducer 函数
  2. 在 dispatch 中执行 combination 函数时,遍历执行所有 reducer 函数。如果某个 reducer 函数返回了新的 state,那么 combination 就返回这个 state,否则就返回传入的 state。
const combineReducers = reducers => {
    const finalReducers = {},
        nativeKeys = Object.keys
    // 收集所有的 reducer 函数
    nativeKeys(reducers).forEach(reducerKey => {
        if(typeof reducers[reducerKey] === "function") {
            finalReducers[reducerKey] = reducers[reducerKey]
        }
    })
    return function combination(state, action) {
        let hasChanged = false;
        const store = {};
        // 遍历执行 reducer 函数
        nativeKeys(finalReducers).forEach(key => {
            const reducer = finalReducers[key];
            // 很明显,store 的 key 来源于 reducers 的 key 值
            const nextState = reducer(state[key], action)
            store[key] = nextState
            hasChanged = hasChanged || nextState !== state[key];
        })
        return hasChanged ? nextState : state;
    }
}

细心的童鞋一定会发现,每次调用 dispatch 都会执行这个 combination 的话,那岂不是不管我发送什么类型的 action,所有的 reducer 函数都会被执行一遍?如果 reducer 函数很多,那这个执行效率不会很低吗?但不执行貌似又无法完全匹配到 switch...case 中的 action.type。如果能通过键值对的形式来匹配 action.type 和 reducer 是不是效率更高一些?类似这样:

// redux
const count = (state = 0, action) => {
    switch(action.type) {
        case 'increment':
            return state + action.payload;
        case 'decrement':
            return state - action.payload;
        default:
            return state;
    }
}
// 改进后的
const count = {
    state: 0, // 初始 state
    reducers: {
        increment: (state, payload) => state + payload,
        decrement: (state, payload) => state - payload
    }
}

这样每次发送新的 action 的时候,可以直接用 reducers 下面的 key 值来匹配了,无需进行暴力的遍历。天啊,你实在太聪明了。小声告诉你,社区中一些类 Redux 的方案就是这样做的。以 rematch 和 relite 为例:rematch:

import { init, dispatch } from "@rematch/core";
import delay from "./makeMeWait";

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload);
    }
  }
};

const store = init({
  models: { count }
});

dispatch.count.incrementAsync(1);

relite:

const increment = (state, payload) => {
    state.count = state.count + payload;
    return state;
}
const decrement = (state, payload) => {
    state.count = state.count - payload;
    return state;
}

3.6 中间件 和 Store Enhancer

考虑到这样的情况,我想要打印每次 action 的相关信息以及 store 前后的变化,那我只能到每个 dispatch 处手动打印信息,这样繁琐且重复。createStore 中提供的第三个参数,可以实现对 dispatch 函数的增强,我们称之为 Store Enhancer。 Store Enhancer 是一个高阶函数,它的结构一般是这样的:

const enhancer = () => {
    return (createStore) => (reducer, initState, enhancer) => {
        ...
    }
}

enhancer 接收 createStore 作为参数,最后返回的是一个加强版的 store,本质上是对 dispatch 函数进行了扩展。logger:

const logger = () => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer);
        const dispatch = (action) => {
            console.log(`action=${JSON.stringify(action)}`);
            const result = store.dispatch(action);
            const state = store.getState();
            console.log(`state=${JSON.stringify(state)}`);
            return result;
        }
        return {
            ...state,
            dispatch
        }
    }
}

createStore 中如何使用呢?一般在参数的时候,会直接返回。

const createStore = (reducer, initialState, enhancer) => {
    if (enhancer && typeof enhancer === "function") {
        return enhancer(createStore)(reducer, initialState)
    }
}

如果你有看过 applyMiddleware 的源码,会发现这两者实现方式很相似。applyMiddleware 本质上就是一个 Store Enhancer。

3.7 实现 applyMiddleware

在创建 store 的时候,经常会使用很多中间件,通过 applyMiddleware 将多个中间件注入到 store 之中。

const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));

applyMiddleware 的实现类似上面的 Store Enhancer。由于多个中间件可以串行使用,因此最终会像洋葱模型一样,action 传递需要经过一个个中间件处理,所以中间件做的事情就是增强 dispatch 的能力,将 action 传递给下一个中间件。那么关键就是将新的 store 和 dispatch 函数传给下一个中间件。

来看一下 applyMiddleware 的源码实现:

const applyMiddleware = (...middlewares) => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer)
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        let chain = middlewares.map(middleware => middleware(middlewareAPI))
        store.dispatch = compose(...chain)(store.dispatch)
        return {
          ...store,
          dispatch
        }
      }
}

这里用到了一个 compose 函数,compose 函数类似管道,可以将多个函数组合起来。compose(m1, m2)(dispatch) 等价于 m1(m2(dispatch))。使用 reduce 函数可以实现函数组合。

const compose = (...funcs) => {
    if (!funcs) {
        return args => args
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))
}

再来看一下 redux-logger 中间件的精简实现,会发现两者恰好能匹配到一起。

function logger(middlewareAPI) {
  return function (next) { // next 即 dispatch
    return function (action) {
      console.log('dispatch 前:', middlewareAPI.getState());
      var returnValue = next(action);
      console.log('dispatch 后:', middlewareAPI.getState(), '\n');
      return returnValue;
    };
  };
}

至此为止,Redux 的基本原理就很清晰了,最后整理一个精简版的 Redux 源码实现。

// 这里需要对参数为0或1的情况进行判断
const compose = (...funcs) => {
    if (!funcs) {
        return args => args
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))
}

const bindActionCreator = (action, dispatch) => {
    return (...args) => dispatch(action(...args))
}

const createStore = (reducer, initState, enhancer) => {
    if (!enhancer && typeof initState === "function") {
        enhancer = initState
        initState = null
    }
    if (enhancer && typeof enhancer === "function") {
        return enhancer(createStore)(reducer, initState)
    }
    let store = initState, 
        listeners = [],
        isDispatch = false;
    const getState = () => store
    const dispatch = (action) => {
        if (isDispatch) return action
        // dispatch必须一个个来
        isDispatch = true
        store = reducer(store, action)
        isDispatch = false
        listeners.forEach(listener => listener())
        return action
    }
    const subscribe = (listener) => {
        if (typeof listener === "function") {
            listeners.push(listener)
        }
        return () => unsubscribe(listener)
    }
    const unsubscribe = (listener) => {
        const index = listeners.indexOf(listener)
        listeners.splice(index, 1)
    }
    return {
        getState,
        dispatch,
        subscribe,
        unsubscribe
    }
}

const applyMiddleware = (...middlewares) => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer);
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        let chain = middlewares.map(middleware => middleware(middlewareAPI))
        store.dispatch = compose(...chain)(store.dispatch)
        return {
          ...store
        }
      }
}

const combineReducers = reducers => {
    const finalReducers = {},
        nativeKeys = Object.keys
    nativeKeys(reducers).forEach(reducerKey => {
        if(typeof reducers[reducerKey] === "function") {
            finalReducers[reducerKey] = reducers[reducerKey]
        }
    })
    return (state, action) => {
        const store = {}
        nativeKeys(finalReducers).forEach(key => {
            const reducer = finalReducers[key]
            const nextState = reducer(state[key], action)
            store[key] = nextState
        })
        return store
    }
}

4. 实现一个 react-redux

如果想要将 Redux 结合 React 使用的话,通常可以使用 react-redux 这个库。看过前面 Redux 的原理后,相信你也知道 react-redux 是如何实现的了吧。react-redux 一共提供了两个 API,分别是 connect 和 Provider,前者是一个 React 高阶组件,后者是一个普通的 React 组件。react-redux 实现了一个简单的发布-订阅库,来监听当前 store 的变化。两者的作用如下:

  1. Provider:将 store 通过 Context 传给后代组件,注册对 store 的监听。
  2. connect:一旦 store 变化就会执行 mapStateToProps 和 mapDispatchToProps 获取最新的 props 后,将其传给子组件。

使用方式:

// Provider
ReactDOM.render({
    <Provider store={store}></Provider>,
    document.getElementById('app')
})
// connect
@connect(mapStateToProps, mapDispatchToProps)
class App extends Component {}

4.1 实现 Provider

先来实现简单的 Provider,已知 Provider 会使用 Context 来传递 store,所以 Provider 直接通过 Context.Provider 将 store 给子组件。

// Context.js
const ReactReduxContext = createContext(null);

// Provider.js
const Provider = ({ store, children }) => {
    return (
        <ReactReduxContext.Provider value={store}>
            {children}
        </ReactReduxContext.Provider>
    )
}

Provider 里面还需要一个发布-订阅器

class Subscription {
    constructor(store) {
        this.store = store;
        this.listeners = [this.handleChangeWrapper];
    }
    notify = () => {
        this.listeners.forEach(listener => {
            listener()
        });
    }
    addListener(listener) {
        this.listeners.push(listener);
    }
    // 监听 store
    trySubscribe() {
        this.unsubscribe = this.store.subscribe(this.notify);
    }
    // onStateChange 需要在组件中设置
    handleChangeWrapper = () => {
        if (this.onStateChange) {
          this.onStateChange()
        }
    }
    unsubscribe() {
        this.listeners = null;
        this.unsubscribe();
    }
}

将 Provider 和 Subscription 结合到一起,在 useEffect 里面注册监听。

// Provider.js
const Provider = ({ store, children }) => {
    const contextValue = useMemo(() => {
        const subscription = new Subscription(store);
        return {
            store,
            subscription
        }
    }, [store]);
    // 监听 store 变化
    useEffect(() => {
        const { subscription } = contextValue;
        subscription.trySubscribe();
        return () => {
            subscription.unsubscribe();
        }
    }, [contextValue]);
    return (
        <ReactReduxContext.Provider value={contextValue}>
            {children}
        </ReactReduxContext.Provider>
    )
}

4.2 实现 connect

再来看 connect 的实现,这里主要有三步:

  1. 使用 useContext 获取到传入的 store 和 subscription。
  2. 对 subscription 添加一个 listener,这个 listener 的作用就是一旦 store 变化就重新渲染组件。
  3. store 变化之后,执行 mapStateToProps 和 mapDispatchToProps 两个函数,将其和传入的 props 进行合并,最终传给 WrappedComponent。

先来实现简单的获取 Context。

const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
    return function Connect(props) {
        const { store, subscription } = useContext(ReactReduxContext);
        return <WrappedComponent {...props} />
    }
}

接下来就要来实现如何在 store 变化的时候更新这个组件。我们都知道在 React 中想实现更新组件只有手动设置 state 和调用 forceUpdate 两种方法,这里使用 useState 每次设置一个 count 来触发更新。

const connect = (mapStateToProps, mapDispatchToProps) => {
    return (WrappedComponent) => {
        return (props) => {
            const { store, subscription } = useContext(ReactReduxContext);
            const [count, setCount] = useState(0)
            useEffect(() => {
                subscription.onStateChange = () => setCount(count + 1)
            }, [count])
            const newProps = useMemo(() => {
                const stateProps = mapStateToProps(store.getState()),
                    dispatchProps = mapDispatchToProps(store.dispatch);
                return {
                    ...stateProps,
                    ...dispatchProps,
                    ...props
                }
            }, [props, store, count])
            return <WrappedComponent {...newProps} />
        }
    }
}

react-redux 的原理和上面比较类似,这里只作为学习原理的一个例子,不建议用到生产环境中。

5. 如何设计 store

在开发中,如果想要查看当前页面的 store 结构,可以使用 [Redux-DevTools][14] 或者 [React Developer Tools][15] 这两个 chrome 插件来查看。前者一般用于开发环境中,可以将 store 及其变化可视化展示出来。后者主要用于 React,也可以查看 store。关于 Redux 中 store 如何设计对初学者来说一直都是难题,在我看来这不仅是 Redux 的问题,在任何前端 store 设计中应该都是一样的。

5.1 store 设计误区

这里以知乎的问题页 store 设计为例。在开始之前,先安装 React Developer Tools,在 RDT 的 Tab 选中根节点。

然后在 Console 里面输入 $r.state.store.getState(),将 store 打印出来。

可以看到 store 中有一个 entities 属性,这个属性中分别有 users、questions、answer 等等。

这是一个问题页,自然包括问题、回答、回答下面的评论 等等。

一般情况下,这里应该是当进入页面的时候,根据 question_id 来分批从后端获取到所有的回答。点开评论的时候,会根据 answer_id 来分批从后端获取到所有的评论。所以你可能会想到 store 结构应当这样设计,就像俄罗斯套娃一样,一层套着一套。

{
    questions: [
        {
            content: 'LOL中哪个英雄最能表达出你对刺客的想象?',
            question_id: '1',
            answers: [
                {   
                    answer_id: '1-1',
                    content: '我就是来提名一个已经式微的英雄的。没错,就是提莫队长...'
                    comments: [
                        {  
                            comment_id: '1-1-1',
                            content: '言语精炼,每一句话都是一幅画面,一组镜头'
                        }
                    ]
                }
            ]
        }
    ]
}

看图可以更直观感受数据结构:

这是初学者经常进入的一个误区,按照 API 来设计 store 结构,这种方法是错误的。以评论区回复为例子,如何将评论和回复的评论关联起来呢?也许你会想,把回复的评论当做评论的子评论不就行了吗?

{
    comments: [
        {
            comment_id: '1-1-1',
            content: '言语精炼,每一句话都是一幅画面,一组镜头',
            children: [
                {
                    comment_id: '1-1-2',
                    content: '我感觉是好多画面,一部电影。。。'
                }
            ]
        },
        {
            comment_id: '1-1-2',
            content: '我感觉是好多画面,一部电影。。。'
        }
    ]
}

这样挺好的,满足了我们的需求,但 children 中的评论和 comments 中的评论数据亢余了。

5.2 扁平化 store

聪明的你一定会想到,如果 children 中只保存 comment_id 不就好了吗?展示的时候只要根据 comment_id 从 comments 中查询就行了。这就是设计 store 的精髓所在了。我们可以将 store 当做一个数据库,store 中的状态按照领域(domain)来划分成一张张数据表。不同的数据表之间以主键来关联。因此上面的 store 可以设计成三张表,分别是 questions、answers、comments,以它们的 id 作为 key,增加一个新的字段来关联子级。

{
    questions: {
        '1': {
            id: '1',
            content: 'LOL中哪个英雄最能表达出你对刺客的想象?',
            answers: ['1-1']
        }
    },
    answers: {
        '1-1': {
            id: '1-1',
            content: '我就是来提名一个已经式微的英雄的。没错,就是提莫队长...',
            comments: ['1-1-1', '1-1-2']
        }
    },
    comments: {
        '1-1-1': {
            id: '1-1-1',
            content: '言语精炼,每一句话都是一幅画面,一组镜头',
            children: ['1-1-2']
        },
        '1-1-2': {
            id: '1-1-2',
            content: '我感觉是好多画面,一部电影。。。'
        }
    }
}

你会发现数据结构变得非常扁平化,避免了数据亢余以及嵌套过深的问题。在查找的时候也可以直接通过 id 来查找,避免了通过索引来查找某一具体项。

推荐React 学习相关文章

深入详解大佬用33行代码实现了React

让你的 React 组件性能跑得再快一点「实践」

React源码分析与实现(三):实践 DOM Diff

React源码分析与实现(一):组件的初始化与渲染「实践篇」

React源码分析与实现(二):状态、属性更新->setState「实践篇」

细说React 核心设计中的闪光点

手把手教你10个案例理解React hooks的渲染逻辑「实践」

React-Redux 100行代码简易版探究原理

手把手深入教你5个技巧编写更好的React代码【实践】

React 函数式组件性能优化知识点指南汇总

13个精选的React JS框架

深入浅出画图讲解React Diff原理【实践】

【React深入】React事件机制

Vue 3.0 Beta 和React 开发者分别杠上了

手把手深入Redux react-redux中间件设计及原理(上)【实践】

手把手深入Redux react-redux中间件设计及原理(下)【实践】

前端框架用vue还是react?清晰对比两者差异

为了学好 React Hooks, 我解析了 Vue Composition API

【React 高级进阶】探索 store 设计、从零实现 react-redux

写React Hooks前必读

深入浅出掌握React 与 React Native这两个框架

可靠React组件设计的7个准则之SRP

React Router v6 新特性及迁移指南

用React Hooks做一个搜索栏

你需要的 React + TypeScript 50 条规范和经验

手把手教你绕开React useEffect的陷阱

浅析 React / Vue 跨端渲染原理与实现

React 开发必须知道的 34 个技巧【近1W字】

三张图详细解说React组件的生命周期

手把手教你深入浅出实现Vue3 & React Hooks新UI Modal弹窗

手把手教你搭建一个React TS 项目模板

全平台(Vue/React/微信小程序)任意角度旋图片裁剪组件

40行代码把Vue3的响应式集成进React做状态管理

手把手教你深入浅出React 迷惑的问题点【完整版】

作者: sheen

转发连接:https://mp.weixin.qq.com/s/QNpsRZFVYFFl_A8WXVW9Yw

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码