hydrate
setState
forceUpdate
所以这篇文章主要围绕这三个API进行源码的阅读
ReactRoot
FiberRoot
和RootFiber
这一部分主要熟悉流程
我们先找到ReactDom的定义
const ReactDOM: Object = {...
}
其中包含一个render方法,我们先来看一看
函数有两个功能,1、判断传入的容器是否是dom元素,2、调用 legacyRenderSubtreeIntoContainer
函数
render(element: React$Element<any>,container: DOMContainer,callback: ?Function,) {return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,);},
我们可以看到这个方法直接return了一个叫legacyRenderSubtreeIntoContainer
,并把自己的参数又作为这个函数的参数进行了传递,看一下这个函数:
function legacyRenderSubtreeIntoContainer(parentComponent: ?React$Component<any, any>,children: ReactNodeList,container: DOMContainer,forceHydrate: boolean, // 是否复用现有的dom节点,通常用于服务端渲染(hydrate方法调用时必传true)callback: ?Function,
) {// TODO: Ensure all entry points contain this checkinvariant(isValidContainer(container),'Target container is not a DOM element.',);if (__DEV__) {topLevelUpdateWarnings(container);}// TODO: Without `any` type, Flow says "Property cannot be accessed on any// member of intersection type." Whyyyyyy.let root: Root = (container._reactRootContainer: any);//container就是我们传入的dom节点if (!root) {//第一渲染的时候root为空,所以会进入这个判断// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(// 第一次 创建容器[1]container,forceHydrate,);//[5]if (typeof callback === 'function') {//简单的封装callbackconst originalCallback = callback;callback = function() {const instance = PublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Initial mount should not be batched.DOMRenderer.unbatchedUpdates(() => { // 第一次渲染时,该回调直接执行if (parentComponent != null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {der(children, callback);}});} else {if (typeof callback === 'function') {const originalCallback = callback;callback = function() {const instance = PublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Updateif (parentComponent != null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {der(children, callback);//[6]}}PublicRootInstance(root._internalRoot);
}
注意:代码中添加部分注释帮助理解
[1]legacyCreateRootFromDOMContainer
()方法
function legacyCreateRootFromDOMContainer(container: DOMContainer,forceHydrate: boolean,
): Root {const shouldHydrate =forceHydrate || shouldHydrateDueToLegacyHeuristic(container);// First clear any existing content.if (!shouldHydrate) {let warned = false;let rootSibling;while ((rootSibling = container.lastChild)) {//[2]while循环的功能if (__DEV__) {if (!warned &&deType === ELEMENT_NODE &&(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)) {warned = true;warningWithoutStack(false,'render(): Target node has markup rendered by React, but there ' +'are unrelated nodes as well. This is most commonly caused by ' +'white-space inserted around server-rendered markup.',);}}veChild(rootSibling);}}if (__DEV__) {if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {warnedAboutHydrateAPI = true;lowPriorityWarning(false,'render(): der() to hydrate server-rendered markup ' +'will stop working in React v17. Replace der() call ' +'with ReactDOM.hydrate() if you want React to attach to the server HTML.',);}}// Legacy roots are not async st isConcurrent = false;return new ReactRoot(container, isConcurrent, shouldHydrate);//[3]
}
我们传入的container
是一个dom元素,所以没有_reactRootContainer
属性,会调用legacyCreateRootFromDOMContainer
方法,给该方法传入了container
forceHydrate
参数。forceHydrate
主要来区别是服务端渲染(hydrate)还是客户端渲染(render)这个参数在render方法中默认的是false,这其实也是hydrate和render唯一的区别,就是服务端在渲染的时候,第一次的渲染结果应该和客户端是一致的,所以可以进行复用,于是hydrate中的这个参数保持true,而在render中保持false。
[2]shouldHydrate
标记了这个标签是否开启服务器渲染,在没有开启服务器渲染的时候,进入while
循环,这个循环的主要功能就是删除container
的所以子元素。
[3] 最后return
了一个 new ReactRoot(container, isConcurrent, shouldHydrate)
其中由于Legacy roots are not async by default.所以参数isConcurrent
直接被指定为false
再看一下new ReactRoot
的方式
function ReactRoot(container: Container,isConcurrent: boolean,hydrate: boolean,
) {const root = ateContainer(container, isConcurrent, hydrate);this._internalRoot = root;
}
实际上是通过
[5] 实际上,root = container._reactRootContainer
是将new ReactRoot
赋值给了root
,到这里是完成了一个创建root
的过程
[6] 我们看一下der
的方法是怎么做的
der = function(children: ReactNodeList,callback: ?() => mixed,
): Work {const root = this._internalRoot;const work = new ReactWork();callback = callback === undefined ? null : callback;if (__DEV__) {warnOnInvalidCallback(callback, 'render');}if (callback !== null) {work.then(callback);}DOMRenderer.updateContainer(children, root, null, work._onCommit);//这一步至关重要[7]return work;
};
[7]updateContainer()
方法接受四个参数
(element,container,parentComponent,callback)
第一个参数是实际的组件也就是我们脚手架上的<App>
而这里第二个参数是root,不再是曾经的container
第三个参数在der时是null
第四个参数经过封装成为了work._onCommit
export function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function,
): ExpirationTime {const current = container.current;const currentTime = requestCurrentTime();const expirationTime = computeExpirationForFiber(currentTime, current);return updateContainerAtExpirationTime(element,container,parentComponent,expirationTime,callback,);
}
后续我们可以看到一大堆渲染的调度相关的方法的调用,大概是updateContainerAtExpirationTime
→scheduleRootUpdate
→createUpdate
→enqueueUpdate
→scheduleWork
VSCODE可以安装BookMarks扩展插件,在阅读单个文件中的长篇幅代码段时,可以通过Ctrl+Alt+k设置书签,然后使用Ctrl+Alt+L/Ctrl+Alt+J可以实现左右书签跳转,非常方便。
稍微提一下关于判断服务端渲染的方式:
function shouldHydrateDueToLegacyHeuristic(container) {const rootElement = getReactRootElementInContainer(container);return !!(rootElement &&deType === ELEMENT_NODE &&rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME));
}
ROOT_ATTRIBUTE_NAME
作为一个标记,表示如果标签上带着data-reactroot
属性,那么表示是服务端渲染
export const ROOT_ATTRIBUTE_NAME = 'data-reactroot';
函数主要是处理传入的 root
节点,创建一个ReactRoot
,同时创建一个FiberRoot
,创建FiberRoot
的过程中也会创建一个FiberRoot
对象,根据创建的FiberRoot
去更新。
什么是FiberRoot
调用的地点我们可以看到[3]之下的createContainer
方法创建了是一个FiberRooter,我们来看看代码
export function createContainer(containerInfo: Container,isConcurrent: boolean,hydrate: boolean,
): OpaqueRoot {return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
我们可以看到传入参数包含三个,并且返回了createFiberRoot
方法。
export function createFiberRoot(containerInfo: any,isConcurrent: boolean,hydrate: boolean,
): FiberRoot {// Cyclic construction. This cheats the type system right now because// stateNode st uninitializedFiber = createHostRootFiber(isConcurrent);let root;if (enableSchedulerTracing) {root = ({current: uninitializedFiber, // 当前应用对应的Fiber对象containerInfo: containerInfo,pendingChildren: null,earliestPendingTime: NoWork,latestPendingTime: NoWork,earliestSuspendedTime: NoWork,latestSuspendedTime: NoWork,latestPingedTime: NoWork,didError: false,pendingCommitExpirationTime: NoWork,finishedWork: null,timeoutHandle: noTimeout,context: null,pendingContext: null,hydrate,nextExpirationTimeToWorkOn: NoWork,expirationTime: NoWork,firstBatch: null,nextScheduledRoot: null,interactionThreadID: unstable_getThreadID(),memoizedInteractions: new Set(),pendingInteractionMap: new Map(),}: FiberRoot);} else {root = ({current: uninitializedFiber,containerInfo: containerInfo,pendingChildren: null,earliestPendingTime: NoWork,latestPendingTime: NoWork,earliestSuspendedTime: NoWork,latestSuspendedTime: NoWork,latestPingedTime: NoWork,didError: false,pendingCommitExpirationTime: NoWork,finishedWork: null,timeoutHandle: noTimeout,context: null,pendingContext: null,hydrate,nextExpirationTimeToWorkOn: NoWork,expirationTime: NoWork,firstBatch: null,nextScheduledRoot: null,}: BaseFiberRootProperties);}uninitializedFiber.stateNode = root;// The reason for the way the Flow types are structured in this file,// Is to avoid needing :any casts everywhere interaction tracing fields are used.// Unfortunately that requires an :any cast for non-interaction tracing capable builds.// $FlowFixMe Remove this :any cast and replace it with urn ((root: any): FiberRoot);
}
我们可以看到这个方法返回了FiberRoot
const uninitializedFiber = createHostRootFiber(isConcurrent);
创建了一个Fiber对象
这里我们可以学习一下react中的数据结构,从而了解一下Fiber对象,详见文章附录
根据视频内容参考简书博客React源码解析之RootFiber
作用
ReactElement
对应一个Fiber对象ClassComponent
中的state
和props
的状态就是记录在Fiber对象上的。ClassComponent
上的this.state
和this.props
上state
和props
是根据Fiber对象的state
、props
更新的。ReactHooks
,因为hooks
是为FunctionalComponent
服务的。虽然FunctionalComponent
没有this
,但Fiber上有,是可以拿到state
和props
的ReactElement
通过props.children
与其他ReactElement
连结起来注意:在串联时,children只指向第一个子元素节点,其他的子元素节点作为第一个子元素节点的兄弟节点用sibling指向。
这里使用的数据结构是附录Fiber中的:
type Fiber = {|
...// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)stateNode: any,// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回return: Fiber | null,// 单链表树结构// 指向自己的第一个子节点child: Fiber | null,// 指向自己的兄弟结构// 兄弟节点的return指向同一个父节点sibling: Fiber | null,
...
|};
FiberRoot
的current
属性指向一个Fiber
对象,叫 RootFiber
RootFiber
的 stateNode
指向 FiberRoot
FiberRoot
接收的App
对象,也就是根对象,它就是RootFiber
的child
,里面的div
之类的props.children
数组的第一个会作为它的child
,也是FIber对象,第二个就会成为child
的sibling
。这样进行节点的查找就非常方便,通过child一路向下找,找到叶子节点,判断有没有兄弟节点,有就遍历兄弟节点,继续这个循环,最后没有字节点也没有兄弟节点了就会返回通过return,最终返回到最上面
串联过程:
① 任一 叶子 节点A,如果有兄弟节点,则去单向向后遍历兄弟节点,最后return到父节点
② 父节点的child节点不是刚刚的子节点A的话,则从child节点遍历到A前的节点,并再次return到父节点
③ 该父节点执行 ①、②
根据图1举例:
比如从左下角的input节点开始,它没有兄弟节点,则return到父组件Input(因为父节点有且只有一个,所以必定return到父节点)
Input有兄弟节点List,List又有child节点,则从child节点往后单向遍历兄弟节点,最后return到List
List又return到div,div的child节点已被遍历,则return到App节点,App,App又return到所有Fiber对象的根对象RootFiber对象
这样,就将整个应用遍历完了
什么叫做多个Update可以同时存在呢,其实是指在调用多个setState的情况下,不会分多次进行更新,而是将多个setState放进队列进行合并更新。
入口是scheduleRootUpdate
→createUpdate
,我们来看看源码
export const UpdateState = 0;
export const ReplaceState = 1;
export const ForceUpdate = 2;
export const CaptureUpdate = 3;export function createUpdate(expirationTime: ExpirationTime,suspenseConfig: null | SuspenseConfig,
): Update<*> {return {//更新的过期时间expirationTime,suspenseConfig,// export const UpdateState = 0;// export const ReplaceState = 1;// export const ForceUpdate = 2;// export const CaptureUpdate = 3;//重点提下CaptureUpdate,在React16后有一个ErrorBoundaries功能//即在渲染过程中报错了,可以选择新的渲染状态(提示有错误的状态),来更新页面tag: UpdateState, //0更新 1替换 2强制更新 3捕获性的更新//更新内容,比如setState接收的第一个参数payload: null,//对应的回调,比如setState({}, callback )callback: null,//指向下一个更新next: null,//指向下一个side effectnextEffect: null,};
}
入参是我们之前提到通过计算获得的expirationTime
,直接返回一个对象,这个对象中的属性都可以在附录中找到对应数据结构加以了解。
updateQueue
是一个带有优先级的链表,主要是为了保证updata的次序。
//创建更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {const queue: UpdateQueue<State> = {//应用更新后的statebaseState,//队列中的第一个updatefirstUpdate: null,//队列中的最后一个updatelastUpdate: null,//队列中第一个捕获类型的updatefirstCapturedUpdate: null,//队列中最后一个捕获类型的updatelastCapturedUpdate: null,//第一个side effectfirstEffect: null,//最后一个side effectlastEffect: null,firstCapturedEffect: null,lastCapturedEffect: null,};return queue;
}
这个比较容易理解,实质上的作用就是
baseState
在组件setState
后,渲染并更新state
,在下次更新时,拿的就是这次更新过的state
firstUpdate
和lastUpdate
之间的update
通过上个update
的next
串联enqueueUpdate
是一个单向链表,用来存放update
,next
来串联update
//每次setState都会update,每次update,都会入updateQueue
//current即fiber
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {// Update queues are created lazily.//alternate即workInProgress//fiber即current//current到alternate即workInProgress有一个映射关系//所以要保证current和workInProgress的updateQueue是一致的const alternate = fiber.alternate;//current的队列let queue1;//alternate的队列let queue2;//如果alternate为空if (alternate === null) {// There's only one fiber.queue1 = fiber.updateQueue;queue2 = null;//如果queue1仍为空,则初始化更新队列if (queue1 === null) {queue1 = fiber.updateQueue = izedState);}} else {// There are two owners.//如果alternate不为空,则取各自的更新队列queue1 = fiber.updateQueue;queue2 = alternate.updateQueue;if (queue1 === null) {if (queue2 === null) {// Neither fiber has an update queue. Create new ones.//初始化queue1 = fiber.updateQueue = izedState);queue2 = alternate.updateQueue = izedState,);} else {// Only one fiber has an update queue. Clone to create a new one.//如果queue2存在但queue1不存在的话,则根据queue2复制queue1queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);}} else {if (queue2 === null) {// Only one fiber has an update queue. Clone to create a new one.queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);} else {// Both owners have an update queue.}}}if (queue2 === null || queue1 === queue2) {// There's only a single queue.//将update放入queue1中appendUpdateToQueue(queue1, update);} else {// There are two queues. We need to append the update to both queues,// while accounting for the persistent structure of the list — we don't// want the same update to be added multiple times.//react不想多次将同一个的update放入队列中//如果两个都是空队列,则添加updateif (queue1.lastUpdate === null || queue2.lastUpdate === null) {// One of the queues is not empty. We must add the update to both queues.appendUpdateToQueue(queue1, update);appendUpdateToQueue(queue2, update);}//如果两个都不是空队列,由于两个结构共享,所以只在queue1加入update//在queue2中,将lastUpdate指向updateelse {// Both queues are non-empty. The last update is the same in both lists,// because of structural sharing. So, only append to one of the lists.appendUpdateToQueue(queue1, update);// But we still need to update the `lastUpdate` pointer of queue2.queue2.lastUpdate = update;}}
}
更新队列是成对出现的(源码enqueueUpdate
方法queue1
&queue2
):
queue1
取的是fiber.updateQueue
queue2
取的是alternate.updateQueue
,在提交前可以异步修改和处理。如果一个正在进行的渲染在完成之前被销毁,我们则可以通过克隆当前队列来创建一个新的正在进行的工作。null
,则调用createUpdateQueue
()获取初始队列null
,则调用cloneUpdateQueue
()从对方中获取队列null
,则将update
作为lastUpdate
这个方法实际上是创建和更新updataQueue的过程
FiberRoot
在der的过程中创建了一个ReactRoot对象,这个对象最主要的就是承担了去创建一个FiberRoot对象,很重要,在后期整个应用的调度过程中都会跟它有关。FiberRoot是整个应用的起点,包含应用挂载的目标节点,记录整个应用更新过程的各种信息,因为应用更新过程中,会涉及到各种个样的东西,比如各种类型的expirationTime,异步调度任务的callback,都会记录在FiberRoot上面。它是在createContainer里调用createFiberRoot创建的。
Fiber
每一个ReactElement都会对应一个Fiber对象,记录节点的各种状态,比如class组件的state,还有props,然后Fiber更新后才会更新到this.state里面,更新了这个节点之后,才会把属性放到this上面,所以才会为hooks的实现提供了方便,因为state之类的都记在了Fiber上面,这样没有this也可以 。此外Fiber串联了整个应用形成树结构,把每个节点串联起来。
Update & updateQueue
Update是用来记录组件状态改变的对象,存在于Fiber的updateQueue里,单向链表的结构,可以算出最终的state的结果。多个Update可以同时存在,比如调用三次setState,就会产生三个Update,不会一次setState就更新一下,而是会等三个setState执行完,三个Update创建完,放到updateQueue里面,再进行更新
type BaseFiberRootProperties = {|// 页面上挂载的dom节点, 就是render方法接收的第二个参数containerInfo: any,// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到pendingChildren: any,// 当前树的根节点,就是FiberRootcurrent: Fiber,// 以下的优先级是用来区分// 1、没有提交(committed)的任务// 2、没有提交的挂起的任务// 3、没有提交的可能被挂起的任务// 我们选择不追踪每个单独的阻塞登记,为了兼顾性能// The earliest and latest priority levels that are suspended from committing.// 提交时候被挂起的最老和最新的任务earliestSuspendedTime: ExpirationTime,latestSuspendedTime: ExpirationTime,// The earliest and latest priority levels that are not known to be suspended.// 在提交时候可能会被挂起的最老和最新的任务(所有任务进来都是这个状态)earliestPendingTime: ExpirationTime,latestPendingTime: ExpirationTime,// The latest priority level that was pinged by a resolved promise and can// be retried.// 最新的通过一个promise被resolve并且可以重新尝试的优先级latestPingedTime: ExpirationTime,// 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染// 在`renderRoot`出现无法处理的错误时会被设置为`true`didError: boolean,// 正在等待提交的任务的`expirationTime`pendingCommitExpirationTime: ExpirationTime,// 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null// 在commit阶段只会处理这个值对应的任务finishedWork: Fiber | null,// 在任务被挂起的时候通过setTimeouth函数的返回值// 用来清理下一次如果有新的任务挂起时还没触发的timeouttimeoutHandle: TimeoutHandle | NoTimeout,// 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用context: Object | null,pendingContext: Object | null,// 用来确定第一次渲染的时候是否需要融合+hydrate: boolean,// 当前root上剩余的过期时间// TODO: 提到renderer里面区处理nextExpirationTimeToWorkOn: ExpirationTime,// 当前更新对应的过期时间expirationTime: ExpirationTime,// List of top-level batches. This list indicates whether a commit should be// deferred. Also contains completion callbacks.// TODO: Lift this into the renderer// 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟// 同时包括完成之后的回调// 貌似用在测试的时候?firstBatch: Batch | null,// Linked-list of roots// nextnextScheduledRoot: FiberRoot | null,
|};
// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|// Fiber的tag,用来标记不同的组件类型tag: WorkTag,// 就是组件的那个keykey: null | string,// 我们调用`createElement`的第一个参数 div/p/func/classelementType: any,// 异步组件resolved之后返回的内容,一般是`function`或者`class`type: any,// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)stateNode: any,// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回return: Fiber | null,// 单链表树结构// 指向自己的第一个子节点child: Fiber | null,// 指向自己的兄弟结构// 兄弟节点的return指向同一个父节点sibling: Fiber | null,index: number,// refref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,// 新的变动带来的新的props,就是nextPropspendingProps: any, // 上一次渲染完成之后的propP,就是当前propsmemoizedProps: any, // 该Fiber对应的组件产生的Update会存放在这个队列里面updateQueue: UpdateQueue<any> | null,// 上一次渲染的时候的statememoizedState: any,// 一个列表,存放这个Fiber依赖的contextfirstContextDependency: ContextDependency<mixed> | null,// 用来描述当前Fiber和他子树的`Bitfield`// 共存的模式表示这个子树是否默认是异步渲染的// Fiber被创建的时候他会继承父Fiber// 其他的标识也可以在创建的时候被设置// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前mode: TypeOfMode,// Effect// 用来记录Side EffecteffectTag: SideEffectTag,// 单链表用来快速查找下一个side effectnextEffect: Fiber | null,// 子树中第一个side effectfirstEffect: Fiber | null,// 子树中最后一个side effectlastEffect: Fiber | null,// 代表任务在未来的哪个时间点应该被完成// 不包括他的子树产生的任务expirationTime: ExpirationTime,// 快速确定子树中是否有不在等待的变化childExpirationTime: ExpirationTime,// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber// 我们称他为`current <==> workInProgress`// 在渲染完成之后他们会交换位置alternate: Fiber | null,// 下面是调试相关的,收集每个Fiber和子树渲染时间的actualDuration?: number,// If the Fiber is currently active in the "render" phase,// This marks the time at which the work began.// This field is only set when the enableProfilerTimer flag is enabled.actualStartTime?: number,// Duration of the most recent render time for this Fiber.// This value is not updated when we bailout for memoization purposes.// This field is only set when the enableProfilerTimer flag is enabled.selfBaseDuration?: number,// Sum of base times for all descedents of this Fiber.// This value bubbles up during the "complete" phase.// This field is only set when the enableProfilerTimer flag BaseDuration?: number,// Conceptual aliases// workInProgress : Fiber -> alternate The alternate used for reuse happens// to be the same as work in progress.// __DEV__ only_debugID?: number,_debugSource?: Source | null,_debugOwner?: Fiber | null,_debugIsCurrentlyTiming?: boolean,
|};
本文发布于:2024-01-31 20:05:01,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170670270331032.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |