欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  web前端

深入redux技术栈

程序员文章站 2022-03-31 22:20:12
...
redux 简介

本文默认大家掌握一些react和flux架构的相关知识,也用过或者了解过redux,所以并不会从最基础的讲起,而是直接对redux进行总结。如果没有用过redux,最好可以先看这里

想要理解redux,我们首先要总结redux的一些设计原则:

  • 单一数据源

Redux中只有用单一个对象大树结构来的存储整个应用的状态,也就是整个应用中会用到的数据,称之为store(存储)。store除了存储的数据,还可以存储整个应用的状态(包括router状态,后文有介绍),所以,通过store,实现一个对整个应用的即时保存功能(建立快照)变为可能,另外这种设计也为服务端渲染提供了可能。

  • 状态是只读的

这一点符合flux的设计理念,我们并不能在components里面更改store的状态(实际上redux会根据reducer生成store),而是只能通过dispatch,触发action对当前状态进行迭代,这里我们也并没有直接修改应用的状态,而是返回了一份全新的状态。

  • 状态修改均由纯函数构成

Redux中的reducer的原型会长得像下面这样,你可以把它当作就是 之前的状态 + 动作 = 新的状态 的公式:

(previousState, action) => newState

每一个reducer都是纯函数,这意味着它没有任何副作用,这种设计的好处不仅在于用reducer对状态修改变的简单,纯粹可以测试,另外,redux可以保存各个返回状态从而方便地生成时间旅行,跟踪每一次因为出发action而导致变更的结果。

我们如果在react中使用redux,同时需要react-redux 和 redux。

redux 架构与源码分析

这一部分主要谈一点自己的理解,可能有些抽象,也可能不完全正确,可直接跳过。

createStore

redux中核心的方法是createStore,react的核心功能全都覆盖在createStore和其最终生成的store中,createStore方法本身支持传入reducer、initialState、enhancer三参数,enhancer可以作为增强的包装函数,这个我们并不是十分常用。

这个函数内部维护了一个currentState,并且这个currentState可以通过getState函数(内置)返回,另外本身实际上是实现了一个发布-订阅模式,通过store.subscribe来订阅事件,这个工作由react-redux来帮助我们隐式完成,这是为了在有dispatch的时候触发所有监听从而更新整个状态树。另外,内置的dispatch函数在经过一系列校验后,触发reducer,之后state被更改,之后依次调用监听,完成整个状态树的更新。

middleWare

用过redux的朋友实际上都对于redux-thunk等中间件并不陌生,实际上很多时候这是不可缺少的,redux对middleWare也有很好的支持,这种理念我认为和nodejs的中间件机制有些类似:action依次经过各个middleWare然后传给下一个,每一个middleWare也可以进行另外的操作比如中断或者改变action,知道最终的处理函数交给reducer。

redux的applyMiddleware函数非常精炼:

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)
      //注意这里的dispatch并不是一开始的store.dispatch,实际上是变化了的
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

核心是dispatch = compose(...chain)(store.dispatch),这句话是对于各个中间件的链式调用,其中compose的源代码:

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

调用上一个函数的执行结果给下一个函数。

实际上我们要写一个middleware的过程也非常简单,比如redux-trunk实际上就这点内容:

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;

redux 与路由

当然,我们首先声明react工具集的react-router并不一定必须搭配redux使用,只是redux另外有一个react-router-redux可以搭配react-router以及redux使用,效果非常好。

因为我们这部分并不是介绍react-router怎么使用的,关于react-router的用法请参考中文文档。

react-router的特性

  • 允许开发者通过JSX标签来声明路由,这一点让我们路由写起来十分友好,并且声明式路由的表述能力比较强。

  • 嵌套路由以及路由匹配:可以在指定的path中传递参数:

<Route path="/mail/:mailId" component={Mail} />

另外如果参数是可选的,我们通过括号包起来即可(:可选参数)。

  • 支持多种路由切换方式:我们知道现在的路由切换方式无外乎使用hashchange和pushState,前者有比较好的浏览器兼容性,但是却并不像一个真正的url,而后者给我们提供优雅的url体验,但是却需要服务端解决任意路径刷新的问题(服务端要自动重定向到首页)。

为什么需要react-router-redux

简单的说,react-router-redux让我们可以把路由也当作状态的一部分,并且可以使用redux的方式改变路由:直接调用dispatch:this.props.push(“/detail/”);,这样把路由也当作一个全局状态,路由状态也是应用状态的一部分,这样可能更有利于前端状态管理。

react-router-redux是需要配合react-router来使用的,并不能单独使用,在原本的项目中添加上react-router-redux也不复杂:

import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import { hashHistory } from 'react-router';

import ThunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';
import DevTools from './DevTools';

const finalCreateStore = compose(
  applyMiddleware(ThunkMiddleware,routerMiddleware(hashHistory)),
  DevTools.instrument()
)(createStore);

console.log("rootReducer",rootReducer);

const reducer = combineReducers({
  rootReducer,
  routing: routerReducer,
});

export default function configureStore(initialState) {
  const store = finalCreateStore(reducer, initialState);
  return store;
}

另外,上文提到的demoreact-router-redux-demo用了react-router和react-router-redux,当然也用到了redux的一些别的比较好的工作,比如redux-devtools,有兴趣的朋友可以点击这里

redux 与组件

这一部分讲述的是一种组件书写规范,并不是一些库或者架构,这些规范有利于我们在复杂的项目中组织页面,不至于混乱。

从布局的角度看,redux强调了三种不同的布局组件,Layouts,Views,Components:

  • Layouts: 指的是页面布局组件,描述了页面的基本结构,可以是无状态函数,一般就直接设置在最外层router的component参数中,并不承担和redux直接交互的功能。比如我项目中的Layouts组件:

const Frame = (props) =>
       <p className="frame">
           <p className="header">
               <Nav />
           </p>
           <p className="container">
               {props.children}
           </p>
       </p>;
  • Views组件,我认为这个组件是Components的高阶组件或者Components group,这一层是可以和redux进行交互并且处理数据的,我们可以将一个整体性功能的组件组放在一个Views下面(注:由于我给出的demo十分简单,因此Views层和Components层分的不是那么开)

  • Components组件,这是末级渲染组件,一般来说,这一层级的组件的数据通过props传入,不直接和redux单向数据流产生交互,可以是木偶般的无状态组件,或者是包含自身少量交互和状态的组件,这一层级的组件可以被大量复用。

总而言之,遵守这套规范并不是强制性的,但是项目一旦稍微复杂一些,这样做的好处就可以充分彰显出来。

redux 与表单

redux的单向数据流相对于双向数据绑定,在处理表单等问题上的确有点力不从心,但是幸运的是已经开源了有几个比较不错的插件:

  • redux-form-utils,好吧,这个插件的star数目非常少,但是他比较简单,源代码也比较短,只有200多行,所以这是一个值得我们看源码学习的插件(它的源码结构也非常简单,就是先定一个一个高阶组件,这个高阶组件可以给我们自己定义的表单组件传入新的props,定制组件,后一部分就是定义了一些action和reducer,负责在内容变化的时候通知改变状态树),但是缺憾就是这个插件没有对表单验证做工作,所以如果我们需要表单验证,还是需要自己做一些工作的。

    • 另外还有一地方,这个插件源代码写法中用到了::这种ES6的语法,这其实是一种在es6中class内部,使用babel-preset-stage-0即可使用的语法糖:::this.[functionName] 等价于 this.[functionName].bind(this, args?)

  • redux-form,这个插件功能复杂,代码完善,体量也非常庞大,可以参考文档进行使用,但是读懂源代码就是比较麻烦的事情了。不过这个插件需要在redux的应用的state下挂载一个节点,这个节点是不需要开发者自己来操控的,他唯一需要做的事情就是写一个submit函数即可。我在自己的demo中也把一个例子稍加改动搬了过来,感觉用起来比较舒服。

redux 性能优化

想要做到redux性能优化,我们首先就要知道redux的性能可能会在哪些地方受到影响,否则没有目标是没有办法性能优化的。

因为我也不是使用redux的老手,所以也并不能覆盖所有性能优化的点,我总结两点:

  • 有的时候,我们需要的数据格式会自带冗余,可以抽取出一些公共的部分从而缩减大小,比如我们需要的数据格式可能是这样的:

[
    {
        name:"Nike",
        title:"国家一级运动员","国家一级裁判员"
    }
    {
        name:"Jenny",
        title:"国家一级裁判员"
    }
    {
        name:"Mark",
        title:"国家一级运动员"
    }
]

这个时候实际上我们可以优化成这样:

[
    {
    "国家一级运动员":"Nike","Mark"
    "国家一级裁判员":"Jenny","Nike"
    }
]

这个时候,我们可以直接把后者当作store的格式,而我们用reselect这个库再转变成我们所要的格式,关于reselect怎么用上述链接有更详细的例子,在这里我就不过多介绍了。

  • 事实上,对于redux来说,每当store发生改变的时候,所有的connect都会重新计算,在一个大型应用中,浪费的时间可想而知,为了减少性能浪费,我们可以对connect中的selector做缓存。

上文提到的reselect库自带了缓存特性,我们可以通过比较参数来确定是否使用缓存,这里用了纯函数的特性。

reselect的缓存函数可以用户自定义


更多深入redux技术栈相关文章请关注PHP中文网!

相关标签: redux