React reRender更新机制
React发生变化的前提:state状态发生改变,无论这个state是否被引用
一个简单的强制刷新的实现:
const [, setState] = useState({})
const forceUpdate = () => {
setState({})
}
- 我们知道更新原理为,只要state代表的对象发生了immutable change,则会导致一次reRender,即使这个state并没有被引用
- 需要注意,setState返回的对象引用没有变化的时候,不会发生reRender,因为React会认为并没有发生改变
- 这里每次调用setState返回的对象的都是一个新的,shallow diff对象地址发生变化,则每次都会发生reRender
redux推演过程
完整初始化代码
import React, { useState, useContext } from 'react'
import './App.css'
const appContext = React.createContext(null)
const App = () => {
const [appState, setAppState] = useState({
user: {
name: 'jico',
age: 18,
}
})
const contextValue = {
appState,
setAppState,
}
return (
<appContext.Provider value={contextValue}>
<Component1 />
<Component2 />
<Component3 />
</appContext.Provider>
)
}
const Component1 = () => {
return (
<section>
Component1
<User />
</section>
)
}
const Component2 = () => {
return (
<section>
Component2
<UserModifier />
</section>
)
}
const Component3 = () => {
return (
<section>
Component3
</section>
)
}
const User = () => {
const contextValue = useContext(appContext)
return (
<div>User: {contextValue.appState.user.name}</div>
)
}
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = e => {
appState.user.name = e.target.value
setAppState(Object.assign({}, appState))
}
return (
<div>
<input type="text" value={appState.user.name} onChange={onChange} />
</div>
)
}
export default App
- onChange这里并不规范,我们采用了
appState.user.name = e.target.value
直接修改原数据的形式 - 规范一下,采用一个创建immutable对象的封装方法的方式,取名reducer
我们做如下变更:
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = e => {
setAppState(reducer(appState, {
type: 'updateUser',
payload: {
name: e.target.value,
}
}))
}
return (
<div>
<input type="text" value={appState.user.name} onChange={onChange} />
</div>
)
}
这里我们发现,还是存在很多固化的样板代码,如setAppState(reducer(appState
,我们将样板代码提取,取名dispatch
const dispatch = action => {
// error
setAppState(reducer(appState, action))
}
需要注意这里其实是报错的状态,因为我们的appState
和setAppState
其实都是通过context上下文获取的,同时这个上下文作为一个hooks,根据React规定,只能在组件内使用hooks
- 碍于作用域限制,以及React hooks使用限制,我们在dispatch无法访问到
appState
和setAppState
- 怎么解决这个问题呢,我们可以引入一个高阶组件来处理,取名connect
此时改动部分的关键代码为:
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
// react-redux
const Connect = () => {
const { appState, setAppState } = useContext(appContext)
const dispatch = action => {
setAppState(reducer(appState, action))
}
return <UserModifier dispatch={dispatch} state={appState} />
}
const UserModifier = ({ dispatch, state }) => {
const onChange = e => {
dispatch({
type: 'updateUser',
payload: {
name: e.target.value,
}
})
}
return (
<div>
<input type="text" value={state.user.name} onChange={onChange} />
</div>
)
}
- 目前这个connect还不够智能,还不能称之为HOC
- 高阶组件接受一个组件作为参数,并返回一个新的组件
- 高阶组件会透传所有参数,并且一般情况下只拓展能力,不包含副作用
我们对代码进行优化:
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
const Connect = (Component) => {
// 我们接收所有props并透传给实际消费的component,包括props.children
return (props) => {
const { appState, setAppState } = useContext(appContext)
const dispatch = action => {
setAppState(reducer(appState, action))
}
return <Component dispatch={dispatch} state={appState} {...props} />
}
}
const UserModifier = Connect(({ dispatch, state }) => {
const onChange = e => {
dispatch({
type: 'updateUser',
payload: {
name: e.target.value,
}
})
}
return (
<div>
<input type="text" value={state.user.name} onChange={onChange} />
</div>
)
})
- 我们接收所有props并透传给实际消费的component,包括props.children
- connect作用:
- 将组件与全局状态链接起来,所以起名connect,这个库是由react-redux提供的
但是现在有一个问题,就是我们一旦调用setAppState
进行数据更新(参考顶部:React reRender更新机制),就会触发App的reRender,则所有子组件都会重新执行
但是从数据变更维度来看的话,只有User
和UserModifier
是需要更新的,其他的是无意义的
我们当然可以通过useMemo
进行子组件的包裹,但是这样是比较代码冗余的,我们期望的一种方式是:只有redux数据发生变化的地方,才进行reRender
因为导致这个问题的setAppState
在根组件App
中,我们需要将这部门逻辑抽离出来,防止自顶向下的reRender
继续优化:
import React, { useState, useContext } from 'react'
import './App.css'
const appContext = React.createContext(null)
const store = {
state: {
user: {
name: 'jico',
age: 18,
}
},
setState(newState) {
store.state = newState
}
}
const App = () => {
return (
<appContext.Provider value={store}>
<Component1 />
<Component2 />
<Component3 />
</appContext.Provider>
)
}
const Component1 = () => {
console.log('component1 executed');
return (
<section>
Component1
<User />
</section>
)
}
const Component2 = () => {
console.log('component2 executed');
return (
<section>
Component2
<UserModifier />
</section>
)
}
const Component3 = () => {
console.log('component3 executed');
return (
<section>
Component3
</section>
)
}
const User = () => {
console.log('user executed');
const { state } = useContext(appContext)
return (
<div>User: {state.user.name}</div>
)
}
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
const Connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
const dispatch = action => {
setState(reducer(state, action))
// 我们只调用了store.setState,但是并没有调用React的setState,没有触发reRender,导致视图根本没有更新
// 我们利用react shallow diff,强制状态变更,发生reRender
update({})
}
return <Component dispatch={dispatch} state={state} {...props} />
}
}
const UserModifier = Connect(({ dispatch, state }) => {
console.log('UserModifier executed');
const onChange = e => {
dispatch({
type: 'updateUser',
payload: {
name: e.target.value,
}
})
}
return (
<div>
<input type="text" value={state.user.name} onChange={onChange} />
</div>
)
})
export default App
- 我们抽离store,将state和setState封装到store中,然后使用store作为上下文传递
- 但是这样会有一个问题,因为我们进行数据变更时调用的setState并不是react的setState
- 所以数据变更的结果是,我们只更改了store,但是没有触发react reRender
- 我们通过如上代码中的方式在connect中进行了一个forceUpdate,但是这样仍然是有问题的
- 这里的forceUpdate只更新了connect包裹的组件,也即只有
UserModifier
,User
并没有发生变化
怎么解决这个问题呢?我们通过发布订阅的方式进行订阅变化
const store = {
state: {
user: {
name: 'jico',
age: 18,
}
},
setState(newState) {
store.state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
},
}
const Connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
useEffect(() => {
// 只进行一次变化订阅,订阅执行时更新视图的方法为forceUpdate
store.subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component dispatch={dispatch} state={state} {...props} />
}
}
- 我们在store中采用发布订阅的模式,进行state变化后需要更新的依赖fn收集;
- 当state发生变化时,我们触发这些收集的依赖,进行reRender;
- 因为Connect已经承载了将组件与全局状态链接的功能,我们只需要在Connect中进行驶入变化订阅即可;
- 结果就是,
- 所有被Connect包裹的组件都能够在store中的数据发生变化时自动更新
- 变化可以相对精准的控制在所有被Connect组件包裹的范围中
- 为什说是相对精准,因为react对于数据控制、检测的颗粒度没办法达到像Vue一样基于对象代理方式可以达到的精准颗粒度
我们将属于redux的实现单独抽离到redux.js中,方便逻辑组织,此时在App.tsx中只保留了和组件相关的逻辑,清晰很多
import React, { useEffect, useContext, useState } from 'react'
export const appContext = React.createContext(null)
export const store = {
state: {
user: {
name: 'jico',
age: 18,
}
},
setState(newState) {
store.state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
},
}
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
export const Connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
useEffect(() => {
// 只进行一次变化订阅,订阅执行时更新视图的方法为forceUpdate
store.subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component dispatch={dispatch} state={state} {...props} />
}
}
selector
我们回过头看一下使用Connect的组件,代码如下:
const User = Connect(({ state }) => {
console.log('user executed');
return (
<div>User: {state.user.name}</div>
)
})
我们发现,组件中接受的参数始终为state,一个全局的store存储的数据状态对象,假如我们在state中存储的数据嵌套比较深,就总是需要使用state.a.b.c.d
的方式,写一大串,那有什么方法可以简化这个repeat吗?
我们将Connect进一步封装:redux selector思想
我们希望通过如下两种方式进行Connect数据整理:
- 当传入Connect selector时,我们通过selector过滤数据,只返回需要的/格式化的数据
- 当没有传入Connect selector时,我们仍旧接收state,进行数据获取
const User = Connect(state => ({
name: state.user.name
}))(({ name }) => {
console.log('user executed');
return (
<div>User: {name}</div>
)
})
const UserModifier = Connect()(({ dispatch, state }) => {
console.log('UserModifier executed');
const onChange = e => {
dispatch({
type: 'updateUser',
payload: {
name: e.target.value,
}
})
}
return (
<div>
<input type="text" value={state.user.name} onChange={onChange} />
</div>
)
})
// redux
// 我们将Connect柯里化实现,首先传入一个selector
export const Connect = (selector?) => (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
// 为了可以直接在props中获取经过selector处理的state,我们需要通过{...data}的方式进行传递,
// 否则会存在object key,无法直接从props解构获取
const data = selector ? selector(state) : { state }
useEffect(() => {
// 只进行一次变化订阅,订阅执行时更新视图的方法为forceUpdate
store.subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component dispatch={dispatch} {...data} {...props} />
}
}
数据精准更新
还记得我们上面说的react store数据基于发布订阅模式,更新检测的颗粒度太粗,假如我们有以下代码:
// 我们在store中加入group数据
export const store = {
state: {
user: {
name: 'jico',
age: 18,
},
group: {
name: 'front-end',
}
},
setState(newState) {
store.state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
},
}
// 我们在Component3中使用这个数据
const Component3 = Connect(state => ({
group: state.group,
}))(({ group }) => {
console.log('component3 executed');
return (
<section>
Component3: {group.name}
</section>
)
})
此时我们输入内容进行modifyUser的操作,会发现,使用到了group数据的Component3也发生了reRender,为什么会这样呢?
- 问题是因为我们在所有Connect的组件中,注入了一个forceUpdate的订阅
- 导致一旦store中数据发生变化,则会触发所有的listeners(这里会执行forceUpdate)
- 则会导致所有组件reRender
那我们如何进行数据的精准更新呢?
- 我们只需要在Connect里注入的订阅函数中增加检测逻辑,只有当数据发生变化时,才进行forceUpdate
核心代码如下:
const changed = (oldState, newState) => {
let changed = false
for (let key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
}
}
return changed
}
// 我们将Connect柯里化实现,首先传入一个selector
export const Connect = (selector?) => (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
const data = selector ? selector(state) : { state }
useEffect(() => {
// store.subscribe会返回一个取消订阅的函数,当selector发生变化时,我们需要取消掉之前的订阅,添加一个新的订阅
// 防止存在冗余重复的订阅导致逻辑冗余刷新
return store.subscribe(() => {
const newData = selector ? selector(store.state) : {
state: store.state
}
if (changed(data, newData)) {
update({})
}
})
}, [selector])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component dispatch={dispatch} {...data} {...props} />
}
}
此时重新触发userModify,会发现Component3不再reRender 额外需要注意的是:
- selector一般是不会变化的,但是存在变化的可能,假如我们写了一个动态的selector计算函数,或者说中间我们修改了这个selector时,我们需要重新订阅才可以生效
- 同时需要在selector变化以后,解除原来的旧的订阅
- 这里正好我们已经在store.subscribe返回了一个取消订阅的函数,在useEffect返回即可
MapDispatchertoProps
我们在selector一节中,通过state selector的方式,简化了冗余的state读取书写逻辑,那有没有办法可以采用类似的形式简化dispatcher呢?
核心代码如下:
const User = Connect(state => ({
name: state.user.name
}))(({ name }) => {
console.log('user executed');
return (
<div>User: {name}</div>
)
})
const UserModifier = Connect(null, (dispatch) => {
return {
updateUser: (data) => dispatch({ type: 'updateUser', payload: data })
}
})(({ updateUser, state }) => {
console.log('UserModifier executed');
const onChange = e => {
// 此时dispatche操作会变得很简洁
updateUser({
name: e.target.value,
})
}
return (
<div>
<input type="text" value={state.user.name} onChange={onChange} />
</div>
)
})
// redux
// 柯里化实现:即我们先接收一个参数,然后返回一个新的函数并内部持有这个参数
export const Connect = (selector, mapDispatchertoProps?) => (Component) => {
return (props) => {
const { state, setState } = useContext(appContext)
const [, update] = useState({})
const dispatch = action => {
setState(reducer(state, action))
}
const data = selector ? selector(state) : { state }
const dispatcher = mapDispatchertoProps ? mapDispatchertoProps(dispatch) : { dispatch }
useEffect(() => {
// store.subscribe会返回一个取消订阅的函数,当selector发生变化时,我们需要取消掉之前的订阅,添加一个新的订阅
// 防止存在冗余重复的订阅导致逻辑冗余刷新
return store.subscribe(() => {
const newData = selector ? selector(store.state) : {
state: store.state
}
if (changed(data, newData)) {
update({})
}
})
}, [selector])
return <Component {...dispatcher} {...data} {...props} />
}
}
- 所谓柯里化实现,即我们先接收一个参数,然后返回一个新的函数并内部持有这个参数
抽取公共Connect参数
为什么Connect要用这种先接收两个参数,返回一个接收组件参数的函数的 柯里化实现的方式呢? 为什么不直接接收三个参数? 其实是有考虑的,考虑的点在于公共逻辑的抽取复用:Connect实际是提供了一种读写接口逻辑抽离的实现方式
核心代码如下:
const userSelector = state => ({
name: state.user.name,
user: state.user,
})
const userDispatcher = dispatch => {
return {
updateUser: (data) => dispatch({ type: 'updateUser', payload: data })
}
}
// 此时抽离了userSelector和userDispatcher以后,我们完全可以更进一步
// 抽离一个公共的Connect
const ConnectToUser = Connect(userSelector, userDispatcher)
// 这里User没有执行是因为没有被Connect,没有被Connect的组件,在store中数据变化以后,无法forceUpdate
const User = ConnectToUser(({ name }) => {
console.log('user executed');
return (
<div>User: {name}</div>
)
})
const UserModifier = ConnectToUser(({ updateUser, user }) => {
console.log('UserModifier executed');
const onChange = e => {
// 此时dispatche操作会变得很简洁
updateUser({
name: e.target.value,
})
}
return (
<div>
<input type="text" value={user.name} onChange={onChange} />
</div>
)
})
此时,我们完全可以根据业务逻辑的构成,根据不同数据的selector和dispatcher的不同,抽离connectors进行管理.
抽离的connectors/connectToUser.ts
如下:
import { Connect } from "../redux"
const userSelector = state => ({
name: state.user.name,
user: state.user,
})
const userDispatcher = dispatch => {
return {
updateUser: (data) => dispatch({ type: 'updateUser', payload: data })
}
}
export const ConnectToUser = Connect(userSelector, userDispatcher)
到这里:
- Connect已经可以抽离封装用来解决数据读取、数据更新的两部分逻辑
解耦state和reducer
正常来说,redux不需要关心state和reducer分别是什么,只需要解决state管理和更新的问题。 我们继续抽离封装
// redux
export const store = {
state: undefined,
reducer: undefined,
setState(newState) {
store.state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
},
}
export const createStore = (reducer, initState) => {
store.state = initState
store.reducer = reducer
return store
}
// App.tsx
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
const initState = {
user: {
name: 'jico',
age: 18,
},
group: {
name: 'front-end',
}
}
const store = createStore(reducer, initState)
const App = () => {
return (
<appContext.Provider value={store}>
<Component1 />
<Component2 />
<Component3 />
</appContext.Provider>
)
}
将appContext.Provider改写为Provider
核心代码如下:
// App.tsx
const App = () => {
return (
<Provider store={store}>
<Component1 />
<Component2 />
<Component3 />
</Provider>
)
}
// redux
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider>
)
}
redux + react-redux思想
- redux提供了一种immutable的数据管理思路
- react-redux提供了connect的方式,将全局的state和dispatcher和局部的component链接起来
对照redux精简store结构
redux Store Methods:
- getState()
- dispatch(action)
- subscribe(listener)
- replaceReducer(nextReducer)
// redux
import React, { useEffect, useContext, useState, Children } from 'react'
export const appContext = React.createContext(null)
let state = undefined
let reducer = undefined
let listeners = []
const setState = (newState) => {
state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
listeners.map(fn => fn(state))
}
export const store = {
getState() {
return state
},
// 因为setState和reducer都抽离到了global,所以dispatch也不再需要耦合在Connect中,可以提升到store中管理
dispatch: action => {
setState(reducer(state, action))
},
subscribe(fn) {
listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = listeners.indexOf(fn)
listeners.splice(index, 1)
}
},
replaceReducer(newReducer) {
reducer = newReducer
},
}
const dispatch = store.dispatch
export const createStore = (_reducer, initState) => {
state = initState
reducer = _reducer
return store
}
const changed = (oldState, newState) => {
let changed = false
for (let key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
}
}
return changed
}
// 我们将Connect柯里化实现,首先传入一个selector
export const Connect = (selector, mapDispatchertoProps?) => (Component) => {
return (props) => {
const [, update] = useState({})
const data = selector ? selector(state) : { state }
const dispatcher = mapDispatchertoProps ? mapDispatchertoProps(dispatch) : { dispatch }
useEffect(() => {
// store.subscribe会返回一个取消订阅的函数,当selector发生变化时,我们需要取消掉之前的订阅,添加一个新的订阅
// 防止存在冗余重复的订阅导致逻辑冗余刷新
return store.subscribe(() => {
const newData = selector ? selector(state) : {
state: state
}
if (changed(data, newData)) {
update({})
}
})
}, [selector])
return <Component {...dispatcher} {...data} {...props} />
}
}
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider>
)
}
此时,从API暴露的角度来看,store已经和redux保持一致了
完整代码
// App.tsx
import React, { useState, useContext, useEffect } from 'react'
import './App.css'
import { Connect, appContext, createStore, Provider } from './redux'
import { ConnectToUser } from './connectors/connectToUser'
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload,
}
}
} else {
return state
}
}
const initState = {
user: {
name: 'jico',
age: 18,
},
group: {
name: 'front-end',
}
}
const store = createStore(reducer, initState)
const App = () => {
return (
<Provider store={store}>
<Component1 />
<Component2 />
<Component3 />
</Provider>
)
}
const Component1 = () => {
console.log('component1 executed');
return (
<section>
Component1
<User />
</section>
)
}
const Component2 = () => {
console.log('component2 executed');
return (
<section>
Component2
<UserModifier />
</section>
)
}
const Component3 = Connect(state => ({
group: state.group,
}))(({ group }) => {
console.log('component3 executed');
return (
<section>
Component3: {group.name}
</section>
)
})
// 这里User没有执行是因为没有被Connect,没有被Connect的组件,在store中数据变化以后,无法forceUpdate
const User = ConnectToUser(({ name }) => {
console.log('user executed');
return (
<div>User: {name}</div>
)
})
const UserModifier = ConnectToUser(({ updateUser, user }) => {
console.log('UserModifier executed');
const onChange = e => {
// 此时dispatche操作会变得很简洁
updateUser({
name: e.target.value,
})
}
return (
<div>
<input type="text" value={user.name} onChange={onChange} />
</div>
)
})
export default App
// ./connectors/connectToUser.ts
import { Connect } from "../redux"
const userSelector = state => ({
name: state.user.name,
user: state.user,
})
const userDispatcher = dispatch => {
return {
updateUser: (data) => dispatch({ type: 'updateUser', payload: data })
}
}
// 此时抽离了userSelector和userDispatcher以后,我们完全可以更进一步
// 抽离一个公共的Connect
export const ConnectToUser = Connect(userSelector, userDispatcher)
// ./redux/index.tsx
import React, { useEffect, useContext, useState, Children } from 'react'
export const appContext = React.createContext(null)
let state = undefined
let reducer = undefined
let listeners = []
const setState = (newState) => {
state = newState
// 订阅执行的时机为state发生变化时,类似于数据监听,但是颗粒度比较粗
listeners.map(fn => fn(state))
}
export const store = {
getState() {
return state
},
// 因为setState和reducer都抽离到了global,所以dispatch也不再需要耦合在Connect中,可以提升到store中管理
dispatch: action => {
setState(reducer(state, action))
},
subscribe(fn) {
listeners.push(fn)
// 订阅的同时,我们返回一个删除订阅的方法
return () => {
const index = listeners.indexOf(fn)
listeners.splice(index, 1)
}
},
replaceReducer(newReducer) {
reducer = newReducer
},
}
const dispatch = store.dispatch
export const createStore = (_reducer, initState) => {
state = initState
reducer = _reducer
return store
}
const changed = (oldState, newState) => {
let changed = false
for (let key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
}
}
return changed
}
// 我们将Connect柯里化实现,首先传入一个selector
export const Connect = (selector, mapDispatchertoProps?) => (Component) => {
return (props) => {
const [, update] = useState({})
const data = selector ? selector(state) : { state }
const dispatcher = mapDispatchertoProps ? mapDispatchertoProps(dispatch) : { dispatch }
useEffect(() => {
// store.subscribe会返回一个取消订阅的函数,当selector发生变化时,我们需要取消掉之前的订阅,添加一个新的订阅
// 防止存在冗余重复的订阅导致逻辑冗余刷新
return store.subscribe(() => {
const newData = selector ? selector(state) : {
state: state
}
if (changed(data, newData)) {
update({})
}
})
}, [selector])
return <Component {...dispatcher} {...data} {...props} />
}
}
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider>
)
}
redux支持异步action
我们将UserModifier简单更新,变成一个异步的操作,如下:
const delay = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
const fetchUser = (updateUser) => {
delay(2000).then(() => {
updateUser({
name: 'after 2000ms',
})
})
}
const UserModifier = ConnectToUser(({ updateUser, user }) => {
console.log('UserModifier executed');
const handlerClick = () => {
fetchUser(updateUser)
// updateUser(fetchUser)
}
return (
<div>
<div>User: {user.name}</div>
<button onClick={handlerClick}>delay 2000ms</button>
</div>
)
})
- 我们目前是通过
fetchUser(updateUser)
的方式进行dispatch逻辑更新,假如我们可以实现一个类似于updateUser(fetchUser)
可能会更理想 - 因为后者更类似于我们dispatch一个数据更新/获取函数
- 当函数的数据ready以后,自动更新数据即可,此时自然支持了异步action
简单包装一下,实现如下:
const UserModifier = ConnectToUser(({ updateUser, user }) => {
console.log('UserModifier executed');
let dispatch = fn => {
fn(updateUser)
}
const handlerClick = () => {
// 此时通过dispatch包装函数,dispatch(fetchUser)则等价于fetchUser(updateUser)
dispatch(fetchUser)
}
return (
<div>
<div>User: {user.name}</div>
<button onClick={handlerClick}>delay 2000ms</button>
</div>
)
})
- 此时通过dispatch包装函数,dispatch(fetchUser)则等价于fetchUser(updateUser)
- 核心的思想其实就是:当dispatch的action为一个pure data时,直接同步提交变更;当action为一个函数时,递归直至为pure data
此时redux中的dispatch支持了两种提交模式,核心代码如下:
let dispatch = store.dispatch
const dispatchPure = dispatch
// action其实就是dispatch的一个数据载荷,可以是一个pure数据(同步),也可以是一个函数(异步)
dispatch = action => {
if (typeof action === 'function') {
// 如果action是一个函数,我们执行这个action,并将dispatch传入
// 注意这里其实是一个递归,如果dispatch依旧是一个函数,会递归直至dispatch为一个pure数据
action(dispatch)
} else {
dispatchPure(action)
}
}
redux支持promise
核心代码如下:
let dispatch = store.dispatch
const dispatchPure = dispatch
// action其实就是dispatch的一个数据载荷,可以是一个pure数据(同步),也可以是一个函数(异步)
dispatch = action => {
if (typeof action === 'function') {
// 如果action是一个函数,我们执行这个action,并将dispatch传入
// 注意这里其实是一个递归,如果dispatch依旧是一个函数,会递归直至dispatch为一个pure数据
action(dispatch)
} else {
dispatchPure(action)
}
}
// 递归取值,直至then拿到的data为pure,执行dispatch
const pureAndFnDispatch = dispatch
dispatch = action => {
if (action.payload instanceof Promise) {
action.payload.then(data => {
dispatch({
...action,
payload: data,
})
})
} else {
pureAndFnDispatch(action)
}
}
redux中间件
redux创建store时,可以传递第三个参数,如:reduxThunk、reduxPromise,其实思想类似上面一节的实现。 具体可以参考github源码
[1] 手写redux