【React源码笔记3】- ReactDom

React | 2020-07-01 22:47:08 600次 0次

一、render

第一篇中创建了 React 节点,那么将节点渲染或者触发节点的更新可以通过 rendersetStateforceUpdate 方法,本篇先分析 ReactDom.render 方法,它是首次渲染。

ReactDom 中会暴露出如下方法,其实这里面常用的很少:

export {
  createPortal,
  batchedUpdates as unstable_batchedUpdates,
  flushSync,
  ReactVersion as version,
  findDOMNode,
  hydrate,
  render,
  unmountComponentAtNode,
  createRoot,
  createBlockingRoot,
  discreteUpdates as unstable_discreteUpdates,
  flushDiscreteUpdates as unstable_flushDiscreteUpdates,
  flushControlled as unstable_flushControlled,
  scheduleHydration as unstable_scheduleHydration,
  renderSubtreeIntoContainer as unstable_renderSubtreeIntoContainer,
};

先看下 render 方法,里面什么也没做,直接调用了 legacyRenderSubtreeIntoContainer,顺带提一下 invariant 这个方法已经没了,可能为了安全,经过编译后去除了这个调用,直接判断然后抛出错误。

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  ...
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  ...
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // 初次创建 先创建一个 root 根
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      ...
    }
    // 非批量更新,因为是初次渲染所以要快
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    //更新时候再来看这里
    ...
  }
  //初始渲染返回的就是 container.current.child.stateNode;
  return getPublicRootInstance(fiberRoot);
}

上面这个方法中接收了一个 forceHydrate 参数,和服务端渲染有关,浏览器端渲染默认为 false,首次渲染会创建一个 root 根节点,另外指向 container._reactRootContainer ,同时赋值 fiberRoot,此时 root、fiberRoot、container(传入的 DOM 节 点)形成了一个数据环。其中 fiberRoot 上的 containerInfo 指向 container,下面会提及。


二、createLegacyRoot

function legacyCreateRootFromDOMContainer(
  container: Container,
  forceHydrate: boolean,
): RootType {
  // 这里又做了一次是否服务端渲染判断 目前是false
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      ...
      //只保留一个干净的根节点
      container.removeChild(rootSibling);
    }
  }
  ...
  return createLegacyRoot(
    container,
    shouldHydrate
      ? {
          hydrate: true,
        }
      : undefined,
  );
}
export function createLegacyRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  // LegacyRoot 默认为 0
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

接下来又一个函数调用,给 this(上面提到的 root) 挂载一个 _internalRoot 属性,所以创建 root 后可以直接取到这个值

...
 this._internalRoot = createRootImpl(container, tag, options);
...
function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  ...// 判断是否服务端相关处理 省略
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  //container[_reactContainere$+随机数] = root.current;
  markContainerAsRoot(root.current, container);
  if (hydrate && tag !== LegacyRoot) {
    ...
  }
  return root;
}

层层调用之后到了这个方法中终于看到了返回 root 对象,但是又是调用了 createContainer 来生成:

...
    return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
...
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  // Suspense开启后的服务端渲染回调
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  const uninitializedFiber = createHostRootFiber(tag);
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  initializeUpdateQueue(uninitializedFiber);

  return root;
}

到了这里终于闻到了一丝丝大名鼎鼎的 fiber 气息,在分析 Fiber 之前有必要先梳理一下目前一些变量的关系图,更方便后续的理解。

微信截图_20200703220806.png

其中 FiberRoot 是整个应用的起点,记录整个应用更新过程中各种信息,只能有一个。

RootFiber 则是具体的 Fiber 节点的根,可以有多个,因为可以 ReactDom.render 多次,可以理解为每一个 DOM 节点都是一个 fiber 结构,记录节点状态,每个节点的连接通过 child 指针指向子节点,sibling 指向自己的兄弟节点,每个子节点有个 return 指针指向自己的父节点,这个 return 就是单纯的一个指针命名,这样整颗 fiber 树就通过链表的形式展现。


三、FiberRoot

这个东西就是单纯的一个对象,先简单的熟悉下里面一些信息说明,这个结构目前还在一直的变化之中:

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag; //0 LegacyRoot 
  // 当前应用对应的 Fiber 对象,RootFiber
  this.current = null;
  
  // 传入的 container 
  this.containerInfo = containerInfo;
  
  // react-dom中无用
  this.pendingChildren = null;
  
  // handleError 时用到 记录错误信息
  this.pingCache = null;
  
  //当前更新对应的过期时间
  this.finishedExpirationTime = NoWork;
  
  // 完成的任务,commit这个标识的数据
  this.finishedWork = null;
  
  // 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时,清理还没触发的timeout
  this.timeoutHandle = noTimeout;
  
  //renderSubtreeIntoContainer调用,Portal代替了这个方法现在
  this.context = null;
  
  //。。。
  this.pendingContext = null;
  
  //服务端渲染
  this.hydrate = hydrate;
  
  //这些应该和 Suspense 相关
  this.callbackPriority = NoPriority;
  this.firstPendingTime = NoWork;
  this.firstSuspendedTime = NoWork;
  this.lastSuspendedTime = NoWork;
  this.nextKnownPendingLevel = NoWork;
  this.lastPingedTime = NoWork;
  this.lastExpiredTime = NoWork;

  if (enableSchedulerTracing) {
    this.interactionThreadID = unstable_getThreadID();
    this.memoizedInteractions = new Set();
    this.pendingInteractionMap = new Map();
  }
  if (enableSuspenseCallback) {
    this.hydrationCallbacks = null;
  }
}


