React的新引擎—React Fiber是什么?

这是一篇讲react Fiber算法的文章,深刻浅出,而且做者本身实现了Fiber的核心代码,能够很好的帮助咱们理解fiber 原文连接html

另外,建议读这篇文章以前先看一下他的另外几篇关于react的文章,本篇是创建在其之上的 DIY Reactnode

Didact Fiber: Incremental reconciliation

github repository updated demoreact

Why Fiber

本文并不会展现一个完整的React Fiber,若是你想了解更多,更多资料git

当浏览器的主线程长时间忙于运行一些事情时,关键任务的执行能够能被推迟。github

为了展现这个问题,我作了一个demo,为了使星球一直转动,主线程须要每16ms被调用一次,由于animation是跑在主线程上的。若是主线程被其余事情占用,假如占用了200ms,你会发现animation会发生卡顿,星球中止运行,直到主线程空闲出来运行animation。算法

究竟是什么致使主线程如此繁忙致使不能空闲出几微秒去保持动画流畅和响应及时呢?数组

还记得之前实现的reconciliation code吗?一旦开始,就没法中止。若是此时主线程须要作些别的事情,那就只能等待。而且由于使用了许多递归,致使很难暂停。这就是为何咱们重写代码,用循环代替递归。浏览器

Scheduling micro-tasks

咱们须要把任务分红一个个子任务,在很短的时间里运行结束掉。可让主线程先去作优先级更高的任务,而后再回来作优先级低的任务。app

咱们将会须要requestIdleCallback()函数的帮助。它在浏览器空闲时才执行callback函数,回调函数中deadline参数会告诉你还有多少空闲时间来运行代码,若是剩余时间不够,那么你能够选择不执行代码,保持了主线程不会被一直占用。dom

const ENOUGH_TIME = 1; // milliseconds

let workQueue = [];
let nextUnitOfWork = null;

function schedule(task) {
  workQueue.push(task);
  requestIdleCallback(performWork);
}

function performWork(deadline) {
  if (!nextUnitOfWork) {
    nextUnitOfWork = workQueue.shift();
  }

  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (nextUnitOfWork || workQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}
复制代码

真正起做用的函数是performUnitOfWork。咱们将会在其中写reconciliation code。函数运行一次占用不多的时间,而且返回下一次任务的信息。

为了组织这些子任务,咱们将会使用fibers

The fiber data structure

咱们将会为每个须要渲染的组件建立一个fiber。nextUnitOfWork是对将要运行的下一个fiber的引用。performUnitOfWork会对fiber进行diff,而后返回下一个fiber。这个将会在后面详细解释。

fiber是啥样子的呢?

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: []
};
复制代码

是一个对象啊,咱们将会使用parent,child,sibling属性去构建fiber树来表示组件的结构树。

stateNode是对组件实例的引用。他多是DOM元素或者用户定义的类组件实例

举个例子:

在上面例子中咱们能够看到将支持三种不一样的组件:

  • b, p, i 表明着host component。咱们将会用tag:HOST_COMPONENT来定义他。type属性将会是字符串。props是dom属性和事件。
  • Foo class component。它的tag:CLASS_COMPONENT, type指向用户定义的类组件
  • div表明着 host root。他相似于host component,stateNode也是DOM element.tag: HOST_ROOT.注意stateNode就是传递给render函数的参数。

另一个重要属性就是alternate,咱们须要它是由于大多数时间咱们将会有两个fiber tree。一个表明着已经渲染的dom, 咱们成其为current tree 或者 old tree。另一个是在更新(当调用setState或者render)时建立的,称其为work-in-progress tree。

work-in-progress tree不会与old tree共享任何fiber。一旦咱们完成work-in-progress tree的构建和dom的改变,work-in-progress tree就变成了old tree。

因此咱们使用alternate属性去连接old tree。fiber与其alternate有相同的tag,type,statenode。有时咱们渲染新的组件,它可能没有alternate属性

而后,还有一个effects 列表和effectTag。当咱们发现work-in-progress须要改变的DOM时,就将effectTag设置为PLACEMENT, UPDATE, DELETION。为了更容易知道总共有哪些须要fiber须要改变DOM,咱们把全部的fiber放在effects列表里。

可能这里讲了许多概念的东西,不要担忧,咱们将会用行动来展现fiber。

Didact call hierarchy

为了对程序有总体的理解,咱们先看一下结构示意图

咱们将会从render()setState()开始,在commitAllWork()结束

Old code

我以前告诉你咱们将重构大部分代码,但在这以前,咱们先回顾一下不须要重构的代码

这里我就不一一翻译了,这些代码都是在文章开头我提到的

class Component {
  constructor(props) {
    this.props = props || {};
    this.state = this.state || {};
  }

  setState(partialState) {
    scheduleUpdate(this, partialState);
  }
}

function createInstance(fiber) {
  const instance = new fiber.type(fiber.props);
  instance.__fiber = fiber;
  return instance;
}
复制代码

render() & scheduleUpdate()

