写点什么

读懂 React 原理之调和与 Fiber

作者:xiaofeng
  • 2022 年 10 月 03 日
    浙江
  • 本文字数:4142 字

    阅读完需:约 14 分钟

一 引沿

Fiber 架构是 React16 中引入的新概念,目的就是解决大型 React 应用卡顿,React 在遍历更新每一个节点的时候都不是用的真实 DOM,都是采用虚拟 DOM,所以可以理解成 fiber 就是 React 的虚拟 DOM,更新 Fiber 的过程叫做调和,每一个 fiber 都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间 expirationTime,来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡。

二 什么是调和

调和是一种算法,就是 React 对比新老虚拟 DOM 的过程,以决定需要更新哪一部分。

三 什么是 Filber

Fiber 的目的是为了让 React 充分利用调度,以便做到如下几点:


  • 暂停工作,稍后再回来

  • 优先考虑不同类型的工作

  • 重用以前完成的工作

  • 如果不再需要,则中止工作


为了实现上面的要求,我们需要把任务拆分成一个个可执行的单元,这些可执行的单元就叫做一个 Fiber,一个 Fiber 就代表一个可执行的单元。


一个 Fiber 就是一个普通的 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创建 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。更新阶段,两颗树互相交替。}
复制代码


type 就是 react 的元素类型


export const FunctionComponent = 0;       // 对应函数组件export const ClassComponent = 1;          // 对应的类组件export const IndeterminateComponent = 2;  // 初始化的时候不知道是函数组件还是类组件 export const HostRoot = 3;                // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素export const HostPortal = 4;              // 对应  ReactDOM.createPortal 产生的 Portal export const HostComponent = 5;           // dom 元素 比如 <div>export const HostText = 6;                // 文本节点export const Fragment = 7;                // 对应 <React.Fragment> export const Mode = 8;                    // 对应 <React.StrictMode>   export const ContextConsumer = 9;         // 对应 <Context.Consumer>export const ContextProvider = 10;        // 对应 <Context.Provider>export const ForwardRef = 11;             // 对应 React.ForwardRefexport const Profiler = 12;               // 对应 <Profiler/ >export const SuspenseComponent = 13;      // 对应 <Suspense>export const MemoComponent = 14;          // 对应 React.memo 返回的组件
复制代码


比如元素结构如下:


export default class Parent extends React.Component{   render(){     return <div>       <h1>hello,world</h1>       <Child />     </div>   }}
function Child() { return <p>child</p>}
复制代码


对应的 Filber 结构如下:



有了上面的概念后我们就自己实现一个 Fiber 的更新机制


四 实现调和的过程

我们通过渲染一段 jsx 来说明 React 的调和过程,也就是我们要手写实现 ReactDOM.render()


const jsx = (  <div className="border">    <h1>hello</h1>    <a href="https://www.reactjs.org/">React</a>  </div>)
ReactDOM.render( jsx, document.getElementById('root'));
复制代码


1. 创建 FiberRoot


react-dom.js


function createFiberRoot(element, container){    return {    type: container.nodeName.toLocaleLowerCase(),    props: { children: element },    stateNode: container  }}

function render(element, container) { const FibreRoot = createFiberRoot(element, container) scheduleUpdateOnFiber(FibreRoot)}export default { render }
复制代码


2. render 阶段


调和的核心是 render 和 commit,本文不讲调度过程,我们会简单的用 requestIdleCallback 代替 React 的调度过程。


ReactFiberWorkloop.js


let wipRoot = null // work in progresslet nextUnitOfwork = null // 下一个fiber节点
export function scheduleUpdateOnFiber(fiber) { wipRoot = fiber nextUnitOfwork = fiber}
function workLoop(IdleDeadline) { while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) { nextUnitOfwork = performUnitOfWork(nextUnitOfwork) }}
function performUnitOfWork() {}
requestIdleCallback(workLoop)
复制代码


每一个 fiber 可以看作一个执行的单元,在调和过程中,每一个发生更新的 fiber 都会作为一次 workInProgress 。那么 workLoop 就是执行每一个单元的调度器,如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树


performUnitOfWork 包括两个阶段:


  1. 是向下调和的过程,就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点

  2. 是向上归并的过程,如果有兄弟节点,会返回 sibling 兄弟,没有返回 return 父级,一直返回到 fiebrRoot


这么一上一下,构成了整个 fiber 树的调和。


import { updateHostComponent } from './ReactFiberReconciler'function performUnitOfWork(wip) {  // 1. 更新wip  const { type } = wip  if (isStr(type)) {    // type是string,更新普通元素节点    updateHostComponent(wip)  } else if (isFn(type)) {    // ...  }
// 2. 返回下一个要更新的任务 深度优先遍历 if (wip.child) { return wip.child } let next = wip while(next) { if (next.sibling) { return next.sibling } next = next.return } return null}
复制代码


根据 type 类型区分是 FunctionComponent/ClassComponent/HostComponent/...本文中只处理 HostComponent 类型,其他类型的处理可以看文末的完整代码链接。


ReactFiberReconciler.js


import { createFiber } from './createFiber'
export function updateHostComponent(wip) { if (!wip.stateNode) { wip.stateNode = document.createElement(wip.type); updateNode(wip.stateNode, wip.props); } // 调和子节点 reconcileChildren(wip, wip.props.children);}
function reconcileChildren(returnFiber, children) { if (isStr(children)) { return }
const newChildren = isArray(children) ? children : [children]; let previousNewFiber = null for(let i = 0; i < newChildren.length; i++) { const newChild = newChildren[i]; const newFiber = createFiber(newChild, returnFiber)
if (previousNewFiber === null) { returnFiber.child = newFiber } else { previousNewFiber.sibling = newFiber } previousNewFiber = newFiber }}
function updateNode(node, nextVal) { Object.keys(nextVal).forEach((k) => { if (k === "children") { if (isStringOrNumber(nextVal[k])) { node.textContent = nextVal[k]; } } else { node[k] = nextVal[k]; } });}
复制代码


createFiber.js


export function createFiber(vnode, returnFiber) {  const newFiber = {    type: vnode.type,   // 标记节点类型    key: vnode.key,     // 标记节点在当前层级下的唯一性    props: vnode.props, // 属性    stateNode: null,    // 如果组件是原生标签则是dom节点,如果是类组件则是类实例    child: null,        // 第一个子节点    return: returnFiber,// 父节点    sibling: null,      // 下一个兄弟节点  };
return newFiber;}
复制代码


至此已经完成了 render 阶段,下面是 commit 阶段,commit 阶段就是依据 Fiber 结构操作 DOM


function workLoop(IdleDeadline) {  while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) {    nextUnitOfwork = performUnitOfWork(nextUnitOfwork)  }
// commit if (!nextUnitOfwork && wipRoot) { commitRoot(); }}
function commitRoot() { commitWorker(wipRoot.child) wipRoot = null;}
function commitWorker(wip) { if (!wip) { return } // 1. 提交自己 const { stateNode } = wip let parentNode = wip.return.stateNode if (stateNode) { parentNode.appendChild(stateNode); }
// 2. 提交子节点 commitWorker(wip.child);
// 3. 提交兄弟节点 commitWorker(wip.sibling);}
复制代码

五 总结

  • Fiber 结构,Fiber 的生成过程。

  • 调和过程,以及 render 和 commit 两大阶段。

用户头像

xiaofeng

关注

努力写代码中 2022.08.18 加入

努力写代码中

评论

发布
暂无评论
读懂React原理之调和与Fiber_React_xiaofeng_InfoQ写作社区