【重识前端】React源码阅读(三)4千字告诉你 Fiber 初始化

我心飞翔 分类:javascript

前言

在第一章中,我们解读到ReactDOM.render之后就没有继续了。因为我看了一下设计到了Fiber的一些知识,所以在第二章补充了Fiber的基础知识之后,我将Fiber拆分成四块来讲

  • 动机(或者说初衷
  • 初始化-Fiber树的准备工作
  • render-Fiber树的构建
  • commit-Fiber树映射到DOM

现在是初始化阶段

我们现在直接进入Fiber的初始化。Action!

前情回顾

回顾一下之前学习JSX的时候,阅读到ReactDOM.render。如果你没有阅读过之前的文章,强烈建议你到我的主页去阅读我之前的博客。下面我们开始。

ReactDOM.render的源码。

export function isValidContainer(node: mixed): boolean {
  return !!(
    node &&
    (node.nodeType === ELEMENT_NODE ||
      node.nodeType === DOCUMENT_NODE ||
      node.nodeType === DOCUMENT_FRAGMENT_NODE ||
      (node.nodeType === COMMENT_NODE &&
        (node: any).nodeValue === ' react-mount-point-unstable '))
  );
}

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  // 用于抛出错误的, 就是判断这个container是不是找得到
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOM.createRoot(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
      );
    }
  }
  
  // 这里面涉及到了fiber的一些架构,开始补坑
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}
 

最后return的这个函数有点意思。

源码攻读

legacyRenderSubtreeIntoContainer

我们首先翻译一下这个函数的名字,进行驼峰拆分:

Google翻译:legacy Render Subtree Into Container(旧版渲染子树放入容器

先别急着翻译。我们先看看源码。

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
// container 对应的是我们传入的真实 DOM 对象
var root = container._reactRootContainer;
// 初始化 fiberRoot 对象
var fiberRoot;
// DOM 对象本身不存在 _reactRootContainer 属性,因此 root 为空
if (!root) {
// 若 root 为空,则初始化 _reactRootContainer,并将其值赋值给 root
// legacyCreateRootFromDOMContainer 主要的功能是创建,并且进行一些“脏判断”。感兴趣的,我在后面也会详细给出
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
// legacyCreateRootFromDOMContainer 创建出的对象会有一个 _internalRoot 属性,将其赋值给 fiberRoot
fiberRoot = root._internalRoot;
// 这里处理的是 ReactDOM.render 入参中的回调函数,你了解即可
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
// 进入 unbatchedUpdates 方法
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// else 逻辑处理的是非首次渲染的情况(即更新),其逻辑除了跳过了初始化工作,与楼上基本一致
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}

梳理一下整个流程,然后我们再一一看这些函数。

以下流程是指初始化,也就是if/else判断里面的if。而不是elseelse是非首次渲染,走的更新流程,注释里面写了。

  1. 创建container._reactRootContainer对象,并赋值给root
  2. root上的_inernalRoot赋值给fiberRoot
  3. legacyRenderSubtreeIntoContainer的一些参数和fiberRoot一起传给updateContainer
  4. 将updateConatiner作为回调函数传给unbatechedUpdates

ok,现在看不太懂没关系。我们将里面用到的函数一一解读,然后再回来补充整个流程。

legacyCreateRootFromDOMContainer

大家现在一定会晕头转向,因为我看的时候也是这样,他的名字和函数名字取的太像了,我们需要给他们独自用自己的方式命名。我会写下Google翻译, 帮助大家取名字。

Google翻译:legacy Create Root From DOM Container(从DOM容器创建根目录

看了翻译之后是不是焕然大悟?别急,我们先看看源码。

function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
// 如果forceHydrate是false,就会调用后面的方法。
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 这个循环,是为了清理所有的内容,然后找到我们要渲染的最初的根div。通常我们的html会有一个
// <div id="root"></div> 为的就是保障他的纯净。
while ((rootSibling = container.lastChild)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
console.error(
'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.',
);
}
}
container.removeChild(rootSibling);
}
}
if (__DEV__) {
if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
warnedAboutHydrateAPI = true;
console.warn(
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
'will stop working in React v18. Replace the ReactDOM.render() call ' +
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
);
}
}
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}

