重学React18(一):render函数

726 阅读3分钟

背景

项目中一直用的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做的事情,可以简单理解为

  1. 根据当前环境选择一个优先级 lane(lane可以看成是为更细粒度的优先级控制而生的,它是一个31位的二进制数字,1为最高优先级,优先级相同的任务batching到一一起执行。lanes是两个lane按位或的结果,表示并行异步更新。todo:后面可以写一个lane的专题☺)

  2. 用步骤1的优先级创建代表本次更新的update对象

  3. 将update对象挂载到当前组件对应的fiber上(放在了一个全局的array concurrentQueues上)

  4. 执行scheduleUpdateOnFiber,进行任务调度

我们采用小步快跑的方法,就先写到这里,下一篇再看下任务调度的过程~

参考文章: #react18新特性及实践总结 给女朋友讲React18新特性:Automatic batching

问题

  1. lane的工作机制
  2. [A] 这里this的指向,也就是说函数原型链上this的指向问题(答案还不明确的小伙伴,需要去补一下this的知识点喽~)
  3. UpdateQueue采用单向循环列表的好处