React Hooks 实现react-redux
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的更多相关文章
- 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 ...
- React Hooks新特性学习随笔
React Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 前言 本篇主要以讲几个常用的api为主. 1.u ...
- 关于React Hooks,你不得不知的事
React Hooks是React 16.8发布以来最吸引人的特性之一.在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks.wikipedia是这样给hook下定义的: In c ...
- React Hooks简单业务场景实战(非源码解读)
前言 React Hooks 是React 16.7.0-alpha 版本推出的新特性.从 16.8.0 开始,React更稳定的支持了这一新特性. 它可以让你在不编写 class 的情况下使用 st ...
- 蒲公英 · JELLY技术周刊 Vol.17: 90 行代码实现 React Hooks
蒲公英 · JELLY技术周刊 Vol.17 React Hooks 相信大家都不陌生,自被设计出以来就备受好评,在很多场景中都有极高的使用率,其中原理更是很多大厂面试中的必考题,很多朋友都能够如数家 ...
- 使用React Hooks新特性useReducer、useContext替代传统Redux高阶组件案例
当我们使用redux进行数据管理的时候,一般都是在根组件通过Provider的方式引入store,然后在每个子组件中,通过connect的方式使用高阶组件进行连接,这样造成的一个问题是,大量的高阶组件 ...
- React Hooks实现异步请求实例—useReducer、useContext和useEffect代替Redux方案
本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据写的一个案例.注意,本文假设了:1.你已经初步了解hooks的含义了,如果不了解还请移步官方文档.(其实有过翻译的想法, ...
- React Hooks +React Context vs Redux
React Hooks +React Context vs Redux https://blog.logrocket.com/use-hooks-and-context-not-react-and-r ...
- 初探React Hooks & SSR改造
Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...
- react hooks 全面转换攻略(三) 全局存储解决方案
针对 react hooks 的新版本解决方案 一.redux维持原方案 若想要无缝使用原来的 redux,和其配套的中间件 promise,thunk,saga 等等的话 可以使用 redux-re ...
随机推荐
- 上周 GitHub 热点速览 vol.09:手撕 LeetCode 一日 star 破两千
作者:HelloGitHub-小鱼干 摘要(用于 公众号/博客园等地方):上周 GitHub 趋势榜相较上上周就如同前故事一般,跌到不行,无论是新晋开源小项,还是坚挺老项目,Star 增长量都不如之前 ...
- 微信小程序支付到第三方商户账号
使用场景:合作商家使用本公司小程序开店,要求支付金额直接到合作商家的公司微信账户; 使用要求:合作商家需提供微信支付关联,商户号,商户API密钥,API证书(该证书只用作退款功能,不开发退款可以不用) ...
- ajax+lazyload时lazyload失效问题及解决
最近写公司的项目的时候遇到一个关于图片加载的问题,所做的页面是一个商城的商品列表页,其中需要显示商品图片,名称等信息,因为商品列表可能会很长,所以其中图片需要滑到可以显示的区域再进行加载. 首先我的图 ...
- Canvas 使用及应用
Canvas canvas 是 HTML5 当中我最喜欢的所有新特性中我最喜欢的一个标签了.因为它太强大了,各种有意思的特效都可以实现. 1. canvas 的基本使用方法 - 它是一个行内块元素 - ...
- 前端面试题-url、href、src
一.URL的概念 统一资源定位符(或称统一资源定位器/定位地址.URL地址等,英语:Uniform Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址).如同在网络上的门 ...
- 进程,线程,Event Loop(事件循环),Web Worker
线程,是程序执行流的最小单位.线程可与同属一个进程的其他线程共享所拥有的全部资源,同一进程中的多个线程之间可以并发执行.线程有就绪,阻塞,运行三种基本状态. 阮一峰大神针对进程和线程的类比,很是形象: ...
- 详细解析kafka之kafka分区和副本
本篇主要介绍kafka的分区和副本,因为这两者是有些关联的,所以就放在一起来讲了,后面顺便会给出一些对应的配置以及具体的实现代码,以供参考~ 1.kafka分区机制 分区机制是kafka实现高吞吐的秘 ...
- 什么是Servlet?Servlet的周期和方法
1.什么是Servlet? Servlet是运行在web服务器或应用服务器的程序,它是作为来自web浏览器或其他http客户端的请求和HTTP服务器上的数据库或应用程序之间的中间层! 2.Servl ...
- js中(event)事件对象
事件对象 • 什么是事件对象? • 就是当你触发了一个事件以后,对该事件的一些描述信息 • 例如: ° 你触发一个点击事件的时候,你点在哪个位置了,坐标是多少 ° 你触发一个键盘事件的时候,你按的是哪 ...
- yolo3各部分代码详解(超详细)
0.摘要 最近一段时间在学习yolo3,看了很多博客,理解了一些理论知识,但是学起来还是有些吃力,之后看了源码,才有了更进一步的理解.在这里,我不在赘述网络方面的代码,网络方面的代码比较容易理解,下面 ...