react-Learing --调和及fiber

218 阅读13分钟

一.小册问题总结

  • 什么是fiber ? Fiber 架构解决了什么问题? 

    • fiber在react中会同react.createElement元素和页面中真实Dom联系起来,fiber也就是虚拟DOM。

    • fiber是react中最小执行单元,他是把一个大任务拆分成了很多个小块任务,一个小块任务的执行必须是一次完成的,不能暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,从而不用像之前一样要等那个大任务一直执行完成再去响应用户。

    • fiber的诞生就是为了解决reactv16之前react一次性大规模递归遍历、不能停顿造成的卡顿问题。

  • 不同fiber 之间如何建立起关联的? 每一个fiber是通过return、child、sibling三个属性建立起联系。

    • return: 指向父级 Fiber 节点。
    • child: 指向此节点的第一个子节点(大儿子)。
    • sibling:- sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。
  • Fiber root 和 root fiber 有什么区别? 

    • fiberRoot:首次构建应用,创建一个fiberRoot,作为整个React应用的根基。
    • rootFiber:通过ReactDom.render渲染出来的。 一个 React 应用可以有多 ReactDOM.render 创建的 rootFiber ,但是只能有一个 fiberRoot(应用根节点)。
ReactDOM.render(<Index/>, document.getElementById('app'));
  • Fiber中更新机制?

    • fiber初始化:时候会创建Fiber root和root fiber,两个之间会建立current联系, rootFiber.current = fiberRoot。再开始进入berginwork渲染流程,先复用现在的current树(rootFiber)的alternate作为workInProgress。再进入调和子节点,完整整个fiber遍历,渲染视图。

    • fiber更新:当发生一次更新时,会复用current的alternate作为新的workInProgress树,并且和current树建立关联。渲染完毕后,workInProgress将变成current树。

  • 什么是双缓冲树? 有什么作用? React用workInProgress树(内存构建的树)和current(渲染树),两个树用alternate指针互相指向,在下一次渲染时候,直接复用缓存树做为下一次渲染树,上一次渲染树作为缓存树。这样可以防止只用一棵树更新状态丢失的情况(一棵树情况下,某次计算偏大时,会导致渲染卡顿出现白屏),也加快了DOM节点更新

  • React 调和流程?两大阶段 commit 和 render 都做了哪些事情?

    • render阶段:执行workLoop函数,会判断workInprogress是否存在,存在就是被中断,不存在代表未中断,workLoop 会遍历一遍 fiber 树(执行performUnitOfWork函数),这里面有两个阶段beginWork、completeWork。
      • beginWork代表向下调和过程。从fiberRoot开始按照child指针逐层向下调和,期间会执行函数组件、实例类组件、diff调和子节点、給需要更新的fiber打上不同effectTag。
      • completeUnitOfWork:向上归并过程,如果有兄弟节点,会返回sibing兄弟,没有返回return父级,一直返回fiebrRoot,期间可以形成effectList,对于初始化流程会创建DOM,对DOM元素进行时间收集,收集style,className等。
    • commit阶段主要做的事情:
      • Before mutation 阶段(执行DOM操作前)
      • mutation阶段 (执行DOM操作)
        1. 置空 ref ,在 ref 章节讲到对于 ref 的处理。
        2. 对新增元素,更新元素,删除元素。进行真实的 DOM 操作。
      • layout阶段 (执行DOM操作后)
        1. commitLayoutEffectOnFiber 对于类组件,会执行生命周期,setState 的callback,对于函数组件会执行 useLayoutEffect 钩子。
        2. 如果有 ref ,会重新赋值 ref

二.fiber

fiber的含义

fiber 诞生在 Reactv16 版本,整个 React 团队花费两年时间重构 fiber 架构,目的就是解决大型 React 应用卡顿;fiber 在 React 中是最小粒度的执行单元,无论 React 还是 Vue ,在遍历更新每一个节点的时候都不是用的真实 DOM ,都是采用虚拟 DOM ,所以可以理解成 **fiber 就是 React 的虚拟 DOM **。

Reactv16 为了解决卡顿问题引入了 fiber ,为什么它能解决卡顿,更新 fiber 的过程叫做 Reconciler(调和器),每一个 fiber 都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间expirationTime( v17 版本叫做优先级 lane )来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡。然后等浏览器空余时间,在通过 scheduler (调度器),再次恢复执行单元上来,这样就能本质上中断了渲染,提高了用户体验。

三.认识fiber

1 React.element,fiber,dom三种什么关系?

  • 认识React.elemengt

image.png

React官网中已经说明,在react中编写的jsx代码都会通过babel成React.createElement形式createElement 参数:

  1. 第一个参数:如果是组件类型,会传入组件对应的类或函数;如果是 dom 元素类型,传入 div 或者 span 之类的字符串。
  2. 第二个参数:一个对象,在 dom 类型中为标签属性,在组件类型中为 props 。
  3. 其他参数:依次为 children,根据顺序排列。

老版本React之所以要引入React,就是因为jsx被编译成React.createElement形式,如果不引入React,防止找不到React报错。

在ReactV17之后,对react的jsx进行了升级,不在需要引入React,而jsx也将会编译成_jsx形式

