# 手摸手实现Redux
# Redux的应用场景
- 随着我们单页面应用项目的不断迭代,管理不断变化的state非常困难
- redux就是解决这种state里的数据问题
- 在react中,数据是单向数据流,也就是自上而下传递数据,而两个同级组件(兄弟组件)之间的通信只能通过不断的查找获取到他们共同的父组件才可以实现数据共享
引入一张很通俗易懂的图
# Redux设计思想
- Redux是将整个应用状态存储到到一个地方,称为store
- 里面保存一棵状态树state tree
- 组件可以派发dispatch行为action给store,而不是直接通知其它组件
- 其它组件可以通过订阅store中的状态(state)来刷新自己的视图
# Redux三大原则
- 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中
- State 是只读的,惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象 使用纯函数来执行修改,为了描述action如何改变state tree ,你需要编写 reducers
- 单一数据源的设计让React的组件之间的通信更加方便,同时也便于状态的统一管理
# 重零实现一个Redux
# 初始化
我们先使用 create-react-app 创建一个项目,实现一个简单的计数器功能
// index.html
<body>
<div id="counter">
<p id="value">0</p>
<button id="increment">+</button>
<button id="decrement">-</button>
</div>
</body>
// src/index.js
let store = {
num: 0,
}
let value = document.getElementById('value')
let increment = document.getElementById('increment')
let decrement = document.getElementById('decrement')
function render() {
value.innerHTML = store.num
}
increment.addEventListener('click', () => {
store.num = store.num + 1
render()
})
decrement.addEventListener('click', () => {
store.num = store.num - 1
render()
})
render()
效果图:
# createStore
我们发现,代码中共享了同一个仓库store
, 如果对仓库进行强制性的手段如store = null
,那影响的就是全部的代码了,那有没有办法让我们把代码藏起来呢?我们知道,js中只有2种作用域,一个是全局作用域,一个是函数作用域,现在看来函数作用域是我们的首选了.
function createStore() { //新
let store = {
num: 0,
}
function getState() {
return store
}
return {
getState,
}
}
let store = createStore() //新
let value = document.getElementById('value')
let increment = document.getElementById('increment')
let decrement = document.getElementById('decrement')
function render(state) {
value.innerHTML = state //新
}
increment.addEventListener('click', () => {
store.getState().num = store.getState().num + 1 //新
render(store.getState().num)
})
decrement.addEventListener('click', () => {
store.getState().num = store.getState().num - 1 //新
render(store.getState().num)
})
render()
现在我们已经实现了把仓库藏起来的这一步了,但是还是没有解决可以随意更改仓库的问题。
# dispatch && reducer
为了解决状态可以被任意改变的问题,我们引入管理员的概念,要修改状态,只能通过dispatch派发动作来修改
TIP
action的格式是有要求的 必须是一个纯对象 new Object() 出来的 或者字面量出来的{ } 而且必须有一个不为undefinedtype属性 可以有payload属性
那我们来实现一个 dispatch
和 reducer
吧
function createStore(reducer) {
let store = {
num: 0,
}
function getState() {
return store
}
function dispatch(action) { //新
store = reducer(store, action)
}
return {
getState,
dispatch,
}
}
const INCREMENT='INCREMENT' //新
const DECREMENT = 'DECREMENT' //新
function reducer(state = {}, action) { //新
switch (action.type) {
case INCREMENT:
return {
...state,
num: state.num + 1
}
case DECREMENT:
return {
...state,
num: state.num - 1
}
default:
return state
}
}
let store = createStore(reducer)
let value = document.getElementById('value')
let increment = document.getElementById('increment')
let decrement = document.getElementById('decrement')
function render(state) {
value.innerHTML = state
}
increment.addEventListener('click', () => {
store.dispatch({type: INCREMENT}) //新
render(store.getState().num )
})
decrement.addEventListener('click', () => {
store.dispatch({type: DECREMENT}) //新
render(store.getState().num )
})
render(store.getState().num )
# subscribe && unsubscribe
我们知道Redux还有一个订阅和取消订阅的功能,也就是当我们的仓库发生改变的时候可以触发某一些事件
function createStore() {
let listeners = []
function dispatch(action) {
store = reducer(store, action)
listeners.forEach(f=>f())
}
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
return {
subscribe,
...
}
}
我们只需要在外部绑定一些subscribe
就可以触发每一次dispatch的时候 触发 subscribe
绑定的事件了
let store = createStore(reducer)
store.subscribe(()=>{
console.log('subscribe')
})
# bindActionCreators
在讲bindActionCreators
之前,我们要先知道一个概念 actionCreator
, actionCreator
顾名思义就是action
的创建者,我们经常会写下面这种业务代码
render() {
return (
<div>
<p>{ this.state.number }</p>
<button onClick={ () => store.dispatch({type: types.INCREMENT1}) }>+</button>
<button onClick={ () => store.dispatch({type: types.DECREMENT1}) }>-</button>
</div>
)
}
从代码上我们可以发现action
是不是很容易写错,而且当项目庞大起来之后维护很麻烦,这就引入了actionCreators、
的概念
我们将上面代码抽离action
的部分出去
function increment(payload) { // actionCreator 简化action的创建的
return {
type: types.INCREMENT1,
payload,
}
}
function decrement(payload) {
return {
type: types.DECREMENT1,
payload,
}
}
render() {
return (
<div>
<p>{ this.state.number }</p>
<button onClick={ () => store.dispatch(increment) }>+</button>
<button onClick={ () => store.dispatch(decrement) }>-</button>
</div>
)
}
是可以实现一致的效果的,而且很容易维护
但是我们可以发现,dispatch的这个步骤似乎可以抽离出去,这就是bindActionCreators
的功能了
import {bindActionCreators} from 'redux'
function increment(payload) {
return {
type: types.INCREMENT1,
payload,
}
}
function decrement(payload) {
return {
type: types.DECREMENT1,
payload,
}
}
increment = bindActionCreators(increment, store.dispatch)
decrement = bindActionCreators(decrement, store.dispatch)
render() {
return (
<div>
<p>{ this.state.number }</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
我们可以发现bindActionCreators
简化了dispatch的流程,而且细心的小伙们可以发现bindActionCreators
是复数的,
也就是支持复数的写法,如下
let action = {increment, decrement}
action = bindActionCreators(action, store.dispatch)
render() {
return (
<div>
<p>{ this.state.number }</p>
<button onClick={action.increment}>+</button>
<button onClick={action.decrement}>-</button>
</div>
)
}
那我们来总结一下把,bindActionCreators
是用来简化dispatch的流程的,如何第一个参数是一个函数,则返回一个函数,功能和dispatch一致。如果第一个参数是一个对象,则返回一个对象,对象里面的键对应相应的简化过的dispatch函数
多说无益,我们来实现一下bindActionCreators
// bindActionCreators.js
function bindActionCreator(actionCreator, dispatch) {
return function(...args) {
dispatch(actionCreator(...args))
}
}
export default function(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
const boundActionCreators = {}
for (const key in actionCreators) {
boundActionCreators[key] = bindActionCreator(actionCreators[key], dispatch)
}
return boundActionCreators
}
# combineReducers
这里就不详细介绍 combineReducers
的用法了,简单的介绍一下,他的作用是把2个reducer合并成一个reducer,
这个合并之后的reducer会返回一个新的状态树,例如
// reducers/index.js
import counter1 from './counter1'
import counter2 from './counter2'
import { combineReducers } from 'redux'
let reducer = combineReducers({
counter1,
counter2
})
// 会生成如下的状态树
// state = {
// counter1: {},
// counter2: {},
// }
export default reducer
这样就不会导致多个组件使用状态树混乱的结果了
那我们试一下实现combineReducers
function combineReducers(reducers) {
return function(state = {}, action) { // 返回一个reducer
let nextState = {}
for (const key in reducers) {
nextState[key] = reducers[key](state[key], action)
}
return nextState
}
}