Redux原理-实现一个Redux

2022/06/12 Redux 共 22151 字,约 64 分钟

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))
}

需要注意这里其实是报错的状态,因为我们的appStatesetAppState其实都是通过context上下文获取的,同时这个上下文作为一个hooks,根据React规定,只能在组件内使用hooks

  • 碍于作用域限制,以及React hooks使用限制,我们在dispatch无法访问到appStatesetAppState
  • 怎么解决这个问题呢,我们可以引入一个高阶组件来处理,取名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,则所有子组件都会重新执行 但是从数据变更维度来看的话,只有UserUserModifier是需要更新的,其他的是无意义的 我们当然可以通过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包裹的组件,也即只有UserModifierUser并没有发生变化

怎么解决这个问题呢?我们通过发布订阅的方式进行订阅变化

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

Search

    Table of Contents