【React源码笔记7】- beginWork

React | 2020-07-17 23:56:17 508次 0次

上一篇磕磕碰碰的总算梳理了一下调度部分代码,还是需要回过头不停的看,目前理解只能说还停留在第二层,其实是在第一层,希望后续能到第五层,接下来开始 render 阶段的笔记。这里开始之前再回顾下之前模拟实现系列文章模拟实现react


一、概述

render 阶段有两个任务

    ① 根据虚拟 DOM 生成 fiber 树 ,diff 也是这个过程中进行

    ② 收集 effectlist ,它记录了节点的更新、修改或删除。

调度最后会执行到 performSyncWorkOnRoot 同步方法或者 performConcurrentWorkOnRoot 异步方法,这两个方法里面做的事情还比较多,但是目前先看主要的一件事,就是根据不同方式去执行 performUnitOfWork在执行过程中创建 fiber 树并按照深度优先遍历的规则进行渲染,如果到达某个节点后此节点无子节点则证明这个节点渲染完成,也是第一个渲染完成的节点,以此类推。

function workLoopSync() {
  //无中止的执行,直到执行完
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // 调度的部分提到过,分片执行
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

在第一次更新整个过程中每一个 fiber 节点都会被创建一个 alternate 指向旧的 fiber 节点,第二次更新时如果节点类型相同会复用上一次更新后的 fiber alternate,并将此时的节点 alternate 指向上一次更新后的节点,这样做的目的就是一直相互复用对象,大量节点的情况下避免了垃圾回收机制不断触发,节省一定的 cpu 开销。图片参考

这个过程是一个深度优先的遍历,其中完成工作是在最左侧的子节点(其没有子节点)开始依次完成到根节点。

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  const current = unitOfWork.alternate;
  ...
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    //性能分析,运行时间计算 放在了 fiber.actualDuration 属性中
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }
  ...
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    //没有子节点了  开始执行完成工作
    next = completeUnitOfWork(unitOfWork);
  }
  ...
  //返回节点 下次继续开始工作
  return next;
}


二、beginWork

开始进行工作处理,分为初始渲染和更新两种情况,更新时候可以选择性的跳过更新进行优化。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes): Fiber | null {
  //过期时间
  const updateExpirationTime = workInProgress.expirationTime;
  
  // 更新 --> 初次创建为空 workInProgress.alternate
  if (current !== null) {
    //本次更新之前的 props
    const oldProps = current.memoizedProps;
    //新产生的 props
    const newProps = workInProgress.pendingProps;

    if (
      //前后props不同
      oldProps !== newProps ||
      //兼容旧版写法 context
      hasLegacyContextChanged() ||
      ...
    ) {
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      // 优先级低
      didReceiveUpdate = false;
      ...
      // 可以跳过更新 或者 复用节点
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    //初始渲染
    didReceiveUpdate = false;
  }

  // 根据tag标识,创建对应的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...
    case FunctionComponent: 
      // ...
    case ClassComponent: 
      // ...
    case HostRoot:
      // ...
    case HostComponent:
      // ...
    ....
  }}
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  ...
  
  const childExpirationTime = workInProgress.childExpirationTime;
  if (childExpirationTime < renderExpirationTime) {
    //子树不需要更新
    return null;
  } else {
    //需要更新  clone节点 等待下一个 performUnitOfWork
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}


三、组件类型

tag 类型里面有很多分支,大概分为五大类,参考来源

1. 有状态组件:ClassComponent,ContextConsumer,  ContextProvider

2. 无状态组件:FunctionComponent,IndeterminateComponent, ForwardRef, MemoComponent 与SimpleMemoComponent

IndeterminateComponent 中会区分类组件或者函数组件,初始不知道组件类型

3. 原生组件:HostRoot, HostPortal, HostComponent 与 HostText

HostRoot 默认为 3,初次要渲染这里 ReactDOM.render 的渲染起点;HostPortal 即为 Portal 组件,独立渲染;

HostComponent 就是元素节点; HostText为文本节点。

4. 虚拟组件:Fragment, Mode,Profiler

Fragment 空标签包裹,相当于数组包裹。

5. 懒加载组件:SuspenseComponent,LazyComponent

接下来进一步的看下每种不同类型的组件做的事情,不一一列举,只阅读部分组件,最终他们都会调用 reconcileChildren


四、HostRoot

它对应着 updateHostRoot 方法:

function updateHostRoot(current, workInProgress, renderExpirationTime) {
  ...
  const updateQueue = workInProgress.updateQueue;
  ...
  //props state
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  //拷贝前者给后者的 updateQueue
  cloneUpdateQueue(current, workInProgress);
  //更新 update 队列,并更新 state
  processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime);
  const nextState = workInProgress.memoizedState;
  // 初始 element挂载到了 update.payload = { element }  就是dom.render传入的第一个节点
  const nextChildren = nextState.element;
  if (nextChildren === prevChildren) {
    //不需要更新则跳过 bailoutOnAlreadyFinishedWork方法上面有说
    ...
  }
  const root: FiberRoot = workInProgress.stateNode;
  if (root.hydrate && enterHydrationState(workInProgress)) {
    ...
  } else {
    // 调和作用  这里面做最重要的事情,后面再分析
    reconcileChildren(
      current,
      workInProgress,
      nextChildren,
      renderExpirationTime,
    );
    ...
  }
  // 给到 beginWork 函数 next  下个时间片继续干活,记录位置
  return workInProgress.child;
}

processUpdateQueue 这个方法先在这里进行简单分析,后面使用还比较多,参考自卡颂代码注释

// 通过遍历update链表,根据fiber.tag不同,通过不同的路径计算新的state
export function processUpdateQueue(workInProgress, nextProps, instance, renderExpirationTime) {
  const queue = workInProgress.updateQueue;
  // base update 为 单向非环链表
  let firstBaseUpdate = queue.firstBaseUpdate;
  let lastBaseUpdate = queue.lastBaseUpdate;

  // 如果有 pendingUpdate,需要将 pendingUpdate单向环状链表剪开并拼在baseUpdate单向链表后面
  let pendingQueue = queue.shared.pending;
  if (pendingQueue) {
    queue.shared.pending = null;
    const lastPendingUpdate = pendingQueue;
    const firstPendingUpdate = pendingQueue.next;
    // 将环剪开
    lastPendingUpdate.next = null;

    // 将pendingUpdate拼入baseUpdate
    if (!lastBaseUpdate) {
      firstBaseUpdate = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;

    const current = workInProgress.alternate;
    // 存在current 更新其updateQueue
    if (current) {
      const currentQueue = current.updateQueue;
      const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
      if (lastBaseUpdate !== currentLastBaseUpdate) {
        if (!currentLastBaseUpdate) {
          currentQueue.firstBaseUpdate = firstPendingUpdate;
        } else {
          currentLastBaseUpdate.next = firstPendingUpdate;
        }
        current.lastBaseUpdate = lastPendingUpdate;
      }
    }
  }

  if (firstBaseUpdate) {
    // 存在update时遍历链表,计算出update后的值
    let newState = queue.baseState;
    let newExpirationTime = NoWork;

    let newBaseState = null;
    let newFirstBaseState = null;
    let newLastBaseState = null;

    let update = firstBaseUpdate;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // 该update优先级不够,跳过
        // 新的state是基于baseUpdate计算得出
        // 如果这是第一个跳过的update,则之前的update/state就是新的 base update/state
        const clone = {
          expirationTime: update.expirationTime,
          tag: update.tag,
          payload: update.payload,
          next: null
        }
        if (newLastBaseState === null) {
          newFirstBaseState = newLastBaseState = clone;
          newBaseState = newState;
        } else {
          newLastBaseState = newLastBaseState.next = clone;
        }
        if (updateExpirationTime > newExpirationTime) {
          newExpirationTime = updateExpirationTime;
        }
      } else {
        // 该update有足够的优先级,基于该update计算newState
        if (newLastBaseState) {
          // 同时将该update加入baseUpdate
          // 对于 !newLastBaseState 的情况在下面 newBaseState = newState; 处处理
          const clone = {
            expirationTime: Sync,
            tag: update.tag,
            payload: update.payload,
            next: null
          };
          newLastBaseState = newLastBaseState.next = clone;
        }
        // Object.assign({}, prevState, update.payload); 计算新的状态
        newState = getStateFromUpdate(workInProgress, queue, update, newState, nextProps, instance);
      }
      update = update.next;
      if (!update) {
        pendingQueue = queue.shared.pending;
        if (!pendingQueue) {
          break;
        } else {
          // 在reducer内部又产生了新的update,则继续计算他
          const lastPendingUpdate = pendingQueue;
          const firstPendingUpdate = lastPendingUpdate.next;
          lastPendingUpdate.next = null;
          update = firstPendingUpdate;
          queue.lastBaseUpdate = lastPendingUpdate;
          queue.shared.pending = null;
        }
      }
    } while(true)

    if (!newLastBaseState) {
      newBaseState = newState;
    }
    //赋值最新的数据
    queue.baseState = newBaseState;
    queue.firstBaseUpdate = newFirstBaseState;
    queue.lastBaseUpdate = newLastBaseState;

    workInProgress.expirationTime = newExpirationTime;
    workInProgress.memoizedState = newState;
  }
}