除了Component, createElement, 咱们将会有两个公共函数render(), setState(),咱们已经看到setState() 仅仅调用了scheduleUpdate()

render()scheduleUpdate()很是相似,他们接收新的更新而且进入队列。

/ Fiber tags
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";

// Global state
const updateQueue = [];
let nextUnitOfWork = null;
let pendingCommit = null;

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: instance,
    partialState: partialState
  });
  requestIdleCallback(performWork);
}
复制代码

咱们将会使用updateQueue数组来存储等待的更新。每一次调用render 或者 scheduleUpdate 都会将数据存储进updateQueue。数组里每个数据都不同,咱们将会在resetNextUnitOfWork()函数中使用。

在将数据push存储进队列以后,咱们将会异步调用performWork()

performWork() && workLoop()

const ENOUGH_TIME = 1; // milliseconds

function performWork(deadline) {
  workLoop(deadline);
  if (nextUnitOfWork || updateQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}

function workLoop(deadline) {
  if (!nextUnitOfWork) {
    resetNextUnitOfWork();
  }
  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  if (pendingCommit) {
    commitAllWork(pendingCommit);
  }
}
复制代码

这里使用了咱们以前看到的performUnitOfWork模式。

workLoop()中判断deadline是否是有足够的时间来运行代码,若是不够,中止循环,回到performWork(),而且nextUnitOfWork还被保留为下次任务,在performWork()中判断是否还须要执行。

performUnitOfWork()的做用是构建 work-in-progress tree和找到哪些须要操做DOM的改变。这种处理方式是递增的,一次只处理一个fiber。

若是performUnitOfWork()完成了本次更新的全部工做,则renturn值为null,而且调用commitAllWork改变DOM。

至今为止,咱们尚未看到第一个nextUnitOfWork是如何产生的

resetUnitOfWork()

函数取出updateQueue第一项,将其转换成nextUnitOfWork.

function resetNextUnitOfWork() {
  const update = updateQueue.shift();
  if (!update) {
    return;
  }

  // Copy the setState parameter from the update payload to the corresponding fiber
  if (update.partialState) {
    update.instance.__fiber.partialState = update.partialState;
  }

  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
  };
}

function getRoot(fiber) {
  let node = fiber;
  while (node.parent) {
    node = node.parent;
  }
  return node;
}
复制代码

若是update包含partialState, 就将其保存的对应fiber上,在后面会赋值给组件实例,已供render使用。

而后,咱们找到old fiber树的根节点。若是update是first render调用的,root fiber将为null。若是是以后的render,root将等于_rootContainerFiber。若是update是由于setState(),则向上找到第一个没有patient属性的fiber。

而后咱们将其赋值给nextUnitOfWork,注意,这个fiber将会是work-in-progress的根元素。

若是没有old root。stateNode将取render()中的参数。props将会是render()的另一个参数。props中children是数组。alternate是 null。

若是有old root。stateNode是以前的root DOM node。props将会是newProps,若是其值不为null的话,不然就是原来的props。alternate就是以前的old root。

咱们如今已经有了work-in-progress的根元素,让咱们构造剩下的吧

performUnitOfWork()

function performUnitOfWork(wipFiber) {
  beginWork(wipFiber);
  if (wipFiber.child) {
    return wipFiber.child;
  }

  // No child, we call completeWork until we find a sibling
  let uow = wipFiber;
  while (uow) {
    completeWork(uow);
    if (uow.sibling) {
      // Sibling needs to beginWork
      return uow.sibling;
    }
    uow = uow.parent;
  }
}
复制代码

performUnitOfWork() 遍历work-in-progress树

beginWork()的做用是建立子节点的fiber。而且将第一次子节点做为fiber的child属性

若是当前fiber没有子节点,咱们就调用completeWork(),而且返回sibling做为下一个nextUnitOfWork.

若是没有sibling,就继续向上操做parent fiber。直到root。

总的来讲,就是先处理叶子节点,而后是其兄弟节点,而后是双亲节点。从下往上遍历。

beginWork(), updateHostComponent(), updateClassComponent()

unction beginWork(wipFiber) {
  if (wipFiber.tag == CLASS_COMPONENT) {
    updateClassComponent(wipFiber);
  } else {
    updateHostComponent(wipFiber);
  }
}

function updateHostComponent(wipFiber) {
  if (!wipFiber.stateNode) {
    wipFiber.stateNode = createDomElement(wipFiber);
  }
  const newChildElements = wipFiber.props.children;
  reconcileChildrenArray(wipFiber, newChildElements);
}

function updateClassComponent(wipFiber) {
  let instance = wipFiber.stateNode;
  if (instance == null) {
    // Call class constructor
    instance = wipFiber.stateNode = createInstance(wipFiber);
  } else if (wipFiber.props == instance.props && !wipFiber.partialState) {
    // No need to render, clone children from last time
    cloneChildFibers(wipFiber);
    return;
  }

  instance.props = wipFiber.props;
  instance.state = Object.assign({}, instance.state, wipFiber.partialState);
  wipFiber.partialState = null;

  const newChildElements = wipFiber.stateNode.render();
  reconcileChildrenArray(wipFiber, newChildElements);
}
复制代码

beginWork()的做用有两个

  • 建立 stateNode
  • 拿到component children,而且调用 reconcileChildrenArray()

由于对不一样类型component的处理方式不一样, 这里分红了updateHostComponentupdateClassComponent两个函数。

updateHostComponennt 处理了host component 和 root component。若是fiber上没有DOM node则新建一个(仅仅是建立一个DOM节点,没有子节点,也没有插入到DOM中)。而后利用fiber props中的children去调用reconcileChildrenArray()

updateClassComponent 处理了用户建立的class component。若是没有实例则建立一个。而且更新了props和state,这样render就是能够计算出新的children。

updateClassComponent并非每次都调用render函数。这有点相似于shouldCompnentUpdate函数。若是不须要调用render,就复制子节点。

如今咱们有了newChildElements, 咱们已经准备好去建立child fiber。

reconcileChildrenArray()

注意,这里是核心。这里建立了work-in-progress 树和决定如何更新DOM

/ Effect tags
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;

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

function reconcileChildrenArray(wipFiber, newChildElements) {
  const elements = arrify(newChildElements);

  let index = 0;
  let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
  let newFiber = null;
  while (index < elements.length || oldFiber != null) {
    const prevFiber = newFiber;
    const element = index < elements.length && elements[index];
    const sameType = oldFiber && element && element.type == oldFiber.type;

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        tag: oldFiber.tag,
        stateNode: oldFiber.stateNode,
        props: element.props,
        parent: wipFiber,
        alternate: oldFiber,
        partialState: oldFiber.partialState,
        effectTag: UPDATE
      };
    }

    if (element && !sameType) {
      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);
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

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

    index++;
  }
}
复制代码

首先咱们肯定newChildElements是一个数组(并不像以前的diff算法,此次的算法的children老是数组,这意味着咱们能够在render中返回数组)

而后,开始将old fiber中的children与新的elements作对比。还记得吗?fiber.alternate就是old fiber。new elements 来自于props.children(function)和 render(Class Component)。

reconciliation算法首先diff wipFiber.alternate.child 和 elements[0],而后是 wipFiber.alternate.child.sibling 和 elements[1]。这样一直遍历到遍历结束。

  • 若是oldFiberelement有相同的type。就经过old fiber建立新的。注意增长了UPDATE effectTag
  • 若是这二者有不一样的type或者没有对应的oldFiber(由于咱们新添加了子节点),就建立新的fiber。注意新fiber不会有alternate属性和stateNode(stateNode就会在beginWork()中建立)。还增长了PLACEMENT effectTag
  • 若是这二者有不一样的type或者没有对应的element(由于咱们删除了一些子节点)。咱们标记old fiber DELETION

cloneChildFibers()

updateClassComponent中有一个特殊状况,就是不须要render,而是直接复制fiber。

function cloneChildFibers(parentFiber) {
  const oldFiber = parentFiber.alternate;
  if (!oldFiber.child) {
    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;
  }
}
复制代码

cloneChildFibers()拷贝了old fiber的全部的子fiber。咱们不须要增长effectTag,由于咱们肯定不须要改变什么。

completeWork()

performUnitOfWork, 当wipFiber 没有新的子节点,或者咱们已经处理了全部的子节点时,咱们调用completeWork.

function completeWork(fiber) {
  if (fiber.tag == CLASS_COMPONENT) {
    fiber.stateNode.__fiber = fiber;
  }

  if (fiber.parent) {
    const childEffects = fiber.effects || [];
    const thisEffect = fiber.effectTag != null ? [fiber] : [];
    const parentEffects = fiber.parent.effects || [];
    fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
  } else {
    pendingCommit = fiber;
  }
}
复制代码

completeWork 中,咱们新建了effects列表。其中包含了work-in-progress中全部包含effecTag。方便后面处理。最后咱们将pendingCommit指向了root fiber。而且在workLoop中使用。

commitAllWork & commitWork

这是最后一件咱们须要作的事情,改变DOM。

function commitAllWork(fiber) {
  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;

  if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
    domParent.appendChild(fiber.stateNode);
  } else if (fiber.effectTag == UPDATE) {
    updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag == DELETION) {
    commitDeletion(fiber, domParent);
  }
}

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 effects。

  • PLACEMENT。将dom插入到父节点上
  • UPDATE。将新旧props交给updateDomProperties()处理。
  • DELETION。若是是Host component。用removeChild()删除就好。若是是class Component,那就要删除fiber subTree下面的全部host Component。

一旦咱们完成了全部的effects,就重置nextUnitOfWorkpendingCommit。work-in-progress tree就变成了old tree。并复制给_rootContainerFiber。 这样咱们完成了更新,而且作好了等待下一次更新的准备。

更多文章请查看个人主页或者blog

相关文章
相关标签/搜索