首頁  >  文章  >  web前端  >  深入redux技術棧

深入redux技術棧

高洛峰
高洛峰原創
2017-02-18 14:24:391375瀏覽

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

reee

每一個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中文網!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn