对 React Hooks 的理解
三个角度
- 工程化角度:Hooks 存在的意义
- 设计者角度:实现 Hooks 要解决的问题、核心 API 分类
- 开发者角度:基于 Hooks 对服务端状态进一步封装
工程化角度:Hooks 存在的意义
Hooks 解决了什么问题?
❌ 使函数式组件拥有状态,从而实现 Class 组件的功能
function App() {
// this.state this.setState
const [state, setState] = useState()
// componentDidMount()
useEffect(() => {
// componentWillUnmount()
return () => {}
}, [])
// componentDidUpdate()
useEffect(() => {})
// render()
return <div></div>
}✅ 👇

React 没有将「逻辑」「抽离出」组件的能力
Class:逻辑属于某个组件
class XXX extends Component {
state = { bool: false }
handleTrue() {}
handleFalse() {}
render() {}
}hooks:独立逻辑,不属于任何组件
function useBoolean() {
const [bool, setBool] = useState(false)
const handleTrue = () => {}
const handleFalse = () => {}
}##「视图」「逻辑」分离
React
- mixins、render-props、Hoc
Vue
- mixins、scope-component、Hoc
Angular
- DI
状态管理方案
- Redux、Mobx ...
为什么要执着于「视图」「逻辑」分离?
软件开发的难点
如何更好的适应无休止的需求变化
巨人的肩膀
分层架构、设计模式、领域驱动、SOLID、KISS、YAGNI、DRY、迪米特法则 ...
小步「重构」
「每次」需求变化时,都将代码「重构」成最适合当前的
##「分离」让下一个程序员更易「重构」
###「分离」为主,「复用」是顺其自然的
如若下次需求需要复用,分离的代码可以让下个人很容易的「重构」,反之则不行。
好的代码不是告诉计算机怎么做,
而是告诉另一个程序员你想要计算机怎么做。
--《趣学设计模式》

视图也可以是逻辑的一部分

