Redux的一些总结

Flux可以说是一个框架,其有本身的 Dispatcher 接口供开发者;也可以说是一种数据流单向控制的架构设计,围绕单向数据流的核心,其定义了一套行为规范,如下图:

Redux的设计就继承了Flux的架构,并将其完善,提供了多个API供开发者调用。借着react-redux,可以很好的与React结合,开发组件化程度极高的现代Web应用。本文是笔者近半年使用react+redux组合的一些总结,不当之处,敬请谅解。

Action

Action是数据从应用传递到 store/state 的载体,也是开启一次完成数据流的开始。

以添加一个todo的Action为例:

{
    type:'add_todo',
    data:'我要去跑步'
}

这样就定义了一个添加一条todo的Action,然后就能通过某个行为去触发这个Action,由这个Action携带的数据(data)去更新store(state/reducer):

store.dispatch({
    type:'add_todo',
    data:'your data'
})

type 是一个常量,Action必备一个字段,用于标识该Action的类型。在项目初期,这样定义Action也能愉快的撸码,但是随着项目的复杂度增加,这种方式会让代码显得冗余,因为如果有多个行为触发同一个Action,则这个Action要写多次;同时,也会造成代码结构不清晰。因而,得更改创建Action的方式:

const ADD_TODO = 'add_todo';

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        data: data
    }
}

//触发action
store.dispatch(addTodo());

更改之后,代码清晰多了,如果有多个行为触发同一个Action,只要调用一下函数 addTodo 就行,并将Action要携带的数据传递给该函数。类似 addTodo 这样的函数,称之为 Action Creator。Action Creator 的唯一功能就是返回一个Action供 dispatch 进行调用。

但是,这样的Action Creator 返回的Action 并不是一个标准的Action。在Flux的架构中,一个Action要符合 FSA(Flux Standard Action) 规范,需要满足如下条件:

  • 是一个纯文本对象
  • 只具备 type 、payloaderror 和 meta 中的一个或者多个属性。type 字段不可缺省,其它字段可缺省
  • 若 Action 报错,error 字段不可缺省,切必须为 true

payload 是一个对象,用作Action携带数据的载体。所以,上述的写法可以更改为:

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        payload: {
            data
        }
    }
}

在 redux 全家桶中,可以利用 redux-actions 来创建符合 FSA 规范的Action:

import {creatAction} from 'redux-actions';

let addTodo = creatAction(ADD_TODO)
//same as
let addTodo = creatAction(ADD_TODO,data=>data)

可以采用如下一个简单的方式检验一个Action是否符合FSA标准:

let isFSA = Object.keys(action).every((item)=>{
   return  ['payload','type','error','meta'].indexOf(item) >  -1
})

中间件

在我看来,Redux提高了两个非常重要的功能,一是 Reducer 拆分,二是中间件。Reducer 拆分可以使组件获取其最小属性(state),而不需要整个Store。中间件则可以在 Action Creator 返回最终可供 dispatch 调用的 action 之前处理各种事情,如异步API调用、日志记录等,是扩展 Redux 功能的一种推荐方式。

Redux 提供了 applyMiddleware(...middlewares) 来将中间件应用到 createStore。applyMiddleware 会返回一个函数,该函数接收原来的 creatStore 作为参数,返回一个应用了 middlewares 的增强后的 creatStore。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    //接收createStore参数
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    //传递给中间件的参数
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }

    //注册中间件调用链
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    //返回经middlewares增强后的createStore
    return {
      ...store,
      dispatch
    }
  }
}

创建 store 的方式也会因是否使用中间件而略有区别。未应用中间价之前,创建 store 的方式如下:

import {createStore} from 'redux';
import reducers from './reducers/index';

export let store = createStore(reducers);

应用中间价之后,创建 store 的方式如下:

import {createStore,applyMiddleware} from 'redux';
import reducers from './reducers/index';

let createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
export let store = createStoreWithMiddleware(reducers);

那么怎么自定义一个中间件呢?

根据 redux 文档,中间件的签名如下:

({ getState, dispatch }) => next => action

根据上文的 applyMiddleware 源码,每个中间件接收 getState & dispatch 作为参数,并返回一个函数,该函数会被传入下一个中间件的 dispatch 方法,并返回一个接收 action 的新函数。