里面又用到两个函数,我为大家召出来。

shouldHydrateDueToLegacyHeuristic

function getReactRootElementInContainer(container: any) {
// 如果没有就返回null
if (!container) {
return null;
}
// 内部定义的一个常量
// export const DOCUMENT_NODE = 9;看名字就顾名思义了
// 如果是内部一个document节点就返回document节点
// 感兴趣可以看看MDN的介绍:https://developer.mozilla.org/zh-CN/docs/Web/API/Document
// 常量的值也有:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
// 否则返回第一个元素
return container.firstChild;
}
}
function shouldHydrateDueToLegacyHeuristic(container) {
// 将container传入函数,进行判断类型,并得到节点(有可能为空,看注释
const rootElement = getReactRootElementInContainer(container);
// 强转为布尔值
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
// export const ROOT_ATTRIBUTE_NAME = 'data-reactroot'; 
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}

我们看到一个hasAttribute为"data-reactroot"。这是 React 在服务端渲染的时候加上去的。之前有说过Hydrate这个操作是服务端渲染的时候需要的东西。

Google翻译:getReactRootElementInContainer(获取容器中的React根元素

Google翻译:shouldHydrateDueToLegacyHeuristic(应当因传统启发而水合

说人话就是是否需要进行hydrate操作

所以legacyCreateRootFromDOMContainer这个函数是保障container的纯净。并且返回createLegacyRoot函数。ok继续看。

createLegacyRoot

function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) {
// 是不是看见老朋友了。_internalRoot。继续看下面的内容,我们来解析一下createRootImpl
this._internalRoot = createRootImpl(container, LegacyRoot, options);
}
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
// 又是熟悉的操作,返回某个东西
return new ReactDOMLegacyRoot(container, options);
}

createRootImpl

需要注意的是,我们通篇都是以非hydrate来看的,因为我们不是服务端渲染。但是我也会介绍如果有hydrate的内容。所以大家看的时候可以稍微带入一点去看,不然很容易走神!

// 创建并返回一个fiberRoot
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
// 判断是否为hydrate模式
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
const mutableSources =
(options != null &&
options.hydrationOptions != null &&
options.hydrationOptions.mutableSources) ||
null;
const strictModeLevelOverride =
options != null && options.unstable_strictModeLevel != null
? options.unstable_strictModeLevel
: null;
// 创建一个fiberRoot
const root = createContainer(
container,
tag,
hydrate,
hydrationCallbacks,
strictModeLevelOverride,
);
// 给container附加一个内部属性用于指向fiberRoot的current属性对应的rootFiber节点
markContainerAsRoot(root.current, container);
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
if (mutableSources) {
for (let i = 0; i < mutableSources.length; i++) {
const mutableSource = mutableSources[i];
registerMutableSourceForHydration(root, mutableSource);
}
}
return root;
}

为什么我在注释里面会加上「创建一个fiberRoot」,不是一个是创建一个container吗?我们看createContainer里面做了什么。

createContainer

export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
strictModeLevelOverride: null | number,
): FiberRoot {
// 通过FiberRootNode构造函数创建一个fiberRoot实例
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 如果有,就补充进这个回调函数
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
// 通过createHostRootFiber方法创建fiber tree的根结点,即rootFiber
// fiber节点也会像DOM树结构一样形成一个fiber tree单链表树结构
// 每个DOM节点或者组件都会生成一个与之对应的fiber节点,在后续的调和(reconciliation)阶段起着至关重要的作用
const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride);
// 创建完rootFiber之后,会将fiberRoot的实例的current属性指向刚创建的rootFiber
root.current = uninitializedFiber;
// 同时rootFiber的stateNode属性会指向fiberRoot实例,形成相互引用
uninitializedFiber.stateNode = root;
if (enableCache) {
const initialCache = new Map();
root.pooledCache = initialCache;
const initialState = {
element: null,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState = {
element: null,
};
uninitializedFiber.memoizedState = initialState;
}
initializeUpdateQueue(uninitializedFiber);
// 最后将创建的fiberRoot实例返回
return root;
}
// 内部调用createFiberRoot方法返回一个fiberRoot实例
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
strictModeLevelOverride: null | number,
): OpaqueRoot {
return createFiberRoot(
containerInfo,
tag,
hydrate,
hydrationCallbacks,
strictModeLevelOverride,
);
}

