看完这篇,你也能够实现一个 redux.javascript
本篇文章对应的代码在: https://github.com/YvetteLau/... 建议先 clone
代码,而后对照代码阅读本文。html
Redux
是什么?Redux
是 JavaScript
状态容器,提供可预测化的状态管理。Redux
除了和 React
一块儿用外,还支持其它界面库。Redux
体小精悍,仅有 2KB
。这里咱们须要明确一点:Redux
和 React
之间,没有强绑定的关系。本文旨在理解和实现一个 Redux
,可是不会涉及 react-redux
(一次深刻理解一个知识点便可,react-redux
将出如今下一篇文章中)。前端
Redux
咱们先忘记 Redux
的概念,从一个例子入手,使用 create-react-app
建立一个项目: toredux
。java
代码目录: myredux/to-redux
中。react
将 public/index.html
中 body
修改成以下:git
<div id="app"> <div id="header"> 前端宇宙 </div> <div id="main"> <div id="content">你们好,我是前端宇宙做者刘小夕</div> <button class="change-theme" id="to-blue">Blue</button> <button class="change-theme" id="to-pink">Pink</button> </div> </div>
咱们要实现的功能如上图所示,在点击按钮时,可以修改整个应用的字体的颜色。github
修改 src/index.js
以下(代码: to-redux/src/index1.js
):redux
let state = { color: 'blue' } //渲染应用 function renderApp() { renderHeader(); renderContent(); } //渲染 title 部分 function renderHeader() { const header = document.getElementById('header'); header.style.color = state.color; } //渲染内容部分 function renderContent() { const content = document.getElementById('content'); content.style.color = state.color; } renderApp(); //点击按钮,更改字体颜色 document.getElementById('to-blue').onclick = function () { state.color = 'rgb(0, 51, 254)'; renderApp(); } document.getElementById('to-pink').onclick = function () { state.color = 'rgb(247, 109, 132)'; renderApp(); }
这个应用很是简单,可是它有一个问题:state
是共享状态,可是任何人均可以修改它,一旦咱们随意修改了这个状态,就能够致使出错,例如,在 renderHeader
里面,设置 state = {}
, 容易形成难以预料的错误。数组
不过不少时候,咱们又的确须要共享状态,所以咱们能够考虑设置一些门槛,好比,咱们约定,不能直接修改全局状态,必需要经过某个途经才能修改。为此咱们定义一个 changeState
函数,全局状态的修改均由它负责。app
//在 index.js 中继续追加代码 function changeState(action) { switch(action.type) { case 'CHANGE_COLOR': return { ...state, color: action.color } default: return state; } }
咱们约定只能经过 changeState
去修改状态,它接受一个参数 action
,包含 type
字段的普通对象,type
字段用于识别你的操做类型(即如何修改状态)。
咱们但愿点击按钮,能够修改整个应用的字体颜色。
//在 index.js 中继续追加代码 document.getElementById('to-blue').onclick = function() { let state = changeState({ type: 'CHANGE_COLOR', color: 'rgb(0, 51, 254)' }); //状态修改完以后,须要从新渲染页面 renderApp(state); } document.getElementById('to-pink').onclick = function() { let state = changeState({ type: 'CHANGE_COLOR', color: 'rgb(247, 109, 132)' }); renderApp(state); }
尽管如今咱们约定了如何修改状态,可是 state
是一个全局变量,咱们很容易就能够修改它,所以咱们能够考虑将其变成局部变量,将其定义在一个函数内部(createStore
),可是在外部还须要使用 state
,所以咱们须要提供一个方法 getState()
,以便咱们在 createStore
获取到 state
。
function createStore (state) { const getState = () => state; return { getState } }
如今,咱们能够经过 store.getState()
方法去获取状态(这里须要说明的是,state
一般是一个对象,所以这个对象在外部实际上是能够被直接修改的,可是若是深拷贝 state
返回,那么在外部就必定修改不了,鉴于 redux
源码中就是直接返回了 state
,此处咱们也不进行深拷贝,毕竟耗费性能)。
仅仅获取状态是远远不够的,咱们还须要有修改状态的方法,如今状态是私有变量,咱们必需要将修改状态的方法也放到 createStore
中,并将其暴露给外部使用。
function createStore (state) { const getState = () => state; const changeState = () => { //...changeState 中的 code } return { getState, changeState } }
如今,index.js
中代码变成下面这样(to-redux/src/index2.js
):
function createStore() { let state = { color: 'blue' } const getState = () => state; function changeState(action) { switch (action.type) { case 'CHANGE_COLOR': state = { ...state, color: action.color } return state; default: return state; } } return { getState, changeState } } function renderApp(state) { renderHeader(state); renderContent(state); } function renderHeader(state) { const header = document.getElementById('header'); header.style.color = state.color; } function renderContent(state) { const content = document.getElementById('content'); content.style.color = state.color; } document.getElementById('to-blue').onclick = function () { store.changeState({ type: 'CHANGE_COLOR', color: 'rgb(0, 51, 254)' }); renderApp(store.getState()); } document.getElementById('to-pink').onclick = function () { store.changeState({ type: 'CHANGE_COLOR', color: 'rgb(247, 109, 132)' }); renderApp(store.getState()); } const store = createStore(); renderApp(store.getState());
尽管,咱们如今抽离了 createStore
方法,可是显然这个方法一点都不通用,state
和 changeState
方法都定义在了 createStore
中。这种状况下,其它应用没法复用此模式。
changeState
的逻辑理应在外部定义,由于每一个应用修改状态的逻辑定然是不一样的。咱们将这部分逻辑剥离到外部,并将其重命名为 reducer
(憋问为何叫 reducer
,问就是为了和 redux
保持一致)。reducer
是干吗的呢,说白了就是根据 action
的类型,计算出新状态。由于它不是在 createStore
内部定义的,没法直接访问 state
,所以咱们须要将当前状态做为参数传递给它。以下:
function reducer(state, action) { switch(action.type) { case 'CHANGE_COLOR': return { ...state, color: action.color } default: return state; } }
function createStore(reducer) { let state = { color: 'blue' } const getState = () => state; //将此处的 changeState 改名为 `dispatch` const dispatch = (action) => { //reducer 接收老状态和action,返回一个新状态 state = reducer(state, action); } return { getState, dispatch } }
不一样应用的 state
定然是不一样的,咱们将 state
的值定义在 createStore
内部必然是不合理的。
function createStore(reducer) { let state; const getState = () => state; const dispatch = (action) => { //reducer(state, action) 返回一个新状态 state = reducer(state, action); } return { getState, dispatch } }
你们注意 reducer
的定义,在碰到不能识别的动做时,是直接返回旧状态的,如今,咱们利用这一点来返回初始状态。
要想 state
有初始状态,其实很简单,我们将初始的 state
的初始化值做为 reducer
的参数的默认值,而后在 createStore
中派发一个 reducer
看不懂的动做就能够了。这样 getState
首次调用时,能够获取到状态的默认值。
function createStore(reducer) { let state; const getState = () => state; //每当 `dispatch` 一个动做的时候,咱们须要调用 `reducer` 以返回一个新状态 const dispatch = (action) => { //reducer(state, action) 返回一个新状态 state = reducer(state, action); } //你要是有个 action 的 type 的值是 `@@redux/__INIT__${Math.random()}`,我敬你是个狠人 dispatch({ type: `@@redux/__INIT__${Math.random()}` }); return { getState, dispatch } }
如今这个 createStore
已经能够处处使用了, 可是你有没有以为每次 dispatch
后,都手动 renderApp()
显得很蠢,当前应用中是调用两次,若是须要修改1000次 state
呢,难道手动调用 1000次 renderApp()
?
能不能简化一下呢?每次数据变化的时候,自动调用 renderApp()
。固然咱们不可能将 renderApp()
写在 createStore()
的 dispatch
中,由于其它的应用中,函数名未必叫 renderApp()
,并且有可能不止要触发 renderApp()
。这里能够引入 发布订阅模式
,当状态变化时,通知全部的订阅者。
function createStore(reducer) { let state; let listeners = []; const getState = () => state; //subscribe 每次调用,都会返回一个取消订阅的方法 const subscribe = (ln) => { listeners.push(ln); //订阅以后,也要容许取消订阅。 //难道我订了某本杂志以后,就不容许我退订吗?可怕~ const unsubscribe = () => { listeners = listeners.filter(listener => ln !== listener); } return unsubscribe; }; const dispatch = (action) => { //reducer(state, action) 返回一个新状态 state = reducer(state, action); listeners.forEach(ln => ln()); } //你要是有个 action 的 type 的值正好和 `@@redux/__INIT__${Math.random()}` 相等,我敬你是个狠人 dispatch({ type: `@@redux/__INIT__${Math.random()}` }); return { getState, dispatch, subscribe } }
至此,一个最为简单的 redux
已经建立好了,createStore
是 redux
的核心。咱们来使用这个精简版的 redux
重写咱们的代码,index.js
文件内容更新以下(to-redux/src/index.js
):
function createStore() { //code(自行将上面createStore的代码拷贝至此处) } const initialState = { color: 'blue' } function reducer(state = initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.color } default: return state; } } const store = createStore(reducer); function renderApp(state) { renderHeader(state); renderContent(state); } function renderHeader(state) { const header = document.getElementById('header'); header.style.color = state.color; } function renderContent(state) { const content = document.getElementById('content'); content.style.color = state.color; } document.getElementById('to-blue').onclick = function () { store.dispatch({ type: 'CHANGE_COLOR', color: 'rgb(0, 51, 254)' }); } document.getElementById('to-pink').onclick = function () { store.dispatch({ type: 'CHANGE_COLOR', color: 'rgb(247, 109, 132)' }); } renderApp(store.getState()); //每次state发生改变时,都从新渲染 store.subscribe(() => renderApp(store.getState()));
若是如今咱们如今但愿在点击完 Pink
以后,字体色不容许修改,那么咱们还能够取消订阅:
const unsub = store.subscribe(() => renderApp(store.getState())); document.getElementById('to-pink').onclick = function () { //code... unsub(); //取消订阅 }
顺便说一句: reducer
是一个纯函数(纯函数的概念若是不了解的话,自行查阅资料),它接收先前的 state
和 action
,并返回新的 state
。不要问为何 action
中必定要有 type
字段,这仅仅是一个约定而已(redux
就是这么设计的)
遗留问题:为何 reducer
必定要返回一个新的 state
,而不是直接修改 state
呢。欢迎在评论区留下你的答案。
前面咱们一步一步推演了 redux
的核心代码,如今咱们来回顾一下 redux
的设计思想:
Redux
设计思想Redux
将整个应用状态(state
)存储到一个地方(一般咱们称其为 store
)dispatch
)一个 action
( action
是一个带有 type
字段的对象)reducer
接收旧的 state
和 action
,并会返回一个新的 state
subscribe
设置订阅,每次派发动做时,通知全部的订阅者。我们如今已经有一个基础版本的 redux
了,可是它还不能知足咱们的需求。咱们平时的业务开发不会像上面所写的示例那样简单,那么就会有一个问题: reducer
函数可能会很是长,由于 action
的类型会很是多。这样确定是不利于代码的编写和阅读的。
试想一下,你的业务中有一百种 action
须要处理,把这一百种状况编写在一个 reducer
中,不只写得人恶心,后期维护代码的同事更是想杀人。
所以,咱们最好单独编写 reducer
,而后对 reducer
进行合并。有请咱们的 combineReducers
(和 redux
库的命名保持一致) 闪亮登场~
首先咱们须要明确一点:combineReducers
只是一个工具函数,正如咱们前面所说,它将多个 reducer
合并为一个 reducer
。combineReducers
返回的是 reducer
,也就是说它是一个高阶函数。
咱们仍是以一个示例来讲明,尽管 redux
不是非得和 react
配合,不过鉴于其与 react
配合最为适合,此处,以 react
代码为例:
这一次除了上面的展现之外,咱们新增了一个计数器功能( 使用 React
重构 ===> to-redux2
):
//如今咱们的 state 结构以下: let state = { theme: { color: 'blue' }, counter: { number: 0 } }
显然,修改主题和计数器是能够分割开得,由不一样的 reducer
去处理是一个更好的选择。
store/reducers/counter.js
负责处理计数器的state。
import { INCRENENT, DECREMENT } from '../action-types'; export default counter(state = {number: 0}, action) { switch (action.type) { case INCRENENT: return { ...state, number: state.number + action.number } case DECREMENT: return { ...state, number: state.number - action.number } default: return state; } }
store/reducers/theme.js
负责处理修改主题色的state。
import { CHANGE_COLOR } from '../action-types'; export default function theme(state = {color: 'blue'}, action) { switch (action.type) { case CHANGE_COLOR: return { ...state, color: action.color } default: return state; } }
每一个 reducer
只负责管理全局 state
中它负责的一部分。每一个 reducer
的 state
参数都不一样,分别对应它管理的那部分 state
数据。
import counter from './counter'; import theme from './theme'; export default function appReducer(state={}, action) { return { theme: theme(state.theme, action), counter: counter(state.counter, action) } }
appReducer
便是合并以后的 reducer
,可是当 reducer
较多时,这样写也显得繁琐,所以咱们编写一个工具函数来生成这样的 appReducer
,咱们把这个工具函数命名为 combineReducers
。
咱们来尝试一下编写这个工具函数 combineReducers
:
思路:
combineReducers
返回 reducer
combineReducers
的入参是多个 reducer
组成的对象reducer
只处理全局 state
中本身负责的部分//reducers 是一个对象,属性值是每个拆分的 reducer export default function combineReducers(reducers) { return function combination(state={}, action) { //reducer 的返回值是新的 state let newState = {}; for(var key in reducers) { newState[key] = reducers[key](state[key], action); } return newState; } }
子 reducer
将负责返回 state
的默认值。好比本例中,createStore
中 dispatch({type:@@redux/__INIT__${Math.random()}
}),而传递给 createStore
的是 combineReducers(reducers)
返回的函数 combination
。
根据 state=reducer(state,action)
,newState.theme=theme(undefined, action)
, newState.counter=counter(undefined, action)
,counter
和 theme
两个子 reducer
分别返回 newState.theme
和 newState.counter
的初始值。
利用此 combineReducers
能够重写 store/reducers/index.js
import counter from './counter'; import theme from './theme'; import { combineReducers } from '../redux'; //明显简洁了许多~ export default combineReducers({ counter, theme });
咱们写的 combineReducers
虽然看起来已经可以知足咱们的需求,可是其有一个缺点,即每次都会返回一个新的 state
对象,这会致使在数据没有变化时进行无心义的从新渲染。所以咱们能够对数据进行判断,在数据没有变化时,返回本来的 state
便可。
combineReducers 进化版
//代码中省略了一些判断,默认传递的参数均是符合要求的,有兴趣能够查看源码中对参数合法性的判断及处理 export default function combineReducers(reducers) { return function combination(state={}, action) { let nextState = {}; let hasChanged = false; //状态是否改变 for(let key in reducers) { const previousStateForKey = state[key]; const nextStateForKey = reducers[key](previousStateForKey, action); nextState[key] = nextStateForKey; //只有全部的 nextStateForKey 均与 previousStateForKey 相等时,hasChanged 的值才是 false hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } //state 没有改变时,返回原对象 return hasChanged ? nextState : state; } }
官方文档中,关于 applyMiddleware
的解释很清楚,下面的内容也参考了官方文档的内容:
考虑一个小小的问题,若是咱们但愿每次状态改变前可以在控制台中打印出 state
,那么咱们要怎么作呢?
最简单的便是:
//... <button onClick={() => { console.log(store.getState()); store.dispatch(actions.add(2)); }}>+</button> //...
固然,这种方式确定是不可取的,若是咱们代码中派发100次,咱们不可能这样写一百次。既然是状态改变时打印 state
,也是说是在 dispatch
以前打印 state
, 那么咱们能够重写 store.dispatch
方法,在派发前打印 state
便可。
let store = createStore(reducer); const next = store.dispatch; //next 的命令是为了和中间件的源码一致 store.dispatch = action => { console.log(store.getState()); next(action); }
假设咱们不只仅须要打印 state
,还须要在派发异常出错时,打印出错误信息。
const next = store.dispatch; //next 的命名是为了和中间件的源码一致 store.dispatch = action => { try{ console.log(store.getState()); next(action); } catct(err) { console.error(err); } }
而若是咱们还有其余的需求,那么就须要不停的修改 store.dispatch
方法,最后致使这个这部分代码难以维护。
所以咱们须要分离 loggerMiddleware
和 exceptionMiddleware
.
let store = createStore(reducer); const next = store.dispatch; //next 的命名是为了和中间件的源码一致 const loggerMiddleware = action => { console.log(store.getState()); next(action); } const exceptionMiddleware = action => { try{ loggerMiddleware(action); }catch(err) { console.error(err); } } store.dispatch = exceptionMiddleware;
咱们知道,不少 middleware
都是第三方提供的,那么 store
确定是须要做为参数传递给 middleware
,进一步改写:
const loggerMiddleware = store => action => { const next = store.dispatch; console.log(store.getState()); next(action); } const exceptionMiddleware = store => action => { try{ loggerMiddleware(store)(action); }catch(err) { console.error(err); } } //使用 store.dispatch = exceptionMiddleware(store)(action);
如今还有一个小小的问题,exceptionMiddleware
中的 loggerMiddleware
是写死的,这确定是不合理的,咱们但愿这是一个参数,这样使用起来才灵活,没道理只有 exceptionMiddleware
须要灵活,而无论 loggerMiddleware
,进一步改写以下:
const loggerMiddleware = store => next => action => { console.log(store.getState()); return next(action); } const exceptionMiddleware = store => next => action => { try{ return next(action); }catch(err) { console.error(err); } } //使用 const next = store.dispatch; const logger = loggerMiddleware(store); store.dispatch = exceptionMiddleware(store)(logger(next));
如今,咱们已经有了通用 middleware
的编写格式了。
middleware
接收了一个 next()
的 dispatch
函数,并返回一个 dispatch
函数,返回的函数会被做为下一个 middleware
的 next()
可是有一个小小的问题,当中间件不少的时候,使用中间件的代码会变得很繁琐。为此,redux
提供了一个 applyMiddleware
的工具函数。
上面咱们可以看出,其实咱们最终要改变的就是 dispatch
,所以咱们须要重写 store
,返回修改了 dispatch
方法以后的 store
.
因此,咱们能够明确如下几点:
applyMiddleware
返回值是 store
applyMiddleware
确定要接受 middleware
做为参数applyMiddleware
要接受 {dispatch, getState}
做为入参,不过 redux
源码中入参是 createStore
和 createStore
的入参,想一想也是,没有必要在外部建立出 store
,毕竟在外部建立出的 store
除了做为参数传递进函数,也没有其它做用,不如把 createStore
和 createStore
须要使用的参数传递进来。//applyMiddleWare 返回 store. const applyMiddleware = middleware => createStore => (...args) => { let store = createStore(...args); let middle = loggerMiddleware(store); let dispatch = middle(store.dispatch); //新的dispatch方法 //返回一个新的store---重写了dispatch方法 return { ...store, dispatch } }
以上是一个 middleware
的状况,可是咱们知道,middleware
多是一个或者是多个,并且咱们主要是要解决多个 middleware
的问题,进一步改写。
//applyMiddleware 返回 store. const applyMiddleware = (...middlewares) => createStore => (...args) => { let store = createStore(...args); let dispatch; const middlewareAPI = { getState: store.getstate, dispatch: (...args) => dispatch(...args) } //传递修改后的 dispatch let middles = middlewares.map(middleware => middleware(middlewareAPI)); //如今咱们有多个 middleware,须要屡次加强 dispatch dispatch = middles.reduceRight((prev, current) => current(prev), store.dispatch); return { ...store, dispatch } }
不知道你们是否是理解了上面的 middles.reduceRight
,下面为你们细致说明一下:
/*三个中间件*/ let logger1 = ({dispatch, getState}) => dispatch => action => { console.log('111'); dispatch(action); console.log('444'); } let logger2 = ({ dispatch, getState }) => dispatch => action => { console.log('222'); dispatch(action); console.log('555') } let logger3 = ({ dispatch, getState }) => dispatch => action => { console.log('333'); dispatch(action); console.log('666'); } let middle1 = logger1({ dispatch, getState }); let middle2 = logger2({ dispatch, getState }); let middle3 = logger3({ dispatch, getState }); //applyMiddleware(logger1,logger2,logger3)(createStore)(reducer) //若是直接替换 store.dispatch = middle1(middle2(middle3(store.dispatch)));
观察上面的 middle1(middle2(middle3(store.dispatch)))
,若是咱们把 middle1
,middle2
,middle3
当作是数组的每一项,若是对数组的API比较熟悉的话,能够想到 reduce
,若是你还不熟悉 reduce
,能够查看MDN文档。
//applyMiddleware(logger1,logger3,logger3)(createStore)(reducer) //reduceRight 从右到左执行 middles.reduceRight((prev, current) => current(prev), store.dispatch); //第一次 prev: store.dispatch current: middle3 //第二次 prev: middle3(store.dispatch) current: middle2 //第三次 prev: middle2(middle3(store.dispatch)) current: middle1 //结果 middle1(middle2(middle3(store.dispatch)))
阅读过 redux
的源码的同窗,可能知道源码中是提供了一个 compose
函数,而 compose
函数中没有使用 reduceRight
,而是使用的 reduce
,于是代码稍微有点不一样。可是分析过程仍是同样的。
compose.js
export default function compose(...funcs) { //若是没有中间件 if (funcs.length === 0) { return arg => arg } //中间件长度为1 if (funcs.length === 1) { return funcs[0] } return funcs.reduce((prev, current) => (...args) => prev(current(...args))); }
关于 reduce
的写法,建议像上面的 reduceRight
同样,进行一次分析
使用 compose
工具函数重写 applyMiddleware
。
const applyMiddleware = (...middlewares) => createStore => (...args) => { let store = createStore(...args); let dispatch; const middlewareAPI = { getState: store.getstate, dispatch: (...args) => dispatch(...args) } let middles = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...middles)(store.dispatch); return { ...store, dispatch } }
redux
还为咱们提供了 bindActionCreators
工具函数,这个工具函数代码很简单,咱们不多直接在代码中使用它,react-redux
中会使用到。此处,简单说明一下:
//一般咱们会这样编写咱们的 actionCreator import { INCRENENT, DECREMENT } from '../action-types'; const counter = { add(number) { return { type: INCRENENT, number } }, minus(number) { return { type: DECREMENT, number } } } export default counter;
在派发的时候,咱们须要这样写:
import counter from 'xx/xx'; import store from 'xx/xx'; store.dispatch(counter.add());
固然,咱们也能够像下面这样编写咱们的 actionCreator:
function add(number) { return { type: INCRENENT, number } }
派发时,须要这样编写:
store.dispatch(add(number));
以上代码有一个共同点,就是都是 store.dispatch
派发一个动做。所以咱们能够考虑编写一个函数,将 store.dispatch
和 actionCreator
绑定起来。
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)); } function bindActionCreators(actionCreator, dispatch) { //actionCreators 能够是一个普通函数或者是一个对象 if(typeof actionCreator === 'function') { //若是是函数,返回一个函数,调用时,dispatch 这个函数的返回值 bindActionCreator(actionCreator, dispatch); }else if(typeof actionCreator === 'object') { //若是是一个对象,那么对象的每一项都要都要返回 bindActionCreator const boundActionCreators = {} for(let key in actionCreator) { boundActionCreators[key] = bindActionCreator(actionCreator[key], dispatch); } return boundActionCreators; } }
在使用时:
let counter = bindActionCreators(counter, store.dispatch); //派发时 counter.add(); counter.minus();
这里看起来并无精简太多,后面在分析 react-redux
时,会说明为何须要这个工具函数。
至此,个人 redux
基本已经编写完毕。与 redux
的源码相比,还相差一些内容,例如 createStore
提供的 replaceReducer
方法,以及 createStore
的第二个参数和第三个参数没有说起,稍微看一下代码就能懂,此处再也不一一展开。