[legacy模式]详解React的一次更新

949 阅读6分钟

什么操作会导致react组件更新

  1. 组件本身state的改变:类组件 setState、函数组件 useState
  2. props发生变化:由父组件传递给子组件(父组件的state更新)
  3. 组件使用到的上下文context发生变化(provider提供者的value变化,而value一般也由state维护)

🚧🚧总结:react组件的更新是由state变化引起的

render过程中重要函数的流程图

  • 更新入口:scheduleUpdateOnFiber,标记childLane,然后发起调度函数ensureRootIsScheduled
  • schedule调度中心执行回调callback后,执行performSyncWorkOnRoot函数,开始真正的渲染
  • render阶段是个workLoop循环,对fiber节点进行调和和更新;commit提交真正的DOM节点,更新结束

调度schedule

为什么使用调度

老版本的react在更新时会一次性遍历大量虚拟DOM,占用js线程,浏览器无法绘制导致页面卡顿。所以新版react的更新交由浏览器控制,让浏览器在空闲的时间段执行更新任务

事件切片 time slicing

每次事件循环React申请时间片,若浏览器有空闲时间,将执行react的更新任务

调度原理

调度入口 ensureRootIsScheduled

  1. 判断是否需要注册新的调度任务,不需要则退出
    • 进行existingCallbackPriority === newCallbackPriority的判断
    • 不相等的情况注册新的任务调度
    • 相等情况下则认为之前已经注册了调度任务了(比如连续进行了两次及以上的setState),退出函数。即多次触发更新只有第一次会进入调度,这个原则与react的批量更新实现有关。
  2. 注册调度任务,将performSyncWorkOnRoot(同步)或performConcurrentWorkOnRoot(异步)封装到scheduleCallback,等到调度中心执行

任务队列 taskQueue timerQueue

scheduleCallback函数内部创建新任务,根据startTime > currentTime判断是否为超时任务

  • 超时任务放入taskQueue中,等待scheduler中的workLoop去执行
  • 未超时的任务放入timerQueue中
    • 调用requestHostTimeout延时执行函数handleTimeOut,延时时间为startTime - currentTime
    • handleTimeOut函数将重新调取requestHostCallback请求回调执行函数
    • handleTimeOut函数中会判断timerQueue中的task是否超时,若超时,则调用advanceTimers将timerQueue中的任务push到taskQueue中

通信实现 MessageChannel

// MessageChannel接受消息
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    // 执行回调callback
    scheduledHostCallback(hasTimeRemaining, currentTime);
  } else {
    isMessageLoopRunning = false;
  }
};

//messageChannel
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

// 请求回调
requestHostCallback = function(callback) {
  // 赋值scheduledHostCallback
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    // MessageChannel发送消息
    port.postMessage(null);
  }
};
  1. 建立channel通道和port接口
  2. requestHostCallback请求回调,将要执行的函数赋值给scheduledHostCallback,然后通过MessageChannel发送消息
  3. MessageChannel接受消息,调用performWorkUntilDeadline,执行scheduledHostCallback

🚧🚧注意:MessageChannel在事件循环中是宏任务,所以callback都是异步执行的

调度中心示意图

render 阶段

react的整个渲染过程(具体看上一篇文章

  1. JSX代码通过React.createElement方法生成ReactElement结构
  2. Element结构通过schedule的调度reconcile去生成fiber结构树
  3. fiber结构树通过commit操作生成真实DOM元素

react的fiber树

举个🌰

export default function App() {
  const [count,setCount] = useState(0)
  
  const handleClick=()=>{
    setCount(count+1)
  }

  return(
    <div>
      hello akechi,
      <p> 这是我的count,值为{count} </p>
      <button onClick={handleClick} >按钮</button>
    </div>
  )
}

画出它的fiber树架构

触发更新:当点击按钮的时候,setState调用了更新

  1. 类组件触发更新
enqueueSetState(inst: any, payload: any, callback) {
  const fiber = getInstance(inst);
  const lane = requestUpdateLane(fiber); //获取任务优先级lane
  scheduleUpdateOnFiber(fiber, lane, eventTime); //调用更新入口
}
  1. 函数组件触发
function dispatchReducerAction(fiber,queue,action){ 
    const lane = requestUpdateLane(fiber); //获取任务优先级lane
    scheduleUpdateOnFiber(fiber, lane, eventTime); //调用更新入口
}

🚧🚧注意:更新并无区别,计算出任务优先级lane后调用更新函数scheduleUpdateOnFiber

scheduleUpdateOnFiber做了什么

  1. 调用了markUpdateLaneFromFiberToRoot方法,在fiber树中从更新节点递归向上标记此次的更新优先级,如果无更新退出函数

举个🌰:当点击了count的时候

  • 红色块为触发更新的区域,后续会被调和且render
  • 蓝色块为它的祖父节点,后续会被调和但不执行render
  • 绿色块将在workLoop的调和中被忽略
  1. 调用ensureRootIsScheduled,开始任务的调度(调度请看上面的章节)。调度中心产生回调callback,最后执行performSyncWorkOnRoot函数,即进行render和commit
function performSyncWorkOnRoot(root) { 
    /* render阶段 */ 
    let exitStatus = renderRootSync(root, lanes); 
    /* commit阶段 */ 
    commitRoot(root); 
    /*更新剩余任务*/ 
    ensureRootIsScheduled(root, now()); 
}

调和阶段

首先先看下render阶段几个比较重要的函数片段

function renderRootSync(root,lanes){ 
    workLoopSync(); 
    /* workLoop完毕后,证明所有节点都遍历完毕,那么重置状态,进入 commit 阶段 */ 
    workInProgressRoot = null; 
    workInProgressRootRenderLanes = NoLanes; 
}

function workLoopSync() { 
    /* 循环执行 performUnitOfWork ,一直到 workInProgress 为空 */ 
    while (workInProgress !== null) { 
        performUnitOfWork(workInProgress); 
    }
}

function performUnitOfWork(unitOfWork){ 
    /*向下调和fiber节点*/
    let next = beginWork(current, unitOfWork, subtreeRenderLanes); 
    if (next === null) { 
    /*向上回溯fiber节点*/
    completeWork(unitOfWork); 
    } else { 
    workInProgress = next; 
    } 
}
  • render阶段的workLoop是个while循环,由fiberRoot为基递归式的遍历子节点
  • beginWork调和的是父子节点child,由父节点向子节点调和
  • complteWork调和的是兄弟节点sibling,如果没有兄弟节点则向上调和return

注意:更新组件时,fiber树中的节点是不需要每个都深度遍历过去的,beginwork有一系列优化策略

  • 以下情况不考虑memo、pureComponet、shouldComponentUpdate等优化策略
  • 判断父组件传的props是否发生变化,props发生变化则该节点需要reRender(优化策略下不刷新)
  • 判断节点本身的lane与renderLane是否相等,相等则说明节点本身有更新,需要进行reRender(前置:判断优化策略)
  • 判断节点本身的childLane与renderLane是否相等,若相等则说明该fiber节点的子节点存在更新节点,继续向下调和;不相等说明该fiber节点的子节点不存在更新,不需要调和。

补充:为什么memo、pureComponet、shouldComponentUpdate等优化策略可以避免组件reRender

  1. 默认情况下react在对比父组件的传值props时采用的是浅比较,即比较对象之间的地址;使用memo、pureComponet优化策略之后采用深比较,即比较对象中的每一个属性是否相等
  2. 浅比较中,即使是同一个对象,比如{name:akechi},由于父组件更新导致props对象重新生成了,地址发生了改变,所以react会判断子组件需要reRender
  3. 深比较中,只要props的对象的值不发生变化,react不会判断子组件需要reRender
  4. shouldComponentUpdate方法可以让class组件在本身的state更新时进行判断,组件是否需要更新

commit阶段

  • commit阶段主要执行commitRootImpl函数
  • 调用flushPassiveEffects,借由schedule异步调用useEffect方法,所以useEffect是在commit阶段完成后才执行
  • before Muation:类组件执行getSnapShotBeforeUpdate获取DOM节点的快照,函数组件同理
  • Muation: 删除、更新、新增不需要的组件,函数组件执行useInsertionEffect方法
  • Layout:类组件执行DidMount、DidUpdate方法,执行setState的callback方法;函数组件执行useLayOutEffect方法
  • 完成commit阶段且浏览器绘制完成后,执行useEffect方法

总结

 react的更新一文到此结束,其实react的每个版本中的关键源码都有些许差异,甚至不同模式下的思路也不同,但是这并不影响我们对react的学习,毕竟源码方法的学习主要是学习一个思路,而不是记API。若在学习中有不同意见,请发表在评论区与我讨论,互相学习。