Redux 是目前 React 系统中最常用的数据管理工具,它落实并发扬了 Flux 的数据单向流动模式,被实践证明为一种成熟可用的模式。

尽管承受着一些非议,Redux 在 React 数据管理界的地位仍然没有被取代。我听到的针对 Redux 最多的非议是它需要遵守的规则和步骤太多,让人们觉得束手束脚。然而个人觉得这正是 Redux 的意思所在,项目大了,如果整体数据流动不遵守规则,就容易乱。数据单向流动模式配合 Redux 规范仍然是一个可行的方案,当然,完全拥抱 mutable 的 Mobx 和 Vuex 也有他们的优势,关于他们之间的对比这里暂且不多做介绍。今天我们的重点是制作我们自己的 Redux,从而深入了解它的思想和原理。

Redux 的主要思想是让系统中的数据按照统一的规则流动,这样所有的操作就都有迹可循,任何 View 上对系统数据状态的更改都要通过 Action 被 Dispatch 到 Store,通过 Reducer 定义的逻辑去更改 State,然后再去更新View。

我们可以看到 Redux 的核心是 Store,我们就从它入手来写自己的 Redux。Store 需要有 State,需要提供 Dispatch 方法来接收 Action,需要根据使用者提供的 Reducer 响应 Action,还要能够在 State 变化的时候通知到外部的观察者。我们先来看看典型的 Action 和 Reducer:

Action:

  {
type: ADD_TODO,
text: 'Build my first Redux app'
} Reducer: function todoApp(state, action) {
switch(action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{ text: action.text, completed: false }
]
})
default:
return state
}
}

  

我们可以看到,Action 是普通的对象,它需要有一个 type 属性来指示要做的动作类型,Reducer 是一个方法,接收当前的 State 和要做的 Action,定义逻辑给出新的 State。

通过应用闭包和观察者模式,我们的 Redux 核心 createStore 方法并不难写:

export default function(reducer) {
let state = undefined
let listeners = [] function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1)
}
} function dispatch(action) {
state = reducer(state, action)
listeners.map(listener => listener())
} function getState() {
return state
} return { dispatch, subscribe, getState }
}

  有了核心部分,接下来是如何让 View 也就是 React components 拿到 state,能够在 state 更新的时候被更新,以及能够使用 dispatch 方法来发出 Action 指令。Redux 需要使用者将 App 包裹在 Redux 的 Provider 中,提供自己创建的 Store 作为属性,再让使用者用 connect 来包裹 component 为它拿到 state 数据和 dispatch 方法:

 const store = createStore(rootReducer)
ReactDOM.render((
<Provider store={store}>
<App />
</Provider>
), document.getElementById('root')) function mapStateToProps(state) {
return { count: state.count }
}
function mapDispatchToProps(dispatch) {
return {
increment: () => dispatch({type: 'increment'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

  

为了让 Component Tree 中任何位置的 Component 都能接收到信息,我们需要 React Context 的帮助,我们的 Provider 将使用者生成的 Store 注入 Context,connect 方法为 Component 拿到相应 Context 中的 Store 信息。以前的 React Context 在 Context 更新时不能确保系统中所有对 Context 的引用都得到更新,详细情况可以参考 https://reactjs.org/docs/legacy-context.html#updating-context ,因而当时实现 State 更新的方式比较复杂,每个 Component 都需要独立订阅 Store。新的 React Context API 解决了 Context 更新问题,我们写起来也就容易了很多,只要在 Provider 中订阅更新就可以了:

Provider:

 import React, { useState, useEffect } from 'react'

    export const MyReduxContext = React.createContext()

    export default function(props) {
const { store } = props
const [ provider, setProvider ] = useState({state: store.getState(), dispatch: store.dispatch}) useEffect(() => {
return store.subscribe(() => setProvider({state: store.getState(), dispatch: store.dispatch}))
}) return (
<MyReduxContext.Provider value={provider}>
{ props.children }
</MyReduxContext.Provider>
)
}

Connect:

   import React from 'react'
import { MyReduxContext } from './Provider'; export default function(mapStateToProps, mapDispatchToProps) {
return function(WrappedComponent) {
return function(props) {
return (
<MyReduxContext.Consumer>
{
({ state, dispatch }) => {
return <WrappedComponent {...mapStateToProps(state, props)} {...mapDispatchToProps(dispatch, props)} />
}
}
</MyReduxContext.Consumer>
)
}
}
}

  

这里我们用到了 React Hooks 的 useState 和 useEffect,注意 state 和 dispatch 都需要被放在 Context 中传递。connect 方法的两个参数 mapStateToProps 和 mapDispatchToProps,正如他们的名字,是用来把 state 和 dispatch 转化成 Component 需要的样子。

Connect 方法是现在 Redux 提供的连接 Component 的方式,然而现在我们有了 React Hooks,是否有办法自定义 Hooks 来连接呢?借用 useContext 我们可以很简单地实现:

   import { useContext } from 'react'
import { MyReduxContext } from './Provider' export default function useStore() {
const { state, dispatch } = useContext(MyReduxContext)
return [ state, dispatch ]
}

  使用时也很简单:

 const [ state, dispatch ] = useStore()
const { count } = mapStateToProps(state)
const { increment, reset } = mapDispatchToProps(dispatch)

  

到此为止我们的 Redux 主流程工具已经完成,接下来我们来看 Redux 的重要概念 Middleware。Middleware是从 Redux 的流程规则中应运而生的,既然所有的 Action 都要流经 Store,那如果我们在 Store 中设置一个可插入的装置,就可以让人们根据需要加入各种管道方法,最常见的有记录日志,报告错误,异步请求处理和路由等。

这个装置需要将 middleware 们一个个插入到 Store 的 Dispatch 管道中,让 Action 一个个地流经他们,最后才被真正的 Dispatch 给到 Reducer。为了灵活可靠地完成这个任务,我们的 Redux 需要做很多让人头晕的工作,准备好迎接挑战了吗?

首先,为了将 middleware 们组合起来,我们需要将他们的流程逻辑嵌套在一起成为一个新的 Dispatch 方法,这就需要用到 Compose 方法。

  d = compose(a, b, c)
d(x) === a(b(c(x)))

我们的 middleware 如果长这样:

  function middlewareWrapper(nextDispatch) {
return function(action) {
some logic ...
nextDispatch(action);
}
}

  

那么想象一下 compose(...middlewares)(store.dispatch) 会得到什么?带入上面的 compose 方法的定义仔细想一下。没错,就是 middleware 们内层函数的逻辑嵌套,等待被执行的一个新的 Dispatch 方法。

Compose 并不难实现,我们可以自己简单写一下。

 function compose(...funcs) {
return function(...parameters) {
let returned = null
for (let i = funcs.length - 1; i >= 0; i --) {
let func = funcs[i]
if (i === 0) {
returned = func(...parameters)
} else {
returned = func(returned)
}
}
return returned
}
}

  知道了如何嵌套 middleware,我们就可以着手改造我们的 createStore,将 middlewares 作为额外参数传入。

export default function createStore(reducer, middlewares) {
let listeners = []
function subscribe(listener) { }
function dispatch(action) {
state = reducer(state, action)
listeners.map(listener => listener())
}
function getState() { return state }
------
const storeContext = {
getState: getState,
dispatch: (action) => dispatch(action)
} const chain = middlewares.map(middleware => middleware(storeContext)) dispatch = compose(...chain)(store.dispatch)
------
return { dispatch, subscribe, getState }
}

  这里将 storeContext 传给 middleware 是希望它们能够拿到 store 的 state 和 dispatch,注意这里的 dispatch 指向的是嵌套后的新的 dispatch,既然多了一步 storeContext 的封装,middleware 们也就又多了一层包裹,最终变成这样:

 function storeWrapper(store) {
function middlewareWrapper(nextDispatch) {
return function(action) {
some logic ...
nextDispatch(action)
}
}
}

  

到此为止 middleware 的内部机制我们已经做好了,可是 Redux 传入 middlewares 的方式并不是这样的,为了使用起来更灵活,Redux 提供了 applyMiddleware 方法,它接收 middlewares 作为参数,返回一个接收 createStore 参数的包装方法,将 createStore 包装为一个新的 createStore 方法,新的方法给出的 dispatch 方法就是嵌套好 middlewares 的新的 dispatch。

因而我们的 Provider 处可以写做:

 <Provider store={applyMiddleware(loggerMiddleware, thunkMiddleware)(createStore)(counterReducer)}>

  而从另一个角度,原有的 createStore 方法也支持接收 applyMiddleware 的返回值作为 enhancer 参数,而且还需要有另一个参数 preloadedState 作为 state 的初始值,最终就变成了:

 <Provider store={createStore(counterReducer, preloadedState, applyMiddleware(loggerMiddleware, thunkMiddleware))}>

  我们的 createStore 方法最终是这样的:

 export default function createStore(reducer, preloadedState, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer, preloadedState)
}
------
let state = preloadedState
let listeners = []
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1)
}
}
function dispatch(action) {
if (typeof action !== 'object') {
return;
}
state = reducer(state, action)
listeners.map(listener => listener())
}
function getState() {
return state
}
return { dispatch, subscribe, getState }
}

  applyMiddleware 是这样的:

 export default function applyMiddleware(...middlewares) {

        return function(createStore) {

            return function(reducer, preloadedState, enhancer) {
const store = createStore(reducer, preloadedState, enhancer)
// this dispatch used in storeConotext should point to the final dispatch,
// or else middlewares will use the real store's dispatch which skips middlewares
let dispatch = store.dispatch const storeContext = {
getState: store.getState,
dispatch: (action) => dispatch(action)
} const chain = middlewares.map(middleware => middleware(storeContext)) dispatch = compose(...chain)(store.dispatch) // purpose of applyMiddleware is to make a enhanced dispatch which can walk through middlewares
return {
...store,
dispatch
}
}
}
}

  

