1.基于Fiber架构实现一个简单的React

React Fiber 架构简介

React 从版本16开始引入了 Fiber reconciler 替代了15版本以前使用的 Stack reconciler。由于 Stack reconciler 采用递归方式渲染,导致无法打断。这就意味着如果浏览器线程需要处理其他任务,如动画渲染,就必须等待 Stack reconciler 结束,这可能导致动画卡顿。Fiber reconciler 将每个渲染任务拆分成更小的单元,可以中断、重启,并根据渲染任务的优先级进行渲染,使用户交互更加流畅。

我做了2个Demo分别是用Stack reconciler和Fiber reconciler实现 大家可以对比一下 Stack reconciler VS Fiber reconciler

JSX

浏览器本身不支持 JSX 语法,需要通过 Babel 插件将其转化为浏览器可识别的代码。

jsx 复制代码
const jsx = (
  <div id="container">
    <span class="content">123</span>
  </div>
);

转化为:

js 复制代码
const jsx = React.createElement(
  "div",
  { id: "container" },
  React.createElement(
    "span",
    { class: "content" },
    "123"
  )
);

createElement 方法最终将 JSX 转成 element tree 结构,其中 element 的结构如下:

js 复制代码
{
  type: "div",
  props: {
    id: "container",
    children: [
      {
        type: "TEXT ELEMENT",
        props: {
          nodeValue: "123"
        }
      }
    ]
  }
}

实现 createElement 方法如下:

js 复制代码
function createElement(type, config, ...args) {
  const props = { ...config };
  const hasChildren = args.length > 0;
  const rawChildren = hasChildren ? [].concat(...args) : [];
  // 如果 children 不是一个对象,说明这是一个 text 节点
  props.children = rawChildren
    .filter(c => c != null && c !== false)
    .map(c => (c instanceof Object ? c : createTextElement(c)));

  return {
    type,
    props
  };
}

function createTextElement(text) {
  return {
    type: "TEXT ELEMENT",
    props: { nodeValue: text }
  };
}

Fiber Node

整个 React 渲染过程可以概括为根据 element tree 构建 fiber tree 的过程,最终根据 fiber tree 更新到 DOM tree。Fiber node 的结构如下:

js 复制代码
let fiber = {
  tag: HOST_COMPONENT,
  type: "div",
  parent: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  stateNode: document.createElement("div"),
  props: { children: [], className: "foo" },
  partialState: null,
  effectTag: PLACEMENT,
  effects: []
};
  • tag 表示节点的类型,有 HOST_ROOT、HOST_COMPONENT 和 CLASS_COMPONENT 三种。
  • type 表示 HTML tag 或 class 定义。
  • parent 表示父 fiber 节点。
  • child 表示子 fiber 节点。
  • sibling 表示同级 fiber 节点。
  • alternate 实际渲染过程中有两个 fiber tree,一个是当前页面内容对应的 fiber tree(old fiber tree),另一个是当页面更新时最新构建的 fiber tree(work-in-progress tree)。
  • stateNode 表示组件实例的引用,可以是 DOM 节点也可以是 class component instance。
  • props 对应 element 的 props 属性。
  • partialState 表示 class component 更新的 state。
  • effectTag 表示进行的 DOM 操作类型,可以是 PLACEMENT(新增)、UPDATE(更新)或 DELETION(删除)。
  • effects 存储了所有 child fiber 和 sibling fiber 的 effects。

注意,fiber tree 和 element tree 结构不同。 举个例子

html 复制代码
<ul>
  <A>1</A>
  <B>2</B>
  <C>3</C>
</ul>

转换成element tree ul有3个child,存在children数组里面 fiber tree ul的child fiber是A,A的sibing是B,B的sibling是C, A,B,C的parent fiber都是ul

Parent Class Component

在编写 class 组件时,通常会继承 Component。Props、state 属性以及 setState 方法都是从 Component 继承而来。

js 复制代码
class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }

  setState(partialState) {
    // scheduleUpdate 后面会介绍
    scheduleUpdate(this, partialState);
  }
}

React 渲染过程

接下来我们按照下面的流程图来实现React渲染过程:

render & scheduleUpdate

javascript 复制代码
// Fiber tags
const HOST_COMPONENT = "host";
const ClASS_COMPONENT = "class";
const HOST_ROOT = "root";

// Global state
const updateQueue = [];

function render(elements, containerDom) {
    updateQueue.push({
        from: HOST_ROOT,
        dom: containerDom,
        newProps: {
            children: elements
        }
    });

    // 浏览器线程空闲开始渲染
    requestIdleCallback(performWork);
}

function scheduleUpdate(instance, partialState) {
    updateQueue.push({
        from: ClASS_COMPONENT,
        instance,
        partialState
    });

    requestIdleCallback(performWork);
}

在这里,我们维护了一个全局变量 updateQueue 用来存储每次的更新。render 方法的第一个参数是要渲染的 element tree,第二个参数是要渲染到哪个 DOM 元素下面。我们往 updateQueue 里面推送了一个渲染任务,通过 from 属性标识这是从根节点渲染。scheduleUpdate 方法是在 ComponentsetState 里面调用的,调用的时候会传入当前组件的实例和要更新的 state,然后往 updateQueue 里面推送了一个渲染任务,通过 from 属性标识这是从 class 组件渲染。

这里有一个关键的方法requestIdleCallback 我们前面说了 fiber架构渲染的时候都会等待浏览器空闲的时候执行 这就是requestIdleCallback要做的事情 requestIdleCallback(callback)会接受一个callback,这个callback会等浏览器线程空闲的时候执行,执行callback会传入一个IdleDeadline对象 这个对象有2个属性

  • timeRemaining() 用来表示当前闲置周期的预估剩余毫秒数。如果闲置期已经结束,则其值为 0。
  • IdleDeadline 如果回调是因为超过了设置的超时时间而被执行的 实际上react并不是用requestIdleCallback来做任务调度,react有一个自己实现的任务调度模块,是通过MessageChannel来实现,为什么要用MessageChannel不用requestIdleCallback 有兴趣的可以看一下xie.infoq.cn/article/a17... 这篇文章

performWork & workLoop & resetNextUnitOfWork

js 复制代码
// 下一个渲染的 Fiber 节点
let nextUnitOfWork = null;
let pendingCommit = null;

function performWork(deadline) {
    workLoop(deadline);
    // 还有 Fiber 节点没有构建或者 updateQueue 不为空
    if (nextUnitOfWork || updateQueue.length > 0) {
        requestIdleCallback(performWork);
    }
}

// 循环渲染 Fiber 节点
function workLoop(deadline) {
    if (!nextUnitOfWork) {
        resetNextUnitOfWork();
    }
    // 如果还有下一个 Fiber 节点没有构建(nextUnitOfWork 不为空)并且浏览器线程还有空闲时间,则继续渲染下一个 Fiber 节点
    while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }

    // 构建好的 Fiber 节点执行 DOM 的更新操作
    // 为什么要所有节点构建好再执行再执行 DOM 渲染,而不是构建了一个 Fiber 节点就渲染一个 DOM?
    // 因为我们构建 Fiber 节点的过程是可打断的,如果构建了一个 Fiber 节点就渲染 DOM,
    // 如果构建过程被打断了,用户看到的 UI 就是不完整的。
    if (pendingCommit) {
        commitAllWork(pendingCommit);
    }
}

function getRoot(fiber) {
    let node = fiber;
    while (node.parent) {
        node = node.parent;
    }
    return node;
}

// 把 updateQueue 的渲染任务转化为构建 Fiber tree 的任务 
// resetNextUnitOfWork 会构建新的 Fiber tree 的 root 节点
function resetNextUnitOfWork() {
    const update = updateQueue.shift();
    if (!update) {
        return;
    }

    if (update.partialState) {
        update.instance.__fiber.partialState = update.partialState;
    }

    // older Fiber tree root
    const root = update.from === HOST_ROOT
        ? update.dom._rootContainerFiber
        : getRoot(update.instance.__fiber);

    nextUnitOfWork = {
        tag: HOST_ROOT,
        stateNode: update.dom || root.stateNode,
        props: update.newProps || root.props,
        alternate: root
    };
}

在这部分代码中,我们声明了两个全局变量 nextUnitOfWork 用来存储下一个要构建的 Fiber 节点,pendingCommit 用来存储 completeWork 结束的 root Fiber,pendingCommit 的 effects 里面有所有需要更新的 Fiber 节点。resetNextUnitOfWork 方法用来构建 root Fiber 节点,关键的一步是通过 instance 拿到对应的 Fiber 节点,然后把 partialState 存到 old Fiber node 上。(为什么instance上可以拿到 fiber节点的引用因为在后面通过createInstance构建class component的instance的时候 在instance上存了对fiber节点的引用)

performUnitOfWork

js 复制代码
function performUnitOfWork(wipFiber) {
    beginWork(wipFiber);
    if (wipFiber.child) {
        return wipFiber.child;
    }

    let uow = wipFiber;
    while (uow) {
        completeWork(uow);
        if (uow.sibling) {
            return uow.sibling;
        }
        uow = uow.parent;
    }
}

performUnitOfWork 会返回下一个要处理的 Fiber 节点,构建完的 Fiber 节点会执行 completeWork 方法。把当前 Fiber 节点的 effects 存到父节点,最终会都存储到 root Fiber 节点(就是 pendingCommit)。

beginWork & updateHostComponent & updateClassComponent

js 复制代码
function beginWork(wipFiber) {
    if (wipFiber.tag === ClASS_COMPONENT) {
        updateClassComponent(wipFiber);
    } else {
        updateHostComponent(wipFiber);
    }
}

function updateHostComponent(wipFiber) {
    // 不是 sameType 的情况时候 Fiber 节点的 stateNode 会是空
    if (!wipFiber.stateNode) {
        wipFiber.stateNode = createDomElement(wipFiber);
    }
    const newChildElements = wipFiber.props.children;
    reconcileChildArray(wipFiber, newChildElements);
}

function updateClassComponent(wipFiber) {
    let instance = wipFiber.stateNode;
    if (!instance) {
        instance = wipFiber.stateNode = createInstance(wipFiber);
    } else if (wipFiber.props === instance.props && !wipFiber.partialState) {
        // 这里其实是 reconcileChildArray 里面 sameType 为 true 的情况 
        // 最新的 element 的 props 和 old Fiber tree 的 stateNode 的 props 相等
        // wipFiber 的 props 是在 reconcileChildArray 里面赋值为最新的 element 的 props
        // wipFiber 的 stateNode 也是在 reconcileChildArray 里面赋值为 old Fiber tree 的 stateNode
        cloneChildFibers(wipFiber);
        return;
    }

    instance.props = wipFiber.props;
    instance.state = Object.assign({}, instance.state, wipFiber.partialState);
    const newChildElements = instance.render();
    reconcileChildArray(wipFiber, newChildElements);
}

function createInstance(fiber) {
    const instance = new fiber.type(fiber.props);
    // 在 instance 里面保留对 Fiber 节点的引用,后面会用到
    instance.__fiber = fiber;
    return instance;
}

// Copy old Fiber tree 的 child 和 sibling 到新的 Fiber tree
function cloneChildFibers(parentFiber) {
    const oldFiber = parentFiber.alternate;
    if (!oldFiber.child) {
        // 没有 child,表示 old Fiber tree 没有节点可以 copy
        return;
    }
    let oldChild = oldFiber.child;
    let prevChild = null;
    while (oldChild) {
        const newChild = {
            type: oldChild.type,
            tag: oldChild.tag,
            stateNode: oldChild.stateNode,
            props: oldChild.props,
            partialState: oldChild.partialState,
            alternate: oldChild,
            parent: parentFiber
        };

        if (prevChild) {
            prevChild.sibling = newChild;
        } else {
            parentFiber.child = newChild;
        }
        prevChild = newChild;
        oldChild = oldChild.sibling;
    }
}

updateHostComponent 如果当前fiber节点的stateNode属性是空的,就先创建dom节点,那什么时候stateNode会是空的呢?在后面的reconcileChildArray方法里面,如果新的fiber node和 老的fiber node不是same type,这时候新的fiber node的stateNode属性就是空的reconcileChildArray方法是根据element tree构建当前fiber node的下一级的fiber node(包括child和sibing)。这里大家应该能感受到fiber架构是怎么把任务拆分成更小的单元 每次循环其实都只构建当前fiber的child和sibling 然后等线程空闲了,再重复这个过程。 updateClassComponent方法里面一样也是在stateNode为空的时候,会创建instance实例 instance实例上会存对当前fiber节点的引用。这个在resetNextUnitOfWork里面就会用到__fiber

  • 如果当前fiber节点的props和old fiber tree的props相同 并且partialState为空,这时候可以直接clone old fiber tree的下一级child和sibling。
  • 如果props有更新或者partialState不为空,这时候就要把最新的props和state赋值给instance实例,然后调用instace.render返回最新的element tree,然后通过reconcileChildArray根据最新的element tree构建下一级的fiber节点, 这里为什么能在new fiber node上拿到partialState,明明在resetNextUnitOfWork方法里面我是存到old fiber node上的,原因是在后面的reconcileChildArray方法里面会把old fiber node的partialState存到new fiber node上

reconcileChildArray

js 复制代码
unction reconcileChildArray(wipFiber, newChildElements) {
  const elements = arrify(newChildElements);

  let index = 0;
  let oldFiber = wipFiber.alternate? wipFiber.alternate.child: null;
  let newFiber = null;
  //oldFiber存在这个判断是重新渲染的时候dom被删除的情况(比如原来有3个children uadate以后有2个)
  while(index < elements.length || oldFiber) {
      const prevFiber = newFiber;
      const element = index < elements.length && elements[index];
      const sameType = oldFiber && element && element.type === oldFiber.type;
      if(sameType) {
          //这个情况是stateNode有值 并且和oldFiber一样
          newFiber = {
              type: oldFiber.type,
              tag: oldFiber.tag,
              stateNode: oldFiber.stateNode,
              props: element.props,
              parent: wipFiber,
              alternate: oldFiber,
              partialState: oldFiber.partialState,

              effectTag: UPDATE
          }
      }

      //不是sameType 创建新的fiber节点 删除老的fiber节点
      if(element && !sameType) {
          //这个情况stateNode没有创建,在下一次执行updateHostComponent or updateClassComponent的时候会创建
          newFiber = {
              type: element.type,
              tag: typeof element.type === 'string'? HOST_COMPONENT: ClASS_COMPONENT,
              props: element.props,
              parent: wipFiber,
              effectTag: PLACEMENT 
          }
      }

      if(oldFiber && !sameType) {
          oldFiber.effectTag = DELETION;
          wipFiber.effects = wipFiber.effects || [];
          wipFiber.effects.push(oldFiber);
      }

      //fiber tree的结构与element tree不同 fiber tree parent指向左边第一个为child 其他都是该child的sibling sibling指向parent(看fiber节点complete顺序.png)
      //element tree parent下面所有都是child 没有sibing
      if(oldFiber) {
          oldFiber = oldFiber.sibling;
      }

      if(index === 0) {
          wipFiber.child = newFiber;
      } else if(prevFiber && element) {
          prevFiber.sibling = newFiber;
      }

      index++;
  }
}

function arrify(val) {
    return val === null? []: Array.isArray(val)? val: [val];
}

reconcileChildArray是整个library的核心,这里会构建work-in-progress tree并且决定要对dom做哪些改动。reconcileChildArray会构建传入fiber节点的child和sibling节点*

