react hooks在react 16.8版本推出后就广受好评,因为它很好的解决了旧版本react无法优雅的复用状态逻辑的问题,同时官方说明hooks会向后兼容不存在breaking changes,在项目中更好的无缝接入。
背景和意义
- 目前项目中hooks使用越来越普及,我们作为开发者不仅要知其然还要知其所以然
- 让我们在使用过程中能更快的定位排查问题、性能调优
- 学习和了解优秀框架的实现思路
从两个阶段出发分析
- 初次渲染
- 更新阶段
DEMO
我们以最基本的demo开始,其中涉及两个基本的hook:
- useState
- useEffect
import React ,{useState,useEffect} from 'react';
const Example = props => {
const [count,setCount] = useState(0);
useEffect(()=> {
console.log('xiaoliu test');
},[count])
return (
<div>
<p>You click {count} me</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
export default Example;
一、初次渲染
核心流程
上图就是我们某一个hook组件在初次渲染时所经历的核心流程,大致分为三步:
- 组件类型解析(是Function、class类型),然后去执行对应类型的处理方法
- 判断当前为Function类型组件,所以执行相应方法。首先在当前组件(
也就是当前fiberNode,react16引入了fiber概念,每一个组件对于react来说其实就是一个fiber节点,所有的fiber节点构成了一颗fiber树
,具体概念参考:https://segmentfault.com/a/11...)上进行hook的创建和挂载,将我们所有的hook api挂载到全局变量上(全局变量为dispatcher) - 然后顺序执行当前组件,每遇到一个hook api通过next把它连接到我们的当前fiberNode的hook链表上
fiberNode结构
初次渲染完成后的当前fiberNode(组件)中的结构关系可以用下图表示:
源码一览
hook api挂载
初次渲染currentDispatcher
为空,先挂载所有hook到当前fiberNode的dispatcher,其实也就是将HooksDispatcherOnMountInDEV
变量赋值到dispatcher上
{
// 首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}
看看HooksDispatcherOnMountInDEV内部做了什么
发现正是我们熟悉的useState等各种原生hook,他们内部其实是调用的mountXXX方法
HooksDispatcherOnMountInDEV = {
useCallback: function (callback, deps) {
return mountCallback(callback, deps);
},
useEffect: function (create, deps) {
return mountEffect(create, deps);
},
useMemo: function (create, deps) {
return mountMemo(create, deps);
},
useState: function (initialState) {
return mountState(initialState);
}
}
回到我们的demo,首先是mountState
看看内部做了什么
其实做了三件事:
- 创建当前hook节点,节点的数据结构为上图红框。memorizedState是我们最终返回的初始值;queue其实是更新队列,当我们多次更新某一状态时需要用queue队列存取和遍历;next用来连接下一个hook
- 将当前hook连接到当前的fiberNode的hook链表上
- 绑定状态更新方法(dispatchAction),并返回[state,dispatchAction]
继续看demo,到useEffect,内部实际上执行mountEffectImpl方法
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 创建并获取当前hook节点信息
var hook = mountWorkInProgressHook();
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
}
function mountWorkInProgressHook() {
// 将当前hook连接到我们的hook链表中
var hook = {
memoizedState: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag, // 更新标识
create: create, // 传入的回调,也就是我们开发时的第一个参数
destroy: destroy, // return 的函数,组件销毁时执行的函数
deps: deps, // 依赖项数组
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 这里做的就是把每个useEffect hook单独以链式结构存到了componentUpdateQueue这个全局变量中
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}
综上useEffect内部做了两件事:
- 先执行
mountWorkInProgressHook
方法,就是将当前hook连接到我们的fiberNode的hook链表中 - 定义effect对象存储我们传入的信息,同时将hook存入
componentUpdateQueue
更新队列(这个队列是用来专门存储useEffect hook的,因为useEffect的回调是异步执行的,所以在一次调和结束页面渲染后会去遍历componentUpdateQueue
更新队列遍历执行我们存入的effect回调)
至此我们初次渲染结束,我们此时fiberNode的hook链式结构为
// 当前fiber节点的内部hook链
currentFiber:{
...
memoizedState:{
memoizedState:xxx,
...
next:{
memoizedState:xxx,
...
next:{
memoizedState:xxx,
...
next:hook4
}
}
}
}
更直观一些看的话如下图所示,其实就是一个单向链表
初次渲染阶段我们根据两个基础hook深入源码来分析了一下它内部的处理机制,那么在状态更新阶段它内部又是如何实现的?(未完待续...)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。