再看 Redux

126 阅读4分钟

先来研究下 Redux 中几个常见方法的原理。

初识 reducer 原理

reducer 是用来干什么的?一句话概括,reducer 是用来规范 react state 的创建流程的。

从下边这段代码我们可以看出,更新用户名时直接修改了 state 的值,这样是不规范的。

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

const appContext = createContext(null)

const User = () => {
  const { appState } = useContext(appContext)
  return <div>User: {appState.user.name}</div>
}
const UserModifier = () => {
  const { appState, setAppState } = useContext(appContext)
  const onChange = (e) => {
    appState.user.name = e.target.value // 直接修改了 state 的值,不规范
    setAppState({ ...appState })
  }
  return <div><input onChange={onChange} value={appState.user.name}></input></div>
}

const FirstChild = () => <section>firstChild<User /></section>
const SecondChild = () => <section>secondChild<UserModifier /></section>
const ThirdChild = () => <section>thirdChild</section>

export const App = () => {
  const [appState, setAppState] = useState({
    user: { name: 'marshall', age: 22 }
  })

  const contextValue = { appState, setAppState }

  return <appContext.Provider value={contextValue}>
    <FirstChild/>
    <SecondChild/>
    <ThirdChild/>
  </appContext.Provider>
}

我们可以这样改写下,这就是一个简化版的 reducer 函数:

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

const appContext = createContext(null)

const reducer = (state, { type, payload }) => { // 简化版 reducer
  if (type === 'updateUser') {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    }
  } else {
    return state
  }
}

const User = () => {
  const { appState } = useContext(appContext)
  return <div>User: {appState.user.name}</div>
}
const UserModifier = () => {
  const { appState, setAppState } = useContext(appContext)
  const onChange = (e) => {
    setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
  }
  return <div><input onChange={onChange} value={appState.user.name}></input></div>
}

const FirstChild = () => <section>firstChild<User /></section>
const SecondChild = () => <section>secondChild<UserModifier /></section>
const ThirdChild = () => <section>thirdChild</section>

export const App = () => {
  const [appState, setAppState] = useState({
    user: { name: 'marshall', age: 22 }
  })

  const contextValue = { appState, setAppState }

  return <appContext.Provider value={contextValue}>
    <FirstChild/>
    <SecondChild/>
    <ThirdChild/>
  </appContext.Provider>
}

dispatch 规范 setState 流程

看下边这段代码,如果我们要修改 state 中的其他参数,那我们每次都要写 setAppState,reducer 和 appState,太冗余了。

const UserModifier = () => {
  const { appState, setAppState } = useContext(appContext)
  const onChange = (e) => {
    // 重复度很高
    setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
    setAppState(reducer(appState, { type: 'updateGroup', payload: { group: e.target.value } }))
    setAppState(reducer(appState, { type: 'updateTitle', payload: { title: e.target.value } }))
  }
  return <div><input onChange={onChange} value={appState.user.name}></input></div>
}

我们可以手写一个 dispatch 优化

const UserModifierWrapper = () => {
  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 } })
     dispatch({ type: 'updateGroup', payload: { group: e.target.value } })
     dispatch({ type: 'updateTitle', payload: { title: e.target.value } })
  }
  return <div><input onChange={onChange} value={state.user.name}></input></div>
}

connect 来历

dispatch 和 connect 由 react-redux 实现。 在上边 dispatch 的模块我们为了获取全局上下文,给 <UserModifier /> 组件封装了一个 Wrapper, 那如果 <User /> 组件也需要这样封装那代码就过于冗余了。于是 react-redux 给我们提供了 connect 方法,方便我们将组件与全局上下文关联起来,下边是一个 connect 方法的简单实现。

const connect = (Component) => {
  return (props) => {
    const { appState, setAppState } = useContext(appContext)
    const dispatch = (action) => {
      setAppState(reducer(appState, action))
    }
    return <Component {...props} dispatch={dispatch} state={appState}/>
  }
}
const UserModifier = connect(
  ({ dispatch, state }) => {
    const onChange = (e) => {
      dispatch({ type: 'updateUser', payload: { name: e.target.value } })
    }
    return <div><input onChange={onChange} value={state.user.name}></input></div>
  }
)

使用 connect 减少 render

目前只有 <FirstChild /> 中使用了 appState.user.name 属性。但当我们修改 appState.user.name 属性时,其他没用到 appState 的组件也被刷新了,这是因为 appState 是 <App /> 组件下的 state, 因此当我们调用 setState 的时候它的子组件都会被刷新。 image.png 我们可以使用 connect 来减少 reRender。

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

const appContext = createContext(null)

// 首先要把 <App /> 中的 setState 干掉,避免子组件全部重新执行
const store = {
  state: {
    user: { name: 'marshall', age: 22 }
  },
  setState (newState) {
    store.state = newState
    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
  }
}

const connect = (Component) => {
  return (props) => {
    const { state, setState, subscribe } = useContext(appContext)
    const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
    useEffect(() => {
      subscribe(() => update({}))
    }, [])
    const dispatch = (action) => {
      setState(reducer(state, action))
    }
    return <Component {...props} dispatch={dispatch} state={state}/>
  }
}

const User = connect(({ state }) => {
  console.log('User reRender' + Math.random())
  return <div>User: {state.user.name}</div>
})

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

const FirstChild = () => {
  console.log('FirstChild reRender' + Math.random())
  return <section>firstChild<User /></section>
}
const SecondChild = () => {
  console.log('SecondChild reRender' + Math.random())
  return <section>secondChild<UserModifier name={'marshallddddd'}/></section>
}
const ThirdChild = () => {
  console.log('thirdChild reRender' + Math.random())
  return <section>thirdChild</section>
}

export const App = () => <appContext.Provider value={store}>
    <FirstChild/>
    <SecondChild/>
    <ThirdChild/>
  </appContext.Provider>

这样我们就可以做到当 state 更新时只有被 connect 了的组件会被更新 image.png

Redux 乍现

通过上边 reducer, dispatch 和 connect 这三个例子,redux 最常用的几个方法就介绍完了,下边我们将这几个方法抽象出来封装一个 redux.js 文件

// redux.jsx
import React, { useState, useContext, createContext, useEffect } from 'react'

export const connect = (Component) => {
  return (props) => {
    const { state, setState, subscribe } = useContext(appContext)
    const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
    useEffect(() => {
      subscribe(() => update({}))
    }, [])
    const dispatch = (action) => {
      setState(reducer(state, action))
    }
    return <Component {...props} dispatch={dispatch} state={state}/>
  }
}

export const reducer = (state, { type, payload }) => {
  if (type === 'updateUser') {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    }
  } else {
    return state
  }
}

export const store = {
  state: {
    user: { name: 'marshall', age: 22 }
  },
  setState (newState) {
    store.state = newState
    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 appContext = createContext(null)

selector from react-redux

connect 支持 selector

export const connect = (selector) => (Component) => {
  return (props) => {
    const { state, setState, subscribe } = useContext(appContext)
    const [_, update] = useState({})
    const data = selector ? selector(state) : { state } // 判断是否传了 selector
    useEffect(() => {
      subscribe(() => update({}))
    }, [])
    const dispatch = (action) => {
      setState(reducer(data, action))
    }
    return <Component {...props} {...data} dispatch={dispatch}/>
  }
}

// 传 selector
const User = connect(state => { return { user: state.user } })(({ user }) => {
  return <div>User: {user.name}</div>
})

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

使用 selector 实现精准渲染

我们在更新 FirstChild 和 SecondChild 的 state.user.name 时会发现 ThirdChild 也重新渲染了,这是我们所不愿意见到的。 image.png 下面我们将通过 selector 实现,组件只在自己的数据变化时渲染。

const changed = (oldState, newState) => {
  let hasChanged = false
  for (const key in oldState) {
    if (oldState[key] !== newState[key]) {
      hasChanged = true
      break
    }
  }
  return hasChanged
}
export const connect = (selector) => (Component) => {
  return (props) => {
    const { state, setState, subscribe } = useContext(appContext)
    const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
    const data = selector ? selector(state) : { state }
    useEffect(() => {
      store.subscribe(() => { // 注意这个 useEffect 会多次执行,要去 store 里取对应属性
        const newData = selector ? selector(store.state) : { state: store.state }
        if (changed(data, newData)) { // 加一步判断,只有当组件数据变化时 reRender。
          console.log('updated')
          update({})
        }
      })
    }, [selector])
    const dispatch = (action) => {
      setState(reducer(state, action))
    }
    return <Component {...props} {...data} dispatch={dispatch}/>
  }
}