Skip to content

Latest commit

 

History

History
872 lines (613 loc) · 32.9 KB

5.Fibre.readme.md

File metadata and controls

872 lines (613 loc) · 32.9 KB

5. Fibre-递增对比

这个故事是系列DIY👋自己-React的一个部分, 但因为我们要重写大部分旧代码的, 无论如何, 我会 tl;dr它一下的:

TL;DR : 到目前为止的系列: 我们正在编写一个React克隆简化版本,来了解React在底层做了什么. 我们称之为Didact. 为了简化代码, 我们只关注-React-的主要函数. 首先我,们介绍如何渲染元素并使JSX工作. 我们编写了对比算法来重新渲染那些仅在更新期间更改了的内容. 然后我们添加了-Component - classsetState().

说一说 ❤️ react didact

现在React-16已经不复存在, 并且有了一个新的内部架构, 需要重写-React-的大部分代码.

这意味着一些期待已久的函数 - 旧,的架构很难发展 - 被送走了🐶.

这也意味着我们在这个系列中编写的大部分代码现在都是毫无价值的. 但是思想/逻辑可以留下😛

在本文中, 我们将重写-didact-系列中的大部分代码, 以遵循 React-16 新架构. 我们将尝试从React代码库中 模拟 它的结构, 变量和函数名称. 我们将跳过我们不需要的-公共API-:

  • Didact.createElement()

  • Didact.render() (只有DOM渲染)

  • Didact.Component(使用setState()但没有context或者生命周期方法)

如果你想跳到前面看代码的运行情况, 你可以去

更新的演示-codepen或访问->github存储库.

现在, 让我解释为什么我们需要重写旧代码.


目录

5.1 为什么选择Fiber

这不会提供React-Fiber的完整过程. 如果您想了解更多信息, 请查看-react-fiber资源列表.

当浏览器的主线程长时间忙于运行时, 关键的简短任务必须等待一段不可接受的时间,才能完成.

为了展示这个问题, 我做了一个小演示. 为了保持行星的旋转, 主线程需要在每16ms左右就要运行一次. 如果主线程被其他东西阻塞, 让我们定个200毫秒, 你会注意到动画丢失帧和行星冻结/卡顿, 直到主线程再次释放.

是什么让主线程如此繁忙, 以至于无法将一些 ms, 花费在保持动画平滑UI响应上呢?

记住-对比算法代码-3-实例-对比和虚拟dom? 一旦开始对比, 它就不会停止. 如果主线程需要做其他任何事情, 它将不得不等待. 而且, 因为很大程度上它取决于递归调用, 所以很难使它停止,再继续. 这就是为什么我们要用一个新的数据结构来重写它, 这将允许我们用循环-替换-递归调用.

fibre 理解提示

如果你是新手,请点击并观察 本🌰中两种不同 jsbin-fibre

  • 看了上面这个🌰, 你需要明白, fibre 带有数据流动 的认知

  • 然后你看 此资源列表 , 这是为了加深-对-react-fibre 的认知, 我们需要其中的demo-🌰子

说回 react-fibre 这个念想 : 带有数据的特性 的 -fibre-中 被记录了-优先级-属性.

可以看到demo-🌰子 带有共 三个选择或输入项

  1. pleace input text

没有变化的

  1. Async mode 默认

使用了-实验性react.unstable_deferredUpdates 会赋予此元素一个低的优先级

  1. sync mode

会到同步, 也就是没有变化

可以看到 -Async mode- 的卡顿, 因为它这个组件元素被分配的优先级低, 而sync mode的优先级比 Async mode 高, Async mode要为优先级高的让道. 比如优先级高的动画.

当然, react-fibre 不止做了这件事:P

5.2 调度微任务

我们需要将工作-分解为更小的部分, 可以短时间运行这些部分, 让主线程执行更高优先级的任务, 并且如果有-任何待处理的事情-再回来完成工作.

