react更新渲染流程源码解析

60 阅读5分钟

本文讲解[[react]]应用通过执行setState``useState更新数据后,页面重新渲染的过程。

在初始化渲染完成之后,fiber树是这样的:
   FiberRootNode
     |       ^
     |       |
  current stateNode              
     |       |
     V       |
   hostRootFiber   <-- alternate -->   hostRootFiber
    |         ^
    |         |
  child     return
    |         |
    V         |
HostComponentFiber(div)<------------------------------+
    |         ^                                       ^
    |         |                                       |
  child     return                                  return
    |         |                                       |
    V         |                                       |
FunctionComponentFiber(FnComp) -- sibling -> ClassComponentFiber(ClazzComp)
    |         ^                                   |       ^
    |         |                                   |       |
  child     return                              child   return
    |         |                                   |       |
    V         |                                   V       |
HostComponentFiber(p)                         HostComponentFiber(p)

触发更新

// 还是之前的例子,类组件添加更新逻辑
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>React App</title>
  </head>
  <body>
    <div id="container"></div>
  </body>
</html>

import ReactDom from 'react-dom/client'

function FnComp(props) {
    return <p>{props.children}</p>
}

import ReactDom from 'react-dom/client'

function FnComp(props) {
    return <p>{props.children}</p>
}

class ClazzComp extends React.Component {
    constructor(props) {
        super(props);
        this.state = { color: 'red' }
    }

    setCount = () => {
        this.setState({ color: this.state.color === 'red' ? 'blue' : 'red' })
    }

    render() {
        return <p style={{ color: this.state.color }} onClick={this.setCount}>{this.props.children}</p>
    }
}

ReactDOM.createRoot(document.getElementById('container')).render(
    <div id="root">
        <FnComp>hello</FnComp>
        <ClazzComp>react</ClazzComp>
    </div>
);
Component.protoptype.setState = function(partialState, callback) {
  // 这里的updater是在实例化类实例的时候添加的。
  this.updater.enqueueSetState(this, partialState, callback);
}
function enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  // 创建一个update对象
  const update = createUpdate();
  update.payload = payload;
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }
  enqueueUpdate(fiber, update);
  scheduleUpdateOnFiber(fiber)
}
function enqueueUpdate(fiber: Fiber, update) {
  const updateQueue = fiber.updateQueue;
  const sharedQueue = updateQueue.shared;
  const pending = sharedQueue.pending;
  // 将update对象串联到fiber.updateQueue.shared.pending上
  if (pending === null) {
    // 如果是空的,则直接赋值
    update.next = update;
  } else {
    // 将update链接到当前pending的后一个
    update.next = pending.next;
    pending.next = update;
  }
  // pending指向最新的update,便于再次链接
  sharedQueue.pending = update;
}

// 只有一个update对象时
   pending--+
            |
            V
    +--> update-1 --+
    |               |
    +------next-----+   

// 再添加一个update对象时
   pending------------------------+
                                  |
                                  V
    +--> update-1 -- next -->  update-2  ---+
    |                                       |
    +----------------next-------------------+  

function scheduleUpdateOnFiber(
  fiber: Fiber
) {
  // 首先根据当前fiber节点拿到FiberRoot
  const root = markUpdateLanFromFiberToRoot(fiber, lane);
  // 让scheduler模块执行自己的回调
  ensureRootIsScheduled(root);
}
function ensureRootIsScheduled(root: FiberRoot) {
  // 让scheduler一段时间后回调performConcurrentWorkOnFiber,可以理解成setTimeout
  // schedulerPriorityLevel表示优先级,不同的优先级表示不同的延迟时间
  // performConcurrentWorkOnFiber在初始化的时候会调用renderRootSync
  // 但是在重新渲染的时候也可能调用renderRootConcurrent
  // 区别在于renderRootSync是不间断的调用performUnitOfWork
  // renderRootConcurrent调用performUnitOfWork过程中可能中断、恢复
  scheduleCallback(
    shedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

准备阶段

// renderRootConcurrent会调用prepareFreshStack,依旧会创建workInProgress树
// 但这次是基于fiberRoot的current树,也是第一次构建的workInProgress树
workInProgress = createWorkInProgress(root.current);
function createWorkInProgress(current: Fiber) {
  // 尝试复用alternate对象,没有的话才新建一个fiber对象
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    // 初次渲染,生成一个fiber对象
  } else {
    // 复用fier对象
    // 重置一些属性
    workInProgrese.flags = NoFlags;
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deleteions = null;
  }
  // child也复制了。
  workInProgress.child = current.child;
  // updateQueue对象也引用
  // 这样workInProgress就可以根据update对象更新state了
  workInProgress.updateQueue = current.updateQueue;
}

render阶段

beginWork

function beginWork(
  current: Fiber,
  workInProgress: Fiber
) {
  if (current !== null) {
    const oldProps = currentMemoizedProps;
    const newProps = workInProgress.pendingProps;
    if (oldProps === newProps) {
      // props没有发生变化
      // 检查当前fiber节点是否需要走更新流程。react通过lanes来判断是否需要更新,后续说明。
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext();
      if (!hasScheduledUpdateOrContext) {
        // 如果不需要更新,则跳过beginWork的过程,即updateXXXComponent这样的函数
        // 在我们的例子中,下面组件会跳过更新
        // HostRootFiber
        // HostComponentFiber div#root
        // FunctionComponentFiber FnComp
        // 而ClassComponentFiber不会跳过更新,会执行updateClassComponent
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress);
      }
    }
  }
  switch(workInProgress.tag) {
    case ClassComponent: {
      const Component = workInProgress.type;
      return updateClassComponent(current, workInProgress, Component)
    }
  }
}
function updateClassComponent(
  current: Fiber,
  workInProgress: Fiber,
  Component: any
) {
  const instance = workInProgress.stateNode;
  if (instance === null) {
    // 这里是初次渲染分支:类组件实例化
    constructClassComponent();
    mountClassInstance();
  } else if (current === null) {
    // current是有值的,就是当前的渲染对应的fiber节点
  } else {
    // 更新流程走这里
    shouldUpdate = updateClassInstance(current, workInProgress, Component);
  }
  const nextUnitOfWork = finishClassComponent(current, workInProgress, Component);
  return nextUnitOfWork;
}

function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any
) {
  const instance = workInProgress.stateNode;
  const oldProps = workInProgress.memoizedProps;
  const oldState = workInProgress.memoizedState;
  let newState = instance.state = oldState;
  // 这里计算新的state,oldState+updateQueue 计算得到 newState
  processUpdateQueue(workInProgress, newProps, instance);
  newState = workInProgress.memoizedState;
  if (oldState === newState) {
    // 如果新旧state一样,就不用更新
    return false;
  }
  // 这里就是调用shouldComponentUpdate,如果pureComponent,就是浅比较,默认返回true
  const shouldUpdate = checkShouldComponentUpdate(
    workInProgress,
    ctor,
    oldProps,
    oldState,
    newState
  )
  // 将新的props state都添加到实例上,render函数才能访问得到
  instance.props = newProps;
  instance.state = newState;
  return shouldUpdate;
}

function finishClassComponent(
  current: Fiber,
  workInProgress: Fiber,
  Component: any
) {
  const instance = workInProgress.stateNode;
  // 生成新的子虚拟dom,因为this.state已更新,所以nextChildren和初始化渲染时有不同。
  // 我们的例子中将得到p对应的虚拟dom,且props包含style,置为{ color: 'blue' }
  // 旧的props中的style是{ color: 'red' }
  const nextChildren = instance.render();
  // 调和
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child
}
// 初始化也会调用mountChildFibers调和,更新渲染会调用reconcileChildFibers调和
// 2个函数都是工厂函数返回的,所以核心逻辑是一样的
// export const reconcileChildFibers = ChildReconciler(true);
// export const mountChildFibers = ChildReconciler(false);
function reconcileChildren(current, workInProgress, nextChildren) {
  if (current === null) {
    // 初始化走这里
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren
    )
  } else {
    // 再次渲染走这里
    // reconcileChildFibers和mountChildFibers是同一个工厂函数返回的,区别就是是否置一些flags
    // 也就是说2个函数就是同一函数。
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren
    )
  }
}