以一个打印 dispatch action 前后的 state 为例,创建一个中间件示例:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('pre state', getState());
        // 调用 middleware 链中下一个 middleware 的 dispatch。
        next(action);
        console.log('after dispatch', getState());
    }
}

在创建 store 的文件中调用该中间件:

import {createStore,applyMiddleware} from 'redux';

import reducers from './reducers/index';
import log from '../lib/log';

//export let store = createStore(reducers);

//应用中间件log
let createStoreWithLog = applyMiddleware(log)(createStore);
export let store = createStoreWithLog(reducers);

可以在控制台看到输出:

可以对 store 应用多个中间件:

import log from '../lib/log';
import log2 from '../lib/log2';

let createStoreWithLog = applyMiddleware(log,log2)(createStore);
export let store = createStoreWithLog(reducers);

log2 也是一个简单的输出:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('我是第二个中间件1');
        next(action);
        console.log('我是第二个中间件2');
    }
}

看控制台的输出:

应用多个中间件时,中间件调用链中任何一个缺少 next(action) 的调用,都会导致 action 执行失败

异步

Redux 本身不处理异步行为,需要依赖中间件。结合 redux-actions 使用,Redux 有两个推荐的异步中间件:

两个中间件的源码都是非常简单的,redux-thunk 的源码如下:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

从源码可知,action creator 需要返回一个函数给 redux-thunk 进行调用,示例如下:

export let addTodoWithThunk = (val) => async (dispatch, getState)=>{
    //请求之前的一些处理

    let value = await Promise.resolve(val + ' thunk');
    dispatch({
        type:CONSTANT.ADD_TO_DO_THUNK,
        payload:{
            value
        }
    });
};

效果如下:

这里之所以不用 createAction,如前文所说,因为 createAction 会返回一个 FSA 规范的 action,该 action 会是一个对象,而不是一个 function:

{
    type: "add_to_do_thunk",
    payload: function(){}
}

如果要使用 createAction,则要自定义一个异步中间件。

export let addTodoWithCustom = createAction(CONSTANT.ADD_TO_DO_CUSTOM, (val) => async (dispatch, getState)=>{
    let value = await Promise.resolve(val + ' custom');
    return {
        value
    };
});

在经过中间件处理时,先判断 action.payload 是否是一个函数,是则执行函数,否则交给 next 处理:

if(typeof action.payload === 'function'){
    let res = action.payload(dispatch, getState);
} else {
    next(action);
}

而 async 函数返回一个 Promise,因而需要作进一步处理:

res.then(
    (result) => {
        dispatch({...action, payload: result});
    },
    (error) => {
        dispatch({...action, payload: error, error: true});
    }
);

这样就自定义了一个异步中间件,效果如下:

当然,我们可以对函数执行后的结果是否是Promise作一个判断:

function isPromise (val) {
    return val && typeof val.then === 'function';
}

//对执行结果是否是Promise
if (isPromise(res)){
    //处理
} else {
    dispatch({...action, payload: res});
}

那么,怎么利用 redux-promise 呢?redux-promise 是能处理符合 FSA 规范的 action 的,其对异步处理的关键源码如下:

action.payload.then(
    result => dispatch({ ...action, payload: result }),
    error => {
        dispatch({ ...action, payload: error, error: true });
        return Promise.reject(error);
    }
)

因而,返回的 payload 不再是一个函数,而是一个 Promise。而 async 函数执行后就是返回一个 Promise,所以,让上文定义的 async 函数自执行一次就可以:

export let addTodoWithPromise = createAction(CONSTANT.ADD_TO_DO_PROMISE, (val) =>
    (async (dispatch, getState)=>{
        let value = await Promise.resolve(val + ' promise');
        return {
            value
        };
    })()
);

结果如下图:

示例源码:redux-demo


组件拆分

在 关于Redux的一些总结(一):Action & 中间件 & 异步 一文中,有提到可以根据 reducer 对组件进行拆分,而不必将 state 中的数据原封不动地传入组件,可以根据 state 中的数据,动态地输出组件需要的(最小)属性。

在常规的组件开发方式中,组件自身的数据和状态是耦合的,这种方式虽能简化开发流程,在短期内能提高开发效率,但只适用于小型且复杂度不高的SPA 应用开发,而对于复杂的 SPA 应用来说,这种开发方式不具备良好的扩展性。以开发一个评论组件 Comment 为例,常规的开发方式如下:

