在前端圈子有这样一种说法,Vue 入门最简单,React 学习曲线太陡,Angular...我仍是选择狗带吧。 在 React 诞生之初,Facebook 宣传这是一个用于前端开发的界面库,仅仅是一个 View 层。前面咱们也介绍过 React 的组件通讯,在大型应用中,处理好 React 组件通讯和状态管理就显得很是重要。 为了解决这一问题,Facebook 最早提出了单向数据流的 Flux 架构,弥补了使用 React 开发大型网站的不足。html
Flux:前端
随后,Dan Abramov 受到 Flux 和函数式编程语言 Elm 启发,开发了 Redux 这个状态管理库。 Redux 源码很是精简,实现也很巧妙,这篇文章将带你从零手写一个 Redux 和 react-redux 库,以及告诉你该如何设计 Redux 中的 store。 在开始前,我已经将这篇文章的完整代码都整理到 GitHub 上,你们能够参考一下。 Redux:simple-redux React-redux:simple-react-reduxreact
在开始讲解状态管理前,咱们先来了解一下现代前端框架都作了些什么。 以 Vue 为例子,在刚开始的时候,Vue 官网首页写的卖点是数据驱动、组件化、MVVM 等等(如今首页已经改版了)。 那么数据驱动的意思是什么呢?无论是原生 JS 仍是 jQuery,他们都是经过直接修改 DOM 的形式来实现页面刷新的。 而 Vue/React 之类的框架不是粗暴地直接修改 DOM,而是经过修改 data/state 中的数据,实现了组件的从新渲染。也就是说,他们封装了从数据变化到组件渲染这一个过程。git
本来咱们用 jQuery 开发应用,除了要实现业务逻辑,还要操做 DOM 来手动实现页面的更新。尤为是涉及到渲染列表的时候,更新起来很是麻烦。github
var ul = document.getElementById("todo-list");
$.each(todos, function(index, todo) {
var li = document.createElement('li');
li.innerHTML = todo.content;
li.dataset.id = todo.id;
li.className = "todo-item";
ul.appendChild(li);
})
复制代码
因此后来出现了 jQuery.tpl 和 Underscore.template 之类的模板,这些让操做 DOM 变得容易起来,有了数据驱动和组件化的雏形,惋惜咱们仍是要手动去渲染一遍。chrome
<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,只须要关心页面上的这些数据以及流动。 因此如何管理好这些数据流动就成了重中之重,这也是咱们常说的“状态管理”。数据库
前面讲了不少例子,可状态管理到底要管理什么呢?在我看来,状态管理的通常就是这两种数据。编程
{
"data": {
"hotels": [
{
"id": "31231231",
"name": "希尔顿",
"price": "1300"
}
]
}
}
复制代码
{
"isLoading": true,
"isShowModal": false,
"isSelected": false
}
复制代码
咱们用 React 写组件的时候,若是须要涉及到兄弟组件通讯,常常须要将状态提高到二者父组件里面。一旦这种组件通讯多了起来,数据管理就是个问题。 结合上面的例子,若是想要对应用的数据流进行管理,那是否是能够将全部的状态放到顶层组件中呢? 将数据按照功能或者组件来划分,将多个组件共享的数据单独放置,这样就造成了一个大的树形 store。这里更建议按照功能来划分。redux
这个大的 store 能够放到顶层组件中维护,也能够放到顶层组件以外来维护,这个顶层组件咱们通常称之为“容器组件”。 容器组件能够将组件依赖的数据以及修改数据的方法一层层传给子组件。 咱们能够将容器组件的 state 按照组件来划分,如今这个 state 就是整个应用的 store。将修改 state 的方法放到 actions 里面,按照和 state 同样的结构来组织,最后将其传入各自对应的子组件中。segmentfault
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 的一个雏形。
根据前面的介绍咱们已经知道了,Redux 是一个状态管理库,它并不是绑定于 React 使用,你还能够将其和其余框架甚至原生 JS 一块儿使用,好比这篇文章:如何在非 React 项目中使用 Redux
Redux 工做原理:
在学习 Redux 以前须要先理解其工做原理,通常来讲流程是这样的:
从这个流程中不难看出,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 入门教程
在 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。
store 的实现原理比较简单,就是根据传入的初始值来建立一个对象。利用闭包的特性来保留这个 store,容许经过 getState 来获取到 store。 之因此经过 getState 来获取 store 是为了获取到当前 store 的快照,这样便于打印日志以对比先后两次 store 变化,方便调试。
const createStore = (reducers, initialState, enhancer) => {
let store = initialState;
const getState = () => store;
return {
getState
}
}
复制代码
固然,如今这个 store 实现的比较简单,毕竟 createStore 还有两个参数没用到呢。 先别急,这俩参数后面会用到的。
既然 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)
}
}
复制代码
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 的默认值是什么呢?若是传入了中间件,那么又是什么工做原理呢?
在刚开始接触 Redux 的 store 的时候,咱们都会有一种疑问,store 的结构到底是怎么定的?combineReducers 会揭开这个谜底。 如今来分析 createStore 接收的第一个参数,这个参数有两种形式,一种直接是一个 reducer 函数,另外一个是用 combineReducers 把多个 reducer 函数合并到一块儿。
能够猜想 combineReducers 是一个高阶函数,接收一个对象做为参数,返回了一个新的函数。这个新的函数应当和普通的 reducer 函数传参保持一致。
const combineReducers = (reducers) => {
return function combination(state = {}, action) {
}
}
复制代码
那么 combineReducers 作了什么工做呢?主要是下面几步:
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;
}
复制代码
考虑到这样的状况,我想要打印每次 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
。
在建立 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
}
}
复制代码
若是想要将 Redux 结合 React 使用的话,一般可使用 react-redux 这个库。 看过前面 Redux 的原理后,相信你也知道 react-redux 是如何实现的了吧。 react-redux 一共提供了两个 API,分别是 connect 和 Provider,前者是一个 React 高阶组件,后者是一个普通的 React 组件。react-redux 实现了一个简单的***发布-订阅***库,来监听当前 store 的变化。 二者的做用以下:
使用方式:
// Provider
ReactDOM.render({
<Provider store={store}></Provider>,
document.getElementById('app')
})
// connect
@connect(mapStateToProps, mapDispatchToProps)
class App extends Component {}
复制代码
先来实现简单的 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>
)
}
复制代码
再来看 connect 的实现,这里主要有三步:
先来实现简单的获取 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 的原理和上面比较相似,这里只做为学习原理的一个例子,不建议用到生产环境中。
在开发中,若是想要查看当前页面的 store 结构,可使用 Redux-DevTools 或者 React Developer Tools 这两个 chrome 插件来查看。 前者通常用于开发环境中,能够将 store 及其变化可视化展现出来。后者主要用于 React,也能够查看 store。 关于 Redux 中 store 如何设计对初学者来讲一直都是难题,在我看来这不只是 Redux 的问题,在任何前端 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 中的评论数据亢余了。
聪明的你必定会想到,若是 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 来查找,避免了经过索引来查找某一具体项。
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: