三个原则
1.store是数据的唯一来源
2.state是只读的
3.只能通过纯函数改变state。
基于上述三点,下面来分析下这三个特点给我们编程过程中带来的便利和麻烦。
1. store是数据的唯一来源
便利:类比于观察者模式。项目的各个部分只需要监听store 的变化,就可以获得相关信息。
麻烦:react中,数据流是自上而下单向的从父节点到子节点。如果子节点要监听store的变化,就需要从顶级父元素一层层传递到对应的子节点,增加了代码的复杂性,降低了可读性。
解决方案:React—Redux里的Provider组件解决了这个问题。Provider组件使得子元素都可以获得store。connect组件将React组件和Redux store连接在一起。
2.state是只读的
具体来说,只有通过发送action,才能改变state,这样将state的改变途径限定了。而且action是一个纯粹的对象,可以打印,或者存储起来。
3.只能通过纯函数改变state
首先说明下纯函数的定义,纯函数的返回值只会由输入值决定。一样的输入,一样的输出,state 的变化成为可预见的。并且输入结果不会带来其他变化,例如,输入值是一个对象,纯函数会返回一个新对象,而不会改变输入的对象。
好处:可实现时间旅行。
解决方案:可以利用spread语法,或者Immutable库来返回新对象。
关键概念
- store
- state
- action
- dispatch
store
store 保存应用的state,并且提供了四个方法。
- store.getState()获得state。
- store.dispatch(action)更新state。
- store.subscribe() 订阅监听事件的发生。
- store.replaceReducer 替换reducer。
action
action 必须是plain object, 并且type是必传项
// https://github.com/reduxjs/redux/blob/master/src/createStore.js
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
dispatch
// https://github.com/reduxjs/redux/blob/master/src/createStore.js
currentState = currentReducer(currentState, action)
// https://github.com/reduxjs/redux/blob/master/src/combineReducers.js
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
dispatch 一个action时,所有的reducer都会执行一次,然后reducer内部有判断action对应的是否是当前reducer。
例子
// action.js
export const ADD_PROJECT = 'ADD_PROJECT'
export const DELETE_PROJECT = 'DELETE_PROJECT'
export function addProject(data) {
return { type: ADD_PROJECT, data } // action 是 a plain object
}
export function deleteProject(index) {
return { type: DELETE_PROJECT, index }
}
// reducers.js
import {
ADD_PROJECT,
DELETE_PROJECT,
} from './actions'
function projects(state = [], action) {
switch (action.type) {
case ADD_PROJECT:
return [
...state,
{
id: state.length,
data: action.data,
}
]
case DELETE_PROJECT:
return state.filter((project) => {
if (project.id !== action.index) {
return project
}
return ''
}).filter(v=>v)
default:
return state
}
}
const rootReducers = combineReducers({
projects
})
export default rootReducers
//store.js
import { createStore } from 'redux'
import rootReducers from './reducers'
const store = createStore(rootReducers)
优化
一个项目只有一个store。随着项目越来越大,reducers越来越多,可以参考flux的命名空间的方式,将reducers按照功能分组。 reducers可以分为两级,第一级判断是否是当前命名空间的方法,第二级在当前命名空间下的具体的数据处理方法。
实践经验
在SPA应用中,在不刷新页面的前提下,各个路径所对应的页面会共享store。这样在实际开发过程中,可能会导致无法准确判断,当前页面对数据请求是否正常。
例如,a路径请求了template相关数据,直接跳转到b页面时,这部分数据已经存在,可以直接使用。但是如果用户直接访问b页面,那么b页面自己本身需要去请求template相关数据。所以为了避免这个盲区,在自测阶段,可以刷新这个页面,来确保每个页面保证了数据获取的正确性。