我们会在[requestIdleCallback()]((https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)函数的帮助下,做到这一点. 它将下一次浏览器空闲时,调用我们的preformWork回调函数, 并加入一个deadline参数, 用于描述我们的代码可用时间:

const ENOUGH_TIME = 1; // milliseconds

let workQueue = [];
let nextUnitOfWork = null; // 全局变量, 那么一次只能走一个回调

function schedule(task) { // 1. 加
  workQueue.push(task); // 2. 存好了
  requestIdleCallback(performWork); // 3. 下一次空闲运行, performWork 函数
}

function performWork(deadline) { // 空闲机会来了
  if (!nextUnitOfWork) {
    nextUnitOfWork = workQueue.shift(); // 4. 拿出来,
  }

// 下一回调 与 看看有没有 足够的时间 再走一趟 
  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    // 5. DO something
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (nextUnitOfWork || workQueue.length > 0) {
     // 6. 如果还没有搞定, 那么 再等空闲咯
    requestIdleCallback(performWork);
  }
}

真正的工作发生在performUnitOfWork函数的内部. 我们需要在那里编写我们的对比-{reconciliation}算法代码. 该函数应该运行一部分工作, 然后-返回-{return}-下一次恢复工作需要的所有信息.

为了跟踪这些工作, 我们将使用Fibre.

5.3 Fibre-数据结构

我们将为每个想要渲染的组件创建一个Fibre.

  • nextUnitOfWork将是对下一个工作Fibre的参考.
  • performUnitOfWork拿到Fibre,并在其上工作, 并返回一个新的Fibre用于下一次 - 直到所有工作完成.

容许下, 我会稍后详细解释这一点.

Fibre是怎样的 ?

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: []
};

这是一个普通的旧JavaScript对象.

我们将使用parent, childsibling-属性-打造的Fibre树来描述组件的树.

stateNode将是对Component实例的引用. 它可以是DOM元素, 也可以是用户定义的Component-class的实例.

例如:

fibre-1

在这个例子中, 我们可以看到我们将支持的三种不同类型的组件:

1. 这 b, piFibre 表示 host components.

我们将用

tag: HOST_COMPONENT,

来识别它们.

  • fibre.type是html元素的标签:string.

  • fibre.props是元素的-属性-和-事件监听器-.

2. 例子中的 <Foo>Fibre 中 表示 class component.

它的tagCLASS_COMPONENTtype来自 用户定义的Didact.Component.

3. div 代表Fibrehost root. 它与 host component 相似,

因为它有DOM元素可以作为stateNode, 但作为树的根, 它会得到特殊处理. Fibre.tag会是HOST_ROOT. 请注意, Fibre.stateNode是传递给Didact.render()的DOM节点.

4. 另一个重要的属性是alternate.

我们需要它, 因为大多数时候我们会有两棵Fibre树.

  • 一棵树将对应于我们已经呈现给-html-DOM-的东西, 我们将它称为当前树或旧树.

  • 另一棵是我们在创建新更新(调用setState()Didact.render() 时构建的树, 我们将此树称为正在进行中的树, 简称为工作树

工作树不会与旧树共享任何Fibre. 一旦我们完成-工作树-的工作建设,并取得所需的 DOM变化, 工作树会成为旧树.

因此, 我们使用alternate链接 正在进行中的Fiber与 相应旧树的Fiber. Fibre和它的alternate分享相同tag, typestateNode. 有时,当我们渲染新的东西时,Fibre不会有alternate.

最后, 我们有effects列表和effectTag. 当我们发现工作树Fibre有需要改变的DOM, 我们将设置effectTagPLACEMENT, UPDATEDELETION. 为了更容易将所有DOM变化一起提交, 我们保留了所有Fibre(来自Fibre子树)的effectTag项到列表effects.

这可能是一次太多的信息, 如果你没有跟上, 不要担心, 我们很快就会看到Fibre树的运行.

5.4 Didact调用层次结构

要了解我们要编写的代码的流程, 请查看此图表:

5-pic

我们将从render()setState()开始, 并遵循以commitAllWork()结尾的流程。

5.4.1 旧代码

我告诉过你, 我们将重写大部分代码, 但我们首先回顾一下我们不会h重写的代码.

  • 元素创建和JSX中, 我们编写了用于编译JSX的函数代码createElement(). 我们不需要改变它, 我们将继续使用相同的元素. 如果你不知道元素中, 关于type, propschildren这些, 请查看旧帖子.

  • 实例, 对比算法和虚拟DOM中, 我们编写了用于更新节点DOM属性的updateDomProperties()函数. 我还扩展了用于创建DOM元素的函数createDomElement(). 你可以在这个dom-utils.js 的 gist中看到这两个函数.

gist 内容在这里 👌
const isEvent = name => name.startsWith("on");
const isAttribute = name =>
  !isEvent(name) && name != "children" && name != "style";
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);

function updateDomProperties(dom, prevProps, nextProps) {
  // Remove event listeners
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, prevProps[name]);
    });

  // Remove attributes
  Object.keys(prevProps)
    .filter(isAttribute)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = null;
    });

  // Set attributes
  Object.keys(nextProps)
    .filter(isAttribute)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name];
    });

  // Set style
  prevProps.style = prevProps.style || {};
  nextProps.style = nextProps.style || {};
  Object.keys(nextProps.style)
    .filter(isNew(prevProps.style, nextProps.style))
    .forEach(key => {
      dom.style[key] = nextProps.style[key];
    });
  Object.keys(prevProps.style)
    .filter(isGone(prevProps.style, nextProps.style))
    .forEach(key => {
      dom.style[key] = "";
    });

  // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });
}

function createDomElement(fiber) {
  const isTextElement = fiber.type === TEXT_ELEMENT;
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(fiber.type);
  updateDomProperties(dom, [], fiber.props);
  return dom;
}

  • 组件和状态中, 我们编写了Component-基类. 让我们改变它,以便setState()可以调用scheduleUpdate(),和createInstance()来保存实例中Fibre的引用:
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;
}

从这段代码开始, 让我们从头开始重写其余部分

5.4.2 renderscheduleUpdate

5-pic1

除了Component class 和 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) { // 提供给 setState 使用
  updateQueue.push({
    from: CLASS_COMPONENT,
    instance: instance,
    partialState: partialState
  });
  requestIdleCallback(performWork);
}

我们将使用该updateQueue数组来跟踪待处理的更新. 每次运行render()scheduleUpdate()会推送一个新的更新到updateQueue. 每个更新中的更新信息都不同, 等会你会看到,我们在之后的resetNextUnitOfWork()函数中使用它.

将更新推送到队列后, 我们触发延迟回调performWork()函数

5.4.3 performWorkworkLoop

5-pic2

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

正如我们在 5.2 调度微任务 中 了解到的循环工作流程, 只是具体分支起了对应的名称

是之前我们使用performUnitOfWork()时,看到的模式.

requestIdleCallback()提供截止日期-{deadline}作为目标函数的参数调用. performWork()deadline传给它的workLoop(). 然后workLoop()运行返回, performWork()检查是否还有待审批工作. 如果有的话, 它会为它自己安排一个新的requestIdleCallback(performWork).

workLoop()是关注时间的函数. 如果deadline时间太少, 它会停止工作循环,并保持下一次nextUnitOfWork更新需要的Fibre状态, 以便下次恢复.

我们使用ENOUGH_TIME(1ms常数, 与React相同)来检查deadline.timeRemaining(),是否有足够运行另一个工作单元的时间. 如果performUnitOfWork()超过这一时间, 我们将留待下次继续. deadline只是来自浏览器的建议, 所以将其超过几毫秒并不是那么糟糕.

performUnitOfWork()将用正在进行的更新,和找出我们需要对-DOM-应用哪些更改来构建工作树. 这将逐步完成, 每次一段Fibre数据.

performUnitOfWork()完成当前更新的所有工作时, 它将返回null和将在DOM中待处理的更改留给pendingCommit. 最后, commitAllWork()获取来自pendingCommiteffects, 和变更DOM.

请注意, commitAllWork()在循环之外调用. performUnitOfWork()完成的工作并不会改变DOM, 因此可以将其分开.

另一方面, commitAllWork()将改变DOM, 它应该一次完成, 以避免不一致的UI.

我们还没有看到第一个nextUnitOfWork来自哪里

5.4.4 resetNextUnitOfWork

5-pic3

resetNextUnitOfWork() 运行在上一小节的workLoop函数中

resetNextUnitOfWork()获取一次更新,并将其转换为第一个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;
}
  • resetNextUnitOfWork() 首先从队列中提取第一个更新.

  • 如果有更新, partialState我们将它存储在属于组件实例的Fibre上, 以便稍后在调用组件render()时使用它.

  • 然后我们找到旧Fibre树的根. 如果更新是第一次调用render()时, 我们不会有一个根Fibre,所以root == null. 如果它来自后续的render()调用, 我们可以在DOM节点的_rootContainerFiber属性上找到根. 如果更新来自一次setState(), 我们需要从实例Fibre往上找, 直到找到一个Fibre是没有-parent的.

  • 然后我们让nextUnitOfWork重新获得了一个新的Fibre.Fibre是一个新的工作树的根.

如果我们没有一个 的根, 那么这个DOM节点stateNode就会在render()调用中,作为参数被接收. 这props将是来自更新的newProps: 对象中的一个children属性具有元素,childrenrender()的另一个参数. 该alternate == null.

如果我们有一个 的根, 那么stateNode将是前一个根的DOM节点. 该props如果不是null,又会是newProps, 否则我们将从 根复制props. 那这alternate将是 根.

我们现在拥有正在进行中的树的根, 让我们开始构建剩余的树.

5.4.5 performUnitOfWork

5-pic4

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 返回, 再次变为 wipFiber, 被 beginWork 调用
      return uow.sibling;
    }
    uow = uow.parent;
  }
}

performUnitOfWork() 会运行 进行中的工作树.

  • 我们称之为beginWork() - 创造一个新的Fibre孩子 - 然后让第一个孩子返回,成为nextUnitOfWork.

  • 如果没有任何的孩子, 我们接着completeWork(),并返回sibling作为nextUnitOfWork.

  • 如果没有sibling, 我们会继续往parent中找, completeWork()直到我们找到sibling(将成为nextUnitOfWork)或到达根部.

performUnitOfWork()多次调用,会沿着 工作树 向下, 创建每根Fibre的第一个childchildren, 若找到没有孩子的Fibre. 就向'右'和向'上'找, 就好像有兄弟姐妹,父母,整个家族图似的. (为了更加生动, 可以尝试在fiber-调试器上渲染一些组件)

5.4.6 beginWork

5-pic4

function 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) {
    // 调用类初始化
    instance = wipFiber.stateNode = createInstance(wipFiber);
  } else if (wipFiber.props == instance.props && !wipFiber.partialState) {
    // 不需要更新,最后 复制 孩子
    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,如果我们没有
  • 获取组件子项并将它们传递给 reconcileChildrenArray()

因为两者都取决于我们处理的组件的类型, 所以我们将它分成两部分: updateHostComponent()updateClassComponent().

  • updateHostComponent()处理 主机组件 以及 根组件. 如果需要的话, 它会创建一个新的DOM节点(只有一个节点, 没有子节点, 并且不会将其附加到DOM). 然后它通过调用reconcileChildrenArray()函数, 使用来自Fibreprops属性的子元素.

  • updateClassComponent()处理类组件实例. 如果需要的话, 它会创建一个新实例,通过调用createInstance函数. 它更新实例的props, state,因此它的render()函数可以获取新的孩子.

  • updateClassComponent()也验证调用render()是否有意义. 这是一个简单版本的shouldComponentUpdate(). 如果看起来我们不需要重新渲染, 那么我们只是将当前的子树克隆到正在进行的工作树中, 而不进行任何调整.

我们现在有了newChildElements,准备好为 工作中的Fibre 创建子Fibre

5.4.7 reconcileChildrenArray

5-pic4

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

这是本库的核心, 正在进行的工作树在不断增长, 我们决定在提交阶段对 DOM 做什么更改.

  • 开始之前, 我们确保newChildElements是一个数组. (与之前的对比算法不同, 这个算法总是与子数组一起工作, 这意味着我们现在可以在组件的render()函数上返回数组)

  • 然后我们开始比较旧Fibre树的孩子和新元素(我们将Fibre与元素进行比较). 来自 Fibre树的孩子们正是wipFiber.alternate的孩子们. 新元素是我们从wipFiber.props.children调用或wipFiber.stateNode.render()调用来的.

我们的对比算法通过 第一个旧Fibre(匹配wipFiber.alternate.child)与第一子元素(elements[0]), 第二个旧FibrewipFiber.alternate.child.sibling)的第二子元素(elements[1])等. 对于每对 oldFiber - element:

  • 如果oldFiberelement同样的type, 好消息, 这意味着我们可以保留旧的stateNode. 我们创建一个基于旧的Fibre的新Fibre. 我们添加UPDATE effectTag. 我们将新Fibre添加到正在进行的工作树中.

  • 如果我们elementoldFiber有一个不同type和我们没有oldFiber(因为我们有更多的新孩子), 我们用element的信息创建一个新的Fibre. 请注意, 这种新Fibre没有alternate, 也不会有stateNode(我们将在beginWork()创建stateNode). 该FibreeffectTagPLACEMENT.

  • 如果oldFiberelement有不同的type和有oldFiber,也没有任何的element(因为我们有更多 孩子), 我们将标记oldFiberDELETION. 鉴于这种Fibre不是正在进行工作的树的一部分, 我们需要将它添加到wipFiber.effects列表中, 以便我们不会丢失它的踪迹.

