简介
React在16.8版本以上可以使用,hooks优点在于能够更好的复用性,也解决无状态组件的生命周期以及状态管理的问题,替代class,可以通过自定义hook的形式将组件分割的更细粒度,方便拓展和维护。
Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题 的解决方案,无需学习复杂的函数式或响应式编程技术。
我们大致看一看react整体渲染流程,方便大家更好的理解react的实现过程,以及接下来的hooks原理讲解
基本使用
useState和useReducer进行派发更新(使函数组件重新渲染)。
useRef缓存变量且更改变量不进行派发更新,ref.current获取缓存的变量
useEffect可以更新副作用,在dom挂载到页面进行渲染之后触发回调函数,比如网络请求,更新时间等。
useLayoutEffect在创建dom之后,挂载dom到页面之前调用,可以进行与渲染无关的操作,比如发布订阅等
useCallback可以缓存函数,当派发更新时,依赖的参数没有变化情况下,不创建新的函数,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useMemo可以缓存状态,做性能优化
useContext可以获取React.createContext创建的上下文对象
const Context = createContext(null);
function Parent () {
return (<Context.Provider value={{ a: 1 }}>
<Child />
</Context.Provider>);
}
function Child () {
const data = useContext(Context)
console.log(data);
return null;
}
export default Parent;
useImperativeHandle配合forwardRef可以将子组件参数透传到父组件,父组件通过ref.current拿到传入的参数
// 使用forwardRef,将接收第二个参数,用于透传ref或结合useImperativeHandle绑定子组件数据
function SetStatePage(props, ref) {
const [count ,setCount] = useState(0)
// 第一个参数接收透传ref,第二个参数接收函数,返回要透传的数据
useImperativeHandle(ref, () => ({
count,
setCount
}))
return (
<div>
<p>You clicked {count} times</p>
</div>
)
}
// forwardRef透传ref
const SetStatePageWithRef = forwardRef(SetStatePage);
function Parent () {
const ref = useRef(null);
return (<div>
<SetStatePageWithRef ref={ref} />
{/* 通过ref.current获取子组件的数据 */}
<button onClick={() => ref.current.setCount(ref.current.count + 1)}>Click me</button>
</div>);
}
export default Parent;
useDebugValue可用于在 React 开发者工具中显示自定义 hook 的标签(必须在自定义Hook中使用),就像下面这样,它还可以接收第二个参数,作为延迟加载的回调函数,接收debug值作为实参,返回处理过的debug值进行显示
export default function SetStatePage(props) {
function useOnline (state) {
const [isOnline, setIsOnline] = useState(state);
useDebugValue(isOnline > 5 ? 'Online' : 'Offline', (debug) => {
if (debug === 'Online') {
return true;
} else {
return false;
}
});
return [isOnline, setIsOnline];
}
function useFriendStatus(friendID) {
const [id, setID] = useState(friendID);
// 在开发者工具中的这个 Hook 旁边显示标签
// "FriendStatus: Online"
useDebugValue(id > 5 ? 'Online' : 'Offline');
return [id, setID];
}
const [id, setID] = useFriendStatus(3);
const [isOnline, setIsOnline] = useOnline(3);
return (
<div>
<p>You clicked {isOnline} times</p>
<button onClick={() => setID(id + 1)}>Click ID</button>
<button onClick={() => setIsOnline(isOnline + 1)}>Click Online</button>
</div>
)
}
Hooks实现原理
像上面useDebugValue中我们可以利用谷歌浏览器的React devtools插件可以查看到当前组件的hooks状态,这个状态是怎么拿到的呢?我们可以深究一下它的实现,然后找到答案
在React中,每次执行useState或者useReducer的更新函数都是触发派发更新,重新执行函数组件进行重新渲染,如果不缓存hooks的状态,那么每次获取到的状态那不都是同一个值吗
那么如何缓存hooks的状态,有缓存在哪里呢? 答案就在react16
fiber架构中
react在大版本16的时候,将虚拟dom的树形结构转换成链表fiber,通过child、sibling、return指向其它节点,这样做的好处是实现了可以打断和设置dom操作优先级的新旧dom对比,diff过程进行碎片化,类似下面的数据结构
{
tag: 标记节点类型,
type: dom标签字符串/函数/类,
elementType: 与type属性基本一致,
key: 节点当前层级下的唯一值,
props: 属性,
stateNode: 原生dom、类实例,
child: 子节点,
return: 父节点,
sibling: 兄弟节点,
ref: ,
alternate: 旧节点,
flags: 操作指令(添加,替换,更新),
deletions: 要删除子节点 null或者[],
index: 当前层级下的下标,
pendingProps: 没有映射到dom时的属性,
memoizedProps: 映射之后的dom属性,
updateQueue: 更新函数队列 进行批量更新,
memorizedState: hook链表的头节点,
flags: 操作节点,
}
hooks的状态其实都存到了fiber.memorizedState上面,这样回答了React devtools插件是怎么拿到状态的,为什么memorizedState为什么是链表结构而不是数组呢,笔者认为是需要非连续的存储空间
为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用
要回答上面的问题,要了解react hooks的实现原理,我们在对Fiber链表进行dfs时,会判断type属性,如果为函数,就会通过type(props)执行函数,获取新的Fiber,reconcileChildren进行新旧节点的diff,以下是对源码实现的简化,便于理解:
// 函数组件
export function updateFunctionComponent(wip) {
renderWithHooks(wip);
const {type, props} = wip;
const newFiber = type(props);
// 协调子节点
reconcileChildren(wip, newFiber);
}
renderWithHooks(重置当前执行Fiber的hook状态)
在执行函数组件之前,会先调用renderWithHooks函数传入当前正在工作的Fiber,将全局变量currentlyRenderingFiber进行赋值,以便后续hooks API的调用
function renderWithHooks(fiber) {
currentlyRenderingFiber = fiber;
currentlyRenderingFiber.memoizedState = null;
// 源码中是一个updateQueue数组进行存储,这里为了简便,分开写区分useEffect和useLayoutEffect
currentlyRenderingFiber.updateQueueOfEffect = [];
currentlyRenderingFiber.updateQueueOfLayout = [];
workInProgressHook = null;
}
updateWorkInProgressHook(获取当前执行的hook)
通过Fiber.alternate(代表旧Fiber)来判断是否为初次渲染
通过全局变量currentlyRenderingFiber当前正在工作的Fiber,workInProgressHook当前正在执行的Hook,currentHook当前正在工作的hook对应的老hook,用于获取当前执行的hook以及提取hook依赖项进行浅比较
function updateWorkInProgressHook() {
let hook = null;
// todo get hook
// 老节点
let current = currentlyRenderingFiber.alternate;
if (current) {
// 更新阶段 新的hook在老的hook基础上更新
currentlyRenderingFiber.memoizedState = current.memoizedState;
if (workInProgressHook) {
// 不是第0个hook
hook = workInProgressHook = workInProgressHook.next;
currentHook = currentHook.next;
} else {
// 是第0个hook
hook = workInProgressHook = current.memoizedState;
currentHook = current.memoizedState;
}
} else {
// 初次渲染阶段
currentHook = null;
hook = {
memoizedState: null, // 状态值
next: null, // 下一个hook
};
if (workInProgressHook) {
// 不是第0个hook
workInProgressHook = workInProgressHook.next = hook;
} else {
// 是第0个hook
workInProgressHook = currentlyRenderingFiber.memoizedState = hook;
}
}
return hook;
}
useState
- 通过
updateWorkInProgressHook获取当前执行的hook - 通过
currentlyRenderingFiber判断是否非首次渲染 - 通过setter函数传入新的状态
newState来更新hook状态以及派发更新
function useState (state) {
const hook = updateWorkInProgressHook();
if (!currentlyRenderingFiber.alternate) {
// 初次渲染
hook.memoizedState = state;
}
const dispatch = (newState) => {
hook.memoizedState = newState;
scheduleUpdateOnFiber(currentlyRenderingFiber);
};
return [hook.memoizedState, dispatch];
}
use(Layout)Effect
- 通过
currentHook保存的之前的依赖和现有依赖进行浅比较 - 通过hookFlag判断异步还是同步执行回调
- Fiber全部diff完成后,对
updateQueue中的回调函数进行同部或异步执行
export function useEffect(create, deps) {
return updateEffectIml(HookPassive, create, deps);
}
export function useLayoutEffect(create, deps) {
return updateEffectIml(HookLayout, create, deps);
}
export function updateEffectIml(hookFlag, create, deps) {
const hook = updateWorkInProgressHook();
const effect = {hookFlag, create, deps};
// 组件更新的时候,且依赖项没有发生变化
if (currentHook) {
const prevEffect = currentHook.memoizedState;
if (deps) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(deps, prevDeps)) {
return;
}
}
}
hook.memoizedState = effect;
if (hookFlag & HookPassive) {
currentlyRenderingFiber.updateQueueOfEffect.push(effect);
} else if (hookFlag & HookLayout) {
currentlyRenderingFiber.updateQueueOfLayout.push(effect);
}
}
当Fiber节点diff完成后,commit进行提交dom挂载时,useLayoutEffect回调立即执行,useEffect函数通过scheduleCallback进行异步调度,会在dom渲染之后执行