2

本文主要讲解redux的核心原理以及相关的周边技术原理。帮助你在使用的时候,知其所以然~

涉及:redux、redux异步解决方法、中间件、react-redux原理、Dva

用途

作为状态容器,提供对状态的查询、改变进行管理。
具体操作 -> 状态变更 -> 触发视图更新,这一单向数据流的控制,不仅使得对状态变化的过程变得可控,同时解耦了数据M和视图V

使用场景

1)应用复杂,将状态及页面逻辑reducer提取出来,利于代码结构的组织;
2)全局状态共享或可能被共享和改变的组件状态。

设计思想

1)web应用是一个状态机,视图和状态一一对应;
2)所有状态保存在一个对象内。

三大原则

单一数据源

state状态对象由store负责存储获取管理;
state为不可变对象(immutable data),保证每次返回一个新的状态对象。

state只读

action是改变状态对象state的唯一方式;
action由dispatch函数触发,其描述了一种行为其常见形式;

{
 type: 'actionType',   //行为
 payload: {}   //需要传递的信息
}

payload是更新store数据的唯一来源。

reducer是纯函数
type Reducer<S, A> = (state: S, action: A) => S

对action做出响应的响应,返回新的状态对象state(保证可回溯的原因),不应该产生副作用;
生成新的state有如下几种方式:

- Object.assign({}, oldState, newState);
- {...oldState, 更新值}
- Immutable

javascript中的基本类型:Boolean、Number、String、Null、undefined、Symbol等都是不可变的(Immutable),只有Object是可变的(Mutable).

store提供的方法:

subscribe

用于订阅事件,每次state变更后,都会触发其订阅的事件。
这里可以处理state -> props,以此更新视图组件的更新渲染(react-redux的connect就是这么实现的);

dispatch
  • 处理同步action
//该方法用于生成action 
let actionCreator = (name) => ({ type: 'ADD_ITEM', payload: { name } });
//生成action
dispatch(actionCreator('M2'));
  • 处理异步

1)redux-thunk
其通过扩展action,使得actionCreator返回一个function作为action。

let asyncActionCreator = postTitle => (dispatch, getState) => {
    dispatch(requestPosts(postTitle));
    return fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(postTitle, json)));
    };
};
//这里需要使用middleware支持异步,例如redux-thunk
var thunkMiddleware = function ({ dispatch, getState }) {
    return function(next) {
        return function (action) {
            //如果是function,则传递dispatch、getState,并执行
            return typeof action === 'function' ?
            //原始的dispatch
            action(dispatch, getState) :
            next(action)
        }
    }
}
//使用1
store.dispatch(fetchPosts('xx'));
// 使用2:注意上面dispatch后返回的Promise,可以在dispatch异步数据,reducer处理后,做一些处理 
store.dispatch(fetchPosts(genPromise)).then(() => console.log(store.getState()) );

thunk的应用:延迟执行、异步控制。作为Generator的next()的返回值,Thunk和Promise都是Generator自动执行的解决方案。

2)redux-sage

dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})

redux-saga是一个 redux 中间件;使用了 ES6 的 Generator 功能.
如何使用?

// 创建saga middleware
const sagaMiddleware = createSagaMiddleware();
// 注入saga middleware
applyMiddleware(sagaMiddleware);
//启动
sagaMiddleWare.run(rootSaga);

先来看rootSaga的写法:

// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* workerSaga(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}
/*
  在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
  允许并发(译注:即同时处理多个相同的 action)
*/
function* watcherSaga() {
  yield takeEvery("USER_FETCH_REQUESTED", workerSaga);
}
export default watcherSaga;

由上例可知,rootSaga是Generator函数,而sagaMiddleWare.run();是其自执行器;
启动后会执行takeEvery(),看下它做了什么事:

function* takeEvery("USER_FETCH_REQUESTED", workerSaga) {
  //启动一个新的独立的task,执行一个构造的Generator
  yield fork(function* () {
    //注册挂起->消费注销,执行对应的saga->注册挂起...
    while(true) {  
      //将当前actionType注册到发布中心,挂起,等待dispatch时在中间件中触发,匹配则执行该task的next()即workerSaga,同时注销该actionType的注册信息;
      yield take("USER_FETCH_REQUESTED"); 
      //执行saga任务
      yield task(workerSaga); 
    }
  });
}

注意这里的task()、take()、fork(),包括saga中call()、put()等返回的都是effect对象,比如call()执行后:

{
  isEffect: true,
  type: 'CALL',
  fn: Api.fetchUser
}

task自执行器在根据next()的返回值,判断effect的类型做对应的操作,比如执行yield take('USER_FETCH_REQUESTED')后,it.next()返回的是:

{
    type: 'take',
    pattern: 'USER_FETCH_REQUESTED'
}

那么自执行器会判断:

//effect为take的执行逻辑
if (effect.type === 'take') {
    runTakeEffect(result.value, next);
}

//runTakeEffect将当前Generator挂起,注册到channel中心,等待唤醒
function runTakeEffect(effect, cb) {
   chan.take({cb, pattern:effect.pattern});
}

发布订阅中心channel的代码如下:

function channel() {
  let _task = null;
  //挂起task
  function take(task) {
    _task = task;
  }
  //dispatch时,中间件put触发对应的actionType,来匹配对应pattern的回调
  function put(pattern, args) {
    if(!_task) return;
    if(pattern == _task.pattern) {
        taker = null;  //仅消费一次,注销,等待下次循环时再注册
        _task.cb.call(null, args);//唤醒挂起task;调用next()继续控制Generator执行
    }
  }
  return {
    take,
    put
  }
}

最后来看下redux-saga中间件是到channel中匹配对应action的:

const sagaMiddleware = store => {
  return next => action => {
    next(action);
    
    const { type, ...payload } = action;
    //调用channel的put方法,会判断是否匹配,匹配的话则执行挂起的task,即对应的saga
    channel.put(type, payload);
  }
} 

总结来说:

核心还是内部实现的Generator自执行器,对不同的命令对象做不同的处理逻辑;
循环过程:启动时,take挂起,等待put唤醒挂起的task调用next(),同时注销effect;让task自执行对应的gennerator函数。执行完后循环,再重新注册
发布订阅模式:chanel发布订阅中心,run执行时首先执行takeEvery,注册effect。在中间件中触发put(action)

问题:
1.为什么不用Async作为自执行器呢?
redux-saga内部实现了Generator自执行器,可以自主的控制流程的暂停,运行等;
2.为什么call()返回effect对象呢?
一个是根据effect类型可以做多种操作;再者这也是其比redux-thunk更利于测试的原理。

redux-sage相较于redux-thunk的优势:

  • 不改动action;
  • Generator同步的写法,实现异步流程的控制管理;

    注意:let res = yield call('xx');, res的值为next(val)传入的参数,而不是yield后面的表达式!
    Generator通过yieldnext来传递数据来控制函数的内部流程(内外部的双向通信)。
  • 单独的文件组织,以及yield后call等关键字返回的是effect对象,便于测试维护;

中间件

洋葱圈模型,和KOA的中间件原理类似;
在action真正处理前,赋予诸如日志记录等能力。

//将中间件绑定到
let applyMiddleware = middlewares => (createStore) => (...args) => {
  let store = createStore(...args);
   let dispatch = store.dispatch;
  let params = {
      dispatch,
      getState: store.getState
  };
   middlewares = middlewares.map(mw => mw(params));
  //组合中间件,增强dispatch
  //装饰者模式 & 洋葱圈,compose对中间件的组合
   dispatch = compose(middlewares)(dispatch);
  //返回增强后的dispatch
   return Object.assign(store, {dispatch});
}

React-Redux

基于容器组件和展示组件相分离的开发思想,将组件分为:UI组件容器组件.

import { connect } from 'react-redux'
const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)
export default VisibleTodoList;

mapStateToProps:将state映射到展示组件的props;
mapDispatchToProps: 定义了组件内部需要的事件,内部注入了dispatch;

怎么获取state?
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。一种方式是把它以 props 的形式传入到所有容器组件中,存在层层繁琐的传递,而且往往中间组件并不需要的问题。建议的方式是使用指定的 React Redux 组件 <Provider>

生产者:<Provider>组件

//使用
<Provider store={store}>
  <App />
</Provider>

//Provider利用了React的context
class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: React.PropTypes.object
}

消费者:connect

最后看下connect的简单实现:

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
    class Connect extends Component {
        static contextTypes = {
             store: PropTypes.object
        }
        constructor () {
            super();
            this.state = {
                allProps: {}
            }
        }
        componentWillMount () {
            const { store } = this.context
            this._updateProps()
            store.subscribe(() => this._updateProps())
        }
        _updateProps () {
            const { store } = this.context
            let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {} 
            let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {} 
            this.setState({
                allProps: {
                  ...stateProps,
                  ...dispatchProps,
                  ...this.props
                }
              })
        }
        render () {
            return <WrappedComponent {...this.state.allProps} />
        }
    }
    return Connect
}

Dva

Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面;
  2. 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作;
  3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️

更多技术分享,欢迎【扫码关注】~

参考:
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...
https://blog.csdn.net/TurkeyC...
https://dvajs.com/guide/fig-s...


夜暮sky
97 声望5 粉丝