react源码学习1-入口

194 阅读8分钟

从入口说起

不同的react版本,执行的入口有所区别,以官方给的案例来看,在react 17以及以前,入口是这样的:

ReactDOM.render(

  <h1>Hello, world!</h1>,

  document.getElementById('root')

);

2022年的时候,react发布了18的版本,入口变成了

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<h1>Hello, world!</h1>);

这两种入口有什么区别?

1、最主要的是前者只支持同步更新,后者支持异步可中断更新

2、二者合成事件的执行方式不同

前者在合成事件中,有自动批处理的功能,即setState体现出来的是异步,如果没有在合成事件上下文中,需要使用unstable_batchedUpdates使其处于该上下文

而对于后者来说,所有的 setState 在默认情况下都是批处理的

我们先以react17为例,来看下执行ReactDOM.render的时候,react做了什么?

如果稍微跟踪下源码,很容易发现,它会进入到一个叫做legacyRenderSubtreeIntoContainer的方法中去,这个方法做的事情,归纳一下,有2点:

1、创建fiberRoot、rootFiber并进行关联

2、开启更新

话是这么说,但实际上为了干这两件事,方法调用层级非常深,我们先来看创建fiberRoot:

let root = container._reactRootContainer
    if (!root) {
        root = legacyCreateRootFromDOMContainer(container)
            return createLegacyRoot(container, false)
                return new ReactDOMBlockingRoot(container, LegacyRoot, options);
                    this._internalRoot = createRootImpl(container, tag, options);
                        const root = createContainer(container, tag, hydrate, hydrationCallbacks);
                            return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
                        listenToAllSupportedEvents(container);
                        return root;

但看源码比较忌讳上来之后就抠细节,因此我们只关注最终创建出来的结构是什么样

很显然,从上述调用栈中,从直观上看,返回的就是ReactDOMBlockingRoot构造函数的实例,该实例上有一个叫_internalRoot的属性,这个_internalRoot其实就是我们想要的fiberRoot

很显然,_internalRoot是通过调用createContainer -> createFiberRoot创建出来的,紧接着还给container绑定了很多监听事件,绑定事件是另一个大的话题,之后会单独写文章说,我们先重点看createContainer -> createFiberRoot又做了哪些具体内容,返回了什么结构

在createFiberRoot内部,我们可以看到以下重要的几步:

const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
const uninitializedFiber = createHostRootFiber(tag);
    return createFiber(HostRoot, null, null, mode);
        return new FiberNode(tag, pendingProps, key, mode);

root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 初始化updateQueue
initializeUpdateQueue(uninitializedFiber);
return root;

可以看到,在这个方法中,我们终于看到了创建fiberRoot的代码,即第1行,它调用了FiberRootNode这样一个构造函数,进去一看,有很多属性,先有个大概印象吧,这些属性只能在后面的学习中挨个理解每个到底是什么意思

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  this.containerInfo = containerInfo;
  this.pendingChildren = null;
  this.current = null;
  this.pingCache = null;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.callbackNode = null;
  this.callbackPriority = NoLanePriority;
  this.eventTimes = createLaneMap(NoLanes);
  this.expirationTimes = createLaneMap(NoTimestamp);
  
  this.pendingLanes = NoLanes;
  this.suspendedLanes = NoLanes;
  this.pingedLanes = NoLanes;
  this.expiredLanes = NoLanes;
  this.mutableReadLanes = NoLanes;
  this.finishedLanes = NoLanes;
  
  this.entangledLanes = NoLanes;
  this.entanglements = createLaneMap(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;
  }
}

目前这个阶段,我觉得最需要知道的是current属性,这个代表当前页面上渲染的fiber树,可以看到目前它的值是null,页面上什么也没有

回到createFiberRoot方法中,可以发现我们通过调用createHostRootFiber,创建了一个fiber节点并挂到了这个current属性上,这个fiber节点我们叫它rootFiber

fiberRoot和rootFiber比较容易混淆,在此总结下它们的联系和区别:

fiberRoot其实是和整个react应用形成一个映射的,所以它上面的属性一定都是对整个react应用的描述,从fiberRoot的属性——current这个名字上也能看出来,current可以翻译成当前,那有当前就有之前,还有之后

事实上,在react执行的不同阶段,会有不同的fiber树,最主要的是两棵fiber树——current和workingProgress,即当前fiber树和正在构建的fiber树

此处还需注意一点,当前fiber树由于是和每个DOM节点、每个组件构成一一映射的,所以这个树节点上的一些属性不能随便乱动,否则可能会直接反应到页面上

fiberRoot的结构上面已经列出,那rootFiber又是什么结构呢?我们进入createHostRootFiber -> createFiber -> new FiberNode

方法中看下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  
  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  
  this.ref = null;
  
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  
  this.mode = mode;
  
  // Effects
  this.flags = NoFlags;
  this.nextEffect = null;
  
  this.firstEffect = null;
  this.lastEffect = null;
  
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  
  this.alternate = null;
}

这个结构也大概看下即可,之后的学习中会慢慢理解各个属性所代表的含义

但此处有一个很经典的问题值得一提:jsx、虚拟DOM、fiber之前的区别是什么?

以下是我个人的理解:

Jsx是一种语法糖,所以我们要学习并遵循它定义的一套规则,例如使用表达式时要在外面包裹花括号、class类名要用className、style要写成对象形式等等,它是为了方便开发人员以html的形式定义虚拟DOM而出现的,在编译阶段会被转换为调用特定方法(在react中就是调用createElement方法)创建虚拟DOM的形式

Fiber和虚拟DOM最基本的共同点就是,它们都有一些节点的基本属性

虚拟DOM,是描述真实DOM的一种数据结构,react中通过createElement来实现,Vue中通过VNode构造函数实现,它只是反应了DOM以及组件的父子结构关系,而且是单向的,单向双向是我个人的理解,给它起的一个名字,下面解释fiber的时候大家就知道怎么回事儿了

Fiber,它其实也是描述真实DOM的一种数据结构,但是在结构描述方面,和虚拟DOM不同的是,它存储子节点的方式是通过链表,而虚拟DOM存储的方式是数组,由于这个链表需要描述一个树结构,又要构成一条链,因此并不是和常规链表一样通过prev next这些属性来描述,而是通过child、sibling这些来映射到next,还添加了return代表其父节点

Fiber通过return这样一个变量名来代表其父节点,而不是用parent,还有一层含义,就是return代表了completeWork的执行方向

Fiber不仅仅有映射DOM结构相关的属性,还有该Fiber映射到的Update更新队列、Effect副作用等信息

总之,Fiber构建了一套方便调度器中止、恢复、高优插队的数据结构,而虚拟dom是不具备这个能力的,再者,Fiber这种数据结构将树状的虚拟dom转变为了线性的链表,操作这样一个一维结构效率要比树结构高很多(这段话出自某篇国外文章的翻译,原文连接:indepth.dev/posts/1008/…

在初始化万rootFiber之后,还在fiber上初始化了updateQueue属性,这项工作在initializeUpdateQueue方法中完成:

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

还有一点需要提一下,在调用new ReactDOMBlockingRoot时,我们传入了LegacyRoot作为tag的值,这个tag最终流转到了FiberNode节点中的tag属性,源码中tag定义了三种类型的值:

export const LegacyRoot = 0;
export const BlockingRoot = 1;
export const ConcurrentRoot = 2;

接下来我们看一下更新是怎么被创建并调度的:

回到legacyRenderSubtreeIntoContainer方法中,在创建完rootFiber之后,下方可以看到这样几行:

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

关于unbatchedUpdates,里面改了一个变量,可以暂时先将其理解为直接执行传入的回调,也就是执行我们这里的updateContainer

需要注意,updateContainer通过一个叫做enableNewReconciler的开关来判断返回new old两个版本:

updateContainer(children, fiberRoot, parentComponent, callback);
    export const updateContainer = enableNewReconciler ? updateContainer_new : updateContainer_old;

通过简单跟踪我们发现,react17版本的代码中,enableNewReconciler是false,因此

我们应该去./ReactFiberReconciler.old.js文件里寻找updateContainer即可:

updateContainer简化版代码如下:

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);
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  const update = createUpdate(eventTime, lane);
  update.payload = {element};
  callback = callback === undefined ? null : callback;

  if (callback !== null) {
    update.callback = callback;
  }

  enqueueUpdate(current, update);
  scheduleUpdateOnFiber(current, lane, eventTime);
  return lane;
}

updateContainer会创建Update更新对象,这个对象就对应首次更新,然后调用enqueueUpdate

将该Update对象加入到fiber的updateQueue更新队列里面

最后调用scheduleUpdateOnFiber进入调度系统

在上面updateContainer方法中,还有一个重要概念没有提到,就是lane,代表更新的优先级,这个之后单独拿一篇文章的篇幅去说明这个问题

接下来我们看react18入口的执行流程:

首先是调用了ReactDOM.createRoot方法,调用栈为:

ReactDOM.createRoot(container, options)
return new ReactDOMRoot(container, options);
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);

再往下创建fiberRoot的方法createRootImpl和react17版本完全一样,可以发现二者分别调用了不一样的构造函数创建fiberRoot,而且createRootImpl的第2个参数,也就是将来传给fiberRoot的tag属性,是ConcurrentRoot,在此区分了是同步更新还是异步可中断更新

接下来,就调用了fiberRoot的render方法,render接收虚拟DOM作为参数

ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
  updateContainer(children, root, null, null);
}

可以看到,在render方法中,也会和react17一样,调用updateContainer方法,之后的流程就完全一样了