设计者角度:实现 Hooks 要解决的问题
以下均为概念性的思考和理解,不等于最佳实践 & 不等于源码实现
useRef:状态保存
React 的运行机制是每次更新时,函数都会重新运行,这意味着作用域重建,函数内的状态丢失
所以需要将状态存在函数作用域之外,且要与组件的生命周期挂钩(申请和释放)
useRef 就提供了这样的能力,这很重要,因为一旦有了状态,我们自己就可以做很多事情
需要框架层提供能力
const root = document.getElementById("root")
function App() {
// let i = 0 ❌
const i = useRef(0)
return <h1>{i.current++}</h1>
}
setInterval(() => {
ReactDOM.render(<App />, root)
}, 1000)useState:触发更新
useState 的核心在于 setter 函数,它会触发视图更新
实际上,我们完全可以使用 useRef + setter 来完成状态更新
只是说明有了持久化状态后我们可以这样做,实际工作中不要这么做(会被打
const root = document.getElementById("root")
const { useState } = React
function App() {
const i = useRef(0)
let [, reRender] = useState(0)
setInterval(() => {
i.current++
reRender()
}, 1000)
return <h1>{i.current}</h1>
}
ReactDOM.render(<App />, root)useCtlCall:控制函数调用
如果你尝试了上面的代码,很快就会页面更新出现了问题
这是因为每次 reRender,组件函数都会重新运行,导致 setInterval 被重复多次的注册
所以我们需要某种方式,来控制函数的调用逻辑,同样的有了状态之后,这并不是一个困难的事情
const root = document.getElementById("root")
const { useState, useRef } = React
const isEq = (value, other) => {
if (Object.is(value, other)) return true
if (Array.isArray(value) && Array.isArray(other)) {
if (value.length === 0 && other.length === 0) return true
return value.every((item, i) => Object.is(item, other[i]))
}
return false
}
function useCtlCall(fn, deps) {
const prevDeps = useRef(undefined)
if (isEq(prevDeps.current, deps)) return
fn()
prevDeps.current = deps
}
function App() {
let [i, setState] = useState(0)
useCtlCall(() => {
setInterval(() => {
setState(i++)
}, 1000)
}, [])
return <h1>{i++}</h1>
}
ReactDOM.render(<App />, root)useMemo & useCallback
是的,轻松就可以实现 useMemo 和 useCallback
不等于实际源码
function useMemo(fn, deps) {
const cacheValue = useRef()
useCtlCall(() => {
cacheValue.current = fn()
}, deps)
return cacheValue.current
}
function useCallback(fn, deps) {
return useMemo(() => fn, deps)
}useLayoutEffect & useEffect
同上是用来控制函数调用时机,但是需要集成进框架中,以在适当的时机触发函数调用
useContext
不等于实际源码
export const createContext = (defaultValue) => {
const context = {
value: defaultValue,
subs: new Set(),
Provider: ({ value, children = "" }) => {
useEffect(() => {
context.subs.forEach((fn: any) => fn(value))
context.value = value
})
return children
},
}
return context
}
export const useContext = (context, selector?) => {
const subs = context.subs
const [, forceUpdate] = useReducer((c) => c + 1, 0)
const selected = selector ? selector(context.value) : context.value
const ref = useRef(null)
useEffect(() => {
ref.current = selected
})
useEffect(() => {
const fn = (nextValue: unknown) => {
if (selector && ref.current === selector(nextValue)) return
forceUpdate(nextValue)
}
subs.add(fn)
return () => subs.delete(fn)
}, [subs])
return selected
}开发者角度:基于 Hooks 对服务端状态进一步封装
Dva 服务端状态管理
如 redux 或其他框架也是一样的
重复代码
观察下面的代码中有多少是重复的,可封装的
const m: IReuseTaskDetailModel = {
namespace: "reuseTaskDetail",
state: {
detail: {},
auditRecord: {},
list: {},
},
effects: {
*getTaskDetail({ payload: id }, { call, put }) {
const { data } = yield call(() => axios.get("xxx"), id)
return yield put({
type: "mergeModel",
payload: {
detail: data,
},
})
},
*getAuditRecord({ payload: params }, { call, put }) {
const { data, paging } = yield call(() => axios.get("xxx"), params)
return yield put({
type: "mergeModel",
payload: {
auditRecord: {
data,
paging,
},
},
})
},
*getList({ payload: params }, { call, put }) {
const { data, paging } = yield call(() => axios.get("xxx"), params)
return yield put({
type: "mergeModel",
payload: {
list: {
data,
paging,
},
},
})
},
},
}封装重复
事实上我们可以把所有请求全放入一个 namespace,使用 api 地址做 state[key] 即可
const mergeState = ({ put, key, data, isLoading, isError }) =>
put({
type: "mergeModel",
payload: {
[key]: {
data,
isLoading,
isError,
},
},
})
interface State {
// 接⼝地址
[key: string]: {
data: any
isLoading: boolean
isError: boolean
}
}
const serverModel = {
namespace: "serverModel",
state: {},
effects: {
*fetch({ payload: { key, fn } }, { call, put }) {
try {
yield mergeState({
put,
key,
data: undefined,
isLoading: true,
isError: false,
})
const data = yield call(fn)
yield mergeState({ put, key, data, isLoading: false, isError: false })
return data
} catch (e) {
yield mergeState({
put,
key,
data: undefined,
isLoading: false,
isError: true,
})
return Promise.reject(e)
}
},
},
}使用
function useTaskList() {
const key = "/api/tasklist"
const dispatch = useDispatch()
useEffect(() => {
dispatch({
type: "serverModel/fetch",
payload: {
key,
fn: fetchTaskList,
},
})
}, [])
return useSelector(({ serveState }) => serveState[key])
}
function TaskList() {
const { data, isLoading } = useTaskList()
return <div>{data}</div>
}接着封装
细看会发现取值逻辑也是重复的,依然可以简化
function useQuery(key, fn) {
const dispatch = useDispatch()
useEffect(() => {
dispatch({
type: "serverModel/fetch",
payload: {
key,
fn,
},
})
}, [])
return useSelector(({ serveState }) => serveState[key])
}
function useTaskList() {
return useQuery("/api/tasklist", fetchTaskList)
}Mutation
查询逻辑是挂载时自动请求的,写入逻辑则需要手动触发,所以可以再简单封装下
function useMutation(key, fn) {
const dispatch = useDispatch()
return {
...useSelector(({ serveState }) => serveState[key]),
mutate() {
dispatch({
type: "serverModel/fetch",
payload: {
key,
fn,
},
})
},
}
}
function useDelTask() {
return useMutation("/api/del/tasklist", fetchTaskList)
}封装后的状态管理代码
是不是干净整洁无异味,
const useTaskList = () => useQuery("/api/xxx", fetchXXX)
const useTaskDetail = () => useQuery("/api/xxx", fetchXXX)
const useAuditTask = () => useQuery("/api/xxx", fetchXXX)
const useAuditInfo = () => useQuery("/api/xxx", fetchXXX)
const useDelTask = () => useMutation("/api/xxx", fetchXXX)
const usePostTask = () => useMutation("/api/xxx", fetchXXX)
最后
上述代码只是想说明 hooks 的封装、简化能力,真实的场景中还会有更多的挑战:
- 问题
- 如何重新请求 & 手动控制请求(e.g. 搜索)
- 相同接⼝相同参数的重复请求处理?
- 相同接⼝不同参数的请求处理?
- 优化
- 依赖查询
- 更⽅便的取消请求
- 缓存
- 窗⼝聚焦重新获取
- 数据预取
- 分⻚
- 轮询
- loading 闪烁
实际上工作中我们可以直接使用 react-query 来帮助我们管理服务端状态
而对于服务端状态管理的话题,终端的异步状态管理 中有更深入的探讨,感兴趣可以继续阅读