深入理解React hooks原理和应用
React Hook
hook解决了什么问题:
- 逻辑复用的问题,hook能够更好的代码组织,UI和逻辑层分离
- class难以理解,复杂组件难以维护,hooks不需要class也能够提供props,state的能力
- hook作为特殊的函数,必须以use作为前缀,使用eslint插件eslint-plugin-react-hooks在检测阶段取保识别到hook代码模块是否符合规则
问题思考
- hook为什么只能在组价最外层调用,不能动态创建或者判断条件创建
- 如何只渲染一次hook
- useRef为什么需要current
Hook的实现原理
我们学习原理主要学习负责状态操作的useState,处理副作用的useEffect,保存比对结果的useMemo,保存数据对象的useRef。
useState
特点
- 设置初始值
- 返回一个数组,包含一个最新值和set方法
- 调用set方法,触发重新渲染
原理
let nextState
const useState = initState => {
nextState = nextState || initState
function setState(newState) {
nextState = newState
render()
}
return [nextState, setState]
};
useEffect
特点
- 处理副作用的hooks,不能理解成监听的概念
原理
// 声明依赖deps的类型
type depsType = ReadonlyArray<any> | undefined
const lastDepsCatch = []
let currIndex = 0
const useEffect = (callBack: Function, deps: depsType) => {
const lastDeps = lastDepsCatch[currIndex]
// 判断依赖deps是否更新
const depsChangBool =
!lastDeps // 首次渲染判断
|| !deps // 没有依赖判断,如果没有deps每次都会执行
|| deps.some((dep, index) => dep !== lastDepsCatch[index])
if (depsChangBool) {
lastDepsCatch[currIndex] = deps
callback()
}
currIndex++;
};
useMemo
特点
- 第一参数参数是回调函数 () => value,第二个参数是依赖deps:any[]
- 当deps发生更改,胡重新计算新的newValue
- 当deps没有发生更改,返回旧的oldValue
原理
// 声明依赖deps的类型
type depsType = ReadonlyArray<any> | undefined
const memoizedState: any[] = [];
let hookIndex = 0;
const useMemo = (callBack: Function, deps: depsType) => {
//判断是不是第一次渲染
if(memoizedState[hookIndex]) {
const [oldVal, oldDeps] = memoizedState[hookIndex];
// 判断是否相等
const isEqual = deps?.every(
(item: any, index: number) => item === oldDeps[index]
);
if (isEqual) {
hookIndex++
return oldVal
}
}
const newValue = callBack()
memoizedState[hookIndex++] = [newValue, deps]
return newValue
};
useRef
特点
- 跨渲染周期保存数据
原理:
useRe方法的调用涉及到两个阶段,分别是mount(第一次渲染)阶段和update(非第一次渲染/更新)阶段,两个不同的阶段涉及的方法的调用逻辑是不相同的
// 参数类型
interface MutableRefObject<T> {
current: T;
}
// mount 阶段
const mountRef<T>(initialValue: T): MutableRefObject<T> => {
const hook = mountWorkInProgressHook()
const ref = {current: initialValue }
hook.memoizedState = ref
return ref
}
const HooksDispatcherOnMount: Dispatcher = {
useRef: mountRef
}
// update 阶段
const updateRef<T>(initialValue: T): MutableRefObject<T> => {
const hook = updateWorkInProgressHook()
return hook.memoizedState
}
const HooksDispatcherOnMount: Dispatcher = {
useRef: updateRef
}
其他Hook:
- useReducer: useReducer和useState的功能和实现原理类似,区别是useReducer使用函数管理状态。
- useCallback:和useMemo的实现原理比较类似,是对useMemo函数的一个封装
多个Hook如何管理
如何确保声明的useState, useEffect的初始化和执行更新顺序:
- 在初始化的时候将所声明的hook保存到memoizedState数组中
- 更新的时候按照数组的执行顺序,将数组的下标依次获取缓存的值进行比对,更新
在React的内部是通过单链表的方式维护memoizedState的
type Hooks = {
memoizedState: any, // 指向当前渲染节点Fiber
baseState: any, //初始化state,每次dispath完成更新
baseUpdate: Update<any> | null,// 需要更新的Update,每次更新完毕之后更新到最新值
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null // 单链表指向的下一个更新的hook
}
Hooks应用
(1)基于hook的性能优化场景之一:子组件只触发一次render
- useMemo:采用useMemo包裹子组件,deps不变的情况下子组件只需要绘制一次
// 需要缓存的组件
const CatchComponent = ...
useMemo(()=> CatchComponent, [deps])
- forwarRef和useImperativeHandle
forwarRef和useImperativeHandle是将子组件的内部状态和方法挂在到ref上,类似下面的伪代码:
const Children = forwardRef((_, ref) => {
useImperativeHandle(ref, ...);
});
const Parent = forwardRef(({ children }, ref) => {
const onClick = () => { ref.update() }
return <>
{children}
</>
});
const ForwardRefExample = () => {
const ref = useRef(null);
return
<Parent ref={ref}>
<Children ref={ref} />
</Parent>
}
(2)封装自己的生命周期hook
-
useMountHook:组件渲染时触发执行
使用场景:初始化数据,网络资源请求,开屏动画,性能埋点等操作
const useMountHook = (fun: () => void) => {
useEffect(() => {
fun()
}, [])
}
-
useUpdateHook:强制函数组渲染,但是不需要通过修改不必要的state完成
使用场景:刷新页面
const useUpdateHook = (fun: () => void) => {
const [, setState] = useState({})
return useCallback(() => setState({}), [])
}
用例:
const UpdateHookExample = () => {
const updateHook = useUpdateHook()
return (
<button type="button" onClick={update}>
currnetTime: {Date.now()}
</button>
);
}
-
useUnMountHook:组件卸载时触发执行
使用场景:组件卸载时需要清除定时器,canvas,webgl内容等内容,释放未完成的网络请求,防止内存泄漏,或者性能埋点上报
如果不封装自己的useUnMountHook组价,我们目前使用的方案是通过useEffec开发完成:
const unMoundeHandle = () => {
console.info('组件卸载,清除定时器!') // 卸载逻辑伪代码
}
useEffect(() => {
...// 业务代码
return unMoundeHandle()
}, [])
我们只需要通过抽象回调函数作为参数,进行简单的组件封装,就可以完成类似unMount的功能实现。
const useUnMountHook = (fun: () => void) => {
useEffect(() => () => {
fun()
}, []);
};
// 使用
useUnMountHook(unMoundeHandle)
(3)逻辑复用:功能hook封装
用例:引用来自社区张立理的代码片段,使用独立的hook实现了用户列表的维护功能,包含了获取用户列表,删除,添加以及网络请求loading状态维护,能够完成耦合功能的模块封装和复用。
const useUserList = () => {
const [pending, setPending] = useState(false);
const [users, setUsers] = useState([]);
const load = async params => {
setPending(true);
setUsers([]);
const users = await request('/users', params);
setUsers(users);
setPending(false);
};
const deleteUser = useCallback(
user => setUsers(users => without(users, user)),
[]
);
const addUser = useCallback(
user => setUsers(users => users.concat(user)),
[]
);
return [users, {pending, load, addUser, deleteUser}];
};
性能优化
react性能优化的原则
- 避免订阅不相关的数据,再采用useMemo和useCallback优化,而是不订阅无相关的数据
hooks的最佳实践
-
useMemo:缓存计算结果,有条件的发生变化
useMemo 在线Demo
interface UserInfoType = {
id:string0
name:string
score:string
}
const UserInfo = ({name, address}) => {
return (
<>
<div>产品名称{name}</div>
<div>产地{address}<>
</>
)
}
const Hook =() => {
const [info, setInfo] = useState<UserInfoType>({})
const updateScore = () => setInfo(s => {...s,score:score+1})
return(
<div>
{
useMemo(()=> UserInfo, [info.name, info.address])
}
<div>用户得分:{info?.score} </div>
<button onClick={updateScore}>加分数<button>
</div>
)
}
-
useRef:缓存数据对象,保持不变
-
useCallback:缓存计算结果,有条件的发生变化,一般和useMemo配合使用
-
useReducer:解决复杂结构的state和state处理逻辑比较复杂的情况
(a) useReducer最常见的使用场景,state是一个对象或者数组,state的变化比较复杂,当我们操作state对象涉及更改多个状态的更新
(b) useReducer 实现UI和业务逻辑分开维护,减少维护成本
useReducer 在线Demo
-
类useUnMount生命周期函数的应用,及时清除定时器,canvas,销毁页面释放请求中的网络请求
const TestComponent = () => {
const timer = setInterval(()=> {
}, 1000)
useEffect(()=> {
return ()=> clearInterval(timer)
}, [])
}
/**
* 涉及三个逻辑的处理
* 1. 分数score的计算
* 2. 提示信息message的维护
* 3. 出局状态gameover状态的维护,状态和ui交互联动
*
*/
const reducer = (state, action) => {
if (state.gameover) {
console.log("比赛已经结束");
return state;
}
if (action === "add") {
console.info("恭喜");
return {
...state,
score: state.score + 1
};
} else if (action === "reduce") {
const score = Math.max(state.score - 1, 0);
const message = !score
? "非常遗憾,你已经淘汰出局"
: "出现失误,扣一分作为处罚";
console.info(message);
return {
...state,
score,
gameover: !score
};
}
return state;
};
function UseReducerExample() {
const initData = {
name: "MrLi",
score: 0,
gameover: false
};
const [player, updatePlayer] = useReducer(reducer initData);
return (
<div>
<p className="title">useReducer-example</p>
<div>
分数:{player.score}
<span className={player.gameover ? "over" : "hidden"}>已经出局!</span>
</div>
<button onClick={() => updatePlayer("add")}>Player 得分</button>
<button onClick={() => updatePlayer("reduce")}>Player 犯规</button>
</div>
);
}
注意事项
hooks的闭包陷阱
function EffectExample() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => {
clearInterval(timer);
};
} , []);
const addCount = () => {
setCount((s) => ++s);
};
return (
<>
<p className="title">useEffect闭包陷阱</p>
<button onClick={addCount}> 加1 </button>
</>
);
}
问题分析:当我们无论触发多少次addCount方法,currCount方法都打印的1,因为在第一次渲染的过程中useEffect的依赖为空数组只触发一次,currCount方法接收参数count:0形成闭包,之后不会在触发currCount函数的执行,count不会更新闭包内的值,所以打印的是1;
解决方案:
- count作为useEffect的依赖,当corrent发生更改,重新触发副作用useEffect
useEffect(currCount , [count])
- useRef
const ref = useRef(null);
ref.current = count;
const fn = () => {
const timer = setInterval(() => {
console.log(ref.current);
}, 1000);
};
避免使用场景
-
过度使用useCallback
(a) 需要和useMemo,reactMemo结合使用,要不然反向优化
-
过度使用useMemo场景
(a) deps为空:空间复杂度没收益
- 可以考虑使用ref方案
(b) deps非空且更新频繁
- 时间复杂度:比对deps依赖时额外的时间复杂度
- 空间复杂度没收益
(c) 太过复杂的deps:需要设计合理的deps结构
(d) 引用相同和开销不大的场景
const useMemoTest = ({id, name}) {
const getUserInfo = useMemo(() => {
queryUserInfo(id,name)
}, [id, name])
return <UserInfo info={getUserInfo}/>
}
极致的性能优化粒度
-
解构的性能消耗
[newCount,setConut]= useCount() 是解构的写法,结构结构复杂和解构数据量比较大也需要考虑优化, 相关资讯Chrome浏览器未来针对于解构的性能优化
参考资料:
- react Hook 官网
- ahooks
- react-use
- React Hooks 里的 useCallback 更新幽灵
- 知乎问题:如何去合理使用 React hook?
- V8 将为 React hooks 改进数组解构的性能
- 「react进阶」一文吃透react-hooks原理
深入理解React hooks原理和应用
React Hook
hook解决了什么问题:
问题思考
Hook的实现原理
我们学习原理主要学习负责状态操作的useState,处理副作用的useEffect,保存比对结果的useMemo,保存数据对象的useRef。
useState
特点
原理
useEffect
特点
原理
useMemo
特点
原理
useRef
特点
原理:
useRe方法的调用涉及到两个阶段,分别是mount(第一次渲染)阶段和update(非第一次渲染/更新)阶段,两个不同的阶段涉及的方法的调用逻辑是不相同的
其他Hook:
多个Hook如何管理
如何确保声明的useState, useEffect的初始化和执行更新顺序:
在React的内部是通过单链表的方式维护memoizedState的
Hooks应用
(1)基于hook的性能优化场景之一:子组件只触发一次render
forwarRef和useImperativeHandle是将子组件的内部状态和方法挂在到ref上,类似下面的伪代码:
(2)封装自己的生命周期hook
useMountHook:组件渲染时触发执行使用场景:初始化数据,网络资源请求,开屏动画,性能埋点等操作
useUpdateHook:强制函数组渲染,但是不需要通过修改不必要的state完成使用场景:刷新页面
用例:
useUnMountHook:组件卸载时触发执行使用场景:组件卸载时需要清除定时器,canvas,webgl内容等内容,释放未完成的网络请求,防止内存泄漏,或者性能埋点上报
如果不封装自己的useUnMountHook组价,我们目前使用的方案是通过useEffec开发完成:
我们只需要通过抽象回调函数作为参数,进行简单的组件封装,就可以完成类似unMount的功能实现。
(3)逻辑复用:功能hook封装
用例:引用来自社区张立理的代码片段,使用独立的hook实现了用户列表的维护功能,包含了获取用户列表,删除,添加以及网络请求loading状态维护,能够完成耦合功能的模块封装和复用。
性能优化
react性能优化的原则
hooks的最佳实践
useMemo:缓存计算结果,有条件的发生变化
useMemo 在线Demo
useRef:缓存数据对象,保持不变
useCallback:缓存计算结果,有条件的发生变化,一般和useMemo配合使用
useReducer:解决复杂结构的state和state处理逻辑比较复杂的情况
(a) useReducer最常见的使用场景,state是一个对象或者数组,state的变化比较复杂,当我们操作state对象涉及更改多个状态的更新
(b) useReducer 实现UI和业务逻辑分开维护,减少维护成本
useReducer 在线Demo
类useUnMount生命周期函数的应用,及时清除定时器,canvas,销毁页面释放请求中的网络请求
注意事项
hooks的闭包陷阱
问题分析:当我们无论触发多少次
addCount方法,currCount方法都打印的1,因为在第一次渲染的过程中useEffect的依赖为空数组只触发一次,currCount方法接收参数count:0形成闭包,之后不会在触发currCount函数的执行,count不会更新闭包内的值,所以打印的是1;解决方案:
避免使用场景
过度使用useCallback
(a) 需要和useMemo,reactMemo结合使用,要不然反向优化
过度使用useMemo场景
(a) deps为空:空间复杂度没收益
(b) deps非空且更新频繁
(c) 太过复杂的deps:需要设计合理的deps结构
(d) 引用相同和开销不大的场景
极致的性能优化粒度
解构的性能消耗
[newCount,setConut]= useCount()是解构的写法,结构结构复杂和解构数据量比较大也需要考虑优化, 相关资讯Chrome浏览器未来针对于解构的性能优化参考资料: