背景
项目中一直用的React版本都是16.8+,之前断断续续地看过一些源码,不过react18发布之后,一些底层实现已经不一样了。最近打算重新看一遍,也一年没有写文章了,正好把过程记录下来,写一个《重学React18》系列,每一篇文章最后提出几个问题,全面看完之后再回过头来一起解答。
版本
文章中用的React版本是18.3.0。
createRoot
'ReactDOM.render is no longer supported in React 18. Use createRoot ' + 'instead. Until you switch to the new API, your app will behave as ' + "if it's running React 17. Learn " + 'more: reactjs.org/link/switch…'
v18中ReactDOM.render会看到这样一个warnning,这个api在v18中已经被标记为Legacy了。那就先从我们最熟悉的render函数开始重新看一下吧
ReactDOM.render(<h1>Hello React</h1>, document.getElementById('root'));
这是我们最常用的方式了吧,在18+的版本中需要这么来用了👇
ReactDOM.createRoot(document.getElementById("root")).render(<h1>Hello React</h1>);
createRoot默认开始Concurrent模式,去源码中看看是怎么实现的吧。
react-dom
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}
// 这里createContainer省略掉了一些options参数,构建fiber的时候会用到...
const root = createContainer(container, ConcurrentRoot);
// ...
return new ReactDOMRoot(root);
}
ReactDOMRoot比较简单
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
createContainer调用createFiberRoot函数生成root fiber, root fiber的current属性值为createHostRootFiber创建的FiberNode,可以理解初次渲染阶段正在工作的fiber,更新阶段就是已经渲染到页面的fiber,即老的fiber(还没有渲染的叫做workInProgress)。
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag, // * root.tag是ConcurrentRoot,值为1
hydrate,
identifierPrefix,
onRecoverableError,
): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(
tag, // * uninitializedFiber.mode是ConcurrentMode,值为1
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// ...
// 省略掉了initialCache的计算
uninitializedFiber.memoizedState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
initializeUpdateQueue(uninitializedFiber); // * 初始化fiber的updateQueue
return root;
}
createFiberRoot的参数tag的类型是RootTag,实参传的也是ConcurrentRoot,我们先记得这里的Concurrent模式,后面遇到具体代码再回来看
export type RootTag = 0 | 1;
export const LegacyRoot = 0;
export const ConcurrentRoot = 1;
好的,接下来看下FiberNode的结构吧
view code
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
/**
* 不同类型的组件, stateNode也不同
* 原生标签是dom节点
* class组件是实例
* ...
*/
this.stateNode = null;
// Fiber
this.return = null; // * 父fiber
this.child = null; // * 第一个子fiber
this.sibling = null; // * 下一个兄弟fiber
this.index = 0; // * 记录节点在当前层级下的位置
this.ref = null;
this.pendingProps = pendingProps; // * 待更新的属性
this.memoizedProps = null; // * 已更新的属性
this.updateQueue = null; // * 任务队列
this.memoizedState = null; // * 函数组件的memoizedState是第1个hook
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null; // * old fiber
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._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
}
}
现在root fiber创建完了,接下来就是执行render方法了
render函数
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot; // [A]
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
// ...
updateContainer(children, root, null, null);
};
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current); // * 通过事件优先级,得到update的优先级
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context; // * 初次渲染是空对象 {}
} else {
container.pendingContext = context;
}
const update = createUpdate(eventTime, lane); // * 创建update对象, 并赋予优先级
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) { // * 初次渲染callback是null
update.callback = callback;
}
const root = enqueueUpdate(current, update, lane); // * update对象挂载到当前fiber的UpdateQueue上,并且是单向循环列表
if (root !== null) {
scheduleUpdateOnFiber(root, current, lane, eventTime);
entangleTransitions(root, current, lane);
}
return lane;
}
updateContainer做的事情,可以简单理解为
-
根据当前环境选择一个优先级
lane(lane可以看成是为更细粒度的优先级控制而生的,它是一个31位的二进制数字,1为最高优先级,优先级相同的任务batching到一一起执行。lanes是两个lane按位或的结果,表示并行异步更新。todo:后面可以写一个lane的专题☺) -
用步骤1的优先级创建代表本次更新的
update对象 -
将update对象挂载到当前组件对应的fiber上(放在了一个全局的array
concurrentQueues上) -
执行
scheduleUpdateOnFiber,进行任务调度
我们采用小步快跑的方法,就先写到这里,下一篇再看下任务调度的过程~
参考文章: #react18新特性及实践总结 给女朋友讲React18新特性:Automatic batching
问题
- lane的工作机制
- [A] 这里this的指向,也就是说函数原型链上this的指向问题(答案还不明确的小伙伴,需要去补一下this的知识点喽~)
- UpdateQueue采用单向循环列表的好处