首先把newChildElements统一转化为数组(updateClassComponent里面调用reconcileChildArray的时候newChildElements是个对象,updateHostComponent里面调用的时候是个数组)。 接下来循环的判断条件有2个(index < elements.length || oldFiber)第一个条件很好理解,为什么要加oldFiber存在的条件,这个其实是为了处理节点被删除的情况。 再看循环里面的逻辑,循环里面会判断old fiber node的tpye和最新的element tree node是否一样

  • 如果一致,就基于old fiber node构建new fiber node,注意props要用你最新的element props
  • 如果不一致,这时候就要创建新的fiber节点,并且删除已经不存在的fiber节点,这里一定要把删除的节点加到wipFiber的effects里面,这样后面再更新dom的时候,就会把这个节点删除

completeWork

js 复制代码
function completeWork(fiber) {
    if(fiber.tag === ClASS_COMPONENT) {
        fiber.stateNode.__fiber = fiber;
    }

    if(fiber.parent) {
        //所有的child fiber的effects
        const childEffects = fiber.effects || [];
        //当前fiber节点的effect
        const thisEffect = fiber.effectTag !== null? [fiber]: [];
        const parentEffects = fiber.parent.effects || [];
        fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
    } else {
        //没有parent 说明已经把所有effect存到root了 可以进行commitAllWork处理pendingCommit
        pendingCommit = fiber;
    }
}

completeWork的作用是把所有节点的effects都放到root fiber的effects,后面就遍历root fiber的effects进行dom更新 completeWork执行完会把root fiber赋值给pendingCommit,接下来就是进行dom的更新.

commitAllWork&commitWork&commitDeletion

js 复制代码
//更新dom
function commitAllWork(fiber) {
    console.log('fiber effetcs', fiber.effects);
    fiber.effects.forEach(f => {
        commitWork(f);
    })
    fiber.stateNode._rootContainerFiber = fiber;
    nextUnitOfWork = null;
    pendingCommit = null;
}

function commitWork(fiber) {
    if(fiber.tag === HOST_ROOT) {
        return;
    }

    let domParentFiber = fiber.parent;
    while(domParentFiber.tag === ClASS_COMPONENT) {
        domParentFiber = domParentFiber.parent;
    }

    const domParent = domParentFiber.stateNode;
    //如果是新增的节点 只要把stateNode append上去就可以了 stateNode会在updateHostComponent方法里面创建
    if(fiber.effectTag === PLACEMENT && fiber.tag === HOST_COMPONENT) {
        domParent.appendChild(fiber.stateNode);
    } else if(fiber.effectTag === UPDATE) {
         //如果是更新的节点 只要把stateNode是在reconcileChildArray里面用的老的stateNode 需要更新最新的prop
        updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props)
    } else if(fiber.effectTag === DELETION) {
        //在effects里面只有被删除的节点的根节点 没有被删除节点的子节点 (比如重新渲染的时候Story组件删除了 effects里面只有Story没有Story里面的子节点)
        commitDeletion(fiber, domParent);
        //domParent.removeChild(fiber.stateNode);
    }
}

//删除的时候其实只要把父节点删除 子节点自然就删了
function commitDeletion(fiber, domParent) {
    let node = fiber;
    while (true) {
      if (node.tag === ClASS_COMPONENT) {
        node = node.child;
        continue;
      }
      domParent.removeChild(node.stateNode);
      while (node != fiber && !node.sibling) {
        node = node.parent;
      }
      if (node == fiber) {
        return;
      }
      node = node.sibling;
    }
}

commitAllWork里面会遍历root fiber node的effects进行更新操作 commitWork里面的逻辑都是比较简单的,就是执行新增更新和删除节点的操作,大家看代码上的注释应该可以理解

这里就实现了react fiber架构的基本功能 大家可以在这里运行试试

下一篇文章会在这个基础上支持hook.

参考资料: engineering.hexacta.com/didact-lear...

相关推荐
飞翔的渴望4 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿7 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
用户305875848912511 小时前
Connected-react-router核心思路实现
react.js
哑巴语天雨1 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情1 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起1 天前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱1 天前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
高山我梦口香糖1 天前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔1 天前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖1 天前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js