redux 核心代码实现

68 阅读6分钟

思考

两个同级组件如何进行通信?

  1. 将状态抽离到公共的父组件中,然后组子件调用父组件的方法,去更新另一个子组件
  2. 使用 Contextvalue 传递到子组件进行调用

方法一的问题在于嵌套很深的情况下,会出现很多子组件会增加完全不需要的属性。当然可以通过 render props 将子组件透传的方式来解决。但是这又会产生严重的嵌套问题。

方法二的问题在于 value 改变的时候,所有的 children 都将 rerender,性能较差。

但是如果 value 不改变的话,那么就没有性能问题了。好像就满足我们的需求了。

那么如果做到 value 不发生变化呢?

  1. 使用 ref
  2. 使用全局变量
  3. 使用 ref.currentclass

初始化 demo

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

const Context = createContext()

const App = () => {
  const [state, setState] = useState({
    user: {
      name: "test",
      age: 10
    }
  })
  return (
    <Context.Provider value={{ state, setState }}>
      <A />
      <B />
      <C />
    </Context.Provider>
  )
}

const A = () => {
  const { state } = useContext(Context)
  return <div>{state.user.name}</div>
}

const B = () => {
  const { state, setState } = useContext(Context)
  return (
    <input
      value={state.user.name}
      onChange={e => {
        setState({
          ...state,
          user: {
            ...state.user,
            name: e.target.value
          }
        })
      }}
    />
  )
}

const C = () => {
  return <div>我的无辜的</div>
}

export default App

这样在组件 B 中的输入后,组件 A 也可以正常更新。这里就使用了最简单的 Context 进行状态管理。

实现 reducer

上述的代码有一个的问题在于,如果其他的地方也使用了这段代码去更新 name 的话,就只能 cv 一下。

// 编写 reducer 函数
const reducer = (state, action) => {
  const { type, payload } = action
  if (type === "updateUserName") {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    }
  }
}

// B 组件的 onChange 改成这样
setState(
  reducer(state, {
	type: "updateUserName",
	payload: { name: e.target.value }
  })
)

代码不会减少,只是换了一种组织形式,但扩展性更强了

reducer 就是为了规范 state 的创建流程

实现 dispatch

上面的代码有一个问题在于,这个 setState(xxx) 每次都要重复写,那要怎么抽象呢?

const dispatch = ({ type, payload }) => {
  const { state, setState } = useContext(Context)
  setState(reducer(state, { type, payload }))
}

由于 dispatch 既不是 hooks,也不是组件,所以 hooks 不能在 dispatch 中使用,那要怎么做呢?

可以使用 hoc

实现 connect

// 实现 connect hoc
const connect = Component => props => {
  const { state, setState } = useContext(Context)
  const dispatch = ({ type, payload }) => {
    setState(reducer(state, { type, payload }))
  }
  return <Component {...props} state={state} dispatch={dispatch} />
}

// 使用 connect 获取 state 和 dispatch
const B = connect(({ state, dispatch }) => {
  return (
    <input
      value={state.user.name}
      onChange={e => {
        dispatch({
          type: "updateUserName",
          payload: { name: e.target.value }
        })
      }}
    />
  )
})```

这样就可以了

**`dispatch` 就是为了规范 `setState` 流程**

## 实现精准 render

上面的代码有一个问题就在于,`C` 组件在没有获取 `state` 的时候也更新了,如何避免这种没必要的更新呢?

```jsx
// 创建一个全部常量 store,provider 的 value 
const store = {
  state: {
    user: {
      name: "test",
      age: 10
    }
  },
  setState(newState) {
    store.state = newState
		store.listeners.forEach($0 => $0())
  },
	listeners: [],
	subscribe(fn) {
		store.listeners.push(fn)
		return () => {
			store.listeners = this.listeners.filter($0 => $0 === fn)
		}
	}
}

// connect 函数中订阅变化,当监听到变化时,强行刷新组件
const connect = Component => props => {
  const { state, setState, subscribe } = useContext(Context)
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  const dispatch = ({ type, payload }) => {
    setState(reducer(state, { type, payload }))
  }
  useEffect(() => {
	const clean = subscribe(forceUpdate)
	return clean
  }, [])
  return <Component {...props} state={state} dispatch={dispatch} />
}

最后将 A 也使用 connect 包裹一下,当 state 变化时也能发生变化了。

实现 mapStateToProps

上面的代码可以优化的一个点就在于

  1. 无法很好的实现 state 的按需变化,目前是只要订阅了就一定会出更新。
  2. state 获取值较长时,没有办法将其封装成函数复用
// 柯西化实现 mapStateToProps
const connect = mapStateToProps => Component => props => {
  const { state, setState, subscribe } = useContext(Context)
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  const dispatch = ({ type, payload }) => {
    setState(reducer(state, { type, payload }))
  }

  const stateProps = mapStateToProps ? mapStateToProps(state) : { state }

  useEffect(() => {
    const clean = subscribe(forceUpdate)
    return clean
  }, [])
  
  return <Component {...props} {...stateProps} dispatch={dispatch} />
}

// 封装 getUser 以复用
const getUser = state => ({ user: state.user })
const A = connect(getUser)(({ user }) => {
  return <div>{user.name}</div>
})

这样就实现了上面的第二个需求

实现精准渲染

那么如何实现上面的第一个需求呢?

其实很简单,在 forceUpdate 之前判断一下是否需要更新即可

// 判断是否需要刷新组件的算法
const changed = (state, newState) => {
  for (const key in state) {
    if (state[key] !== newState[key]) {
      return true
    }
  }
  return false
}

const connect = mapStateToProps => Component => props => {
  const { state, setState, subscribe } = useContext(Context)
  const stateProps = mapStateToProps ? mapStateToProps(state) : { state }
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  const dispatch = ({ type, payload }) => {
    setState(reducer(state, { type, payload }))
  }
  useEffect(() => {
    const clean = subscribe(() => {
      const newStateProps = mapStateToProps
        ? mapStateToProps(store.state)
        : { state: store.state }
      if (changed(stateProps, newStateProps)) forceUpdate()
    })
    return clean
  }, [])
  return <Component {...props} {...stateProps} dispatch={dispatch} />
}

实现 mapSelectorToProps

同理上面的代码也还有一个问题

  1. 这个 dispatch 也可以封装成一个函数,直接传给对应的组件使用
const connect = (mapStateToProps, mapSelectorToProps) => Component => props => {
  const { state, setState, subscribe } = useContext(Context)
  const stateProps = mapStateToProps ? mapStateToProps(state) : { state }
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  const dispatch = ({ type, payload }) => {
    setState(reducer(state, { type, payload }))
  }
  const dispatchProps = mapSelectorToProps
    ? mapSelectorToProps(dispatch)
    : { dispatch }

  useEffect(() => {
    const clean = subscribe(() => {
      const newStateProps = mapStateToProps
        ? mapStateToProps(store.state)
        : { state: store.state }
      if (changed(stateProps, newStateProps)) forceUpdate()
    })
    return clean
  }, [])
  return <Component {...props} {...stateProps} {...dispatchProps} />
}

const updateUser = dispatch => ({
  updateUser(attr) {
    dispatch({
      type: "updateUserName",
      payload: attr
    })
  }
})
const B = connect(
  getUser,
  updateUser
)(({ user, updateUser }) => {
  return (
    <input
      value={user.name}
      onChange={e => {
        updateUser({ name: e.target.value })
      }}
    />
  )
})

这样这个 updateUser 就可以得到公用了

实现 connectToUser

上面的代码还有一个优化的地方,就在于说如果每个地方如果要获取或者修改 user 的属性,都要重复为对应的组件写 connect

可以将其包成另一个高阶函数

const connectToUser = connect(getUser, updateUser)

const B = connectToUser(({ user, updateUser }) => {
  return (
    <input
      value={user.name}
      onChange={e => {
        updateUser({ name: e.target.value })
      }}
    />
  )
})

