目录

深入理解Redux

深入理解Redux

本文代码详见我的 GitHub 源码链接

Redux 和 React-Redux

Redux是一个状态管理库,用于解决组件间状态共享/通信的问题。

Redux的整体思路是通过仓库(Store)存储状态(state)和更新状态(setState),并通过发布订阅模式在状态更新时,重新渲染依赖状态组件。

React Redux则是将仓库(Store)和组件进行连接的工具,让React组件可以使用注入其中的读写状态接口。

基本概念

store:仓库,用来存储状态及更新状态所需的发布订阅系统

state:状态,用于存储状态信息(数据)

setState:状态变更方法,用于更新状态,并发布数据变更信息

核心概念

Redux及React Redux有以下核心概念:

action: 操作,包括操作类型(type)和更新的数据(payload)

reducer:规范state状态(数据),确保更新数据时,生成一个新的对象

dispatch:派发函数,用来派发操作(action)。

是写接口的封装,简化setState(reducer(旧数据state,操作action))

connect:将组件与全局状态(state)连接起来。

输入一个组件,返回一个包装好的组件(为原组件提供读接口state和写接口dispatch)。这样直接使用包装好的组件即可。 connect是React Redux实现的

除此之外connect还是一个经过柯里化(currying)的函数,connect提供MapStateToProps(类似selector)功能,只在选中的数据发生变化时进行更新(提供读接口封装)。提供MapDispatchToProps功能,对dispatcher进行封装简化 (提供写接口的封装)

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。 详见链接

Provider: 全局状态(state)由Provider提供给子组件,connect获取后包装好后,新的组件即可使用。

动手实践!手写Redux

分步详解,可通过每一个commit及代码注释获取。详见 Commits

  1. store:仓库,用来存储状态及更新状态所需的发布订阅系统

将数据及操作单独存储为store变量,state/setState读写数据,并提供订阅发布接口,在setState数据变动时,调用组件传入的函数(发布)。所有依赖数据的组件都需要订阅,因此在connect中提供数据给组建时,可以同时进行订阅(由于只需订阅一次,因此使用useEffect)。

//用于初始化redux(reducer和state)
export const createStore = (reducer, initState) => {
  store.state = initState
  store.reducer = reducer
  return store
}
export const store = {
  state: undefined,
  reducer: undefined,
  listeners: [],
  subscribe(fn) {
    store.listeners.push(fn)
	//返回取消订阅函数
    return () => {
      const idx = store.listeners.indexOf(fn)
      this.listeners.splice(idx, 1)
    }
  },
  update() {
    store.listeners.map(fn => fn({}))
  },
  setState(newValue) {
    store.state = newValue
    store.update()
  }
}
  1. reducer:规范state状态(数据),确保更新数据时,生成一个新的对象。封装reducer函数,输入旧对象和操作(操作类型type+新数据payload),输出一个新的对象,避免因引用相同,Hooks不触发重新渲染。
const initState = {user: {name: "howard", age: 23}}
const reducer = (state, action) => {
  const {type, payload} = action
  if (type === "updateUser") {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    }
  } else {
    return state
  }
}
  1. dispatch:用来简化setState(reducer(旧数据,操作)),组件通过props获取dispatch。封装dispatch函数名,输入操作action(type+payload),执行操作。因为只有操作action会变化,其他都不会,算是一个语法糖。 dispatch需要通过props提供给组件,即需要对组件进行一层包装。这样直接使用包装好的组件即可。
  2. connect:将组件与全局状态连接起来。输入一个组件,返回一个包装好的组件(为原组件提供dispatch)。这样直接使用包装好的组件即可。
//connect中
const connect = (Component)=>{
	...
	const dispatch = (action) => {
				setState(store.reducer(state, action))
	}
	...
	return <Component {...props} {dispatcher} {state}/>
}

注意:

  1. 此时可以通过三种方式访问数据,似乎接口不统一,目前统一使用connect传入的props获取。
    1. 全局state,因为要提供给provider value,所以必须引入
    2. 通过useContext获取
    3. 通过connect封装时传入的dispatch和appState获取(本质上也是使用useContext)
  2. 增加测试,发现所有通过connect依赖store的组件,在任何数据产生变化时都会渲染,不论自己依赖的数据是否变化,需要优化

通过柯里化(currying)为connect提供MapStateToProps(selector)功能,只在选中的数据发生变化时进行更新。

MapStateToProps封装读功能:

//所有用到store数据的组件都应该用connect包裹
//通过柯里化增加selector细化组件依赖的数据
export const connect = (MapStateToProps, MapDispatchToProps) => (Component) => {
  //返回一个对Component进行了包装的新函数组件
  return (props) => {
    //使用一个setState,在dispatch变更数据时,进行渲染
    const [, update] = useState({})
    const {state, setState} = store
    //每个依赖state的组件订阅数据变动
    //每次产生新数据时(包括第一次生成),connect都会记下数据,监听器发布时,数据已经变化,对比store的数据和记录的数据即可,然后connect会重新执行,记下新的数据。
    const data = MapStateToProps ? MapStateToProps(state) : {state}
		...
    return <Component {...props} {...dispatchers} {...data}/>
  }
}

疑问:如何获取旧数据以确认是否更新?

  1. 每次产生新数据时(包括第一次生成),connect都会记下数据,监听器发布时,数据已经变化,对比store的数据和记录的数据即可,然后connect会重新执行,记下新的数据。

为防止重复订阅,useEffect中返回取消订阅的函数,在下次调用useEffect之前取消订阅。 useEffect返回的函数是在组件卸载的时候执行/执行当前 effect 之前对上一个 effect 进行清除

//同样在connect函数中
...
useEffect(() => {
	//subscribe的返回值是取消该订阅的函数,在下次调用useEffect时执行
	return store.subscribe(() => {
		//更新订阅中调用的函数,如果依赖的数据变化了,再更新。
		const newData = MapStateToProps ? MapStateToProps(store.state) : {state: store.state}
		if (hasChanged(data, newData)) {
			update({})
		}
	})
}, [MapStateToProps])
...

同样通过柯里化为connect提供MapDispatchToProps(提供写接口的封装)功能

MapDispatchToProps对dispatcher进行封装简化:封装写功能

//同样在connect函数中
 const dispatch = (action) => {
      setState(store.reducer(state, action))
      update({})
    }
const dispatchers = MapDispatchToProps ? MapDispatchToProps(dispatch) : {dispatch}
return <Component {...props} {...dispatchers} {...data}/>