与 React 不同的是, 我们没有使用 key 来进行比对, 所以我们不知道一个孩子是否从之前的位置移动过来.

5.4.8 cloneChildFibers

5-pic4

updateClassComponent() 有一个特殊情况, 我们采取了快捷方式, 将旧的Fibre子树克隆到正在进行中的工作树, 而不是进行调整.

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()克隆每个wipFiber.alternate孩子并将其附加到正在进行中的树中. 我们不需要添加任何内容, 因为我们确信effectTag没有任何变化.

5.4.9 completeWork

5-pic4

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()首先更新与CLASS_COMPONENT实例相关的Fibre引用. (说实话, 这并不是真的需要在这里, 但它必须在某个地方)

  • 然后它建立一个effects列表. 该列表将包含来自 正在进行中的子树 的所有Fibre.effectTag(它也包含来自旧子树的Fibre-DELETION effectTag). 这个想法是在根effects列表中集合所有的Fibre.effectTag.

  • 最后, 如果Fibre没有parent, 我们拿到了 正在进行工作的树的根. 所以我们完成了这次更新的所有工作,并收集了所有的effects. 我们分配根到pendingCommit,以便workLoop()commitAllWork()可以调用.

5.4.10 commitAllWork

5-pic4

我们需要做的最后一件事是: 改变DOM.

function commitAllWork(fiber) {
  fiber.effects.forEach(f => {
    commitWork(f);
  });
  fiber.stateNode._rootContainerFiber = fiber;
  nextUnitOfWork = null; // Reset
  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); // add
  } 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()首先迭代effects中每个项调用commitWork(). commitWork()检查每根FibreeffectTag:

  • 如果是PLACEMENT, 我们查找父DOM节点, 然后简单地追加Fibre.stateNode.

  • 如果是UPDATE, 我们会把stateNode旧道具和新道具放在一起, 然后updateDomProperties()决定要更新什么.

  • 如果它是DELETION并且Fibre是主机组件, 那很简单, 我们只是removeChild(). 但是如果Fibre是类组件, 在调用removeChild()之前, 我们需要从Fibre子树中,找到需要删除的所有主机组件.

一旦我们完成了所有的效果, 我们可以重置nextUnitOfWorkpendingCommit. 正在进行的工作树 不再是 正在进行中的工作树, 并成为旧树, 因此我们将其根指定给_rootContainerFiber. 之后, 我们完成当前的更新, 我们准备开始下一个🚀

5.5 Didact的使用

如果你想把所有的东西放在一起, 只公开API, 你可以这样做:

function importDidact() {
  // ...
  // All the code we wrote
  // ...

  return {
    createElement,
    render,
    Component
  };
}

/** @jsx Didact.createElement */
const Didact = importDidact();

class HelloMessage extends Didact.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}

Didact.render(
  <HelloMessage name="John" />,
  document.getElementById("container")
);

或者你可以查看的codepen.IO. 所有这些代码也可以在Didact的仓库npm - didact中获得.

5.6 下一步是什么?

Didact缺少很多React的函数, 但我特别有兴趣根据优先级安排更新:

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  TaskPriority: 2, // Completes at the end of the current tick.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};

react-源

所以啊, 下一篇文章可能会这样.

就这样!如果你喜欢它, 不要忘记拍手👏, 在Twitter上关注我, 发表评论和所有这些内容.

谢谢阅读. !!