四、Fiber

一直说的 fiber 架构,我的理解是并不只是单纯的有了 fiber 这种数据结构在里面,而是因为要去避免之前的递归方式,因为不可暂停,大量节点导致渲染时间过长,从而使一些页面的交互变得延迟影响用户体验,所以借助 fiber 数据结构形成一种链表的结构串联整棵树,从而使得调和阶段是可以中断的,然后利用浏览器的空闲时间再来执行我们的业务代码,浏览器中会优先执行一些事件输入等优先级高的任务,能够提升用户体验。这里不再详细介绍,可以参考之前的文章:浏览器渲染 和 React模拟实现

接着第二小节的代码然后执行了 createHostRootFiber 来创建节点:

export function createHostRootFiber(tag: RootTag): Fiber {
  let mode;
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BlockingMode | StrictMode;
  } else if (tag === BlockingRoot) {
    mode = BlockingMode | StrictMode;
  } else {
    mode = NoMode;
  }
  ...
  return createFiber(HostRoot, null, null, mode);
}

const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // tag : 3
  return new FiberNode(tag, pendingProps, key, mode);
};

目前 mode 还在处于实验阶段,如果后面时间充裕的话就跟着官方的脚步维护这个系列的文章。第一篇中介绍了三种不同的模式,最终这里进行了判断采用哪一种,然后执行创建 fiber 节点方法,返回一个 Fiber 节点对象:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 初始3 标记不同的组件类型
  this.tag = tag;
  
  //节点中的key
  this.key = key;
  
  //createElement中第一个参数
  this.elementType = null;
  
  //异步组件resolved之后返回的内容,一般是`function`或者`class`
  this.type = null;
  
  //对应的组件实例
  this.stateNode = null;

  // Fiber中的最重要的链条结构,对照之前文章中出现的那张图
  this.return = null; //父节点
  this.child = null;  //孩子节点
  this.sibling = null;//兄弟节点
  this.index = 0;     //索引
   
  this.ref = null;
   
  //新的变动带来的新的props
  this.pendingProps = pendingProps;
  
  //上一次渲染完成之后的props
  this.memoizedProps = null;
  
  //该Fiber对应的组件产生的Update会存放在这个队列里面
  this.updateQueue = null;
  
  //上一次渲染的时候的state
  this.memoizedState = null;
  this.dependencies = null;
  
  //legacy blocking concurrent 
  this.mode = mode;

  // 它记录了节点的更新、修改或删除。这个也对照之前的react模拟实现的文章看下
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;
  
  //代表任务在未来的哪个时间点应该被完成,不包括子树
  this.expirationTime = NoWork;
  //子树中过期时间
  this.childExpirationTime = NoWork;
   
  //为了优化,替身
  this.alternate = null;

  if (enableProfilerTimer) {
    //以下是为了避免v8引擎的性能悬崖。和 Object.preventExtensions 这个有关系
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;

    //没太明白,后面再研究研究,应该是更优化的方式
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }

}

这个创建完之后,还有一步操作,初始化一个更新队列,这个数据结构较之前的小版本发生了改变;当更新数据时候,把更新的东西仍然放到更新队列中,调用完 render 方法后,取到最新的虚拟节点,再执行后续的对比渲染操作。

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState, //先前的状态,作为 payload 函数的 prevState 参数
    baseQueue: null,  //存储执行中的更新任务 Update 队列
    shared: {
      pending: null, //存储待执行的更新任务 Update 队列
    },
    effects: null, //有影响的节点记录,commit阶段使用
  };
  fiber.updateQueue = queue;
}


五、updateContainer

最终回到第一部分,创建好了这个 root 节点,并且这个过程中也创建了相互依赖的关系和基本的数据模型,接下来执行 unbatchedUpdates(后续再介绍)回调中执行 updateContainer,这里面做的事情就开始变得抽象起来了,先简要的看下这里面的主要逻辑:

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  ...
  //RootFiber
  const current = container.current;
  ...
  //一个任务的过期时间,结合之前的文章浏览器调度那里的说明
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  ...
  const update = createUpdate(expirationTime, suspenseConfig);
  ...
  //更新队列有两条,baseQueue 执行中的更新队列,
  //这里是创建一个 pendingQueue(即 shared.pending)待执行的更新队列
  enqueueUpdate(current, update);
  //开始调度,这里面是一个核心部分,下下篇开始介绍
  scheduleWork(current, expirationTime);

  return expirationTime;
}

这里面进行任务队列的创建,最终会去触发 scheduleWork 调度过程,其中还有很多的小细节,比如渲染时间相关的一些计算,下一篇继续。

0人赞

分享到: