先说论断
- Redux 是状态治理库,也是一种架构
- Redux 与 React 无关,但它是为了解决 React 组件中状态无奈共享而出的一种解决方案
- 单纯的 Redux 只是一个状态机, store 中寄存了所有的状态 state,要想扭转外面的状态 state,只能 dispatch 一个动作
- 收回去的 action 须要用 reducer 来解决,传入 state 和 action,返回新的 state
- subscribe 办法能够注册回调办法,当 dispatch action 的时候会执行外面的回调
- Redux 其实是一个公布订阅模式
- Redux 反对 enhancer,enhancer 其实就是一个装璜器模式,传入以后的 createStore,返回一个加强的 createStore
- Redux 应用 applyMiddleware 函数反对中间件,它的返回值其实就是一个 enhancer
- Redux 的中间件也是一个装璜器模式,传入以后的 dispatch,返回一个加强了的 dispatch
- 单纯的 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.dispatchchain.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.js
、info.js
、index.js
// reducer.jsexport default (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1, } case 'DECREMENT': { return { count: state.count - 1, } } default: return state }}
// info.jsexport 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 和 reducerlet 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.jslet 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.dispatchchain.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 上的属性,只能用字符串,不反对通过
.
的形式创立多层命名空间
- model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不反对通过
state
- 初始值
reducers
- 纯函数,以 key/value 格局定义 reducer。用于解决同步擦做,惟一能够批改
state
的中央,由action
触发 - 格局为:
(state, action) => newState
或[(state, action) => newState, enhancer]
- 纯函数,以 key/value 格局定义 reducer。用于解决同步擦做,惟一能够批改
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 思否征文「如何“反杀”面试官?」,欢送正在浏览的你也退出。