思考
两个同级组件如何进行通信?
- 将状态抽离到公共的父组件中,然后组子件调用父组件的方法,去更新另一个子组件
- 使用
Context将value传递到子组件进行调用
方法一的问题在于嵌套很深的情况下,会出现很多子组件会增加完全不需要的属性。当然可以通过 render props 将子组件透传的方式来解决。但是这又会产生严重的嵌套问题。
方法二的问题在于 value 改变的时候,所有的 children 都将 rerender,性能较差。
但是如果 value 不改变的话,那么就没有性能问题了。好像就满足我们的需求了。
那么如果做到 value 不发生变化呢?
- 使用
ref - 使用全局变量
- 使用
ref.current和class
初始化 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
上面的代码可以优化的一个点就在于
- 无法很好的实现
state的按需变化,目前是只要订阅了就一定会出更新。 - 当
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
同理上面的代码也还有一个问题
- 这个
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 里面的 state、setState 都是写死的,应该改成由外部传入的才对
const createStore = (reducer, initState) => {
store.state = initState
store.reducer = reducer
return store
}
这样我们就可以从外部传入 initState 及 reducer 了。
实现支持异步 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
上面支持了异步 action 和 promise,如果还有其他需求的话,那么这个里面的代码会一直增长,违反了开闭原则。
这个时候就可以使用 middleware 这个模式来改写,具体写法见实现支持 middleware。
像我们常用的 redux-thunk 及 redux-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 中的核心功能都实现了。