react架构浅析

157 阅读3分钟

旧的流程

编译阶段

jsx通过babel编译成render function的形式,render function的返回值是vdom

渲染阶段(vdom的渲染)

  1. 初次渲染:不需要diff,直接通过 dom api 增删改 dom
  2. 再次渲染:需要和上一轮的vdom进行diff算法,对比出可以复用的节点,边对比边通过 dom api 增删改 dom

存在的问题

  1. react 的 setState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就会很大,而js的计算时间太长会阻塞渲染,动画卡顿

fiber架构

编译阶段

  jsx编译成render function(vdom)

渲染阶段

render阶段(reconcile + schdule)

  1. reconcile调和

    • diff阶段发生在vdom 转换成 fiber数据结构(这种数据结构是单链表的形式)的过程中

    • diff算法找到vdom中变化的部分,然后打个effectTag的增删改查的标记,并且把有effectTag标记的节点收集到effectList队列里面

      react怎么做diff

      react怎么做的优先级调度

  2. schdule

    • reconcile 是可以打断的,由 schedule 调度
    // 以前的递归渲染改成循环,方便打断
    // 每次循环做一个fiber 的 reconcile,当前处理的fiber会放在workInProgress这个全局变量上
    // 当循环完了,也就是 wip 为空了,就去执行commit阶段
    function workLoop() {
    **// shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。**
      while (wip && shouldYield()) {
        performUnitOfWork();
      }
    
      if (!wip && wipRoot) {
        commitRoot();
      }
    }
    
    // 根据fiber类型做不同的处理
    // 当前fiber处理完后,久把wip指向下一个fiber
    function performUnitOfWork() {
      const { tag } = wip;
    
      switch (tag) {
        case HostComponent:
          updateHostComponent(wip);
          break;
    
        case FunctionComponent:
          updateFunctionComponent(wip);
          break;
    
        case ClassComponent:
          updateClassComponent(wip);
          break;
        case Fragment:
          updateFragmentComponent(wip);
          break;
        case HostText:
          updateHostTextComponent(wip);
          break;
        default:
          break;
      }
    
      if (wip.child) {
        wip = wip.child;
        return;
      }
    
      let next = wip;
    
      while (next) {
        if (next.sibling) {
          wip = next.sibling;
          return;
        }
        next = next.return;
      }
    
      wip = null;
    }
    
    // 函数组件和类组件的处理,就是调用render拿到vdom,然后继续处理渲染出的vdom
    function updateClassComponent(wip) {
      const { type, props } = wip;
      const instance = new type(props);
      const children = instance.render();
    
      reconcileChildren(wip, children);
    }
    
    function updateFunctionComponent(wip) {
      renderWithHooks(wip);
    
      const { type, props } = wip;
    
      const children = type(props);
      reconcileChildren(wip, children);
    }
    

commit阶段

  1. before mutation:在 dom 操作之前,会异步调度 useEffect 的回调函数
  2. mutation: 遍历effectList队列,根据 effectTag 来增删改 真实dom
  3. layout:同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref。

解决的问题

  1. js阻塞渲染的问题

    1. 打断计算,分多次进行,由于按照以前边diff边操作dom无法打断,所以改成计算完了再一次性更新
  2. 那怎么打断计算

    1. 不再使用递归,改用循环,循环可以方便地打断恢复
    2. 并且把vdom数据结构换成fiber数据结构,fiber数据结构中存储着child,sibling,return(父节点)的信息,方便打断后还能找到其他的兄弟节点

react版本的重要更新

16还有其他的重要更新,这边先忽略

  1. React16:

    1. 新的调度方式,Fiber
    2. hook
  2. react17:

    1. lanes模型
  3. react18:

    1. root api的变更

      之前是ReactDOM.render(, container);

      后来是ReactDOM.createRoot(container).render();(并发渲染)

    2. startTransition API(用于非紧急状态更新)

    3. 渲染的自动批处理 Automatic batching 优化

    4. SSR 架构(Server-Side Rendering )

参考:

juejin.cn/post/711705…

www.xuanbiyijue.com/2021/10/10/…