这里多次应用了类似 React HOC 的包装思想,让整个设计灵活巧妙,但是从某种程度上增加了理解的难度,让我想到 Redux Saga 的巧妙设计和最初的难以理解。

看这部分时如果有细节不清楚还可以参考 Redux 官方对于 applyMiddleware 的介绍。

有了 applyMiddleware,接下来让我们写一下两个最常用的 middleware —— logger 和 thunk。

初步的 logger 非常简单,就是增加一个 console 的逻辑:

 export default function loggerMiddleware(storeContext) {
return function(nextDispatch) {
return function(action) {
nextDispatch(action)
console.log(storeContext.getState())
}
}
}

  

注意,我们把 console.log 放在 nextDispatch 的后面,是希望它拿到此次 dispatch 之后的 state,当然也可以将之前的记录下来在后面一起 console 出来,就更接近真正的 Redux logger 的做法了。

Redux thunk 是要让 dispatch 接受 Action Creator 作为参数,详细内容可以参考 https://github.com/reduxjs/redux-thunk#why-do-i-need-this ,它的实现也很简单,只要做一个类型判断就可以了:

export default function thunkMiddleware(storeContext) {
return function(nextDispatch) {
return function(action) {
if (typeof action === 'function') {
const { dispatch, getState } = storeContext
action(dispatch, getState)
nextDispatch(action)
} else {
nextDispatch(action)
}
}
}
}

  

