利用 typescript 写 react-redux 和 redux-thunk,以及 thunk 等中间件的实现过程

react-redux 的常规使用步骤

  • Provider作为顶层全局状态的提供者,需要传递一个参数,全局状态 store
import { Provider } from 'react-redux';
<Provider store={ store }></Provider>
  • storecreateStore函数创建生成,需要传递 reducer纯函数作为参数
import { createStore, combineReducers } from 'redux';
const rootReducer = combineReducers({
    reducer
});
const store = createStore(rootReducer);
  • reducer又接收两个参数,stateaction,根据不同的 action返回一个新的 state
const reducer = (state, action) => {
    switch (action) {
        default:
            return state;
    }
};
  • 接下来就可以在组件中使用 redux 了
  • mapStateToProps会接收全局 state作为参数,返回一个对象,对象的属性作为提供给组件的 props
{ props.reducer };

const mapStateToProps = (state) => ({
    reducer: state.reducer
})
  • mapDispatchToProps接收 dispatch作为参数,返回的对象的属性是方法,方法中会调用这个 dispatch进行更新,可以往 dispatch中传递对象(只能触发一次dispatch)或者函数(可以触发任意次数,异步操作当然也可以)
props.action();

const mapDispatchToProps = (dispatch) => ({
    action: () => dispatch(action)
})
  • actionCreator用来生成传递给 dispatch的参数,也就是通常会在 actionCreator中返回 action 对象
// 返回对象
const actionCreator = function(data) {
    return {
        type: ACTION_TYPE,
        data
    }
}

// 使用
props.action(someData);

const mapDispatchToProps = (dispatch) => ({
    action: (data) => dispatch(actionCreator(data))
})

TypeScript 写法

上面的代码在 js 环境下执行没有问题,直接将文件名后缀改为 .ts,不出意外是会提示错误的……

例如:

// action.ts
// error: 参数“data”隐式具有“any”类型
export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data) => ({
    type: CHANGE_NAME,
    data
});

// reducer.ts
// error: 参数“action”隐式具有“any”类型
import { CHANGE_NAME } from './action';
const initialState = {
    name: 'lixiao'
};
export default (state = initialState, action) => {
    switch (action.type) {
        case CHANGE_NAME:
            retutn Object.assign({}, state, {
                name: action.data
            });
        default:
            return state;
    }
};

// rootReducer.ts
import { combineReducers } from 'redux';
import home from './pages/Home/reducer';
export default combineReducers({
    home
});

// Home.tsx
// error: 参数“state”隐式具有“any”类型
//        参数“dispatch”隐式具有“any”类型
//        参数“data”隐式具有“any”类型
const mapStatetoProps = (state) => ({
    name: state.home.name
});
const mapDispatchToProps = (dispatch) => ({
    changeName: (data) => dispatch(changeName(data)),
})

ts 做了静态编译,一部分目的就是为了避免随意取属性这样的操作。例如 mapStateToPropsstate.home这样的操作,需要告诉 ts,参数中的 state有哪些属性可以取,同样的道理,拆分之后的每一个 reducer 有哪些属性可以取,都需要明确的告诉 ts,(使用 any类型就不太友好了)。接下来对这些提示错误的变量进行类型声明。

actionCreator

参数中的 data作为该类型 action的负载,变量类型设置为 any是可以接受的

// action.ts
export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data: any) => ({
    type: CHANGE_NAME,
    data
});

reducer

reducer 接收 stateaction两个参数,action通常会有一个动作类型属性 type和 动作负载 data,可以在整个应用中统一为这个形式;而 这里的state会在其他连接上 redux 的组件中进行访问,因此有必要定义好一个接口,约定能访问到这个 state的哪些属性。

// global.d.ts
declare interface IAction {
    type: string;
    data: any;
}

// reducer.ts
import { CHANGE_NAME } from './action';

// 这个接口约定子 reducer 能访问到的属性
export interface IHomeState {
    name: string;
}
const initialState: IHomeState = {
    name: 'lixiao'
};
export default (state = initialState, action: IAction): IHomeState => {
    switch (action.type) {
        case CHANGE_NAME:
            return Object.assign({}, state, {
                name: action.data
            });
        default:
            return state;
    }
};

rootReducer

作为顶层 state,在子组件中通常会通过它再去访问子 reducer 中的 state,前面已经约定好了子 reducer 中的 state能够取到哪些属性,现在还需要约定好顶层 state能取到哪些子 state

// rootReducer.ts
import { combineReducers } from 'redux';
import home, { IHomeState } from './pages/Home/reducer';

// 每增加一个子 reducer,都在这个接口中同步更新
export interface IStore {
    home: IHomeState
}
export default combineReducers({
    home
});

组件中使用 redux

// Home.tsx
import { Dispatch } from 'redux';
import { IStore } from '../../rootReducer';

// 组件的 props 类型
interface IProps {
    name: string;
    // 这里的 changeName 和 actionCreator 中的不是同一个,所以返回值类型不是对象
    changeName(data: any): void;
}
const mapStatetoProps = (state: IStore) => ({
    name: state.home.name
});
// Dispatch 接收的参数为对象
const mapDispatchToProps = (dispatch: Dispatch) => ({
    changeName: (data: any) => dispatch(changeName(data)),
})

redux-thunk

在前面的例子中,将 action传递给 dispatch,然后根据 reducer 函数返回新的 state,这个操作是同步的,类似于 vuex中的 mutation。redux-thunk 是处理异步 action 的一种解决方式。

异步 action

一个很常见的场景,发送 ajax 请求成功之后触发 dispatch。最简单的实现方式是将这个操作封装成一个函数:

const asyncAction = () => {
    dispatch({ type, data });
    ajax().then(res => dispatch(res));
}

若这个操作需要在多个地方调用呢?复制粘贴这个函数也是可行的,但更好的做法是使用 redux-thunk 这样的中间件,在 actionCreator 中生成一个包含异步操作的 action

redux-thunk 用法

  • 在创建 store 的时传入 redux-thunk 中间件
// index.tsx
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));
  • actionCreator 返回的是一个函数,在这个函数中可以做任意的事情,触发任意次数的 dispatch,当然也包括异步操作。
// action.ts
import { Dispatch } from 'redux';

export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data: any) => ({
    type: CHANGE_NAME,
    data
});
export const changeNameAsync = (data?: any) => (dispatch: Dispatch) => {
    dispatch(changeName('loading'));
    fetch('/api').then(res => res.json()).then(res => dispatch(changeName(res.data)));
}
// Home.tsx
import { changeName, changeNameAsync } from './action';

// dispatch 可以传入对象、函数,这里不能直接简单的使用 Dispatch 类型
const mapDispatchToProps = (dispatch: any) => ({
    changeName: (data: any) => dispatch(changeName(data)),
    changeNameAsync: () => dispatch(changeNameAsync())
});
// 也可以使用 bindActionCreators
const mapDispatchToProps = (dispatch: Dispatch) => (bindActionCreators({
    changeName,
    changeNameAsync
}, dispatch))

redux-thunk 实现过程

使用一次之后就发现,同步 action与异步 action的区别在于,同步操作 dispatch一个对象,而异步操作是 dispatch一个函数,在这个函数中可以包含异步、dispatch同步 action等任意操作。

createStore

基础版本

在引入 redux-thunk 之前,生成 store的方式为

const store = createrStore(reducer, initialState)

第二个参数 initialState为初始状态,可选

// 两个参数
function createStore(reducer, preloadedState) {
    let currentReducer = reducer;
    let currentState = preloadedState;

    // 比较重要的函数 dispatch
    function dispatch(action) {
        // 这就是为什么也可以在 reducer 中初始化 state
        // 也可以发现这里的参数 preloadedState 的优先级高于 reducer 中的 initialState
        currentState = currentReducer(currentState, action);
    }

    // 页面刚打开的时候,调用 createStore,再执行一次 dispatch,更新 currentState
    // 所以 redux 开发工具中会有一个 @@INIT 类型的 action
    dispatch({ type: ActionTypes.INIT });
}
高级版本

引入 redux-thunk 之后,生成 store的方式也有所变化

const store = createrStore(reducer, initialState, enhancer)

第三个参数是一个函数,接下来看看传入第三个参数时, createStore内部是如何处理的

function createStore(reducer, preloadedState, enhancer) {
    // 由于第二个参数是可选的,所以需要一些代码进行参数数量和参数类型的检测
    // ts 就方便了……
    // 检测通过之后就提前 return 了,
    return enhancer(createStore)(reducer, preloadedState)
}

进行一下代码转换

const store = createStore(rootReducer, applyMiddleware(thunk));
// enhancer 就是 applyMiddleware(thunk)
// preloadedState 为 undefined
// 代码转换
const store = applyMiddleware(thunk)(createStore)(rootReducer);

applyMiddleware

import compose from './compose';

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
    }
  }
}

接着进行代码装换

const store = applyMiddleware(thunk)(createStore)(rootReducer);
// 相当于在执行下面这串代码
const store = createStore(rootReducer) // 这个 store 是基础版本中的 store
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: (rootReducer) => dispatch(rootReducer)
    dispatch: dispatch
}
const chain = [thunk].map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
// 这是最终返回的 store 
return {
    ...store,
    dispatch
}

再看看 thunk(middlewareAPI)compose做了什么

thunk

// redux-thunk/src/index.js 简化之后
const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }

    return next(action);
};

compose

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)))
}

传入的多个中间件,会依次作为参数;只有一个中间件则直接返回这个函数

dispatch 怎么被拓展的

const store = createStore(rootReducer)
// const chain = [thunk].map(middleware => middleware(middlewareAPI))
// const chain = [thunk(middlewareAPI)];
const chain = [(next => action => {
    if (typeof action === 'function') {
        return action(dispatch, store.getState);
    }
    return next(action);
})]
// dispatch = compose(...chain)(store.dispatch)
dispatch = action => {
    if (typeof action === 'function') {
        return action(dispatch, store.getState);
    }
    return store.dispatch(action);
}

当传给 dispatch的参数不是函数时,就是基础版本,直接调用 reducer 函数进行更新 state;传入的是函数时,执行函数体的内容并把 dispatch传进去,在这个函数内部就又可以使用 dispatch做想做的事情了。

总结

大部分函数都是 redux 提供的,闭包用的比较多,(params1) => (params2) => { someFunction(params1, params2) }这样的用法很多,看起来容易头晕,但也就是每执行一次函数传入一次参数。

原文链接:segmentfault.com

上一篇:upnode
下一篇:Lodash笔记 slice

相关推荐

  • (转载)如何通俗易懂的理解Redux

    作者:Wang Namelos 链接:https://www.zhihu.com/questio...(https://www.zhihu.com/question/41312576/answer/9...

    2 年前
  • 面试还问redux?那我从头手撸源码吧(中间件篇)

    昨天的文章手写了一版redux的核心源码(https://segmentfault.com/a/1190000016799698),redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我...

    2 年前
  • 阅读redux源码_compose

    先上源码: 重点看一句就够了: 现在我们先假设一个数组,有3个函数,分别是x,y,z 那么发生什么了,接下来就一步一步解释 1. 变成reduce模式: 2. reduce第一次执行,...

    2 年前
  • 通过编写一个路由中间件来学习 Koa

    混了四年的大学生活结束了,校招没有找到工作的我还面临着失业。没办法,只有临时抱抱佛脚看看能不能找个工作了。据说最近前端圈里不会 NodeJs 是不可能找到工作的,于是抱起了 NodeJs 里比较流行的...

    2 年前
  • 通过变量来使用next()在expressjs未来中间件

    cchamberlainuser2791897(https://stackoverflow.com/users/769871/cchamberlain)提出了一个问题:passing variable...

    2 年前
  • 跟着例子一步步学习redux+react-redux

    前言 本文不会拿redux、reactredux等一些react的名词去讲解,然后把各自用法举例说明,这样其实对一些react新手或者不太熟悉redux模式的开发人员不够友好,他们并不知道这样使用...

    2 年前
  • 超级易懂的redux-saga原理解析

    原文地址(https://github.com/zyl1314/blog/issues/14) 前言 笔者最近在做一些后台项目,使用的是Ant Design Pro(https://github...

    2 年前
  • 读redux源码总结

    redux介绍 redux给我们暴露了这几个方法 我们来依次介绍下 createStore 源码 个人觉得compose(a,b,c)(d)后的代码就是a(b(c(d))),co...

    2 年前
  • 详解redux异步操作示例

    一、redux基础 (https://img.javascriptcn.com/3c838d74abd0220ea77efc3756edb0cd) redux 通过 dispatch(a...

    2 年前
  • 详解react、redux、react-redux之间的关系

    本文介绍了react、redux、reactredux之间的关系,分享给大家,也给自己留个笔记,具体如下: React 一些小型项目,只使用 React 完全够用了,数据管理使用props、sta...

    2 年前

官方社区

扫码加入 JavaScript 社区