其他分享
首页 > 其他分享> > React17 Hooks系列实现

React17 Hooks系列实现

作者:互联网

1.前置说明

本次react版本采用react17.0.0版本  下载zip解压即可  https://github.com/facebook/react/tree/v17.0.0

2. 如何调试源码

代码调试方式(下载链接

3.一些前置内容

关于fiber节点和fiber树

react运行的时候存在一个根节点FiberRootNode以及两颗树

current tree是已经渲染到dom上的树,workInProgress tree是由本次更新产生的树 用于生成最新的视图。比如以下app组件

 

 

 

 

你可以通过command+F搜索commitRoot(root)并在这行代码打上断点 刷新后查看root的结构,此时的root是整个应用的根节点FiberRootNode,查看其finishWork属性就是本次render阶段产生的workInProgress树。整颗树中的fiber节点通过child 、return、 sibling相关联

 

 

 

因为hooks是在函数组件中使用的,我们顺着以上的结构通过root.finishedWork.child就可以找到App所对应的fiber tree。控制台打印root.finishedWork.child 查看输出的节点信息。

 

 


 

与hooks相关的信息保存在memorizedState中。updateQueue保存着useEffect这个hook的effect信息。

react运行的几个阶段

1.schedule阶段

2.render阶段 涉及fiber树的创建与更新,diff算法打tag

3.commit阶段 涉及到dom的真实操作,class组件生命周期的执行,函数组件useEffect,useLayoutEffect的执行。

4.useState的初始化阶段

useState的初始化发生在render阶段中,因为创建或更新fiber需要对比App函数返回的jsx对象,就会执行function App()这时候就会调用useState。

我们可以在render阶段的beginWork函数打上断点。

 

 

刷新页面,跳过第一次beginWork的执行,来到第二次,查看workInProgress的信息,会发现本次对应着App的fiber。接着向下执行会发现beginWork根据tag进行不同的update或mount。由于是首次渲染,走的是mountIndeterminateComponent的逻辑,之后会根据jsx返回的信息判断是函数组件还是Class组件。

 

 

这个tag是react内部对fiber节点的分类,比如函数组件=0,类组件=1,HostComponent表示原生dom元素对应的fiber=5,Fragment=7.

具体信息查看react/packages/react-reconciler/ReactWorkTags.js。

 

在mountIndeterminateComponent函数中,会执行一行代码。

value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);

renderWithHooks的精简代码如下

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
// currentlyRenderingFiber$1 currentHook workInProgressHook都是全局变量
// workInProgress对应App函数组件的fiber节点,Component = function App()
currentlyRenderingFiber$1 = workInProgress;
workInProgress.memoizedState = null; // 存储hook节点
workInProgress.updateQueue = null; // 存储useEffect的effect
// react中我们通过验证workInProgress所对应的current是否存在来判断此时是更新阶段还是挂载阶段
// 如果是挂载阶段 我们会选中HooksDispatcherOnMount这个dispatcher
// 如果是更新阶段 我们会选中HooksDispatcherOnUpdate这个dispatcher
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

var children = Component(props, secondArg);
currentlyRenderingFiber$1 = null; // 表示当前的workInProgress节点
currentHook = null; // 表示workInProgress所对应的current节点的hook
workInProgressHook = null; // 表示当前workInProgress所对应的hook
return children // 返回jsx对象 便于child的创建或更新
}

dispatcher是什么?具体信息查看react/packages/react-reconciler/ReactFiberHooks.old.js。这里简单说明下,dispatcher就是个对象,这个对象包含了hooks的所有方法,只不过不同的dispatcher对应的hooks方法执行不同的逻辑。这里由于是首次渲染,不存在App这个fiber节点对应的current节点 所以current=null,会选用HooksDispatcherOnMount这个dispatcher。

执行上述的第19行代码,进入App函数组件的运行,便会执行useState的初始化。我们看看useState函数执行了什么。

