网站的源代码有什么用,少儿编程加盟培宝未来,订阅号 小程序,润滑油网站建设写在前面的小结
每一个 hook 函数都有对应的 hook 对象保存状态信息useContext是唯一一个不需要添加到 hook 链表的 hook 函数只有 useEffect、useLayoutEffect 以及 useImperativeHandle 这三个 hook 具有副作用#xff0c;在 render 阶段需要给函数组件 fiber 添加对应的副…写在前面的小结
每一个 hook 函数都有对应的 hook 对象保存状态信息useContext是唯一一个不需要添加到 hook 链表的 hook 函数只有 useEffect、useLayoutEffect 以及 useImperativeHandle 这三个 hook 具有副作用在 render 阶段需要给函数组件 fiber 添加对应的副作用标记。同时这三个 hook 都有对应的 effect 对象保存其状态信息每次渲染都是重新构建 hook 链表以及 收集 effect list(fiber.updateQueue)初次渲染调用 mountWorkInProgressHook 构建 hook 链表。更新渲染调用 updateWorkInProgressHook 构建 hook 链表并复用上一次的 hook 状态信息
Demo
可以用下面的 demo 在本地调试
import React, {useState,useEffect,useContext,useCallback,useMemo,useRef,useImperativeHandle,useLayoutEffect,forwardRef,
} from react;
import ReactDOM from react-dom;
const themes {foreground: red,background: #eeeeee,
};
const ThemeContext React.createContext(themes);const Home forwardRef((props, ref) {debugger;const [count, setCount] useState(0);const myRef useRef(null);const theme useContext(ThemeContext);useEffect(() {console.log(useEffect, count);}, [count]);useLayoutEffect(() {console.log(useLayoutEffect..., myRef);});const res useMemo(() {console.log(useMemo);return count * count;}, [count]);console.log(res..., res);useImperativeHandle(ref, () ({focus: () {myRef.current.focus();},}));const onClick useCallback(() {setCount(count 1);}, [count]);return (div style{{ color: theme.foreground }} ref{myRef} onClick{onClick}{count} /div);
});ReactDOM.render(Home /, document.getElementById(root));fiber
React 在初次渲染或者更新过程中都会在 render 阶段创建新的或者复用旧的 fiber 节点。每一个函数组件都有对应的 fiber 节点。
fiber 的主要属性如下
var fiber {alternate,child,elementType: () {},memoizedProps: null,memoizedState: null, // 在函数组件中memoizedState用于保存hook链表pendingProps: {},return,sibling,stateNode,tag, // fiber的节点类型初次渲染时函数组件对应的tag为2后续更新过程中对应的tag为0type: () {}updateQueue: null,
}在函数组件的 fiber 中有两个属性和 hook 有关memoizedState 和updateQueue 属性。
memoizedState 属性用于保存 hook 链表hook 链表是单向链表。updateQueue 属性用于收集hook的副作用信息保存useEffect、useLayoutEffect、useImperativeHandle这三个 hook 的 effect 信息是一个环状链表其中 updateQueue.lastEffect 指向最后一个 effect 对象。effect 描述了 hook 的信息比如useLayoutEffect 的 effect 对象保存了监听函数清除函数依赖等。
hook 链表
React 为我们提供的以use开头的函数就是 hook本质上函数在执行完成后就会被销毁然后状态丢失。React 能记住这些函数的状态信息的根本原因是在函数组件执行过程中React 会为每个 hook 函数创建对应的 hook 对象然后将状态信息保存在 hook 对象中在下一次更新渲染时会从这些 hook 对象中获取上一次的状态信息。
在函数组件执行的过程中比如上例中当执行 Home() 函数组件时React 会为组件内每个 hook 函数创建对应的 hook 对象这些 hook 对象保存 hook 函数的信息以及状态然后将这些 hook 对象连成一个链表。上例中第一个执行的是useState hookReact 为其创建一个 hookstateHook。第二个执行的是useRef hook同样为其创建一个 hookrefHook然后将 stateHook.next 指向 refHookstateHook.next refHook。同理refHook.next effectHook…
需要注意
useContext是唯一一个不会出现在 hook 链表中的 hook。useState 是 useReducer 的语法糖因此这里只需要用 useState 举例就好。useEffect、useLayoutEffect、useImperativeHandle这三个 hook 都是属于 effect 类型的 hook他们的 effect 对象都需要被添加到函数组件 fiber 的 updateQueue 中以便在 commit 阶段执行。
上例中hook 链表如下红色虚线中所示 hook 对象及其属性介绍
函数组件内部的每一个 hook 函数都有对应的 hook 对象用来保存 hook 函数的状态信息hook 对象的属性如下
var hook {memoizedState,,baseState,baseQueue,queue,next,
};注意hook 对象中的memoizedState属性和 fiber 的memoizedState属性含义不同。next 指向下一个 hook 对象函数组件中的 hook 就是通过 next 指针连成链表
同时不同的 hook 中memoizedState 的含义不同下面详细介绍各类型 hook 对象的属性含义
useState Hook 对象
hook.memoizedState 保存的是 useState 的 state 值。比如 const [count, setCount] useState(0)中memoizedState 保存的就是 state 的值。hook.queue 保存的是更新队列是个环状链表。queue 的属性如下
hook.queue {pending: null,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: initialState,
};比如我们在 onClick 中多次调用setCount
const onClick useCallback(() {debugger;setCount(count 1);setCount(2);setCount(3);
}, [count]);每次调用setCount都会创建一个新的 update 对象并添加进 hook.queue 中update 对象属性如下
var update {lane: lane,action: action, // setCount的参数eagerReducer: null,eagerState: null,next: null,
};queue.pending 指向最后一个更新对象。queue 队列如下红色实线所示
参考 前端进阶面试题详细解答 在 render 阶段会遍历 hook.queue计算最终的 state 值并存入 hook.memoizedState 中
useRef Hook
hook.memoizedState 保存的是 ref 的值。比如
const myRef useRef(null);那么 memoizedState 保存的是 myRef 的值即
hook.memoizedState {current,
};useEffect、useLayoutEffect 以及 useImperativeHandle
memoizedState 保存的是一个 effect 对象effect 对象保存的是 hook 的状态信息比如监听函数依赖清除函数等属性如下
var effect {tag: tag, // effect的类型useEffect对应的tag为5useLayoutEffect对应的tag为3create: create, // useEffect或者useLayoutEffect的监听函数即第一个参数destroy: destroy, // useEffect或者useLayoutEffect的清除函数即监听函数的返回值deps: deps, // useEffect或者useLayoutEffect的依赖第二个参数// Circularnext: null, // 在updateQueue中使用将所有的effect连成一个链表
};这三个 hook 都属于 effect 类型的 hook即具有副作用的 hook
useEffect 的副作用为Update | Passive即 516useLayoutEffect 和 useImperativeHandle 的副作用都是Update即 4
在函数组件中也就只有这三个 hook 才具有副作用在 hook 执行的过程中需要给 fiber 添加对应的副作用标记。然后在 commit 阶段执行对应的操作比如调用useEffect的监听函数清除函数等等。
因此React 需要将这三个 hook 函数的 effect 对象存到 fiber.updateQueue 中以便在 commit 阶段遍历 updateQueue执行对应的操作。updateQueue 也是一个环状链表lastEffect 指向最后一个 effect 对象。effect 和 effect 之间通过 next 相连。
const effect {create: () { console.log(useEffect, count); },deps: [0]destroy: undefined,tag: 5,
}
effect.next effect
fiber.updateQueue {lastEffect: effect,
};fiber.updateQueue 如下图红色实线所示 hook 对应的 effect 对象如下图红色实线所示 useMemo
hook.memoizedState 保存的是 useMemo 的值和依赖。比如
const res useMemo(() {return count * count;
}, [count]);那么 memoizedState 保存的是返回值以及依赖即
hook.memoizedState [count * count, [count]];useCallback
hook.memoizedState 保存的是回调函数和依赖比如
const onClick useCallback(callback dep);那么 memoizedState[callback, dep]
构建 Hook 链表的源码
React 在初次渲染和更新这两个过程构建 hook 链表的算法不一样因此 React 对这两个过程是分开处理的
var HooksDispatcherOnMount {useCallback: mountCallback,useContext: readContext,useEffect: mountEffect,useImperativeHandle: mountImperativeHandle,useLayoutEffect: mountLayoutEffect,useMemo: mountMemo,useRef: mountRef,useState: mountState,
};
var HooksDispatcherOnUpdate {useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useImperativeHandle: updateImperativeHandle,useLayoutEffect: updateLayoutEffect,useMemo: updateMemo,useRef: updateRef,useState: updateState,
};如果是初次渲染则使用HooksDispatcherOnMount此时如果我们调用 useState实际上调用的是HooksDispatcherOnMount.useState执行的是mountState方法。
如果是更新阶段则使用HooksDispatcherOnUpdate此时如果我们调用 useState实际上调用的是HooksDispatcherOnUpdate.useState执行的是updateState
初次渲染和更新渲染执行 hook 函数的区别在于
构建 hook 链表的算法不同。初次渲染只是简单的构建 hook 链表。而更新渲染会遍历上一次的 hook 链表构建新的 hook 链表并复用上一次的 hook 状态依赖的判断。初次渲染不需要判断依赖。更新渲染需要判断依赖是否变化。对于 useState 来说更新阶段还需要遍历 queue 链表计算最新的状态。
renderWithHooks 函数组件执行
不管是初次渲染还是更新渲染函数组件的执行都是从renderWithHooks函数开始执行。
function renderWithHooks(current, workInProgress, Component, props) {currentlyRenderingFiber workInProgress;workInProgress.memoizedState null;workInProgress.updateQueue null;ReactCurrentDispatcher.current current null || current.memoizedState null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;var children Component(props, secondArg);currentlyRenderingFiber null;currentHook null;workInProgressHook null;return children;
}renderWithHooks 的Component参数就是我们的函数组件在本例中就是Home函数。
Component 开始执行前会重置 memoizedState 和 updateQueue 属性因此每次渲染都是重新构建 hook 链表以及收集 effect list
renderWithHooks 方法初始化以下全局变量
currentlyRenderingFiber。fiber 节点。当前正在执行的函数组件对应的 fiber 节点这里是 Home 组件的 fiber 节点ReactCurrentDispatcher.current。负责派发 hook 函数初次渲染时指向 HooksDispatcherOnMount更新渲染时指向 HooksDispatcherOnUpdate。比如我们在函数组件内部调用 useState实际上调用的是
function useState(initialState) {var dispatcher resolveDispatcher();return dispatcher.useState(initialState);
}
function resolveDispatcher() {var dispatcher ReactCurrentDispatcher.current;return dispatcher;
}每一个 hook 函数在执行时都会调用resolveDispatcher方法获取当前的dispatcher然后调用dispatcher中对应的方法处理 mount 或者 update 逻辑。
以 useEffect 为例在初次渲染时调用的是
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {var hook mountWorkInProgressHook();var nextDeps deps undefined ? null : deps;currentlyRenderingFiber.flags | fiberFlags;hook.memoizedState pushEffect(HasEffect | hookFlags,create,undefined,nextDeps);
}在更新渲染时调用的是
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {var hook updateWorkInProgressHook();var nextDeps deps undefined ? null : deps;var destroy undefined;if (currentHook ! null) {var prevEffect currentHook.memoizedState;destroy prevEffect.destroy;if (nextDeps ! null) {var prevDeps prevEffect.deps;if (areHookInputsEqual(nextDeps, prevDeps)) {pushEffect(hookFlags, create, destroy, nextDeps);return;}}}currentlyRenderingFiber.flags | fiberFlags;hook.memoizedState pushEffect(HasEffect | hookFlags,create,destroy,nextDeps);
}pushEffect 方法构建一个 effect 对象并添加到 fiber.updateQueue 中同时返回 effect 对象。
mountEffectImpl 方法逻辑比较简单而 updateEffectImpl 方法还多了一个判断依赖是否变化的逻辑。
mountWorkInProgressHook以及updateWorkInProgressHook方法用来在函数组件执行过程中构建 hook 链表这也是构建 hook 链表的算法。每一个 hook 函数在执行的过程中都会调用这两个方法
构建 hook 链表的算法
初次渲染和更新渲染构建 hook 链表的算法不同。初次渲染使用mountWorkInProgressHook而更新渲染使用updateWorkInProgressHook。
mountWorkInProgressHook 直接为每个 hook 函数创建对应的 hook 对象updateWorkInProgressHook 在执行每个 hook 函数时同时遍历上一次的 hook 链表以复用上一次 hook 的状态信息。这个算法稍稍复杂
React 使用全局变量workInProgressHook保存当前正在执行的 hook 对象。比如本例中第一个执行的是useState则此时workInProgressHookstateHook。第二个执行的是useRef则此时workInProgressHookrefHook…。
可以将 workInProgressHook 看作链表的指针
mountWorkInProgressHook 构建 hook 链表算法
代码如下
function mountWorkInProgressHook() {var hook {memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,};if (workInProgressHook null) {// hook链表中的第一个hookcurrentlyRenderingFiber.memoizedState workInProgressHook hook;} else {// 添加到hook链表末尾workInProgressHook workInProgressHook.next hook;}return workInProgressHook;
}可以看出初次渲染构建 hook 链表的算法逻辑非常简单为每一个 hook 函数创建对应的 hook 对象然后添加到 hook 链表末尾就行
updateWorkInProgressHook 构建 hook 链表算法
更新渲染阶段构建 hook 链表的算法就比较麻烦。我们从 fiber 开始
我们知道 React 在 render 阶段会复用 fiber 节点假设我们第一次渲染完成的 fiber 节点如下
var firstFiber {..., // 省略其他属性alternate: null, // 由于是第一次渲染alternate为nullmemoizedState, // 第一次渲染构建的hook链表updateQueue, // 第一次渲染收集的effect list
};当我们点击按钮触发更新renderWithHooks 函数开始调用但 Home 函数执行前此时workInProgressHook、currentHook都为 null。同时新的 fiber 的memoizedState、updateQueue都被重置为 null workInProgressHook用于构建新的 hook 链表
currentHook用于遍历上一次渲染构建的 hook 链表即旧的链表或者当前的链表(即和当前显示的页面对应的 hook 链表)
按照本例中调用 hook 函数的顺序一步步拆解updateWorkInProgressHook算法的过程
第一步 调用 useState
由于此时 currentHook 为 null因此我们需要初始化它指向旧的 hook 链表的第一个 hook 对象。
if (currentHook null) {var current currentlyRenderingFiber.alternate;if (current ! null) {nextCurrentHook current.memoizedState;} else {nextCurrentHook null;}
}currentHook nextCurrentHook;创建一个新的 hook 对象复用上一次的 hook 对象的状态信息并初始化 hook 链表
var newHook {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null, // 注意next被重置了!!!!!
};if (workInProgressHook null) {currentlyRenderingFiber.memoizedState workInProgressHook newHook;
}第二步 调用 useRef
此时 currentHook 已经有值指向第一个 hook 对象。因此将 currentHook 指向它的下一个 hook 对象即第二个
if (currentHook null) {
} else {nextCurrentHook currentHook.next;
}
currentHook nextCurrentHook;同样的也需要为 useRef 创建一个新的 hook 对象并复用上一次的 hook 状态 后面的 hook 的执行过程和 useRef 一样都是一边遍历旧的 hook 链表为当前 hook 函数创建新的 hook 对象然后复用旧的 hook 对象的状态信息然后添加到 hook 链表中
从更新渲染的过程也可以看出hook 函数的执行是会遍历旧的 hook 链表并复用旧的 hook 对象的状态信息。这也是为什么我们不能将 hook 函数写在条件语句或者循环中的根本原因我们必须保证 hook 函数的顺序在任何时候都要一致
完整源码
最终完整的算法如下
function updateWorkInProgressHook() {var nextCurrentHook;if (currentHook null) {var current currentlyRenderingFiber$1.alternate;if (current ! null) {nextCurrentHook current.memoizedState;} else {nextCurrentHook null;}} else {nextCurrentHook currentHook.next;}var nextWorkInProgressHook;if (workInProgressHook null) {nextWorkInProgressHook currentlyRenderingFiber$1.memoizedState;} else {nextWorkInProgressHook workInProgressHook.next;}if (nextWorkInProgressHook ! null) {// Theres already a work-in-progress. Reuse it.workInProgressHook nextWorkInProgressHook;nextWorkInProgressHook workInProgressHook.next;currentHook nextCurrentHook;} else {// Clone from the current hook.if (!(nextCurrentHook ! null)) {{throw Error(formatProdErrorMessage(310));}}currentHook nextCurrentHook;var newHook {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null,};if (workInProgressHook null) {// This is the first hook in the list.currentlyRenderingFiber$1.memoizedState workInProgressHook newHook;} else {// Append to the end of the list.workInProgressHook workInProgressHook.next newHook;}}return workInProgressHook;
}