React Hooks 实现react-redux的更多相关文章

  1. React Hooks vs React Class vs React Function All In One

    React Hooks vs React Class vs React Function All In One React Component Types React Hooks Component ...

  2. React Hooks新特性学习随笔

    React Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 前言 本篇主要以讲几个常用的api为主. 1.u ...

  3. 关于React Hooks,你不得不知的事

    React Hooks是React 16.8发布以来最吸引人的特性之一.在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks.wikipedia是这样给hook下定义的: In c ...

  4. React Hooks简单业务场景实战(非源码解读)

    前言 React Hooks 是React 16.7.0-alpha 版本推出的新特性.从 16.8.0 开始,React更稳定的支持了这一新特性. 它可以让你在不编写 class 的情况下使用 st ...

  5. 蒲公英 · JELLY技术周刊 Vol.17: 90 行代码实现 React Hooks

    蒲公英 · JELLY技术周刊 Vol.17 React Hooks 相信大家都不陌生,自被设计出以来就备受好评,在很多场景中都有极高的使用率,其中原理更是很多大厂面试中的必考题,很多朋友都能够如数家 ...

  6. 使用React Hooks新特性useReducer、useContext替代传统Redux高阶组件案例

    当我们使用redux进行数据管理的时候,一般都是在根组件通过Provider的方式引入store,然后在每个子组件中,通过connect的方式使用高阶组件进行连接,这样造成的一个问题是,大量的高阶组件 ...

  7. React Hooks实现异步请求实例—useReducer、useContext和useEffect代替Redux方案

    本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据写的一个案例.注意,本文假设了:1.你已经初步了解hooks的含义了,如果不了解还请移步官方文档.(其实有过翻译的想法, ...

  8. React Hooks +React Context vs Redux

    React Hooks +React Context vs Redux https://blog.logrocket.com/use-hooks-and-context-not-react-and-r ...

  9. 初探React Hooks & SSR改造

    Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...

  10. react hooks 全面转换攻略(三) 全局存储解决方案

    针对 react hooks 的新版本解决方案 一.redux维持原方案 若想要无缝使用原来的 redux,和其配套的中间件 promise,thunk,saga 等等的话 可以使用 redux-re ...

随机推荐

  1. 上周 GitHub 热点速览 vol.09:手撕 LeetCode 一日 star 破两千

    作者:HelloGitHub-小鱼干 摘要(用于 公众号/博客园等地方):上周 GitHub 趋势榜相较上上周就如同前故事一般,跌到不行,无论是新晋开源小项,还是坚挺老项目,Star 增长量都不如之前 ...

  2. 微信小程序支付到第三方商户账号

    使用场景:合作商家使用本公司小程序开店,要求支付金额直接到合作商家的公司微信账户; 使用要求:合作商家需提供微信支付关联,商户号,商户API密钥,API证书(该证书只用作退款功能,不开发退款可以不用) ...

  3. ajax+lazyload时lazyload失效问题及解决

    最近写公司的项目的时候遇到一个关于图片加载的问题,所做的页面是一个商城的商品列表页,其中需要显示商品图片,名称等信息,因为商品列表可能会很长,所以其中图片需要滑到可以显示的区域再进行加载. 首先我的图 ...

  4. Canvas 使用及应用

    Canvas canvas 是 HTML5 当中我最喜欢的所有新特性中我最喜欢的一个标签了.因为它太强大了,各种有意思的特效都可以实现. 1. canvas 的基本使用方法 - 它是一个行内块元素 - ...

  5. 前端面试题-url、href、src

    一.URL的概念 统一资源定位符(或称统一资源定位器/定位地址.URL地址等,英语:Uniform Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址).如同在网络上的门 ...

  6. 进程,线程,Event Loop(事件循环),Web Worker

    线程,是程序执行流的最小单位.线程可与同属一个进程的其他线程共享所拥有的全部资源,同一进程中的多个线程之间可以并发执行.线程有就绪,阻塞,运行三种基本状态. 阮一峰大神针对进程和线程的类比,很是形象: ...

  7. 详细解析kafka之kafka分区和副本

    本篇主要介绍kafka的分区和副本,因为这两者是有些关联的,所以就放在一起来讲了,后面顺便会给出一些对应的配置以及具体的实现代码,以供参考~ 1.kafka分区机制 分区机制是kafka实现高吞吐的秘 ...

  8. 什么是Servlet?Servlet的周期和方法

    1.什么是Servlet?  Servlet是运行在web服务器或应用服务器的程序,它是作为来自web浏览器或其他http客户端的请求和HTTP服务器上的数据库或应用程序之间的中间层! 2.Servl ...

  9. js中(event)事件对象

    事件对象 • 什么是事件对象? • 就是当你触发了一个事件以后,对该事件的一些描述信息 • 例如: ° 你触发一个点击事件的时候,你点在哪个位置了,坐标是多少 ° 你触发一个键盘事件的时候,你按的是哪 ...

  10. yolo3各部分代码详解(超详细)

    0.摘要 最近一段时间在学习yolo3,看了很多博客,理解了一些理论知识,但是学起来还是有些吃力,之后看了源码,才有了更进一步的理解.在这里,我不在赘述网络方面的代码,网络方面的代码比较容易理解,下面 ...