实现简易版的 Redux

56 阅读5分钟

1. 简易版的修改方法

此版本仅是使用最简单的方法修改名字后实现同步变化。

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

const appContext = React.createContext(null)
 const App = () => {
  const [appState, setAppState] = useState({
    user: { name: 'frank', age: 18 }
  })
  const contextValue = { appState, setAppState }
  return (
    <appContext.Provider value={contextValue}>
      <大儿子 />
      <二儿子 />
      <幺儿子 />
    </appContext.Provider>
  )
}
const 大儿子 = () => (
  <section>
    大儿子
    <User />
  </section>
)
const 二儿子 = () => (
  <section>
    二儿子
    <UserModifier />
  </section>
)
const 幺儿子 = () => <section>幺儿子</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({ ...appState })
  }
  return (
    <div>
      <input value={appState.user.name} onChange={onChange} />
    </div>
  )
}

export default App

2. 优化上述过程

2.1 reducer 雏形

由于上述修改的过程可以单独写个函数,因此有了如下代码:

const reducer = (state, { type, payload }) => {
  switch (type) {
    case 'updateUser':
      return {
        ...state,
        user: {
          ...state.user,
          ...payload
        }
      }
    default:
      return state
  }
}

const UserModify = () => {
  ...
  const onChange = e => {
    setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
  }
  ...
}

2.2 使用 dispatch 优化

上述代码有些问题,如果我们要修改 type 或者 payload 中的某些参数,那么我们需要复制很多次,为解决这个问题,我们可以增加一个函数,每次将需要修改的数据传给此函数就可以实现部分优化;但是方法中并不能使用 hooks,于是我们需要新增一个组件。

...
const 二儿子 = () => (
  <section>
    二儿子
    <Wrapper />
  </section>
)
...
const Wrapper = () => {
  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 value={state.user.name} onChange={onChange} />
    </div>
  )
}

2.3 封装 connect 函数

...
const 二儿子 = () => (
  <section>
    二儿子
    <UserModifier >context</UserModifier>
  </section>
)
...
const connect = Component => {
  const Wrapper = props => {
    const { appState, setAppState } = useContext(appContext)

    const dispatch = action => {
      setAppState(reducer(appState, action))
    }

    return <Component {...props} dispatch={dispatch} state={appState} />
  }

  return Wrapper
}

const UserModifier = connect(({ dispatch, state, children }) => {
  const onChange = e => {
    dispatch({ type: 'updateUser', payload: { name: e.target.value } })
  }
  return (
    <div>
      {children}
      <input value={state.user.name} onChange={onChange} />
    </div>
  )
})

2.4 实现精准 render

当在打印上述每个组件的日志信息时,发现输入框中的内容每次发生变化,每个组件都会重新渲染,这是因为 setAppState 是写到主组件中的,一旦发生变化,那么磁组件中的所有内容都会重新渲染,为解决这个问题,我们可以采用如下方法:

2.4.1 useMemo

const App = () => {
  const [appState, setAppState] = useState({
    user: { name: 'frank', age: 18 }
  })
  const contextValue = { appState, setAppState }

  const x = useMemo(() => {
    return  <幺儿子 /> 
  }, [])
  return (
    <appContext.Provider value={contextValue}>
      <大儿子 />
      <二儿子 />
      { x } 
    </appContext.Provider>
  )
}

不过这种方式的弊端在于组价很多的情况下我们每个组件都要使用 useMemo,相对来说会很繁琐。

2.4.2 抽离静态数据保存为 store 数据

const store = {
  state: { user: { name: 'frank', age: 18 } },
  setState(newState) {
    store.state = newState
  }
}
const appContext = React.createContext(null)
const App = () => {
  return (
    <appContext.Provider value={store}>
      <大儿子 />
      <二儿子 />
      <幺儿子 />
    </appContext.Provider>
  )
}

const connect = Component => {
  const Wrapper = props => {
    const { state, setState } = useContext(appContext)

    // 这里 setState 修改的是 store 中的数据,而页面中的数据不会更新,因此我们需要手动更新以确保页面重     // 新加载
    const [, update] = useState({})

    const dispatch = action => {
      setState(reducer(state, action))
      update({})
    }

    return <Component {...props} dispatch={dispatch} state={state} />
  }

  return Wrapper
}

const UserModifier = connect(({ dispatch, state, children }) => {
  console.log('UserModifyer执行了' + Math.random())
  const onChange = e => {
    dispatch({ type: 'updateUser', payload: { name: e.target.value } })
  }
  return (
    <div>
      {children}
      <input value={state.user.name} onChange={onChange} />
    </div>
  )
})

2.4.3 使用发布订阅者模式

const connect = Component => {
  const Wrapper = props => {
    const { state, setState, subscribe } = useContext(appContext)

    const [, update] = useState({})

    // 在首次的时候订阅一次后,之后用的都是相同的更新函数
    useEffect(() => {
      subscribe(() => {
        update({})
      })
    }, [])

    const dispatch = action => {
      setState(reducer(state, action))
    }

    return <Component {...props} dispatch={dispatch} state={state} />
  }

  return Wrapper
}

const store = {
  state: { user: { name: 'frank', age: 18 } },
  setState(newState) {
    store.state = newState
    // 在每次更新的时候通知所有订阅者
    store.listeners.forEach(fn => fn(store.state))
  },
  listeners: [],
  subscribe(fn) {
    store.listeners.push(fn)

    // 在订阅完成之后希望可以删除此调用
    return () => {
      const index = store.listeners.indexOf(fn)
      store.listeners.splice(index, 1)
    }
  }
}

2.5 抽离为 redux 文件

import { createContext, useContext, useEffect, useState } from 'react'



export const store = {
  state: { user: { name: 'frank', age: 18 } },
  setState(newState) {
    store.state = newState
    // 在每次更新的时候通知所有订阅者
    store.listeners.forEach(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 }) => {
  switch (type) {
    case 'updateUser':
      return {
        ...state,
        user: {
          ...state.user,
          ...payload
        }
      }
    default:
      return state
  }
}
export const connect = Component => {
  const Wrapper = props => {
    const { state, setState, subscribe } = useContext(appContext)

    const [, update] = useState({})

    useEffect(() => {
      console.log('connect执行了' + Math.random())
      subscribe(() => {
        update({})
      })
    }, [])

    const dispatch = action => {
      setState(reducer(state, action))
    }

    return <Component {...props} dispatch={dispatch} state={state} />
  }

  return Wrapper
}

export const appContext = createContext(null)

2.6 实现 react-redux 中的 selector

export const connect = selector => Component => {
  const Wrapper = props => {
    const { state, setState, subscribe } = useContext(appContext)

    const [, update] = useState({})

    const data = selector ? selector(state) : { state }

    useEffect(() => {
      subscribe(() => {
        update({})
      })
    }, [])

    const dispatch = action => {
      setState(reducer(state, action))
    }

    return <Component {...props} {...data} dispatch={dispatch} />
  }
  
  const User = connect(state => {
  return {user: state.user}
})(({ user }) => {
  // 如果后端返回的数据形式为 state.xxx.yyy.zzz.user.name
  return <div>User:{user.name}</div>
})
const UserModifier = connect()(({ dispatch, state, children }) => {
 ...
 }

2.7 实现不同数据的精准渲染

const changed = (oldState, newState) => {
  let changed = false

  for (const key in oldState) {
    if (oldState[key] !== newState[key]) {
      changed = true
      break
    }
  }
  return changed
}
export const connect = selector => Component => {
  const Wrapper = props => {
    const { state, setState, subscribe } = useContext(appContext)

    const [, update] = useState({})

    const data = selector ? selector(state) : { state }

    useEffect(
      () =>
        subscribe(() => {
          const newData = selector ? selector(store.state) : { state: store.state }
          if (changed(data, newData)) {
            console.log('update')
            update({})
          }
        }),
      // 这里最好 取消订阅,否则在 selector 变化时会重复订阅
      [selector]
    )

    const dispatch = action => {
      setState(reducer(state, action))
    }

    return <Component {...props} {...data} dispatch={dispatch} />
  }

  return Wrapper
}

2.8 实现 mapDispatchToProps

connect 的第一个函数中的参数可以包含两个参数,第一个参数为简化语法,第二个参数是一个函数,传入一个 dispatch,返回的是一个方法。

const UserModifier = connect(null, dispatch => {
  return {
    updateUser: attrs => dispatch({ type: 'updateUser', payload: attrs })
  }
})(({ updateUser, state, children }) => {
  ...
})

// redux.jsx
export const connect = (selector, dispatchSelector) => Component => {
  const Wrapper = (props) => {
    ...
    const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch }
    ...
    return <Component {...props} {...data} {...dispatchers} />
  }
  return Wrapper
}

2.9 抽离 connect 的第一个函数调用

connect 的使用方式:

connect(MapStateToProps, MapDispatchToProps)(Component)

connect 的意义就在于能够将第一个函数调用抽离出去,形成一个半成品,后续使用的时候可以直接调用。

import { connect } from "../redux"

const userSelector = state => {
  return { user: state.user }
}
const userDispatcher = dispatch => {
  return {
    updateUser: attrs => dispatch({ type: 'updateUser', payload: attrs })
  }
}
export const connectToUser = connect(userSelector, userDispatcher)


const User = connectToUser(({ user }) => {
  console.log('User执行了' + Math.random())
  // 如果后端返回的数据形式为 state.xxx.yyy.zzz.user.name
  return <div>User:{user.name}</div>
})
const UserModifier = connectToUser(({ updateUser, user, children }) => {
  console.log('UserModifier执行了' + Math.random())
  const onChange = e => {
    updateUser({ name: e.target.value })
  }
  return (
    <div>
      {children}
      <input value={user.name} onChange={onChange} />
    </div>
  )
})

2.10 封装 Provider 和 createStore

// redux.jsx
const store = {
  state: null,
  reducer: null,
  ...
}
export const createStore = (reducer, initialState) => {
  store.state = initialState
  store.reducer = reducer
  return store
}

...

export const Provider = ({ children, store }) => {
  return <appContext.Provider value={store}>{children}</appContext.Provider>
}

// App.jsx
const initialState = {
  user: { name: 'frank', age: 18 },
  group: { name: '前端组' }
}
const reducer = (state, { type, payload }) => {
  switch (type) {
    case 'updateUser':
      return {
        ...state,
        user: {
          ...state.user,
          ...payload
        }
      }
    default:
      return state
  }
}
const store = createStore(reducer, initialState)

...
const App = () => {
  return (
    <Provider store={store}>
      <大儿子 />
      <二儿子 />
      <幺儿子 />
    </Provider>
  )
}
...

2.11 重构 redux 文件

import { createContext, useContext, useEffect, useState } from 'react'

const innerStore = {
  state: undefined,
  reducer: undefined,
  listeners: [],
  setState(newState) {
    innerStore.state = newState
    // 在每次更新的时候通知所有订阅者
    innerStore.listeners.forEach(fn => fn(innerStore.state))
  }
}

const store = {
  getState() {
    return innerStore.state
  },

  dispatch(action) {
    innerStore.setState(innerStore.reducer(innerStore.state, action))
  },

  subscribe(fn) {
    innerStore.listeners.push(fn)

    // 在订阅完成之后希望可以删除此调用
    return () => {
      const index = innerStore.listeners.indexOf(fn)
      innerStore.listeners.splice(index, 1)
    }
  }
}

const dispatch = store.dispatch

export const createStore = (reducer, initialState) => {
  innerStore.state = initialState
  innerStore.reducer = reducer
  return store
}

const changed = (oldState, newState) => {
  let changed = false

  for (const key in oldState) {
    if (oldState[key] !== newState[key]) {
      changed = true
      break
    }
  }
  return changed
}
export const connect = (selector, dispatchSelector) => Component => {
  const Wrapper = props => {
    const { subscribe } = useContext(appContext)

    const [, update] = useState({})

    const data = selector ? selector(innerStore.state) : { state: innerStore.state }

    const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch }

    useEffect(
      () =>
        subscribe(() => {
          const newData = selector ? selector(innerStore.state) : { state: innerStore.state }
          if (changed(data, newData)) {
            update({})
          }
        }),
      // 这里最好 取消订阅,否则在 selector 变化时会重复订阅
      [selector]
    )

    return <Component {...props} {...data} {...dispatchers} />
  }

  return Wrapper
}

const appContext = createContext(null)

// eslint-disable-next-line react/prop-types
export const Provider = ({ children, store }) => {
  return <appContext.Provider value={store}>{children}</appContext.Provider>
}

2.12 实现异步 action

上面所写的代码只能支持形如 fetchUser(dispatch) 的函数方式,但是如果想要写成 dispatch(fetchUser),需要对自己封装的 dispatch 改写。

// redux.jsx
let dispatch = store.dispatch

const prevDispatch = dispatch
dispatch = action => {
  if (typeof action === 'function') {
    action(dispatch)
  } else {
    prevDispatch(action)
  }
}

2.13 实现 promise 形式的异步

const prevDispatch2 = dispatch
dispatch = action => {
  if ( action.payload instanceof Promise) {
    action.payload.then(data => {
      dispatch({...action, payload: data})
    })
  } else {
    prevDispatch2(action)
  }
}

上述代码具体地址:github.com/fogjoe/my-r…