这个方法很长,目前里面有些东西还不太确定对应场景,其中调用的 getStateFromUpdate 方法,计算新的 state,遵守 immutable 思想,返回一个全新的对象。


五、IndeterminateComponent

 这个是一个默认值,createFiberFromTypeAndProps 方法会生成对应的各种组件类型,方法在后面再分析,先了解下即可,但是此方法没有指定 FunctionComponent,导致 tag 使用的就是默认值,所以如果组件内包含了一个函数组件,初始进入时候会进入这个分支,并不是直接进入 FunctionComponent,但是初始渲染之后会标记为函数组件,之后就不会再进入这里了。其中又有一个判断是否为类组件的情况,难道是要函数组件内支持类组件 api ?答案:是的!可以通过如下方式,可以正常渲染但是会给出警告信息:

//这是一种非常不好的做法,尽管可以渲染
export default function Com() {
  return {
    render(){
        return (
            <div>
                1111111111111
            </div>
          )
    }
  }
}
function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderExpirationTime,
) {
  ...

  if (
    typeof value === 'object' &&
    value !== null &&
    typeof value.render === 'function' &&
    value.$$typeof === undefined
  ) {
    // 因为函数组件支持类组件的方法,通过return 一个对象,这部分逻辑和最下面的classCom差不多
  } else {
    // 打标记 FunctionComponent
    workInProgress.tag = FunctionComponent;
    ...
    reconcileChildren(null, workInProgress, value, renderExpirationTime);
    ...
    return workInProgress.child;
  }
}


六、FunctionComponent

函数组件类型:

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderExpirationTime,
) {
  ...
  if (__DEV__) {
    ...
  } else {
    // hooks 相关 后面再看
    nextChildren = renderWithHooks(...);
  }

  if (current !== null && !didReceiveUpdate) {
    //跳过 hooks 更新
    bailoutHooks(current, workInProgress, renderExpirationTime);
    // 普通的跳过更新
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }

  // React DevTools reads this flag.
  workInProgress.effectTag |= PerformedWork;
  //调和过程
  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}


七、HostComponent

元素节点类型,对于只包含纯文本的节点 <div>111</div>  或者文本域、选择项做一种优化措施,回顾一下模拟实现2中的那张图,从 beginWork 到 completeWork 的过程中走到如上举例的 div 后如果内容为文本则立即完成,避免再走到 111 文本内容再完成。

function updateHostComponent(current, workInProgress, renderExpirationTime) {
   ...

  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  if (isDirectTextChild) {
    //对于文本节点的一种优化
    nextChildren = null;
  }
  ...
  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime
  )
  return workInProgress.child;
}
// 只包含纯文本的节点  <div>111</div>  或者文本域、选择项
export function shouldSetTextContent(type: string, props: Props): boolean {
  return (
    type === 'textarea' ||
    type === 'option' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number' ||
    (typeof props.dangerouslySetInnerHTML === 'object' &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  );
}


八、ClassComponent

类组件的一些逻辑处理,包括生命周期的触发,这部分源码是更加贴近我们的业务,所以看起来没有太抽象。

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps,
  renderExpirationTime: ExpirationTime,
) {
  ...
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    /...
    // 构建类组件实例
    constructClassInstance(workInProgress, Component, nextProps);
    //生命周期钩子调用
    mountClassInstance(...);
    shouldUpdate = true;
  } else if (current === null) {
    // 渲染被中断过,所以会被反复触发
    // 调用生命周期(componentWillMount,componentDidMount),返回 shouldUpdate
    shouldUpdate = resumeMountClassInstance(...);
  } else {
    //当已经创建实例并且不是第一次渲染的话
    //调用更新的生命周期方法为componentWillReceiveProp componentWillUpdate,componentDidUpdate(),
    shouldUpdate = updateClassInstance(...);
  }
  //判断是否执行 render,并返回 render 下的第一个 child
  const nextUnitOfWork = finishClassComponent(...);
  ...
  return nextUnitOfWork;
}

第一次渲染如果还没创建实例,初始化生成实例instance,然后挂载实例更新 instance.state,并且执行一些生命周期

渲染被中断:复用实例并且调用首次渲染的生命周期,所以 react 需要抛弃旧的生命周期钩子

更新渲染:处理更新阶段的生命周期钩子

最终执行 finishClassComponent 调和子节点 reconcileChildren

在进入 reconcileChildren 之前,下一篇先对类组件详细分析。

0人赞

分享到: