react初始化渲染都做了什么?如果你不知道带你从源码中一探究竟

有的同学写了很久的react但是并不知道其内部原理是怎么执行的,比如在页面初始化展示的过程中内部都发生了什么,做了什么事都不是很清楚,今天看完这篇文章就能让你知道react初始化挂载页面的时候内部都做了些什么事

目标:阅读完之后了解ReactDOM.render从0到1的过程都做了什么

准备工作

前期准备工作需要去react官网下载一份源码,之后我在下面的篇幅中会具体指出是哪一个文件哪一行代码具体做了什么事情,大家可以根据自己下载的源码对照着文章去解读源码,在解读的过程中可能会有些痛苦,但是结果有收获就是好的,可能react的源码解读起来也有一些难度不过这也正常毕竟是比较顶尖的程序猿编写的

1.首先去下面这个地方下载一下源码,防止看的不一样所以就指定tag17.0.2的源码去阅读

1291646989449_.pic.jpg

2.下面是我们即将要看的相关文件目录都是一些核心的部分

1311646991316_.pic_hd.jpg

  • react-dom核心文件都在这里暴露出来比如render、createPortal、hydrate等核心api
  • react-reconciler看英文也知道这里主要做的是节点之间的协调,包括节点的创建和节点之间的组装、提交和diff比较更新等操作
  • scheduler任务之间的调度主要发生在这个文件

开始

下面会按照文件划分模块去依次讲解里面都做了什么,都会以截图的方式和增加注释进行展示,然后里面我觉得不需要讲解的都会折叠起来或者删掉免得影响阅读,如果大家懒得下载源码自己对着阅读,可以多看两遍这个文章心里有个大概就好

render

我们直接来看render方法,这个就是我们外面调用的ReadtDOM.render接下来揭开它神秘的面纱

文件目录 react-dom -> src -> client -> ReactDOMLegacy.js

1341646992644_.pic.jpg

  • render接收三个参数

    • 第一个是我们传进来的JSX对象也就是常说的Virtal DOM,这里直接变成了Virtal DOM是因为在webpack启动打包中,就编译成这样子了
    • 第二个是渲染元素乘载的容器一般是<div id='root'></div>
    • 第三个是一个回调函数当应用挂载完成之后就会执行,类似于window.onload,监测应用挂载完成
  • render方法里面调用了legacyRenderSubtreeIntoContainer,这个函数在hydrate里面也有调用也就是服务端渲染的时候,接下来看这个方法内部都干了点啥

legacyRenderSubtreeIntoContainer

文件目录 react-dom -> src -> client -> ReactDOMLegacy.js

1381647334774_.pic.jpg

  • 这个方法做的第一件事判断是否有root如果没有说明是首次渲染

    // Initial mount should not be batched.
    unbatchedUpdates(() => {
      //首次渲染嵌套了unbatchedUpdates 走非批量更新
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
    复制代码

    外层通过调用unbatchedUpdates方法使当前环境变为非批量更新阶段

  • 如果有root证明是更新阶段进行批量更新渲染

    // Update
    //进行批量更新任务
    updateContainer(children, fiberRoot, parentComponent, callback);
    复制代码

    可以看到外层没有嵌套unbatchedUpdates方法说明当前是走批量更新的流程

  • 接下来我们看unbatchedUpdates方法内部是怎么定义的变量,因为今天主要看初始化流程

unbatchedUpdates

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1391647335586_.pic_hd.jpg

  • 设置当前环境为非批量更新

    const prevExecutionContext = executionContext;
    //表示当前上下文环境中BatchedContext的位置是非批量更新
    executionContext &= ~BatchedContext;
    //在executionContext上设置为非批量更新
    executionContext |= LegacyUnbatchedContext;
    复制代码
  • 执行fn方法这个其实就是执行updateContainer进行更新

    try {
      return fn(a);
    } finally {
    }
    复制代码
  • 接下来继续看updateContainer方法内部做了啥

updateContainer

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1401647336242_.pic.jpg

  • updateContainer方法主要是用来执行更新任务接收四个参数

    • 第一个element要更新的节点
    • 第二个container是容器元素
    • 第三个parentComponent是上一次的节点主要是用在更新阶段的
    • 第四个callback参数是完成之后的通知方法
  • 第一步创建update节点,用于之后挂载到fiber上面方便后续diff更新

    //创建更新任务fiber节点
    const update = createUpdate(eventTime, lane);
    ​
    //将JSX对象挂载到payload上
    update.payload = {element};
    复制代码
  • createUpdate方法内部生成fiber节点并返回

    export function createUpdate(eventTime: number, lane: Lane): Update<*> {
      const update: Update<*> = {
        eventTime,
        lane,
    ​
        tag: UpdateState,
        payload: null,
        callback: null,
    ​
        next: null,
      };
      return update;
    }
    复制代码
  • 第二步将任务挂载到fiber节点上

    //将任务挂载到fiber节点上方便后期更新
    enqueueUpdate(current, update);
    复制代码
  • enqueueUpdate方法内部进行挂载操作

    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
      const updateQueue = fiber.updateQueue;
      //这里是当执行销毁期函数时会发生
      if (updateQueue === null) {
        // Only occurs if the fiber has been unmounted.
        return;
      }
    ​
      const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
      const pending = sharedQueue.pending;
      if (pending === null) {
        // This is the first update. Create a circular list.
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      sharedQueue.pending = update;
    }
    复制代码
  • 第三步开始更新fiber节点接下来主要看scheduleUpdateOnFiber这个文件有点大

scheduleUpdateOnFiber

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1461647420938_.pic.jpg

  • 这个方法主要看红色标注的逻辑

    if (
      // 判断当前存在的环境是否是批量更新的情况
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // 判断当期的环境是否出是初始化环境
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      //初始化并且不是批量更新的情况会走进来
      schedulePendingInteractions(root, lane);
    ​
      //执行任务开始挂载节点
      performSyncWorkOnRoot(root);
    }
    复制代码
  • performSyncWorkOnRoot方法里面调用了renderRootSync核心逻辑都在这个方法里面

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1471647421252_.pic.jpg

renderRootSync

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1481647421408_.pic.jpg

  • 这个方法在do while这里主要是循环提交任务,提交任务的操作在workLoopSync

    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    复制代码

workLoopSync

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1491647421528_.pic.jpg

  • workInProgress是全局存的一个变量主要用来执行当前的任务

1501647421612_.pic_hd.jpg

  • 接下来继续看performUnitOfWork

performUnitOfWork

文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js

1511647421685_.pic.jpg

  • 这里开始调用beginWork,我们直接看beginWork内部做了什么

beginWork

文件目录 react-reconciler -> src -> ReactFiberBeginWork.old.js

  • 这个文件比较大我会把咱们需要看的单独拆出来

    //新旧两个props比较
    //前后context进行比较
    //如果有一个为真didReceiveUpdate设置为true
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true; //! 如果需要更新设置为true
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;//! 如果不需要更新设置为false
    }
    复制代码
  • 在往下是根据workInProgress上面的tag去判断执行不同的任务

     switch (workInProgress.tag) {
        case LazyComponent: {
          const elementType = workInProgress.elementType;
          return mountLazyComponent(
            current,
            workInProgress,
            elementType,
            updateLanes,
            renderLanes,
          );
        }
        case FunctionComponent: {
          const Component = workInProgress.type;
          const unresolvedProps = workInProgress.pendingProps;
          const resolvedProps =
            workInProgress.elementType === Component
              ? unresolvedProps
              : resolveDefaultProps(Component, unresolvedProps);
          return updateFunctionComponent(
            current,
            workInProgress,
            Component,
            resolvedProps,
            renderLanes,
          );
        }
         //更新类组件
        case ClassComponent: {
          const Component = workInProgress.type;
          const unresolvedProps = workInProgress.pendingProps;
          const resolvedProps =
            workInProgress.elementType === Component
              ? unresolvedProps
              : resolveDefaultProps(Component, unresolvedProps);
          return updateClassComponent(
            current,
            workInProgress,
            Component,
            resolvedProps,
            renderLanes,
          );
        }
        case HostRoot:
          return updateHostRoot(current, workInProgress, renderLanes);
        case HostComponent:
          return updateHostComponent(current, workInProgress, renderLanes);
        case HostText:
          return updateHostText(current, workInProgress);
        case SuspenseComponent:
          return updateSuspenseComponent(current, workInProgress, renderLanes);
        case HostPortal:
          return updatePortalComponent(current, workInProgress, renderLanes);
      }
    复制代码
  • 这里的switch方法删除了一部分然后留下了一部分常见的,之后我们今天用ClassComponent来举例看看内部是怎么挂载实现的,类组件case里面调用了updateClassComponent

updateClassComponent

文件目录 react-reconciler -> src -> ReactFiberBeginWork.old.js

1521647422691_.pic.jpg

这里主要看resumeMountClassInstance方法和finishClassComponent方法

resumeMountClassInstance

文件目录 react-reconciler -> src -> ReactFiberClassComponent.old.js

  • 1.先看resumeMountClassInstance方法内部做了啥,这里会把咱们要看的代码拿出来因为整个方法比较长

    const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
    const hasNewLifecycles =
          typeof getDerivedStateFromProps === 'function' ||
          typeof instance.getSnapshotBeforeUpdate === 'function';
    复制代码

这里判断了是否有这两个生命周期如果有在下面会执行

  • 2.看一下下面处理state的逻辑

    const oldState = workInProgress.memoizedState;
    let newState = (instance.state = oldState);
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
    newState = workInProgress.memoizedState;
    复制代码

这里processUpdateQueue内部会进行state合并并且返回一个合并好的新的state,之后在下面进行挂载

  • 3.这里判断组件上是否有这个生命周期

    if (
      unresolvedOldProps === unresolvedNewProps &&
      oldState === newState &&
      !hasContextChanged() &&
      !checkHasForceUpdateAfterProcessing()
    ) {
      if (typeof instance.componentDidUpdate === 'function') {
        if (
          unresolvedOldProps !== current.memoizedProps ||
          oldState !== current.memoizedState
        ) {
          workInProgress.flags |= Update;
        }
      }
      if (typeof instance.getSnapshotBeforeUpdate === 'function') {
        if (
          unresolvedOldProps !== current.memoizedProps ||
          oldState !== current.memoizedState
        ) {
          workInProgress.flags |= Snapshot;
        }
      }
      return false;
    }
    复制代码

    判断如果组件中有生命周期就会在当前环境标注一下

  • 4.了解一下getDerivedStateFromProps方法

    if (typeof getDerivedStateFromProps === 'function') {
      applyDerivedStateFromProps(
        workInProgress,
        ctor,
        getDerivedStateFromProps,
        newProps,
      );
      newState = workInProgress.memoizedState;
    }
    复制代码

    这个方法是react新增的可以用来给state赋初始值

  • 5.神秘的ShouldComponentUpdate方法背后的代码

    const shouldUpdate =
          checkHasForceUpdateAfterProcessing() ||
          checkShouldComponentUpdate(
            workInProgress,
            ctor,
            oldProps,
            newProps,
            oldState,
            newState,
            nextContext,
          );
    if (shouldUpdate) {
      if (
        !hasNewLifecycles &&
        (typeof instance.UNSAFE_componentWillMount === 'function' ||
         typeof instance.componentWillMount === 'function')
      ) {
        if (typeof instance.componentWillMount === 'function') {
          instance.componentWillMount();
        }
        if (typeof instance.UNSAFE_componentWillMount === 'function') {
          instance.UNSAFE_componentWillMount();
        }
      }
      if (typeof instance.componentDidMount === 'function') {
        workInProgress.flags |= Update;
      }
    } else {
      if (typeof instance.componentDidMount === 'function') {
        workInProgress.flags |= Update;
      }
      workInProgress.memoizedProps = newProps;
      workInProgress.memoizedState = newState;
    }
    复制代码

这里判断shouldComponentUpdate是否返回true并且内部是否有用了PureReactComponent然后之后执行不同的生命周期,最后给workInProgres上面的memoizedProps和memoizedState重新赋值,因为在后期渲染的时候会用到

  • 6.这个方法的尾部给实例上的属性赋值props、state、context,这就是为什么在组件中可以用this调用这些东西

    instance.props = newProps;
    instance.state = newState;
    instance.context = nextContext;
    复制代码

finishClassComponent

文件目录 react-reconciler -> src -> ReactFiberBeginWork.old.js

  • 这里主要看里面的reconcileChildren方法,这个方法内部进行节点调和

1531647424128_.pic.jpg

reconcileChildren

文件目录 react-reconciler -> src -> ReactFiberBeginWork.old.js

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  //current为null证明是首次渲染走下面这个if, mountChildFibers
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
复制代码

接下来继续看mountChildFibers,这个方法调用的是一个工厂函数叫做ChildReconciler

// 可以看到 都是调用的ChildReconciler方法只是入参不一样 true 是进行更新操作  false是进行初始化操作
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
复制代码

ChildReconciler

文件目录 react-reconciler -> src -> ReactChildFiber.old.js

  • 这个方法是一个工厂函数里面包含了20几个方法总共要一千多行,里面主要包括了对各种类型的节点比较处理更新操作,如果大家感兴趣可以跟着文件目录去看一下这个方法大概在270多行,下面我会把初始化的逻辑代码拿出来解释

工厂函数返回了这个方法reconcileChildFibers里面主要是协调子节点

  • 当是对象的情况会单独处理对象里面的子节点
//根据对应的节点进行更新操作,针对数组对象还有普通文本节点
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE:
      return placeSingleChild(
        reconcileSingleElement(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        ),
      );
    case REACT_PORTAL_TYPE:
      return placeSingleChild(
        reconcileSinglePortal(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        ),
      );
    case REACT_LAZY_TYPE:
      if (enableLazyElements) {
        const payload = newChild._payload;
        const init = newChild._init;
        // TODO: This function is supposed to be non-recursive.
        return reconcileChildFibers(
          returnFiber,
          currentFirstChild,
          init(payload),
          lanes,
        );
      }
  }
}
复制代码

