关于javascript:从-Redux-说起到手写再到状态管理

24次阅读

共计 14626 个字符,预计需要花费 37 分钟才能阅读完成。

先说论断

  1. Redux 是状态治理库,也是一种架构
  2. Redux 与 React 无关,但它是为了解决 React 组件中状态无奈共享而出的一种解决方案
  3. 单纯的 Redux 只是一个状态机,store 中寄存了所有的状态 state,要想扭转外面的状态 state,只能 dispatch 一个动作
  4. 收回去的 action 须要用 reducer 来解决,传入 state 和 action,返回新的 state
  5. subscribe 办法能够注册回调办法,当 dispatch action 的时候会执行外面的回调
  6. Redux 其实是一个公布订阅模式
  7. Redux 反对 enhancer,enhancer 其实就是一个装璜器模式,传入以后的 createStore,返回一个加强的 createStore
  8. Redux 应用 applyMiddleware 函数反对中间件,它的返回值其实就是一个 enhancer
  9. Redux 的中间件也是一个装璜器模式,传入以后的 dispatch,返回一个加强了的 dispatch
  10. 单纯的 Redux 是没有 View 层的

为什么呈现 Redux?

咱们默认应用 React 技术栈,当页面少且简略时,齐全没必要应用 Redux。Redux 的呈现,是为了应答简单组件的状况。即当组件简单到三层甚至四层时(如下图),组件 4 想扭转组件 1 的状态

依照 React 的做法,状态晋升,将状态晋升至同一父组件(在图中为祖父组件)。但层级一多,根组件要治理的 state 就很多了,不方便管理。

所以当初有了 context(React 0.14 确定引入),通过 context 能实现”远房组件“的数据共享。但它也有毛病,应用 context 意味着所有的组件都能够批改 context 外面的状态,就像谁都能够批改共享状态一样,导致程序运行的不可预测,这不是咱们想要的

facebook 提出了 Flux 解决方案,它引入了单向数据流的概念(没错,React 没有单向数据流的概念,Redux 是集成了 Flux 的单向数据流理念),架构如下图所示:

这里不表 Flux。简略了解,在 Flux 架构中,View 要通过 Action(动作)告诉 Dispatcher(派发器),Dispatcher 来批改 Store,Store 再批改 View

Flux 的问题或者说毛病在哪?

store 之间存在依赖关系、难以进行服务器端渲染、stores 混淆了逻辑和状态

笔者在学习的 React 技术栈时是 2018 年,那是未然风行 React + Redux 的解决方案,Flux 曾经被淘汰了,理解 Flux 是为了引出 Redux

Redux 的呈现

Redux 次要解决状态共享问题

官网:Redux 是 JavaScript 状态容器,它提供可预测的状态治理

它的作者是 Dan Abramov

其架构为:

能够看得出,Redux 只是一个状态机,没有 View 层。其过程能够这样形容:

  • 本人写一个 reducer(纯函数,示意做什么动作会返回什么数据)
  • 本人写一个 initState(store 初始值,可写可不写)
  • 通过 createStore 生成 store,此变量蕴含了三个重要的属性

    • store.getState:失去惟一值(应用了闭包老哥)
    • store.dispatch:动作行为(扭转 store 中数据的惟一指定属性)
    • store.subscribe:订阅(公布订阅模式)
  • 通过 store.dispatch 派发一个 action
  • reducer 解决 action 返回一个新的 store
  • 如果你订阅过,当数据扭转时,你会收到告诉

依照行为过程,咱们可手写一个 Redux,下文在表,先说特点

三大准则

  • 繁多数据源

    • 整个利用的 全局 state 被贮存在一棵 object tree 中,并且这个 object tree 只存在于惟一一个 store 中
  • State 是只读的

    • 惟一扭转 state 的办法就是触发 action,action 是一个用于形容已产生工夫的一般对象
  • 应用纯函数来执行批改

    • 为了形容 action 如何扭转 state tree,你须要编写纯的 reducers

三大准则是为了更好地开发,依照单向数据流的理念,行为变得可回溯

让咱们入手写一个 Redux 吧

手写 redux

依照行为过程和准则,咱们要防止数据的随便批改、行为的可回溯等问题

根底版:23 行代码让你应用 redux

export const createStore = (reducer, initState) => {
  let state = initState
  let listeners = []

  const subscribe = (fn) => {listeners.push(fn)
  }

  const dispatch = (action) => {state = reducer(state, action)
    listeners.forEach((fn) => fn())
  }

  const getState = () => {return state}

  return {
    getState,
    dispatch,
    subscribe,
  }
}

搞个测试用例

import {createStore} from '../redux/index.js'

const initState = {count: 0,}

const reducer = (state, action) => {switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(reducer, initState)

store.subscribe(() => {let state = store.getState()
  console.log('state', state)
})

store.dispatch({type: 'INCREMENT',})

PS:俺是在 node 中应用 ES6 模块,须要降级 Node 版本至 13.2.0

第二版:难点冲破:中间件

一般的 Redux 只能做最根底地依据动作返回数据,dispatch 只是一个取数据的命令,例如:

dispatch({type: 'INCREMENT',})
// store 中的 count + 1

但在开发中,咱们有时候要查看日志、异步调用、记录日常等

怎么办,做成插件

在 Redux 中,相似的概念叫中间件

Redux 的 createStore 共有三个参数

createStore([reducer], [initial state], [enhancer]);

第三个参数为 enhancer,意为增强器。它的作用就是代替一般的 createStore,转变成为附加上中间件的 createStore。打几个比如:

  • 托尼·斯塔克原本是一个一般有钱人,加上增强器(盔甲)后,成了钢铁侠
  • 地方下发一笔救灾款,加上增强器(大小官员的打点)后,到灾民手上的钱只有一丢丢
  • 路飞用武装色打人,武装色就是一个中间件

enhancer 要做的就是:货色还是那个货色,只是通过了一些工序,增强了它 。这些工序由 applyMiddleware 函数实现。依照行业术语,它是一个 装璜器模式。它的写法大抵是:

applyMiddleware(...middlewares)
// 联合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares))

所以咱们须要先对 createStore 进行革新,判断当有 enhancer 时,咱们需传值给中间件

export const createStore = (reducer, initState, enhancer) => {if (enhancer) {const newCreateStore = enhancer(createStore)
        return newCreateStore(reducer, initState)
    }

    let state = initState;
    let listeners = [];
    ...
}

如果有 enhancer 的话,先传入 createStore 函数,生成的 newCreateStore 和原来的 createStore 一样,会依据 reducer, initState 生成 store。可简化为:

if (enhancer) {return enhancer(createStore)(reducer, initState)
}

PS:为什么要写成这样,因为 redux 是依照函数式写法来写的

为什么 createStore 能够被传值,因为函数也是对象,也能够作为参数传递(老铁闭包了)

这样咱们的 applyMiddleware 天然就明确了

const applyMiddleware = (...middlewares) => {return (oldCreateStore) => {return (reducer, initState) => {const store = oldCreateStore(reducer, initState)
            ...
        }
    }
}

这里的 store 示意的是一般版中的 store,接下来咱们要加强 store 中的属性

我愿称之为:五行代码让女人为我花了 18 万

export const applyMiddleware = (...middlewares) => {return (oldCreateStore) => {return (reducer, initState) => {const store = oldCreateStore(reducer, initState)
      // 以下为新增
      const chain = middlewares.map((middleware) => middleware(store))
      // 取得老 dispatch
      let dispatch = store.dispatch
      chain.reverse().map((middleware) => {
        // 给每个中间件传入原派发器,赋值中间件革新后的 dispatch
        dispatch = middleware(dispatch)
      })
      // 赋值给 store 上的 dispatch
      store.dispatch = dispatch
      return store
    }
  }
}

当初写几个中间件来测试一下

// 记录日志
export const loggerMiddleware = (store) => (next) => (action) => {console.log('this.state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

// 记录异样
export const exceptionMiddleware = (store) => (next) => (action) => {
  try {next(action)
  } catch (error) {console.log('错误报告', error)
  }
}

// 工夫戳
export const timeMiddleware = (store) => (next) => (action) => {console.log('time', new Date().getTime())
  next(action)
}

引入我的项目中,并运行

import {createStore, applyMiddleware} from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'

const initState = {count: 0,}

const reducer = (state, action) => {switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

store.subscribe(() => {let state = store.getState()
  console.log('state', state)
})

store.dispatch({type: 'INCREMENT',})

运行发现曾经实现了 redux 最重要的性能——中间件

来剖析下中间件的函数式编程,以 loggerMiddleware 为例:

export const loggerMiddleware = (store) => (next) => (action) => {console.log('this.state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

在 applyMiddleware 源码中,

const chain = middlewares.map((middleware) => middleware(store))

相当于给每个中间件传值一般版的 store

let dispatch = store.dispatch
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)))

相当于给每个中间件在传入 store.dispatch,也就是 next,原 dispatch = next。这个时候的中间件曾经本成品了,代码中的 (action) => {...} 就是函数 const dispatch = (action) => {}。当你执行 dispatch({type: XXX}) 时执行中间件这段(action) => {...}

PS:柯里化一开始比拟难了解,用多习惯就缓缓能懂

第三版:构造复杂化与拆分

中间件了解起来或者有些简单,先看看其余的概念换换思路

一个利用做大后,单靠一个 JavaScript 文件来保护代码显然是不迷信的,在 Redux 中,为防止这类状况,它提供了 combineReducers 来整个多个 reducer,应用办法如:

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

combinReducers 中传入一个对象,什么样的 state 对应什么样的 reducer。这就好了,那么 combinReducers 怎么实现呢?因为比较简单,不做多剖析,间接上源码:

export const combinReducers = (...reducers) => {
  // 拿到 counter、info
  const reducerKey = Object.keys(reducers)
  // combinReducers 合并的是 reducer,返回的还是一个 reducer,所以返回一样的传参
  return (state = {}, action) => {const nextState = {}
    // 循环 reducerKey,什么样的 state 对应什么样的 reducer
    for (let i = 0; i < reducerKey.length; i++) {const key = reducerKey[i]
      const reducer = reducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
    }
    return nextState
  }
}

同级目录下新建一个 reducer 文件夹,并新建 reducer.jsinfo.jsindex.js

// reducer.js
export default (state, action) => {switch (action.type) {
    case 'INCREMENT':
      return {count: state.count + 1,}
    case 'DECREMENT': {
      return {count: state.count - 1,}
    }
    default:
      return state
  }
}
// info.js
export default (state, action) => {switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

合并导出

import counterReducer from './counter.js'
import infoReducer from './info.js'

export {counterReducer, infoReducer}

咱们当初测试一下

import {createStore, applyMiddleware, combinReducers} from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import {counterReducer, infoReducer} from './reducer/index.js'

const initState = {
  counter: {count: 0,},
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

store.dispatch({type: 'INCREMENT',})

combinReducers 也实现了

既然拆分了 reducer,那么 state 是否也能拆分,并且它是否须要传,在咱们平时的写法中,个别都不传 state。这里须要两点革新,一是每个 reducer 中蕴含了它的 state 和 reducer;二是革新 createStore,让 initState 变得可传可不传,以及初始化数据

// counter.js 中写入对应的 state 和 reducer
let initState = {
  counter: {count: 0,},
}

export default (state, action) => {if (!state) {state = initState}
  switch (action.type) {
    case 'INCREMENT':
      return {count: state.count + 1,}
    case 'DECREMENT': {
      return {count: state.count - 1,}
    }
    default:
      return state
  }
}
// info.js
let initState = {
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

export default (state, action) => {if (!state) {state = initState}
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

革新 createStore

export const createStore = (reducer, initState, enhancer) => {if (typeof initState === 'function') {
        enhancer = initState;
        initState = undefined
    }
    ...
    const getState = () => {return state}
    // 用一个不匹配任何动作来初始化 store
    dispatch({type: Symbol() })

    return {
        getState,
        dispatch,
        subscribe
    }
}

主文件中

import {createStore, applyMiddleware, combinReducers} from './redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import {counterReducer, infoReducer} from './reducer/index.js'

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

const store = createStore(
  reducer,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

console.dir(store.getState())

到此为止,咱们曾经实现了一个七七八八的 redux 了

完整体的 Redux

退订

const subscribe = (fn) => {listeners.push(fn)
  return () => {const index = listeners.indexOf(listener)
    listeners.splice(index, 1)
  }
}

中间件拿到的 store

当初的中间件能拿到残缺的 store,他甚至能够批改咱们的 subscribe 办法。依照 最小凋谢策略,咱们只用给 getState 即可,批改下 applyMiddleware 中给中间件传的 store

// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = {getState: store.getState}
const chain = middlewares.map((middleware) => middleware(simpleStore))

compose

在咱们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),成果是:

const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map((middleware) => {dispatch = middleware(dispatch)
})

Redux 提供了一个 compose,如下

const compose = (...funcs) => {if (funcs.length === 0) {return (args) => args
  }
  if (funcs.length === 1) {return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

2 行代码 replaceReducer

替换以后的 reudcer,应用场景:

  • 代码宰割
  • 动静加载
  • 实时 reloading 机制
const replaceReducer = (nextReducer) => {
  reducer = nextReducer
  // 刷新一次,播送 reducer 曾经替换,也同样把默认值换成新的 reducer
  dispatch({type: Symbol() })
}

bindActionCreators

bindActionCreators 是做什么的,他通过闭包,把 dispatch 和 actionCreator 暗藏起来,让其余中央感知不到 redux 的存在。个别与 react-redux 的 connect 联合

这里间接贴源码实现:

const bindActionCreator = (actionCreator, dispatch) => {return function () {return dispatch(actionCreator.apply(this, arguments))
  }
}

export const bindActionCreators = (actionCreators, dispatch) => {if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

以上,咱们就曾经实现了 Redux 中所有的代码。大体上这里 100 多行的代码就是 Redux 的全副,真 Redux 无非是加了些正文和参数校验

总结

咱们把与 Redux 相干的名词列出来,梳理它是做什么的

  • createStore

    • 创立 store 对象,蕴含 getState、dispatch、subscribe、replaceReducer
  • reducer

    • 纯函数,承受旧的 state、action,生成新的 state
  • action

    • 动作,是一个对象,必须包含 type 字段,示意 view 发出通知通知 store 要扭转
  • dispatch

    • 派发,触发 action,生成新的 state。是 view 收回 action 的惟一办法
  • subscribe

    • 订阅,只有订阅了,当派发时,会执行订阅函数
  • combineReducers

    • 合并 reducer 成一个 reducer
  • replaceReudcer

    • 代替 reducer 的函数
  • middleware

    • 中间件,扩大 dispatch 函数

砖家已经画过一张对于 Redux 的流程图

换种思考形式了解

咱们说过,Redux 只是一个状态治理库,它是由数据来驱动,发动 action,会引发 reducer 的数据更新,从而更新到最新的 store

与 React 联合

拿着刚做好的 Redux,放到 React 中,试试什么叫 Redux + React 汇合,留神,这里咱们先不应用 React-Redux,单拿这两个联合

先创立我的项目

npx create-react-app demo-5-react

引入手写的 redux 库

App.js 中引入 createStore,并写好初始数据和 reducer,在 useEffect 中监听数据,监听好之后当发动一个 action 时,数据就会扭转,看代码:

import React, {useEffect, useState} from 'react'
import {createStore} from './redux'
import './App.css'

const initState = {count: 0,}

const reducer = (state, action) => {switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(reducer, initState)

function App() {const [count, setCount] = useState(store.getState().count)

  useEffect(() => {const unsubscribe = store.subscribe(() => {setCount(store.getState().count)
    })
    return () => {if (unsubscribe) {unsubscribe()
      }
    }
  }, [])

  const onHandle = () => {
    store.dispatch({type: 'INCREMENT',})
    console.log('store', store.getState().count)
  }
  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onHandle}>add</button>
    </div>
  )
}

export default App

点击 button 后,数据跟着扭转

PS:尽管咱们能够用这种形式订阅 store 和扭转数据,然而订阅的代码反复过多,咱们能够用高阶组件将他提取进来。这也是 React-Redux 所做的事件

与原生 JS+HTML 联合

咱们说过,Redux 是个独立于 Redux 的存在,它不仅可在 Redux 充当数据管理器,还能够在原生 JS + HTML 中充当起职位

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <div id="count">1</div>
      <button id="btn">add</button>
    </div>
    <script type="module">
      import {createStore} from './redux/index.js'

      const initState = {count: 0,}

      const reducer = (state, action) => {switch (action.type) {
          case 'INCREMENT':
            return {
              ...state,
              count: state.count + 1,
            }
          case 'DECREMENT':
            return {
              ...state,
              count: state.count - 1,
            }
          default:
            return state
        }
      }

      const store = createStore(reducer, initState)

      let count = document.getElementById('count')
      let add = document.getElementById('btn')
      add.onclick = function () {
        store.dispatch({type: 'INCREMENT',})
      }
      // 渲染视图
      function render() {count.innerHTML = store.getState().count
      }
      render()
      // 监听数据
      store.subscribe(() => {let state = store.getState()
        console.log('state', state)
        render()})
    </script>
  </body>
</html>

成果如下:

状态生态

咱们从 Flux 说到 Redux,再从 Redux 说了各种中间件,其中 React-saga 就是为解决异步行为而生的中间件,它次要采纳 Generator(生成器)概念,比起 React-thunk 和 React-promise,它没有像其余两者将异步行为放在 action creator 上,而是把所有的异步操作看成“线程”,通过 action 触发它,当操作实现后再次收回 action 作为输入

function* helloWorldGenerator() {
  yield 'hello'
  yield 'world'
  yield 'ending'
}

const helloWorld = helloWorldGenerator()

hewlloWorld.next() // { value: 'hello', done: false}
hewlloWorld.next() // { value: 'world', done: false}
hewlloWorld.next() // { value: 'ending', done: true}
hewlloWorld.next() // { value: undefined, done: true}

简略来说:遇到 yield 表达式,就暂停执行前面的操作,并将紧跟 yield 前面的那个表达式的值,作为返回值 value,等着下一个调用 next 办法,再持续往下执行

Dva

Dva 是什么?

官网:Dva 首先是一个基于 Redux + Redux-saga 的数据流计划。为了简化开发体验,Dva 还额定内置了 react-router 和 fetch,所以能够了解为一个轻量级的利用框架

简略来说,它是整合了当初最风行的数据流计划,即一个 React 技术栈:

dva = React-Router + Redux + Redux-saga + React-Redux

它的数据流图为:

view dispatch 一个动作,扭转 state(即 store),state 与 view 绑定,响应 view

其余不表,可去 Dva 官网查看,这里讲讲 Model,它蕴含了 5 个属性

  • namespace

    • model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不反对通过 . 的形式创立多层命名空间
  • state

    • 初始值
  • reducers

    • 纯函数,以 key/value 格局定义 reducer。用于解决同步擦做,惟一能够批改 state 的中央,由 action 触发
    • 格局为:(state, action) => newState[(state, action) => newState, enhancer]
  • effects

    • 解决异步操作和业务逻辑,以 key/value 格局定义 effect
    • 不间接批改 state。由 action 触发
    • call:执行异步操作
    • put:收回一个 Action,相似于 dispatch
  • subscriptions

    • 订阅
    • app.start() 时被执行,数据源能够是以后的工夫、服务器的 websocket 链接、keyboard 输出、history 路由变动、geolocation 变动等等

Mobx

View 通过订阅也好,监听也好,不同的框架有不同的技术,总之 store 变动,view 也跟着变动

Mobx 应用的是响应式数据流计划。后续会独自写一篇,此篇太长,先不写

补充:单向数据流

先介绍 React 中数据传递,即通信问题

  • 向子组件发消息
  • 向父组件发消息
  • 向其余组件发消息

React 只提供了一种通信形式:传参。

即父传值给子,子不能批改父传的数据,props 具备不可修改性。子组件想把数据传给父组件怎么办?通过 props 中的事件来传值告诉父组件

仓库地址:https://github.com/johanazhu/…

本文参加了 SegmentFault 思否征文「如何“反杀”面试官?」,欢送正在浏览的你也退出。

正文完
 0