前言
当今不管作为一个前端小白还是一个资深的前端攻城狮。如果不掌握几种前端框架(React,Vue,ng),都不好意思出去说自己是做前端。但是面对如此之多的前端框架,尤其是React
、Vue
这种纯负责UI展示的架子来说。有一件事是绕不开的就是前端的数据存储问题。
作为业界层出不穷的数据处理框架Redux
(React的数据存储框架)又是不得不提起的。 Vue
的数据处理一般用Vuex
。但是他的设计思路都是基于Redux等。
所以,有必要看看Redux是如何实现数据存储,又如何使得存储的数据被组件获取,并且组件在触发CRUD的时候,能够及时更新数据呢。
我们就按照Redux的实现原理来剖析一下这些数据存储框架(Redux
,Vuex
)的原理。
接下里我们会用代码来实现createStore()
(Redux的核心概念)、combineReducers()
、链接Redux和React的connect()
、Redux的异步中间件Redux Thunk
。
Redux
Redux是什么
用浅显的的话来概括Redux的作用,其实Redux就是一个前端数据库。
他将前端的所有数据(不管是从后台返回的还是前端自己用的数据,这些数据是你想共享的)存入一个变量。这个变量学名叫做Redux Store(是一个js对象)。并且该变量对外界是只读的。如果想要改变改变量中的数据,需要发送一个action
来通过特定的修改机制来更新Store
中的特定的值。
用后台操作数据库来类比:
//创建了一个名为Store的数据库 //该操作等同于调用createStore() CREATE DATABASE Store //创建一个名为Persons的表,该表包含的字段为Id,LastName,FirstName //该操作等同在构建Store的时候,初始化state CREATE TABLE Persons(Id int,LastName varchar(255),FirstName varchar(255)) 复制代码
当store
中的值发生变化的时候(state
的值发生变化),你的程序能够察觉到。当Redux配合React使用的时候,当state的值发生变化,React
组件能够接收到变化的消息,与该state有关系的组件就会按照最新的state来触发re-render。
如上图展示,当store
接收一个特定的action
的时候,store需要一个指定的方式来更新store
中特定的state
。而这个特定的方式就是reducer
的职责。它是一个js函数。用于规定,如何根据指定的key去更新store
中指定的某些state
。该函数在store
创建的时候,做为参数传入到createStore()
中。
//更新对应表中的指定字段 //reducer根据action来更新特定的state UPDATE Persons SET FirstName = 'Fred' WHERE LastName = 'Wilson' 复制代码
code
针对store,我们需要有一个大致的理解
- 获取当前
store
的最新的state - dispatch一个action,传入
reducer()
,用于计算最新的state - 对store的变化设置监听函数
获取最新的state
我们创建了一个接收reducre
,initialState
作为参数的函数。该实现方式可以在调用createStore()
之后,获取到store,调用该对象的getState()
获取最新的state。
funtion createStore(reducer,initialState){ var currentReducer = reducre; var currentState = iniitialState; return { getState(){ return currentState; } } } 复制代码
Dispatch action
接下来,我们要实现dispatch action.
funtion createStore(reducer,initialState){ var currentReducer = reducre; var currentState = initialState; return { getState(){ return currentState; } dispatch(action){ currentState = currentReducer(currentState,action); return action; } } } 复制代码
dispatch()
向指定的reducer中传入action和当前的state,用于根据指定的方式来更新
store中的值。
设置监听函数
funtion createStore(reducer,initialState){ var currentReducer = reducre; var currentState = initialState; var listener = ()={} return { getState(){ return currentState; }, dispatch(action){ currentState = currentReducer(currentState,action); listener(); return action; }, subscribe(newListener) { listener = newListener; } } } 复制代码
当需要触发一次action的时候,我们可以去调用已一个回调函数作为参数的subscribe
如上代码所示:利用subscibe()
的参数来设置store中的listener
,从而使每次action
被触发的时候,调用该函数,起到监听数据的作用。
代码实战
我们可以利用redux官网的例子来验证一下我们自己的redux。
function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } let store = createStore(counter) store.subscribe(() => console.log(store.getState()) ) store.dispatch({ type: 'INCREMENT' }) store.dispatch({ type: 'INCREMENT' }) store.dispatch({ type: 'DECREMENT' }) 复制代码
CombineReducers
combineReducre的作用
在上面的例子中,我使用reducer来算数。从代码上看,reducer()
看起来就是一个switch()
,根据不同的key去处理不同的情况。
针对小的demo而已,这种处理方式很够用,但是如果项目比较大的话,这种处理方式,就会使得reducer变得冗长,且不易管理。
针对上面的问题,combineReducer()
就产生了,它能使将多个小的reducer
合并成一个大的,最后供Redux
使用。
编写自己的combineReducer
如下是官网的一些简单的示例:
// reducers.js export default theDefaultReducer = (state = 0, action) => state; export const firstNamedReducer = (state = 1, action) => state; export const secondNamedReducer = (state = 2, action) => state; // Use ES6 object literal shorthand syntax to define the object shape const rootReducer = combineReducers({ theDefaultReducer, firstNamedReducer, secondNamedReducer }); const store = createStore(rootReducer); console.log(store.getState()); // {theDefaultReducer : 0, firstNamedReducer : 1, secondNamedReducer : 2} 复制代码
定义函数
combineReducer
接收由很多小的reducer构成的对象。
function combineReducers(reducers){ } 复制代码
返回一个空reducer
从上面的例子中看到,调用combineReducer
返回了一个rootReducer,作为createStore()
的参数。也就是说,rootReducer其实就是一个简单的Redux reducer函数。
function combineReducers(reducers){ return function combination(state={},action){ } } 复制代码
生成state
function combineReducers(reducers) { // 获取所有子reducer对应的key的名称 const reducerKeys = Object.keys(reducers); return function combination(state = {}, action) { //零时变量 const nextState = {} for (let i = 0; i < reducerKeys.length; i++) { //获取当前的key const key = reducerKeys[i]; // 对应的reducer const reducer = reducers[key] // 获取未修改之前的state const previousStateForKey = state[key] // 通过redcuer计算之后的state const nextStateForKey = reducer(previousStateForKey, action) // 更新store nextState[key] = nextStateForKey; } return nextState; } } 复制代码
和React进行数据沟通
Redux
是一个独立的前端数据管理库。可以和很多框架一起使用,但是如果在React开发中,想使用Redux带来的快感,可以是配合react-redux
一起使用。
React Redux
React组件能够获取到store
首先,我们将react的root component内嵌到以store
做为props的react-redux
组件中。这样就可以使得store能够在所有的react组件中可见。
import { Provider } from 'react-redux'; const store = createStore(myReducer); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ) 复制代码
在react组件中使用store
构建一个简单的组件
let AddTodo = ({ todos }) => { // 省去部分代码 } 复制代码
react-redux
有一个connect()
用于将storeconnect到你的React组件中。
const mapStateToProps = (state) => { return { todos: state.todos } } const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } } AddTodo = connect(mapStateToProps, mapDispatchToProps)(AddTodo) 复制代码
该connect()
会自动从store中拿对应的值,并且将拿到的值作为props传入到被connect的组件中。当store中的值发生变化,对应的props也会变化,从而触发组件的重新渲染。
代码实现connect
参数指定
当connect()
被调用的时候,会立即返回一个新的函数。
function connect(mapStateToProps, mapDispatchToProps) { return function (WrappedComponent) { //something happens here } } 复制代码
函数输出结果
从上面的示例中,看到,connect()
函数接受一个react组件做完参数,并且也返回了一个新的组件。
function connect(mapStateToProps, mapDispatchToProps) { return function (WrappedComponent) { //返回了一个新的组件 return class extends React.Component { render() { return ( <WrappedComponent {...this.props} /> ) } } } } 复制代码
如果对React开发比较熟悉的同学,很快就会定位到,这是利用了React的Hoc。或者可以参考,我写的关于HOC的看法。有兴趣的可以研究一下,封装一些公共组件很有用。
向返回的新组件中添加参数
由于使用connect()
构建了一个空的组件,所以需要像组件中传入需要的数据,而这个数据的来源有3个地方。 使用connect()
构建的新组件的数据来源有3个地方。
- 通过
store.getState()
获取state存贮的值 - 调用mapDispatch(),获取到触发action的动作
- 获取原始组件中的值
function connect(mapStateToProps, mapDispatchToProps) { return function (WrappedComponent) { return class extends React.Component { render() { return ( <WrappedComponent {...this.props}//获取原始组件的值 {...mapStateToProps(store.getState(), this.props)}//获取store存贮的值 {...mapDispatchToProps(store.dispatch, this.props)}//触发动作 /> ) } } } } 复制代码
如何在组件中获取到store的实例
从上面的代码中,有的同学可能存在疑问,为什么我没有找到定义或者存储store的地方,但是却可以直接使用。在进行react和redux数据关联的时候,使用了Provider
对root component进行包装。其实这里就是将store进行了全局注册,你也可以认为是函数中的全局变量。具体如何实现的,可以参考react中的Context。
订阅store
通过上述的操作,我们现在已经可以在我们自己的组件获取到store中的值,并且能够在组件中dispatch一些action来更新对应的state。 如果在实际项目中,需要用到针对某个事件变化,进行组件的强制渲染。可以和该事件和store中的值,进行绑定,但凡触发,就更新组件状态。
function connect(mapStateToProps, mapDispatchToProps) { return function (WrappedComponent) { return class extends React.Component { render() { return ( <WrappedComponent {...this.props}//获取原始组件的值 {...mapStateToProps(store.getState(), this.props)}//获取store存贮的值 {...mapDispatchToProps(store.dispatch, this.props)}//触发动作 /> ) } componentDidMount() { this.unsubscribe = store.subscribe(this.handleChange.bind(this)) } componentWillUnmount() { this.unsubscribe() } handleChange() { this.forceUpdate()//利用react组件的事件,来强制重新渲染组件 } } } } 复制代码
Redux thunk Middleware
Redux
是一个极其简单的数据存贮状态库,所以在他自己的代码实现中,只是针对了同步操作。但是对于前端来说,如果仅仅只是同步操作的话,一些后台接口返回的数据就无法存入到store中,这样70%的数据就无法享受使用redux的快感。
Redux Thunk Middleware
完美解决了Redux
异步数据存贮问题。(redux的异步处理,不只是redux,thunk一种,有很多)
thunk的代码实现
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { //如果action是一个函数,将会被处理,进行异步操作,同时可以在异步的结果中获取 if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk; 复制代码
这就是thunk的所有代码。意不意外,惊不惊喜。
核心思路
其实核心代码就是将Redux作为一个中间件。
if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } 复制代码
当Redux Thunk
配置在你的项目中,每次dispatch
一个action的时候,上面的代码都会执行。如果被触发的action是一个函数,上面的代码就会去判断,并且会调用以dispatch
作为参数的函数(也就是异步函数)。
总结:
- 当redux没有配置中间件的时候,action总是一个简单的js对象。
- 当使用了中间件的时候,action可以是一个对象也可以是函数。
使用redux thunk
使用redux thunk创建的action格式如下:
function loadPostsAction() { return (dispatch, getState) => { // 进行异步处理 }; } 复制代码
调用上述函数,又返回了一个能够获取到redux dispatch函数的函数。
使用异步action来处理异步数据。
function loadPostsAction() { return (dispatch, getState) => { get("/api/posts").then( (paylod)=>dispath({type:"success",paylod}), (err)=>dispath({typs:"err",}) ) }; } 复制代码