1. redux

首先redux的流程:首先会设置一些监听函数。

1
store.subscribe(listener);

然后进入正式流程:dispatch一个action,然后store自动调用reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。State 一旦有变化,Store 就会调用监听回调函数,回调函数可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
其中着重说一下subscribe这个函数,react-redux对这个api进行了封装。

2. 异步action

异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?
奥妙就在 Action Creator 之中。

1
2
3
4
5
6
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
}
// ...

上面代码是一个异步组件的例子。加载成功后(componentDidMount方法),它送出了(dispatch方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)。这里的fetchPosts就是 Action Creator。
下面就是fetchPosts的代码,关键之处就在里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);

上面代码中,fetchPosts是一个Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个Action(requestPosts(postTitle)),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json))。
上面代码中,有几个地方需要注意:
(1)fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。
(2)返回的函数的参数是dispatch和getState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
(3)在返回的函数之中,先发出一个 Action(requestPosts(postTitle)),表示操作开始。
(4)异步操作结束之后,再发出一个 Action(receivePosts(postTitle, json)),表示操作结束。
这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。
这时,就要使用中间件redux-thunk。

1
2
3
4
5
6
7
8
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const { subscribe, dispatch, getState } = createStore(
reducer,
applyMiddleware(thunk)
);

上面代码使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。
因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用redux-thunk中间件改造store.dispatch。

3. applyMiddleware()

看到这里,你可能会问,applyMiddleware这个方法到底是干什么的?
它是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。使用方法参见:

1
2
3
4
5
6
7
8
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);

摘了核心的createStore的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}

if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
...
}

我们不妨再简化一下这个createStore源码:

1
2
3
4
5
function createStore(reducer,preloadedState,enhancer){
...
return enhancer(createStore)(reducer,preloadedState)
...
}

我们知道,其实enhancer === applyMiddleware(thunk),这样子,我们再将enhancer换为applyMiddleware(thunk)
这时变成这样子:

1
2
3
4
5
function createStore(reducer,preloadedState,enhancer){
...
return applyMiddleware(thunk)(createStore)(reducer,preloadedState)
...
}

我们再回到applyMiddleware的源码上来。看到,定义的applayMiddlware为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
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);

return {...store, dispatch}
}
}

执行如下代码,可以看出如果createStore的参数enhancer未传,那么就是普通的createStore了,如果传了,那实际上,createStore已经被enhancer接管了,然后相当于再返回一个普通的createStore而已。这才是其中的精妙之处!

1
2
applyMiddleware(thunk)(createStore)(reducer,preloadedState)
}

因此再createStore中传入applyMiddleware(thunk),返回的store和dispatch,其中dispatch通过如下方式生成:

1
2
3
4
5
6
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
compose(...chain)(store.dispatch);

定义了一个chain数组,存放每一个被处理过的middleware。首先为每一个middleware以{getState,dispatch}为参数执行一遍,其实是为了给middleware一个原生的{getState,dispatch}两个方法的指针,以便在middleware中调用。为了描述清楚,请看一个简单的middleware:

1
2
3
4
5
6
const logger = ({getState,dispatch}) => next => action {
console.log('dispatching',action)
let result = next(action)
console.log('next state',getState())
return result
}

因此chain数组中存放的都是function(以next作为参数)。继续看代码,有一个这样的语句:

1
dispatch = compose(...chain)(store.dispatch)

下面看一个compose的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

4. react-redux

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。
UI 组件有以下几个特征。

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API
    容器组件的特征恰恰相反。
  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API
    首先react-redux库提供Provider组件将store注入整个React应用的某个入口组件,通常是应用的顶层组件。Provider组件使用context向下传递store。
    接下来需要做的是连接组件和store。而且我们知道Redux不提供直接操作store state的方式,我们只能通过其getState访问数据,或通过dispatch一个action来改变store state。
    React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。
    因此,connect方法的完整 API 如下。
1
2
3
4
5
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
react-redux 类库的意义是将 redux 状态注入到组件中,且在状态更新时驱动组件重绘。在这个类库的设计和实现上,需要面临两个问题:如何将 store 中缓存的状态注入到组件中;在状态更新时,如何驱动组件重绘。
对于第一个问题,react-redux 解决手法也极为简单且常见。通过顶层 Provider 容器接受 store 作为 props,再将 store 作为 context 内容传入子孙组件;在用户端实际消费状态的组件(自定义组件)外层构造直属父级容器(HOC 高阶组件),由直属父级容器将通过 context.store 属性获取的状态数据输入为自定义组件的 props。
对于第二个问题,react-redux 解决手法是:mapStateToProps 通过 store.subscribe 方法注册状态变更后待执行的回调函数 listener,在该回调函数执行直属父级容器的 forceUpdate 或 setState 方法,重新计算注入自定义组件的 props 数据,由此重绘自定义组件。在这个过程中,无论 store 中的状态数据作何种更新,均会触发 listener 回调的执行,因而实现上有一个优化性能的关键点,即怎样使组件在指定状态更新时启用重绘机制。这一优化点通过重新计算注入自定义组件的 props 实现。在 react-redux 源码中,重新计算 props 数据由 selector 筛选器完成。除此以外,react-redux 还有另一处优化:为减少 listener 的数量,当某个 HOC 组件的重绘机制(在 redux 源码中,表现为该 HOC 组件的 onStateChange 方法)已挂载为 listener 时,其下子孙容器(同样是 HOC 组件)的重绘机制将在这个 listener 的函数体中实现。

参考:
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
https://huangguangjie.github.io/2017/03/30/redux-applyMiddleware/