• Scheduler【调度器】:调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler【协调器】:负责找出变化的组件,标识effectTag
  • Renderer【渲染器】 :负责将变化的组件渲染到页面上

调度器

异步可中断:浏览器是否有剩余时间作为任务中断的标准,需要浏览器提供机制在空闲时间进行回掉。

requestIdleCallback:方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

React当中没有使用requestIdleCallback,而是自己实现的功能,这里使用requestIdleCallback代替调度器的功能。

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

requestIdleCallback会自己在空余时间回掉执行workLoop方法,加上workLoop内部调用requestIdleCallback,所以workLoop会在浏览器有空余的时间内一直进行循环回掉。


调度器和协调器调用关系

while (nextUnitOfWork && !shouldYield) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  shouldYield = deadline.timeRemaining() < 1;
}

如果nextUnitOfWork不为空,并且浏览器存在空余时间的情况下,调用performUnitOfWork方法。这里的nextUnitOfWrok是什么呢?

nextUnitOfWork是fiber节点,每一个fiber节点。React16将以前的同步递归遍历,改成异步可中断遍历。这里的nextUnitOfWork就是每一个需要进行调和处理的节点。处理完当前节点之后,nextUnitOfWork会被赋值为下一个需要调和处理的节点。这里下一个节点什么时候进行调和,受调度器控制。

理解 build your own react_javascript

调度器和渲染器调用关系

if (!nextUnitOfWork && wipRoot) {
  commitRoot();
}

假设当前fiber节点调和完毕,则会进行渲染阶段,虚拟dom映射到真实dom上。

协调器

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }

  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;

  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

调和阶段是负责找出变化的组件,标识effectTag。那主要为了对fiber树节点的遍历,对比,找出差异,打上tag。

时间复杂度为O(n)的遍历方式

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  reconcileChildren(fiber, fiber.props.children);
}

function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

核心在于,fiber调和阶段对于每个节点的处理,每个节点会进行父子,兄弟关系的关联。假设树节点如下,当进行深度优先遍历的时候,顺序如连接线的顺序。

理解 build your own react_react_02

如果遍历完node4之后,知道下一个节点就是node5,知道node5遍历之后,需要回到node2节点,然后下一个节点是node3。这样的话,每一个节点执行之后,可以按照规律找到下一个节点是什么。那就不用递归循环遍历了。

理解 build your own react_react_03

所以这里需要对每个节点进行链表节点处理。

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;

    /* fiber 节点寻找差异这部分先不看 */
    
    /*
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,  // 着重看这一行
        alternate: oldFiber,
        effectTag: "UPDATE"
      };
    */

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

    prevSibling = newFiber;
    index++;
  }
}

举例:

  1. 当nextUnitOfWork为node1的时候

理解 build your own react_javascript_04

  1. 当nextUnitOfWork为node2的时候

理解 build your own react_javascript_05

  1. 处理完节点

理解 build your own react_java_06

如何进行节点遍历的?

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  
 /* 这里不关心 */

  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;

  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}



  • node1 -》node2:因为node2是node1的第一个节点,设置了node1.child = node2。
  • node4 -》node5:node2调和之后,因为node2当前节点有child节点,指向node4。node4没有child节点,所会执行nextFiber.sibling,所以node4.sibling = node5。
  • node5 -》node3:node5没有child节点,也没有sibling节点,所以执行nextFiber = nextFiber.parent,nextFiber指向了node2。node2.sibling = node3。

找出差异

先了解代码里的两个变量:


  • currentRoot:当前已经渲染在页面上的fiber root节点。
  • wipRoot:workInProgress root,本次更新在调和阶段创建的root fiber节点。

双缓存概念

这里两个fiber树,是为了在wipRoot完成之后,直接替换现有的currentRoot,不会造成过多的等待时间。

两个阶段

首屏渲染

首屏渲染的时候currentRoot为null,wipRoot创建成功之后,直接替换currentRoot,所有的阶段的effectTag均为PLACEMENT。

这里需要关心的是下面的代码

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

render方法执行的时候,会将alternate属性赋值为currentRoot。因为这里是首屏渲染,所以currentRoot是null。

更新阶段

此时currentRoot已经存在,在调和阶段会进行currnetRoot树和wipRoot树进行对比。

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

更新阶段,首屏渲染已经结束,所以currentRoot是首屏渲染的节点数据。

对比逻辑

这里的问题是如何进行每个节点和原先的节点进行处理。

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;

    const sameType = oldFiber && element && element.type == oldFiber.type;

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE"
      };
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT"
      };
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }

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

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

    prevSibling = newFiber;
    index++;
  }
}


判断分为三种类型:


  • sameType:相同类型,标识 UPDATE
  • element && !sameType:不同类型,新元素标识 PLACEMENT
  • oldFiber && !sameType:不同类型,老元素标识 DELETION

渲染器

更新页面元素

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;

  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

操作的还是根节点#root。经过首屏渲染之后,current Fiber树和wip Fiber(workInProgress)之间的差异会被同步到dom节点上。

Hooks

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };

  const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action => {
    hook.state = action(hook.state);
  });

  const setState = action => {
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

这里是将hooks作为根节点的属性进行存储。每一个更新,都会先去currentRoot查看之前的state数据,进行赋值。

const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };

当然这里的hooks处理是同步的,不会进行batchUpdate处理。

代码:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  };
}

function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);

  updateDom(dom, {}, fiber.props);

  return dom;
}

const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
  //Remove old or changed 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 old properties
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = "";
    });

  // Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name];
    });

  // 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 commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;

  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };

  const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action => {
    hook.state = action(hook.state);
  });

  const setState = action => {
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  reconcileChildren(fiber, fiber.props.children);
}

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;

    const sameType = oldFiber && element && element.type == oldFiber.type;

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE"
      };
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT"
      };
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }

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

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

    prevSibling = newFiber;
    index++;
  }
}

const Didact = {
  createElement,
  render,
  useState
};

/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1);
  const [state1, setState1] = Didact.useState(1);

  return (
    <div>
      <h1 onClick={() => setState(c => c + 1)} style="user-select: none">
        Count: {state}
      </h1>
      <h1 onClick={() => setState1(c => c + 1)} style="user-select: none">
        Count: {state1}
      </h1>
    </div>
  );
}
const element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);