这样所有用到的地方就可以直接使用 connectToUser

实现 Provider

我们平时写 Context 都会将 Provider 封装在一起,这里也一样

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

实现 createStore

上面的 store 里面的 statesetState 都是写死的,应该改成由外部传入的才对

const createStore = (reducer, initState) => {
	store.state = initState
	store.reducer = reducer
	return store
}

这样我们就可以从外部传入 initStatereducer 了。

实现支持异步 Action

首先我们定义一个 fetchUser 函数,由于无法内部获取 updateUser,所以只能由外部传入

const fetchUser = dispatch => {
  setTimeout(() => {
    dispatch({ type: 'updateUserName', payload: { name: '1秒后的值' }})
  }, 1000)
}

但是这样写看起来非常的不优化,按我们的理解应该是 updateUser(fetchUser),这样更符合编程直觉,那如何才能够做到呢?

// dispatch 中增加 action 类型的判断
const dispatch = action => {
  if (typeof action === "function") {
    action(dispatch)
  } else {
    setState(store.reducer(state, action))
  }
}

// 组件中就可以这个调用了
<button onClick={() => dispatch(fetchUser)}>异步</button>

这样就可以支持异步 action

实现支持 Promise

const dispatch = action => {
  if (typeof action === "function") {
    action(dispatch)
  } else if (action.payload instanceof Promise) {
    action.payload.then(value =>
      dispatch({ type: action.type, payload: value })
    )
  } else {
      setState(store.reducer(state, action))
  }
}

// 组件中这样使用就可以了
<button
	onClick={() =>
	  dispatch({
		type: "updateUserName",
		payload: new Promise((res, rej) => {
		  setTimeout(() => {
			res("1秒后的值 - promise")
		  }, 1000)
		}).then(name => ({ name }))
	  })
	}
>
	promise
  </button>

这样就可以支持异步 promise

实现支持 middleware

上面支持了异步 actionpromise,如果还有其他需求的话,那么这个里面的代码会一直增长,违反了开闭原则。

这个时候就可以使用 middleware 这个模式来改写,具体写法见实现支持 middleware。

像我们常用的 redux-thunkredux-saga 都是一个中间件。

封装 API

下面是完整代码

import React, { createContext, useContext, useEffect, useReducer } from "react"

let state_ = undefined
let reducer_ = undefined
const listeners = []

const store = {
  getState() {
    return state_
  },
	dispatch(action) {
		if (typeof action === "function") {
      action(store.dispatch)
    } else if (action.payload instanceof Promise) {
      action.payload.then(value => {
        store.dispatch({ type: action.type, payload: value })
      })
    } else {
			state_ = reducer_(store.getState(), action)
			listeners.forEach($0 => $0())
		}
	},
  subscribe(fn) {
    listeners.push(fn)
    return () => {
      listeners = listeners.filter($0 => $0 === fn)
    }
  }
}

const createStore = (reducer, initState) => {
  state_ = initState
  reducer_ = reducer
  return store
}

const Context = createContext()

const changed = (state, newState) => {
  for (const key in state) {
    if (state[key] !== newState[key]) {
      return true
    }
  }
  return false
}

const connect = (mapStateToProps, mapSelectorToProps) => Component => props => {
  const { getState, dispatch, subscribe } = useContext(Context)
  const stateProps = mapStateToProps
    ? mapStateToProps(getState())
    : { state: getState() }
  const [, forceUpdate] = useReducer(x => x + 1, 0)

  const dispatchProps = mapSelectorToProps
    ? mapSelectorToProps(dispatch)
    : { dispatch }

  useEffect(() => {
    const clean = subscribe(() => {
      const newStateProps = mapStateToProps
        ? mapStateToProps(store.getState())
        : { state: store.getState() }
      if (changed(stateProps, newStateProps)) forceUpdate()
    })
    return clean
  }, [])
  return <Component {...props} {...stateProps} {...dispatchProps} />
}

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

至此,我们把 redux 中的核心功能都实现了。

参考

# 来,跟我一起手写 Redux!