这里也比较简单,只是单纯的调用函数,然后返回。真正的秘密就在这些函数里面,耐心点继续看!黎明就在眼前

FiberRootNode

一个非常恐怖的内容。但是看名字也能猜个大概。还记得我们之前说过。一个fiber节点要存的内容还蛮多的。比如被暂停时的状态,恢复时要做的事情,优先级等等....这里我尽可能的去写注释。帮助大家还要我自己理解。

function FiberRootNode(containerInfo, tag, hydrate) {
//root 节点类型()
this.tag = tag;
//root根节点,render方法的第二个参数
this.containerInfo = containerInfo;
//在持久更新中会用到,不支持增量更新的平台,react-dom 是整个应用更新,所以用不到
this.pendingChildren = null;
//当前应用root节点对应的Fiber对象
this.current = null;
//缓存
this.pingCache = null;
//已经完成任务的FiberRoot对象,在commit(提交)阶段只会处理该值对应的任务
this.finishedWork = null;
//在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout
this.timeoutHandle = noTimeout;
//顶层 context 对象,只有主动调用renderSubtreeIntoContainer才会生效
this.context = null;
this.pendingContext = null;
// 老朋友了,用来确定第一次渲染的时候是否需要注水
this.hydrate = hydrate;
//回调节点
this.callbackNode = null;
//回调属性
this.callbackPriority = NoLane;
// export const NoLanes: Lanes = 0b0000000000000000000000000000000;
/*
export function createLaneMap<T>(initial: T): LaneMap<T> {
// Intentionally pushing one by one.
// https://v8.dev/blog/elements-kinds#avoid-creating-holes
const laneMap = [];
for (let i = 0; i < TotalLanes; i++) {
laneMap.push(initial);
}
return laneMap;
}
*/
//任务时间。由于该字段在未来会重构,当前我们不需要理解他。
this.eventTimes = createLaneMap(NoLanes);
// 过期时间。函数上面的注释有。export const NoTimestamp = -1;
this.expirationTimes = createLaneMap(NoTimestamp);
// 优先级的初始化
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
if (enableCache) {
this.pooledCache = null;
this.pooledCacheLanes = NoLanes;
}
if (supportsHydration) {
this.mutableSourceEagerHydrationData = null;
}
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
if (enableSuspenseCallback) {
this.hydrationCallbacks = null;
}
if (enableProfilerTimer && enableProfilerCommitHooks) {
this.effectDuration = 0;
this.passiveEffectDuration = 0;
}
if (__DEV__) {
switch (tag) {
case ConcurrentRoot:
this._debugRootType = 'createRoot()';
break;
case LegacyRoot:
this._debugRootType = 'createLegacyRoot()';
break;
}
}
}

createHostRootFiber

通篇大论都是在判断mode类型。感兴趣的可以再深挖。这里我就略过了。眼睛已经看花了。。。

export function createHostRootFiber(
tag: RootTag,
strictModeLevelOverride: null | number,
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (strictModeLevelOverride >= 2) {
mode |= StrictEffectsMode;
}
}
} else {
if (enableStrictEffects && createRootStrictEffectsByDefault) {
mode |= StrictLegacyMode | StrictEffectsMode;
} else {
mode |= StrictLegacyMode;
}
}
} else {
mode = NoMode;
}
if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}
// export const HostRoot = 3;
return createFiber(HostRoot, null, null, mode);
}

createFiber

也是包一个壳子。主要还是为了创建FiberNode。

const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};

FiberNode

官方其实已经注释了属性的要素。但是我还是会尽可能的完善。

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
// 用于标记fiber节点的类型
this.tag = tag;
// 用于唯一标识一个fiber节点
this.key = key;
// 节点类型
this.elementType = null;
// ReactNode的节点类型
this.type = null;
// FiberNode会通过stateNode绑定一些其他的对象,例如FiberNode对应的Dom、FiberRoot、ReactComponent实例
this.stateNode = null;
// Fiber
// 表示父级 FiberNode
this.return = null;
// 表示第一个子 FiberNode
this.child = null;
// 表示紧紧相邻的下一个兄弟 FiberNode
this.sibling = null;
// 下标
this.index = 0;
this.ref = null;
// 表示新的props
this.pendingProps = pendingProps;
// 表示经过所有流程处理后的新props
this.memoizedProps = null;
// 更新队列
this.updateQueue = null;
// 表示经过所有流程处理后的新state
this.memoizedState = null;
// 依赖
this.dependencies = null;
this.mode = mode;
// Effects
// 标志更新的类型:删除、新增、修改。
this.flags = NoFlags;
// 子Fiber树的标志更新的类型:删除、新增、修改。
this.subtreeFlags = NoFlags;
// 需要删除的内容
this.deletions = null;
// 优先级
this.lanes = NoLanes;
// 子的优先级
this.childLanes = NoLanes;
// 双缓冲:防止数据丢失,提高效率(之后Dom-diff的时候可以直接比较或者使用
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
if (__DEV__) {
// This isn't directly used but is handy for debugging internals:
this._debugID = debugCounter++;
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}

initializeUpdateQueue

createContainer还有最后一个函数,初始化更新队列。

export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes,
},
effects: null,
};
fiber.updateQueue = queue;
}

小结一番

大费周章的做这些阅读,其实回过神来看。就是为了创建fiberNodeFiberRootNode实例)和rootFiber对象。这两兄弟是整个Fiber树构建的起点。

  • fiberNodeFiberRootNode实例) -> 的关联对象是真实 DOM 的容器节点
  • rootFiber -> 虚拟 DOM 的根节点

然后再回过神来看,确实。他们各自创建都是为他们所代表的东西而创建,无论是属性或者打入方法。

收尾

最后他们都会被unbatchedUpdates所收编(看看最上面梦开始的地方「legacyRenderSubtreeIntoContainer」)。最后我们所创建的都会归入到它的手上。一起看看最后一个函数吧。

方便阅读

unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});

unbatchedUpdates

function unbatchedUpdates(fn, a) {
// 这里是对上下文的处理,不必纠结
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// 重点在这里,直接调用了传入的回调函数 fn,对应当前链路中的 updateContainer 方法
return fn(a);
} finally {
// finally 逻辑里是对回调队列的处理,此处不用太关注
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}

updateContainer

function updateContainer(element, container, parentComponent, callback) {
......
// 这是一个 event 相关的入参,此处不必关注
var eventTime = requestEventTime();
......
// 这是一个比较关键的入参,lane 表示优先级
var lane = requestUpdateLane(current);
// 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
var update = createUpdate(eventTime, lane); 
// update 的 payload 对应的是一个 React 元素
update.payload = {
element: element
};
// 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
// 将 update 入队
enqueueUpdate(current, update);
// 调度 fiberRoot 
scheduleUpdateOnFiber(current, lane, eventTime);
// 返回当前节点(fiberRoot)的优先级
return lane;
}

updateContainer 的逻辑相对来说丰富了点,但大部分逻辑也是在干杂活,它做的最关键的事情可以总结为三件:

  • 请求当前 Fiber 节点的 lane(优先级);

  • 结合 lane(优先级),创建当前 Fiber 节点的 update 对象,并将其入队;

  • 调度当前节点(rootFiber)。

看到整理的伙伴,谢谢你的耐心。

最后

因为我也是在一边看,一边记录,如果有问题,大家一定要及时提出来。我也会及时更新的。这部分我应该会屡次阅读。毕竟大神写了几年,我不可能几天就看明白的。

索引

zh-hans.reactjs.org/docs

回复

我来回复
  • 暂无回复内容