function useState(initialState) {
var dispatcher = resolveDispatcher(); // 获取ReactCurrentDispatcher$1.current的dispatcher
return dispatcher.useState(initialState); // 执行当前dispatcher上的useState方法
}
// 最终会执行mountState
function mountState(initialState) {
var hook = mountWorkInProgressHook(); // 创建hook对象 一个hook对应一个hooks方法

if (typeof initialState === 'function') {
// 如果useState传入一个函数 执行函数将返回值作为初始值
initialState = initialState();
}
// hook.memoizedState 保存着初始值
hook.memoizedState = hook.baseState = initialState;
// hook.queue保存着更新
// useState的返回数组第二个参数setXXX当调用时就会产生一个更新Update放入hook.queue 用于计算最新的状态。
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer, // 预先传入一个reducer
lastRenderedState: initialState
};
// dispatchAction用于产生更新
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}

注意第20行代码,为什么会传入一个reducer?因为reducer与useReducer的实现是一致的。

先来看看mountWorkInProgressHook做了什么。

 

// 创建hook 赋值给fiber节点的memoizedState
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
// workInProgressHook是全局变量,保存着App执行时产生的hook 单向链表
// currentlyRenderingFiber$1是全局变量 在renderWithHooks的时候被赋值WorkInProgress
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}

return workInProgressHook;
}

再来看看dispatchAction做了什么。

// 精简代码
function dispatchAction(fiber, queue, action) {
// 创建了一个update对象 保存着更新的信息
var update = {
eventTime: eventTime,
lane: lane,
suspenseConfig: suspenseConfig,
action: action, // setXXX传入的参数
eagerReducer: null,
eagerState: null,
next: null // 指向下一个update
};
// 把update加入到hook.queue.pending中 构成一个环状链表
// queue.pending指向最新的update update.next指向第一个update
var pending = queue.pending;

if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}

queue.pending = update;

const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
// 获取上一次的值
const currentState: S = (queue.lastRenderedState: any);
// 计算本次的值
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// 如果两次计算结果一致 不会走到scheduleUpdateOnFiber的逻辑,也就是不会重新更新
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
// 可以简单理解为把render阶段的入口函数放入一个任务队列 会等到所有的update创建后再执行render阶段·
scheduleUpdateOnFiber(fiber, lane, eventTime);
}

至此,useState的初始化就完成了。总结一下,useState的初始化过程发生在render阶段,在beginWork中函数组件对应的workInProgress节点根据自身的tag进入mountIndeterminateComponent这个函数,这个函数中会通过判断当前current是否存在来决定调用哪个dispatcher,每一次hooks的调用都会创建一个hook对象,这个hook对象会赋值给hook.memorizedState,hook之间通过next属性关联。

5.useState的更新阶段

上面讲到,当我们调用useState的返回值setXXX时会产生一个Update对象,这个会加入hook.queue.pending里,我们可以通过打断点查看。

首次渲染完成后,我们在function beginWork上打上断点,点击按钮,跳过第一次hostRoot的beginWork,来到函数组件App的beginWork。

打开控制台,打印workInProgress.memorizedState。

 memorizedState对应的第一个hook对象与以下代码对应。

const [count, setCount] = useState(0);

而queue.pending对应的update对象与以下代码对应。

 <button onClick={() => setCount((count) => count + 1)}>+++</button> 接下来我们在function renderWithHooks这个函数打下断点。首次渲染后点击按钮,触发更新阶段的renderWithHooks。更新阶段采用HooksDispatcherOnUpdate作为hooks的dispatcher。

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
currentlyRenderingFiber$1 = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;

ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

var children = Component(props, secondArg);
currentlyRenderingFiber$1 = null; // 表示当前的workInProgress节点
currentHook = null; // 表示workInProgress所对应的current节点的hook
workInProgressHook = null; // 表示当前workInProgress所对应的hook
return children // 返回jsx对象 便于child的创建或更新
}

执行Component(props, secondArg)也就是App再次执行以下代码。

const [count, setCount] = useState(0);

执行useState函数。

 

// 此次的dispatcher是HooksDispatcherOnUpdateInDEV而不是首次渲染时的HooksDispatcherOnMountInDEV
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

本次的useState最终会执行updateState, updateState其实是执行了useReducer这个hooks的更新阶段的逻辑。

function updateState(initialState) {
return updateReducer(basicStateReducer);
}


updateReducer忽略传入的initialState,并传入了一个内置的reducer函数。

 

function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}

接下来看看updateReducer做了什么。以下是精简代码,具体代码请查看react/packages/react-reconciler/src/ReactFiberHooks.old.js

 

// 为了好理解,这里删掉了一些关于处理优先级的逻辑
// 因为我们采用ReactDom.render()渲染视图采用的是react的legacy模式,没有优先级的概念,
// 而如果通过ReactDom.createRoot().render()开启concurrent模式,那么就会出现高优先级的更新中断低优先级更新的情况
// 如果低优先级更新任务被中断 那么就会保存在baseQueue中在下一次更新中执行,所以理论上认为legacy模式下baseQueue一直为null
// updateReducer做的就是把hook.queue.pending里的update放入baseQueue里,循环调用update上的action,返回最终状态.
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;

queue.lastRenderedReducer = reducer;
var current = currentHook;

var baseQueue = current.baseQueue;

var pendingQueue = queue.pending;

if (pendingQueue !== null) {
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

if (baseQueue !== null) {
// We have a queue to process.
var first = baseQueue.next;
var newState = current.baseState;
var newBaseState = null;
var update = first;
var newBaseQueueLast = null;

do {
var action = update.action;
newState = reducer(newState, action);

update = update.next;
} while (update !== null && update !== first);

hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}

var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}

下面这幅图描述了此时我们处于的阶段,current树是我们初次渲染的结果。

 


 

我们看看updateWorkInProgressHook这个函数做了什么。可以简单理解为hook.memorizedState是个链表,沿着链表顺序把current tree上对应fiber的hook对象复制过来作为自己的hook。

/*
这里有几个全局变量
currentlyRenderingFiber当前正在执行的workInProgress节点App
currentHook 表示对应的current tree中App fiber节点上的hook,也就是首次渲染时初始化的hook对象
workInProgressHook 表示本次构建的hook节点
这三个变量会在renderWithHooks结尾置为null
currentlyRenderingFiber在执行renderWithHooks会置为当前的WorkInProgress节点也就是App fiber
*/
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate; // 获取current树上的App fiber
if (current !== null) {
nextCurrentHook = current.memoizedState; // 获取App fiber上的第一个hook
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next; // 如果currentHook存在 获取下一个hook
}

let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
// 当前workInProgress fiber节点上的memoizedState在renderWithHooks阶段置为null
} else {
nextWorkInProgressHook = workInProgressHook.next;
}

if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

currentHook = nextCurrentHook;
} else {
// 走这段逻辑
currentHook = nextCurrentHook;
// 创建一个hook对象 copy了currentHook的属性
const newHook: Hook = {
memoizedState: currentHook.memoizedState,

baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,

next: null,
};

if (workInProgressHook === null) {
// 把这个newHook作为workInProgress的第一个hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}

那么我们可以解释这条编写hook的准则了:不要在循环,条件或嵌套函数中调用 Hook。

// 比如我们调用了三个useState
function Test() {
const [a, setA] = useState(true)
if(a) {
const [b, setB] = useState('string b')
}
const [c, setC] = useState(100)

<button onClick={() => {
setA(false);
setC(c => c + 100)
}>+++</button>
}
我们第一次初始化时 产生的fiber如下
Test_fiber_1.memorizedState = hookA -> hookB -> hookC

当我们点击button时 workInProgress节点会遍历current tree上的对应fiber
遍历 Test_fiber_1.memorizedState的hook

对于 const [a, setA] = useState(true) 获取到了正确的hook 执行action:false
此时a=false 以下代码不会执行 跳过
if(a) {
const [b, setB] = useState('string b')
}
执行 const [c, setC] = useState(100) 但此时获取到的是第二个hook 也就是useState('string b')产生的hook
hook.baseState = 'string b' 执行action: c => c + 100 新的hook状态就会出错.

至此,useState的更新状态结束

6.useReducer的实现

useReducer与useState的实现非常相似,除了两者初始化调用的函数不一致,更新阶段是一致的都是调用了updateReducer。两者在fiber节点上的存储形式也是一致的.

// 初始化调用的useReducer最终调用了mountReducer 内部根据传入的initialArg设为初始值
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

   

标签:current,系列,Hooks,queue,hook,useState,React17,null,workInProgress
来源: https://www.cnblogs.com/dorae91/p/16390645.html