普通文本节点的处理

if (typeof newChild === 'string' || typeof newChild === 'number') {
  return placeSingleChild(
    reconcileSingleTextNode(
      returnFiber,
      currentFirstChild,
      '' + newChild,
      lanes,
    ),
  );
复制代码

这些节点更新方法内部都会调用一个createFiber方法主要用来创建fiber节点

  • 文件目录 react-reconciler -> src ->ReactFiber.old.js
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

文件目录 react-reconciler -> src ->ReactFiber.old.js

在看一下FiberNode创建节点内部,这个方法是fiber创建的一个基类

1561647487540_.pic.jpg

解析一下fiber中的节点

this.tag = tag; //标记组件类型
this.key = key; //节点key值在后期diff的时候会用到
this.type = null;//节点type类型可能是div或者其他标签,是组件的时候就是函数 
this.stateNode = null; //真实节点比如就是div或者是其他标签// Fiber
this.return = null; //指向的父节点
this.child = null; //指向的子节点
this.sibling = null; //指向的兄弟节点this.memoizedState = null; //存储渲染完成结束后的state
this.pendingProps = pendingProps; //一般是初始化之后的props
this.memoizedProps = null; // 执行完渲染结束之后的props
this.lanes = NoLanes; //当前节点任务的优先级是否要渲染它,还是有更高的优先级任务
this.alternate = null; //存储上一次的节点,在diff的时候会根据这个节点上面的属性和当前节点属性做判断
复制代码

结语

到这里初始化源码分析就结束了,如果大家有时间的话可以对这源码文件看自己梳理一遍,心里有个大概流程,可能要花上半个小时的时间,但是梳理完了自己对初始化流程的理解肯定是有一定提升的

猜你喜欢

转载自juejin.im/post/7075991100216836110