Redux 暴露非常少的 API, 优雅的将单向数据流落地. 但有这些, Redux 的作者 Dan Abramov 仍然觉得远远不够. 一个工具的强大之处体现在它的扩展能力上. Redux 的中间件机制让这种扩展能力同样变的异常优雅.
中间件在前端的意思是插入某两个流程之间的一段逻辑. 具体到 Redux, 就是在 dispatch 一个动作前后插入第三方的处理函数.
使用
还记得吗? Store 构造器 createStore 有三个参数, 第三个参数叫做 enhancer, 翻译过来就是增强器. 我们先将 enhancer 按下不表, 并且告诉你其实 Redux 的另一个 APIapplyMiddleware 就是一个 enhancer.
- import {createStore, combineReducers, applyMiddleware} from 'redux';
- import thunk from 'redux-thunk';
- import logger from 'redux-logger';
- import { userReducer } from './user/reducer';
- import { todoReducer } from './todo/reducer';
- const reducers = combineReducers({
- userStore: userReducer,
- todoStore: todoReducer,
- });
- const enhancer = applyMiddleware(thunk, logger);
- const store = createStore(reducers, null, enhancer);
- export default store;
复制代码
只需要把所有中间件依次传入 applyMiddleware, 就生成了一个增强器, 它们就可以发挥作用了.
如果 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.');
- }
复制代码
服务器请求
一个组件免不了向服务器请求数据, 然而开发者不希望组件内部有过多的逻辑, 请求应该封装成函数给组件调用, 同时组件需要实时获取请求的状态以便展示不同的界面. 最好的办法就是将请求也纳入 Redux 的管理中.
- import api from './api';
- export const fetchMovieAction = () => {
- dispatch({ type: 'FETCH_MOVIE_START' });
- api.fetchMovie().then(res => {
- dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
- }).catch(err => {
- dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
- });
- };
复制代码
- import React, { Component } from 'react';
- import { connect } from 'react-redux';
- import { fetchMovieAction } from './actions';
- import Card from './Card';
- class App extends Component {
- render() {
- const { movies } = this.props;
- return (
- <div className="movie">
- {movies.map(movie => <Card key={movie.id} {...movies} />)}
- </div>
- );
- }
- componentDidMount() {
- this.props.fetchMovie();
- }
- }
- const mapState = (state) => {
- return {
- movies: state.payload.movies,
- };
- };
- const mapDispatch = (dispatch) => {
- return {
- fetchMovie: () => dispatch(fetchMovieAction()),
- };
- };
- export default connect(mapState, mapDispatch)(App);
复制代码
大功告成了.
只需要将请求封装成一个函数, 然后伪装成 Action 被发射出去, 请求调用前后, 真正的 Action 会被发射, 在 Store 中存储请求的状态, 并且能够被组件订阅到.
异步 Action
你是不是发现了什么? 对咯, 这里的 Action 不是一个纯对象.
因为请求一定是一个函数, 为了让请求入会, 只能反过头来修改大会章程. 但是大会章程岂能随便推翻, 这时意见领袖出来说话了:
当初规定 Action 必须是一个纯对象不是为了搞个人崇拜, 而是出于实际需要. 因为 reducer 必须是一个纯函数, 这决定了 dispatch 的参数 Action 必须是一个带 type 字段的纯对象. 现如今我们要拉异步请求入会, 而中间件又可以中途拦截做一些处理, 那 Action 为什么不能是一个函数呢? Action 必须是一个纯对象这种说法是完完全全的教条主义!
大家还动脑筋想出了一个异步 Action 的名头, 这下函数类型的 Action 终于名正言顺了.
闭包
你是不是还发现了什么? 对咯, 请求函数中的 dispatch 哪去了.
不知道, 可能会报错吧 (无辜脸).
其实我们还有一件事没干: 把 dispatch 方法偷渡到请求函数中.
- export const fetchMovieAction = () => {
- return (dispatch) => {
- dispatch({ type: 'FETCH_MOVIE_START' });
- api.fetchMovie().then(res => {
- dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
- }).catch(err => {
- dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
- });
- };
- };
复制代码
很简单哪, 加一个闭包, dispatch 从返回函数的参数中偷渡进来.
脑洞
我们要的不就是一个 dispatch 方法么, 我能不能这样:
- export const fetchMovie = (dispatch) => {
- dispatch({ type: 'FETCH_MOVIE_START' });
- api.fetchMovie().then(res => {
- dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
- }).catch(err => {
- dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
- });
- };
复制代码
- const mapDispatch = (dispatch) => {
- return {
- fetchMovie: () => fetchMovie(dispatch),
- };
- };
复制代码
貌似是能行得通的, 只不过这时候请求函数已经不能叫 Action 了. 考虑到之前请求函数伪装成 Action 浑水摸鱼, 还要插入中间件来帮助特殊处理, 我们这样做也不过分是吧.
好处就是不再需要能够处理异步 Action 的中间件了.
坏处就是这不符合规范, 是我的脑洞, 闯了祸不要打我 (蔑视).
redux-thunk
前面多次提到处理异步 Action 的中间件, 到底是何方神圣?
市面上流行的方案有很多种, 我们挑最简单的一种来说一说 (都不点赞怪我咯).
redux-thunk 算是 Redux 官方出品的异步请求中间件, 但是它没有集成到 Redux 中, 原因还是为了扩展性, 社区可以提出各种方案, 开发者各取所需.
让我们来探讨一下 redux-thunk 的思路: 原来 Action 只有一种, 就是纯对象, 现在 Action 有两种, 纯对象和异步请求函数. 只不过多了一种情况, 不算棘手嘛. 如果 Action 是一个对象, 不为难它直接放走; 如果 Action 是一个函数, 就地执行, 调用异步请求前后, 真正的 Action 自然会释放出来, 又回到第一步, 放它走.
这是 redux-thunk 简化后的代码, 其实源代码也跟这差不多. 是不是很恐慌?
- const thunk = ({ dispatch, getState }) => next => action => {
- if (typeof action === 'function') {
- return action(dispatch, getState);
- }
- return next(action);
- };
复制代码
上面的函数就部署在我的个人博客中用来处理异步请求, 完全没有问题. 既然它这么简单, 而且可以预计它万年不会变, 那我为什么要凭空多一个依赖包. 就将它放在我的眼皮底下不是挺好的嘛.
不过, 它是一个研究中间件很好的范本.
我们先将 thunk 先生降级成普通函数的写法:
- const thunk = function({ dispatch, getState }) {
- return function(next) {
- return function(action) {
- if (typeof action === 'function') {
- return action(dispatch, getState);
- }
- return next(action);
- }
- }
- };
复制代码
compose
我知道 compose 是 Redux 的五大护法之一, 可为什么挑在这个时候讲它呢?
先不告诉你.
compose 在函数式编程中的含义是组合. 假如你有一堆函数要依次执行, 而且上一个函数的返回结果是下一个函数的参数, 我们怎样写看起来最装逼?
const result = a(b(c(d(e('redux')))));
复制代码
这种写法让人一眼就看穿了调用细节, 装逼明显是不够的.
我们来看 Redux 是怎么实现 compose 的:
- export default function compose(...funcs) {
- if (funcs.length === 0) {
- return arg => arg;
- }
- if (funcs.length === 1) {
- return funcs[0];
- }
- return funcs.reduce((a, b) => (...args) => a(b(...args)));
- }
复制代码
诶, 我看见 reduce 了, 然后... 就没有然后了.
假设我们现在有三个函数:
- const funcA = arg => console.log('funcA', arg);
- const funcB = arg => console.log('funcB', arg);
- const funcC = arg => console.log('funcC', arg);
复制代码
执行 reduce 的第一步返回的 accumulator(accumulator 是 reduce 中的概念), 结果显而易见:
(...args) => funcA(funcB(...args));
复制代码
执行 reduce 的第二步返回的 accumulator, 注意到, 这时 reduce 已经执行完了, 返回的是一个函数.
(...args) => funcA(funcB(funcC(...args)));
复制代码
特别提醒: 执行 compose 最终返回的是一个函数. 也就是说开发者得这么干 compose(a, b, c)() 才能让传入的函数依次执行.
另外需要注意的是: 传入的函数是从右到左依次执行的.
applyMiddleware
废话少说, 先上源代码:
- export default function applyMiddleware(...middlewares) {
- return createStore => (...args) => {
- const store = createStore(...args);
- let dispatch = () => {
- throw new Error(
- `Dispatching while constructing your middleware is not allowed. ` +
- `Other middleware would not be applied to this dispatch.`
- );
- };
- const middlewareAPI = {
- getState: store.getState,
- dispatch: (...args) => dispatch(...args),
- };
- const chain = middlewares.map(middleware => middleware(middlewareAPI));
- dispatch = compose(...chain)(store.dispatch);
- return { ...store, dispatch };
- }
- }
复制代码
还记得中间件闭包好几层的写法吗? 现在我们就来一层一层的剥开它.
middlewareAPI 是一个对象, 正好是传给第一层中间件函数的参数. 执行它, 返回的 chain 是由第二层函数组成的中间件数组. 贴一下 redux-thunk 第二层转正后的样子:
- function(next) {
- return function(action) {
- if (typeof action === 'function') {
- return action(dispatch, getState);
- }
- return next(action);
- }
- }
复制代码
中间件第二层函数接收一个 next 参数, 那这个 next 具体指什么呢? 我先透露一下, next 是整个 Redux 中间件机制的题眼, 理解了 next 就可以对 Redux 中间件的理解达到大彻大悟的化境.
之前我们已经拆解了 compose 的内部机制, 从右到左执行, 最右边的中间件的参数就是 store.dispatch, 它返回的值就是倒数第二个中间件的 next. 它返回什么呢? 我们再剥一层:
- function(action) {
- if (typeof action === 'function') {
- return action(dispatch, getState);
- }
- return next(action);
- }
复制代码
别看 redux-thunk 麻雀虽小, 大家发现没有, 第三层函数才是它的逻辑, 前面两层都是配合 redux 的演出. 也就是说呀同学们, 除了最后一个中间件的 next 是原始的 dispatch 之外, 倒数往前的中间件传入的 next 都是上一个中间件的逻辑函数.
Redux 中间件本质上是将 dispatch 套上一层自己的逻辑.
最终 applyMiddleware 里得到的这个 dispatch 是经过无数中间件精心包装, 植入了自己的逻辑的 dispatch. 然后用这个臃肿的 dispatch 覆盖原有的 dispatch, 将 Store 的 API 返回.
每一个 Action 就是这样穿过重重的逻辑代码才能最后被发射成功. 只不过处理异步请求的中间件不再往下走, 直到异步请求发生, 真正的 Action 被发射出来, 才会走到下一个中间件的逻辑.
构建 dispatch 过程中禁止执行 dispatch
middlewareAPI 中的 dispatch 为什么是一个抛出错误的函数?
我们现在已经知道, applyMiddleware 的目的只有一个: 用所有中间件组装成一个超级 dispatch, 并将它覆盖原生的 dispatch. 但是如果超级 dispatch 还没组装完成, 就被中间件调用了原生的 dispatch, 那这游戏别玩了.
所以 Redux 来了一手掉包.
middlewareAPI 初始传入的 dispatch 是一个炸弹, 中间件的开发者胆敢在头两层闭包函数的外层作用域调用 dispatch, 炸弹就会引爆. 而一旦超级 dispatch 构建完成, 这个超级 dispatch 就会替换掉炸弹.
怎么替换呢?
函数也是引用类型对吧, 炸弹 dispatch 之所以用 let 定义, 就是为了将来修改它的引用地址:
- let dispatch = () => {
- throw new Error(
- `Dispatching while constructing your middleware is not allowed. ` +
- `Other middleware would not be applied to this dispatch.`
- );
- };
- // ...
- dispatch = compose(...chain)(store.dispatch);
复制代码
当然, 这是对中间件开发者的约束, 如果你只是一个中间件的使用者, 这无关紧要.
applyMiddleware 的花式调用
我们注意到, 执行 applyMiddleware 返回的是一个函数, 这个函数有唯一的参数 createStore.
WTF?
applyMiddleware 不是 createStore 的参数之一么:
const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
复制代码
怎么 createStore 也成了 applyMiddleware 的参数了?
贵圈真乱.
首先我们明确一点, applyMiddleware 是一个增强器, 增强器是需要改造 Store 的 API 的, 这样才能达到增强 Store 的目的. 所以 applyMiddleware 必须传入 createStore 以生成初始的 Store.
所以生成一个最终的 Store 其实可以这样写:
- const enhancedCreateStore = applyMiddleware(middleware1, middleware2, middleware3)(createStore);
- const store = enhancedCreateStore(reducer);
复制代码
那通常的那种写法, Redux 内部是怎么处理的呢?
- if (typeof enhancer !== 'undefined') {
- if (typeof enhancer !== 'function') {
- throw new Error('Expected the enhancer to be a function.')
- }
- return enhancer(createStore)(reducer, preloadedState)
- }
复制代码
上面是 createStore 源代码中的一段.
如果 enhancer 存在并且是一个函数, 那么直接传入 createStore 执行, 再传入 reducer 和 preloadedState 执行 (这时候再传入 enhancer 就没完没了了), 然后直接返回.
喵, 后面还有好多代码呢, 怎么就返回了?
不, 就这么任性.
这么看下来, 以下写法才是正宗的 Redux:
const store = applyMiddleware(middleware1, middleware2, middleware3)(createStore)(reducer);
复制代码
以下写法只是 Redux 为开发者准备的语法糖:
const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
复制代码
洋葱圈模型
想必大家都听说过中间件的洋葱圈模型, 这个比喻非常形象, 乍听上去, 啊, 好像明白了. 但是大家真的对洋葱圈模型有一个具象化的理解吗?
假设现在有三个中间件:
- const middleware1 = ({ dispatch, getState }) => next => action => {
- console.log('middleware1 start');
- next(action);
- console.log('middleware1 end');
- }
- const middleware2 = ({ dispatch, getState }) => next => action => {
- console.log('middleware2 start');
- next(action);
- console.log('middleware2 end');
- }
- const middleware3 = ({ dispatch, getState }) => next => action => {
- console.log('middleware3 start');
- next(action);
- console.log('middleware3 end');
- }
复制代码
现在将它传入 applyMiddleware:
- function reducer(state = {}, action) {
- console.log('reducer return state');
- return state;
- }
- const middlewares = [middleware1, middleware2, middleware3];
- const store = createStore(reducer, applyMiddleware(...middlewares));
复制代码
我们看一下打印的结果:
- middleware1 start
- middleware2 start
- middleware3 start
- reducer return state
- middleware3 end
- middleware2 end
- middleware1 end
复制代码
对结果感到惊讶吗? 其实理解函数调用栈的同学就能明白为什么是这样的结果. reducer 执行之前也就是 dispatch 真正执行之前的日志好理解, dispatch 被一层一层包装, 一层一层的深入调用. 但是 dispatch 执行完以后呢? 这时候的执行权在调用栈最深的那一层逻辑那里, 也就是最接近原始 dispatch 的逻辑函数那里, 所以之后的执行顺序是从最深处往上调用.
总的看下来, 一个 Action 的更新 Store 之旅就像穿过一个洋葱圈的旅行. 一堆中间件簇拥着 Action 钻到洋葱的中心, Action 执行自己的使命更新 Store 后就地圆寂, 然后中间件带着它的遗志再从洋葱的中心钻出来.
回看 compose
其实我解释上面的打印日志, 还有一个关节没有打通.
记得 applyMiddleware 的源代码吗? 内部调用了 compose 来执行 chain.
我们强调过, compose 的函数类型参数的执行顺序是从右到左的, 我相信大家在不少的地方都见到过这样的表述. 但是大家想过没有, 为什么要从右到左执行? 原生 JavaScript 除了实现 reduce 之外还有一个 reduceRight, 从左到右执行并没有什么技术障碍, 那么为什么要让执行顺序这么别扭呢?
答案就在上面的打印日志里.
打印日志很好哇, 根据传入的顺序执行. 对, 执行 compose 是从右到左, 但是 compose 返回的终极 dispatch 是一层一层从外面包裹的呀, 最后一个中间件也就是最左边的中间件的逻辑, 包裹在最外面一层, 自然它的日志最先被打印出来.
所以 compose 被设计成参数从右到左执行, 不是有技术障碍, 也不是 Redux 特立独行, 而是其中本来就要经历一次反转, compose 只有再反转一次才能将它扭转过来.
Redux 专题一览
来源: https://juejin.im/post/5ba04bb5e51d450e531c7300