// 如果只有一个新的虚拟dom,就是我们的例子中的情况。
function reconcileSingleElement(
  returnFiber: Fiber, // 当前的workInProgress
  currentFirstChild: Fiber, // current.child
  newChild: any // 新的虚拟dom
) {
  if (typeof newChild === 'object' && newChild !== null) {
    switch(newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return reconcileSingleElement(returnFiber, currentFirstChild, newChild);
    }
  }
}

// 只有一个新的虚拟dom,老的fiber对象不知道几个
// 所以需要尝试复用,且删除多余的fiber对象
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  const key = element.key;
  let child = currentFirstChild;
  while(child !== null) {
    if (child.key === key) {
      if (child.elementType === element.type) {
        // 删除所有的兄弟节点
        deleteRemainingChildren(returnFiber, child.sibling);
        // 复用fiber,内部也是调用createWorkInProgress
        // 我们的例子会走到这里,会复用p标签对应的fiber对象
        const existing = useFiber(child, element.props);
        existing.return = returnFiber;
        return existing;
      }
      // key一样,那是没有复用,则全部删除
      deleteRemainingChildren(returnFiber, child);
    } else {
      // key不匹配则删除
      deleteChild(returnFiber, child);
    }
    // 从前往后尝试复用
    child = child.sibling;
  }

  // 走到这里是没能复用的情况:创建新的fiber节点
  const created = createFiberFromElement(element, returnFIber.mode);
  created.return = returnFiber;
  return create;
}

completeWork

function updateHostComponent(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props
) {
  // 计算得到新的props对象
  const updatePayload = prepareUpdate(
    workInProgress.stateNode,
    type,
    current.memoizedProps,
    newProps
  )
  // render阶段不提交,等到commit阶段再提交
  workInProgress.updateQueue = updatePayload;
}
render阶段结束后,内存中有2颗fiber树,current代表当前页面展示的树,workInProgress代表下次展示的树
同时同一层的对应的fiber节点之间是通过alternate相互引用的,如同2个hostRootFiber一样
以后createWorkInProgress就可以复用alternate的fiber对象。

   FiberRootNode
     |       ^
     |       |
  current stateNode                             workInProgress
     |       |                                        |
     V       |                                        V
   hostRootFiber  <-------- alternate -------->  hostRootFiber
    |         ^                                   |         ^
    |         |                                   |         |
  child     return                              child     return
    |         |                                   |         |
    V         |                                   V         |
HostComponentFiber(div)<------------+         HostComponentFiber(div)<------------+
    |         ^                     ^             |         ^                     ^
    |         |                     |             |         |                     |
  child     return                return        child     return                return
    |         |                     |             |         |                     |
    V         |                     |             V         |                     |
 FunctionComponent -sibling-> ClassComponent  FunctionComponent -sibling-> ClassComponent
    |         ^                 |       ^         |         ^                 |       ^
    |         |                 |       |         |         |                 |       |
  child     return            child   return    child     return            child   return
    |         |                 |       |         |         |                 |       |
    V         |                 V       |         V         |                 V       |
  HostComponent(p)           HostComponent(p)   HostComponent(p)             HostComponent

commit阶段

function commitMutationEffectsEffectsOnFiber(
  finishedWork: Fiber
) {
  switch(finishedWork.tag) {
    case HostComponent: {
      const instance = finishedWork.stateNode;
      if (instance !== null) {
        const updatePayload = finishedWork.updateQueue;
        finishedWork.updateQueue = null
        if (updatePayload !== null) {
          // 将render阶段的计算结果应用到instance上
          commitUpdate(instance, updatePayload);
        }
      }
    }
  }
}

总结

  • 类组件通过setState提交一个update对象,同样也需要经历准备阶段、render阶段、commit阶段,最终将修改应用到页面上
  • 首次渲染,react会为所有组件(函数组件、类组件、host组件)创建对应的fiber对象;再次渲染时react会复用老的fiber对象。具体来说,初次渲染和第二次渲染都会创建新的fiber对象,之后都会复用老的fiber对象