class CommentList extends Component {
    constructor(){
        super();
        this.state = {commnets: []}
    }

    componentDidMount(){
        $.ajax({
            url:'/my-comments.json',
            dataType:'json',
            success:function(data){
                this.setState({comments:data});
            }.bind(this)
        })
    }

    render(){
        return <ul>{this.state.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

随着应用的复杂度和组件复杂度的双重增加,现有的组件开发方式已经无法满足需求,它会让组件变得不可控制和难以维护,极大增加后续功能扩展的难度。并且由于组件的状态和数据的高度耦合,这种组件是无法复用的,无法抽离出通用的业务无关性组件,这势必也会增加额外的工作量和开发时间。

在组件的开发过程中,从组件的职责角度上,将组件分为 容器类组件(Container Component) 和 展示类组件(Presentational Component)。前者主要从 state 获取组件需要的(最小)属性,后者主要负责界面渲染和自身的状态(state)控制,为容器组件提供样式。

按照上述的概念,Comment应该有两部分组成:CommentListContainer和CommentList。首先定义一个容器类组件(Container Component):

//CommentListContainer
class CommentListContainer extends Component {
    constructor(){
        super();
        this.state = {commnets: []}
    }

    componentDidMount(){
        $.ajax({
            url:'/my-comments.json',
            dataType:'json',
            success:function(data){
                this.setState({comments:data});
            }.bind(this)
        })
    }

    render(){
        return <CommnetList comments={this.state.comments}/>;
    }
}

容器组件CommentListContainer获取到数据之后,通过props传递给子组件CommentList进行界面渲染。CommentList是一个展示类组件:

//CommentList
class CommentList extends Component {
    constructor(props){
        super(props);
        this.state = {commnets: []}
    }


    render(){
        return <ul>{this.props.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

将Comment组件拆分后,组件的自身状态和异步数据被分离,界面样式由展示类组件提供。这样,对于后续的业务数据变化需求,只需要更改容器类组件或者增加新的展示类业务组件,极大提高了组件的扩展性。

Container Component

容器类组件主要功能是获取 state 和提供 action,渲染各个子组件。各个子组件或是一个展示类组件,或是一个容器组件,其职责具体如下:

  • 获取 state 数据;
  • 渲染内部的子组件;
  • 无样式;
  • 作为容器,嵌套其它的容器类组件或展示类组件;
  • 为展示类组件提供 action,并提供callback给其子组件。

Presentational Component

展示类组件自身的数据来自于父组件(容器类组件或展示类组件),组件自身提供样式和管理组件状态。展示类组件是状态化的,其主要职责如下:

  • 接受props传递的数据;
  • 接受props传递的callback;
  • 定义style;
  • 使用其它的展示类组件;
  • 可以有自己的状态(state)。

连接器:connect

react-redux 为 React 组件和 Redux 提供的 state 提供了连接。当然可以直接在 React 中使用 Redux:在最外层容器组件中初始化 store,然后将 state 上的属性作为 props 层层传递下去。

class App extends Component{

  componentWillMount(){
    store.subscribe((state)=>this.setState(state))
  }

  render(){

    return <Comp state={this.state}
                 onIncrease={()=>store.dispatch(actions.increase())}
                 onDecrease={()=>store.dispatch(actions.decrease())}/>
  }
}

但这并不是所推荐的方式,相比上述的方式,更好的一个写法是结合 react-redux。

首先在最外层容器中,把所有内容包裹在 Provider 组件中,将之前创建的 store 作为 prop 传给 Provider。

const App = () => {
  return (
    <Provider store={store}>
      <Comp/>
    </Provider>
  )
};

Provider 内的任何一个组件(比如这里的 Comp),如果需要使用 state 中的数据,就必须是「被 connect 过的」组件——使用 connect 方法对「你编写的组件(MyComp)」进行包装后的产物。

class MyComp extends Component {
  // content...
}

const Comp = connect(...args)(MyComp);


connect 会返回一个与 store 连接后的新组件。那么,我们就可以传一个 Presentational Component 给 connect,让 connect 返回一个与 store 连接后的 Container Component。

connect 接受四个参数,返回一个函数:

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}){
        //code

        return function wrapWithConnect(WrappedComponent){
            //other code
            ....
            //merge props
            function computeMergedProps(stateProps, dispatchProps, parentProps) {
                    const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
                    if (process.env.NODE_ENV !== 'production') {
                           checkStateShape(mergedProps, 'mergeProps')
                    }
                   return mergedProps
        }

    ....

    render(){

        //other code
        ....

        if (withRef) {
              this.renderedElement = createElement(
WrappedComponent, {
                        ...this.mergedProps,
                       ref: 'wrappedInstance'
              })
        } else {
              this.renderedElement = createElement(
                WrappedComponent,
                this.mergedProps
               )
        }

             return this.renderedElement
      }

        }
}

wrapWithConnect 接受一个组件作为参数,在 render 会调用 React 的 createElement 基于传入的组件和新的 props 返回一个新的组件。

以 connect 的方式来改写Comment组件:

//CommentListContainer
import getCommentList '../actions/index'
import CommentList '../comment-list.js';

function mapStateToProps(state){
    return {
        comment: state.comment,
        other: state.other
    }
}

function mapDispatchToProps(dispatch) {
    return {
        getCommentList:()=>{ 
            dispatch(getCommentList());
        }
    }
}

export default connect(mapStateToProps,mapDispatchToProps)(CommentList);

在Comment组件中,CommentListContainer 只作为一个连接器作用,连接
CommentList 和 state:

//CommentList
class CommentList extends Component {
    constructor(props){
        super(props);
    }

    componentWillMount(){
        //获取数据
        this.props.getCommentList();
    }

    render(){
        let {comment}  = this.props;

        if(comment.fetching){
            //正在加载
            return <Loading />
        }

        //如果对CommentList item的操作比较复杂,也可以将item作为一个独立组件
        return <ul>{this.props.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

关于 connect 比较详细的解释可以参考:React 实践心得:react-redux 之 connect 方法详解

Redux:自问自答

  前段时间看了Redux的源码,写了一篇关于Redux的源码分析: Redux:百行代码千行文档,没有看的小伙伴可以看一下,整篇文章主要是对Redux运行的原理进行了大致的解析,但是其实有很多内容并没有明确地深究为什么要这么做?本篇文章的内容主要就是我自己提出一些问题,然后试着去回答这个问题,再次做个广告,欢迎大家关注我的掘金账号和我的博客。   

为什么createStore中既存在currentListeners也存在nextListeners?

  看过源码的同学应该了解,createStore函数为了保存store的订阅者,不仅保存了当前的订阅者currentListeners而且也保存了nextListenerscreateStore中有一个内部函数ensureCanMutateNextListeners:   

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}

  这个函数实质的作用是确保可以改变nextListeners,如果nextListenerscurrentListeners一致的话,将currentListeners做一个拷贝赋值给nextListeners,然后所有的操作都会集中在nextListeners,比如我们看订阅的函数subscribe:

function subscribe(listener) {
// ......
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
        // ......
        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
}

  我们发现订阅和解除订阅都是在nextListeners做的操作,然后每次dispatch一个action都会做如下的操作:

function dispatch(action) {
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // 相当于currentListeners = nextListeners const listeners = currentListeners
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

  我们发现在dispatch中做了const listeners = currentListeners = nextListeners,相当于更新了当前currentListenersnextListeners,然后通知订阅者,到这里我们不禁要问为什么要存在这个nextListeners?
  
  其实代码中的注释也是做了相关的解释:

The subscriptions are snapshotted just before every dispatch() call.If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the dispatch() that is currently in progress.However, the next dispatch() call, whether nested or not, will use a more recent snapshot of the subscription list. 

  来让我这个六级没过的渣渣翻译一下: 订阅者(subscriptions)在每次dispatch()调用之前都是一份快照(snapshotted)。如果你在listener被调用期间,进行订阅或者退订,在本次的dispatch()过程中是不会生效的,然而在下一次的dispatch()调用中,无论dispatch是否是嵌套调用的,都将使用最近一次的快照订阅者列表。用图表示的效果如下:
  

  

  

  我们从这个图中可以看见,如果不存在这个nextListeners这份快照的话,因为dispatch导致的store的改变,从而进一步通知订阅者,如果在通知订阅者的过程中发生了其他的订阅(subscribe)和退订(unsubscribe),那肯定会发生错误或者不确定性。例如:比如在通知订阅的过程中,如果发生了退订,那就既有可能成功退订(在通知之前就执行了nextListeners.splice(index, 1))或者没有成功退订(在已经通知了之后才执行了nextListeners.splice(index, 1)),这当然是不行的。因为nextListeners的存在所以通知订阅者的行为是明确的,订阅和退订是不会影响到本次订阅者通知的过程。

  这都没有问题,可是存在一个问题,JavaScript不是单线程的吗?怎么会出现上述所说的场景呢?百思不得其解的情况下,去Redux项目下开了一个issue,得到了维护者的回答:

  

  得了,我们再来看看测试相关的代码吧。看完之后我了解到了。的确,因为JavaScript是单线程语言,不可能出现出现想上述所说的多线程场景,但是我忽略了一点,执行订阅者函数时,在这个回调函数中可以执行退订或者订阅事件。例如:

const store = createStore(reducers.todos)
const unsubscribe1 = store.subscribe(() => {
    const unsubscribe2 = store.subscribe(()=>{})
})

  这不就实现了在通知listener的过程中混入订阅subscribe与退订unsubscribe吗?   

为什么Reducer中不能进行dispatch操作?

  我们知道在reducer函数中是不能执行dispatch操作的。一方面,reducer作为计算下一次state的纯函数是不应该承担执行dispatch这样的操作。另一方面,即使你尝试着在reducer中执行dispatch,也并不会成功,并且会得到"Reducers may not dispatch actions."的提示。因为在dispatch函数就做了相关的限制:   

function dispatch(action) {
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    //...notice listener
}

  在执行dispatch时就会将标志位isDispatching置为true。然后如果在currentReducer(currentState, action)执行的过程中由执行了dispatch,那么就会抛出错误('Reducers may not dispatch actions.')。之所以做如此的限制,是因为在dispatch中会引起reducer的执行,如果此时reducer中又执行了dispatch,这样就落入了一个死循环,所以就要避免reducer中执行dispatch

为什么applyMiddleware中middlewareAPI中的dispathc要用闭包包裹?

  关于Redux的中间件之前我写过一篇相关的文章Redux:Middleware你咋就这么难,没有看过的同学可以了解一下,其实文章中也有一个地方没有明确的解释,当时初学不是很理解,现在来解释一下:  

export default function applyMiddleware(...middlewares) {            
    return (next)  => 
        (reducer, initialState) => {

              var store = next(reducer, initialState);
              var dispatch = store.dispatch;
              var chain = [];

              var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
              };

              chain = middlewares.map(middleware =>
                            middleware(middlewareAPI));
              dispatch = compose(...chain, store.dispatch);
              return {
                ...store,
                dispatch
              };
           };
} 

这个问题的就是为什么middlewareAPI中的dispathc要用闭包包裹,而不是直接传入呢?首先用一幅图来解释一下中间件:

  
  

  

  如上图所示,中间件的执行过程非常类似于洋葱圈(Onion Rings),假设我们在函数applyMiddleware中传入中间件的顺序分别是mid1、mid2、mid3。而中间件函数的结构类似于:

export default function createMiddleware({ getState }) {
    return (next) => 
        (action) => {
            //before
            //......
            next(action)
            //after
            //......
        };
}

  那么中间件函数内部代码执行次序分别是:
  

  但是如果在中间件函数中调用了dispatch(用mid3-before中为例),执行的次序就变成了:

  

  所以给中间件函数传入的middlewareAPIdispatch函数是经过applyMiddleware改造过的dispatch,而不是redux原生的store.dispatch。所以我们通过一个闭包包裹dispatch:

(action) => dispatch(action)

  这样我们在后面给dispatch赋值为dispatch = compose(...chain, store.dispatch);,这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应用也会发生变化。如果我们写成:

var middlewareAPI = {
    getState: store.getState,
    dispatch: dispatch
};

那中间件函数中接受到的dispatch永远只能是最开始的redux中的dispatch

最后,如果大家在阅读Redux源码时还有别的疑惑和感受,欢迎大家在评论区相互交流,讨论和学习。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值