image.png

React官网jsx博客地址: react.docschina.org/blog/2020/0…

  • React.element 是 React 视图层在代码层级上的表象,也就是开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式。上面保存了 props , children 等信息。
  • DOM 是元素在浏览器上给用户直观的表象。
  • fiber 可以说是是 element 和真实 DOM 之间的交流枢纽站,一方面每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 DOM 做视图渲染

image.png

2.### fiber保存了那些信息

react-reconciler/src/ReactFiber.js

function FiberNode(){

  this.tag = tag;                  // fiber 标签 证明是什么类型fiber。
  this.key = key;                  // key调和子节点时候用到。 
  this.type = null;                // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。  
  this.stateNode = null;           // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
 
  this.return = null;              // 指向父级fiber
  this.child = null;               // 指向子级fiber
  this.sibling = null;             // 指向兄弟fiber 
  this.index = 0;                  // 索引

  this.ref = null;                 // ref指向,ref函数,或者ref对象。

  this.pendingProps = pendingProps;// 在一次更新中,代表element创建(将要改变的 props )
  this.memoizedProps = null;       // 记录上一次更新完毕后的props
  this.updateQueue = null;         // 类组件存放setState更新队列,函数组件存放
  this.memoizedState = null;       // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
  this.dependencies = null;        // context或是时间的依赖项

  this.mode = mode;                //描述fiber树的模式,比如 ConcurrentMode 模式

  this.effectTag = NoEffect;       // effect标签,用于收集effectList
  this.nextEffect = null;          // 指向下一个effect

  this.firstEffect = null;         // 第一个effect
  this.lastEffect = null;          // 最后一个effect

  this.expirationTime = NoWork;    // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。

  this.alternate = null;           //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}

3.每一个fiber如何建立起关联的

每一个 element 都会对应一个 fiber ,每一个 fiber 是通过 return , child ,sibling 三个属性建立起联系的。

  • return: 指向父级 Fiber 节点。
  • child: 指向此节点的第一个子节点(大儿子)。
  • sibling:- sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。

所有fiber节点都通过以下属性: child,sibling 和 return来构成一个 fiber node 的 linked list(后面我们称之为链表)

image.png

三 Fiber更新机制

1 初始化

第一步:创建fiberRoot和rootFiber

  • fiberRoot:首次构建应用, 创建一个 fiberRoot ,作为整个 React 应用的根基。
  • rootFiber: 如下通过 ReactDOM.render 渲染出来的,如上 Index 可以作为一个 rootFiber。一个 React 应用可以有多 ReactDOM.render 创建的 rootFiber ,但是只能有一个 fiberRoot(应用根节点)。 第一次挂载的过程中,会将 fiberRoot 和 rootFiber 建立起关联。并且fiberRoot的current指向rootFiber
react-reconciler/src/ReactFiberRoot.js
function createFiberRoot(containerInfo,tag){
    /* 创建一个root */
    const root = new FiberRootNode(containerInfo,tag)
    const rootFiber = createHostRootFiber(tag);
    root.current = rootFiber
    return root
}

第二步:workInProgress和current 经过第一步的处理,开始到正式渲染阶段,会进入 beginwork 流程,在讲渲染流程之前,要先弄明白两个概念:

  • workInProgress是:正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。在一次更新中,所有的更新都是发生在 workInProgress 树上。在一次更新之后,workInProgress 树上的状态是最新的状态,那么它将变成 current 树用于渲染视图。
  • current:正在视图层渲染的树叫做 current 树。

接下来会到 rootFiber 的渲染流程,首先会复用当前 current 树( rootFiber )的 alternate 作为 workInProgress ,如果没有 alternate (初始化的 rootFiber 是没有 alternate ),那么会创建一个 fiber 作为 workInProgress 。会用 alternate 将新创建的 workInProgress 与 current 树建立起关联。这个关联过程只有初始化第一次创建 alternate 时候进行。

image.png

第三步:深度调和子节点,渲染视图

接下来会按照上述第二步,在新创建的 alternates 上,完成整个 fiber 树的遍历,包括 fiber 的创建。

image.png 最后会以 workInProgress 作为最新的渲染树,fiberRoot 的 current 指针指向 workInProgress 使其变为 current Fiber 树。到此完成初始化流程。

image.png

2 更新

如果对于上述 demo ,开发者点击一次按钮发生更新,接下来会发生什么呢? 首先会走如上的逻辑,重新创建一颗 workInProgresss 树,复用当前 current 树上的 alternate ,作为新的 workInProgress ,由于初始化 rootfiber 有 alternate ,所以对于剩余的子节点,React 还需要创建一份,和 current 树上的 fiber 建立起 alternate 关联。渲染完毕后,workInProgresss 再次变成 current 树

image.png

四. 两大阶段:render和commit

Reconciler时两个核心阶段,render阶段和commit阶段。

1.render阶段

react-reconciler/src/ReactFiberWorkLoop.js
function workLoop (){
    while (workInProgress !== null ) {
      workInProgress = performUnitOfWork(workInProgress);
    }
}

每一个fiber都是一个执行单元,在调和过程中,每一个发生更新的fiber都会作为workInProgress,那么workLoop就是执行每一个单元的调度器,如果没有渲染中断,那么workLoop会遍历一遍fiber。performUnitOfWork 包括两个阶段 beginWork 和 completeWork 。

react-reconciler/src/ReactFiberWorkLoop.js
function performUnitOfWork(){
    next = beginWork(current, unitOfWork, renderExpirationTime);
    if (next === null) {
       next = completeUnitOfWork(unitOfWork);
    }
}

向下调和beginWork的作用

  • 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
  • 向下遍历调和 children ,复用 oldFiber ( diff 算法),
  • 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新。

EffectTag

export const Placement = /*             */ 0b0000000000010;  // 插入节点
export const Update = /*                */ 0b0000000000100;  // 更新fiber
export const Deletion = /*              */ 0b0000000001000;  // 删除fiebr
export const Snapshot = /*              */ 0b0000100000000;  // 快照
export const Passive = /*               */ 0b0001000000000;  // useEffect的副作用
export const Callback = /*              */ 0b0000000100000;  // setState的 callback
export const Ref = /*                   */ 0b0000010000000;  // ref

向上归并 completeUnitOfWork

completeUnitOfWork 的流程是自下向上的,那么 completeUnitOfWork 过程主要做写什么呢?

  • 首先 completeUnitOfWork 会将 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。在 commit 阶段,将不再需要遍历每一个 fiber ,只需要执行更新 effectList 就可以了。
  • completeWork 阶段对于组件处理 context ;对于元素标签初始化,会创建真实 DOM ,将子孙 DOM 节点插入刚生成的 DOM 节点中;会触发 diffProperties 处理 props ,比如事件收集,style,className 处理.

reconcileChildren

image.png

收集effect list

收集effect list的具体步骤为:

  1. 如果当前节点需要更新,则打tag更新当前节点状态(props, state, context等)
  2. 为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点
  3. 如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点
  4. 如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。

2 commit阶段

commit阶段分为: - Before mutation 阶段(执行 DOM 操作前); - mutation 阶段(执行 DOM 操作); - layout 阶段(执行 DOM 操作后)

Before mutation

  • 因为 Before mutation 还没修改真实的 DOM ,是获取 DOM 快照的最佳时期(获取更新前 DOM 的状态),如果是类组件有 getSnapshotBeforeUpdate ,那么会执行这个生命周期。
  • 会异步调用 useEffect, useEffect 是采用异步调用的模式,其目的就是防止同步执行时阻塞浏览器做视图渲染。

Mutation

  • 置空 ref
  • 对新增元素,更新元素,删除元素。进行真实的 DOM 操作。

Layout

  • commitLayoutEffectOnFiber 对于类组件,会执行生命周期,setState 的callback,对于函数组件会执行 useLayoutEffect 钩子。
  • 如果有 ref ,会重新赋值 ref 。

commit 阶段做,需要将上阶段计算出来的需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。此阶段需要根据effect list,将所有更新都 commit 到DOM树上。

根据一个 fiber 的 effect list 更新视图

小结:

render阶段:

  1. 生成新的fiber节点,通过diff算法对比节点差异创建出用于更新操作的workinprogressFiber树,给需要更新的fiber打上相对应的effectTag并且生成用于更新的effectList链表。具体可以拆分为beginWork以及completeWork两个阶段,通过深度优先遍历的形式来进项这两个阶段。

  2. 相比于react15的递归处理虚拟dom节点,Reconciler通过链表的形式改成了循环处理。每处理完一个fiber节点都会检查时间是否充足或者是否又高优先级任务。 commit阶段:

  3. 当前阶段不会被打断,会根据上面两阶段生成的effectList一口气执行完成渲染操作。

  4. 遍历render阶段生成的effectList,effectList上的Fiber节点保存着对应的props变化。之后会遍历effectList进行对应的dom操作和生命周期、hooks回调或销毁函数。

  5. 通过双缓存的技术workInProgress Fiber完成渲染后会变为current Fiber树。

在更新组件在需要更新时 react 进行 setState 操作然后组件更新,合并 state、执行render 方法获取虚拟 dom,生成 fiber 链表, 然后遍历链表生成 WIP 树,收集 effect 等都算是一个个任务,加入到任务队列里。这算是协调阶段了(双缓冲机制和任务优先可以说是相辅相成,fiber 通过生成 wip 树将所有的变更先收集起来,这样可以防止屏幕抖动,优化渲染性能等,同时这个阶段可以进行打断,如果边收集边提交,就不能控制了),而这些任务又全部都通过 requestIdleCallback 调用。此时如果用户进行其他操作,比如在输入框输入文字,这会被看作高优先级任务。浏览器进行渲染,每一帧结束如果有多余时间都会去执行一下任务队列里的任务。直到进入commit 阶段,此时 wip 树完全生成,react 通过将所有的 effect ,交付给浏览器渲染 dom。此时已经不能被打断了。这时浏览器会将所有的变更一次性渲染。

参考博客: juejin.cn/post/694389…

  • 掘金 - 作者我不是外星人:react进阶实践指南