构建自己的React(Build your own React)

234 阅读13分钟

非原创, 阅读时长3h, 建议直接读原文. 翻译自: pomb.us/build-your-…

我们将从头开始重写 React。一步步遵循真实 React 代码的架构,但没有所有优化和非必要功能。

  1. createElement 函数
  2. render 函数
  3. Concurrent Mode
  4. Fibers
  5. Render and Commit Phases
  6. Reconciliation
  7. 函数组件 Function Components
  8. Hooks

Step0: 初步概览Review

我们要做的事情是将react代码删除, 转化成好理解的原生js代码

const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);

我们有使用 JSX 定义的元素。它甚至不是有效的 JavaScript,所以为了用普通 JS 替换它,首先我们需要用有效的 JS 替换它。

JSX通过Babel等工具转化为JS,转化通常很简单,使用createElement函数通过将标签名称、props 和 children 作为参数传递

const element = React.createElement(
    "h1",
    { title: "foo" },
    "Hello"
);

React.createElement 从它的参数创建一个对象。除了一些验证之外,这就是它所做的一切。所以我们可以安全地用它的输出替换函数调用。

const element = {
    type: "h1",
    props: {
        title: "foo",
        children: "Hello",
    },
}

这就是元素,一个具有两个属性的对象:type 和 props(它还有更多,但我们只关心这两个)

type 是一个字符串,它指定了我们要创建的 DOM 节点的类型,它是您在要创建 HTML 元素时传递给 document.createElement 的 tagName。它也可以是一个函数,但我们将把它留给Step7。

props 是另一个对象,它具有 JSX 属性中的所有键和值。它还有一个特殊的属性:children

在这种情况下,children 是一个字符串,但它通常是一个包含更多元素的数组。这就是为什么元素也是树。

ReactDOM.render

我们需要替换的另一段 React 代码是对 ReactDOM.render 的调用。

ReactDOM.render(element, container)

render 是 React 更改 DOM 的地方,所以让我们自己进行更新。

const node = document.createElement(element.type); // h1元素
node["title"] = element.props.title;
  1. 首先, 我们使用元素类型创建一个node元素*,在本例中为h1

然后, 我们将所有props分配给该node元素。这里只是title

为避免混淆,我将使用element来指代 React 元素,使用node来指代 DOM 元素。

const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
  1. 然后, 我们为children创建node。我们只有一个字符串作为子node,因此我们创建了一个文本node。

使用 textNode 而不是设置 innerText 将允许我们稍后以相同的方式处理所有元素。还要注意我们如何设置 nodeValue 就像我们对 h1 标题所做的那样,它几乎就像字符串有props:{nodeValue:“hello”}

const container = document.getElementById("root");
node.appendChild(text);
container.appendChild(node);
  1. 最后, 我们将 textNode 添加到 h1 并将 h1 添加到container

完整的代码 React => 原生Js

完整的React代码

const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);

转化成原生Js代码

const element = {
    type: "h1",
    props: {
        title: "foo",
        children: "Hello",
    },
}

const container = document.getElementById("root");
const node = document.createElement(element.type);
node["title"] = element.props.title;
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
node.appendChild(text);
container.appendChild(node);

没有React代码, 只有原生的js代码了, 这样看着就很清楚, 原生的js代码到底做了什么事情,

Step1: createElement函数

让我们从另一个应用程序重新开始。这次我们将用我们自己的 React 版本替换 React 代码。

我们将从编写自己的 createElement 开始。

让我们将 JSX 转换为 JS,以便我们可以看到 createElement 调用。

const element = (
    <div id="foo">
        <a>bar</a>
        <b />
    </div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);

正如我们在Step0中看到的,element是具有typeprops的对象。我们的函数唯一需要做的就是创建那个对象。

const element = React.createElement(
    "div",
    { id: "foo" },
    React.createElement("a", null, "bar"),
    React.createElement("b")
);

我们对 props 使用扩展运算符,对子项使用 rest 参数语法,这样 children prop 将始终是一个数组。

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children,
        },
    }
}

举例, createElement("div") 返回:

{
  "type": "div",
  "props": { "children": [] }
}

createElement("div", null, a) 返回:

{
  "type": "div",
  "props": { "children": [a] }
}

createElement("div", null, a, b) 返回:

{
  "type": "div",
  "props": { "children": [a, b] }
}

children 数组还可以包含原始值,如字符串或数字。所以我们将把所有不是对象的东西都包装在它自己的元素中,并为它们创建一个特殊的类型:TEXT_ELEMENT

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

React 不会在没有children时包装原始值或创建空数组,但我们这样做是因为它会简化我们的代码,并且对于我们的库,我们更喜欢简单的代码而不是高性能的代码。

我们仍在使用 React 的 createElement。但为了替换它, 我们取了个新的名字叫Didact

所以我们会把代码修改为如下:

const Didact = {
    createElement,
};

const element = Didact.createElement(
    "div",
    { id: "foo" },
    Didact.createElement("a", null, "bar"),
    Didact.createElement("b")
);

而通过jsx语法:

/** @jsx Didact.createElement */
const element = (
    <div id="foo">
        <a>bar</a>
        <b />
    </div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);

如果我们有这样的评论,当 babel 转译 JSX 时,它将使用我们定义的函数(Didact.createElement)。

Step2: render 函数

接下来,我们需要编写我们版本的 ReactDOM.render 函数。

function render() {
    // TODO create dom nodes
}

const Didact = {
    render
};

Didact.render(element, container);

现在,我们只关心向 DOM 添加内容。我们稍后会处理更新和删除。

function render(element, container) {
    const dom = document.createElement(element.type);
    container.appendChild(dom);
}

我们首先使用元素类型(element.type)创建 DOM 节点,然后将新节点附加到容器(container)中。


function render(element, container) {
    const dom = document.createElement(element.type);
   
    // 新增代码
    element.props.children.forEach(child =>
        render(child, dom)
    );
    
    container.appendChild(dom);
}

我们递归地为每个孩子做同样的事情。


function render(element, container) {
    const dom = document.createElement(element.type);
   
    // 新增代码
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);
   
    element.props.children.forEach(child =>
        render(child, dom)
    );
    
    container.appendChild(dom);
}

我们还需要处理文本元素,如果元素类型是 TEXT_ELEMENT 我们创建一个文本node而不是常规node。


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

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

function render(element, container) {
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);

    const isProperty = key => key !== "children";
    Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = element.props[name]
        });

    element.props.children.forEach(child =>
        render(child, dom)
    );

    container.appendChild(dom)
}

const Didact = {
    createElement,
    render,
};

/** @jsx Didact.createElement */
const element = (
    <div id="foo">
        <a>bar</a>
        <b />
    </div>
);
const container = document.getElementById("root");
Didact.render(element, container);

最后, 我们在这里需要做的一件事是将元素props分配给node。 以上就是一个可以将 JSX 渲染到 DOM 的库Didact。

Step3: Concurrent Mode并发模式

在开始添加更多的代码之前, 我们需要重构之前的代码.


function render() {
    ...

    element.props.children.forEach(child =>
        render(child, dom)
    )
    
    ...
}

这个递归调用有问题。

一旦我们开始渲染,我们将不会停止,直到我们渲染完完整的element树。如果element树很大,它可能会阻塞主线程太久。如果浏览器需要做高优先级的事情,比如处理用户输入或保持动画流畅,它必须等到渲染完成

let nextUnitOfWork = null;

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

requestIdleCallback(workLoop);

function performUnitOfWork(nextUnitOfWork) {
    // TODO
}

所以我们要把工作分解成小的单元,当我们完成每个单元后, 如果还有其他需要做的事情,我们会让浏览器终端渲染(rendering)

function workLoop(deadline) {
    // ...
    
    requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

我们使用 requestIdleCallback 进行循环, 可以将 requestIdleCallback视为 setTimeout, 但不是我们告诉它何时运行, 浏览器将在主线程空闲时运行回调

注: React不再使用 requestIdleCallback,现在它使用sheduler package, 但对于这个用例, 它在概念上是相同的

let nextUnitOfWork = null;

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

requestIdleCallback还为我们提供了deadline参数, 我们可以通过用它来检查在浏览器需要再次控制之前我们有多少时间

截止2019年11月, Concurrent Mode并发模式在React中还不稳定, 稳定的版本更像这样:

    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }

这里要开始去写performUnitOfWork函数

function performUnitOfWork(nextUnitOfWork) {
    // TODO
}

再开始使用循环, 我们需要设置的一个工作单元, 然后编写performUnitOfWork函数, 它不仅执行工作而且返回下一个工作单元。

Step4: Fibers

为了组织工作单元, 我们需要一个数据结构: fiber tree.

我们将为每一个element提供一个fiber,每一个fiber都是一个工作单元.

假设我们要渲染这样一个element树:

Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

在render中, 我们将创建root fiber并将其设置为nextUnitOfWork. 剩下的工作将发生在performUnitOfWork函数上, 在每一个fiber要做三件事:

  1. 将element添加到DOM
  2. 为element的children添加fibers
  3. 选择下一个工作单元

image.png

该数据结构的目标之一就是便于查找下一个工作单元, 这就是为什么每一个fiber都可以链接到它的第一个child、它下一个兄弟和它的parent.

image.png

当我们完成一个fiber工作时, 如果她有一个child, 那么该child的fiber将要成为下一工作单元.

如上: 当我们完成对divfiber的处理后, 下一个工作单元将是h1fiber.

image.png

如果该fiber没有child, 我们就会将兄弟作为下一个工作单元.

如上: pfiber没有child, 所以我们完成后就会移动到afiber.

image.png

如果该fiber既没有child也没有兄弟,我们就去找 叔叔: parent的兄弟, 就像以上的ah2的fiber.

此外, 如果parent没有兄弟, 我们会继续向上遍历parent, 直到我们找到一个有兄弟的parent或直到root, 如果我们已经到达root, 则意味着我们已经完成所有的render工作.

让我们把流程变成代码


function render(element, container) {
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);

    const isProperty = key => key !== "children";
    Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = element.props[name]
        });

    element.props.children.forEach(child =>
        render(child, dom)
    );

    container.appendChild(dom)
}

首先, 从render函数中删除这段代码

function createDom(fiber) {

}

function render(element, container) {
    // TODO set next unit of work
}

我们将创建DOM节点的部分保留在自己的函数中.

在渲染函数中, 我们将nextUnitOfWork设置为fiber树的root.

function render(element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        }
    }
}

let nextUnitOfWork = null;

然后, 当浏览器准备就绪时, 它将调用workLoop,我们将从root开始处理.


function workLoop(deadLine) {
    // ...

    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
    
    // ...
}

function performUnitOfWork(fiber) {
    // TODO 添加Dom节点
    // TODO 创建新的fiber
    // TODO 返回下一个工作单元
}

performUnitOfWork

首先, 我们创建一个新的node并将其添加到Dom. 我们在fiber.dom属性中跟踪Dom节点.

function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
}

然后, 我们为每个child创建一个新的fiber

function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    
    // 新增
    const elements = fiber.props.children;
    let index = 0;
    let prevSibling = null;
    
    while (index < elements.length) {
        const element = elements[index];
        
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null;
        };
    }
}

然后我们将它添加到fiber树中, 将其设置为child节点或兄弟节点, 具体取决于它是否是第一个child节点.

function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    
    const elements = fiber.props.children;
    let index = 0;
    let prevSibling = null;
    
    while (index < elements.length) {
        const element = elements[index];
        
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null;
        };
            
        // 新增
        if (index === 0) {
            fiber.child = newFiber;
        } else {
            prevSibling.sibling = newFiber;
        }
        
        prevSibling = newFiber;
        index++;
    }
}

最后, 我们搜索下一个工作单元, 我们先尝试child, 然后是兄弟, 然后是叔叔.

function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    
    const elements = fiber.props.children;
    let index = 0;
    let prevSibling = null;
    
    while (index < elements.length) {
        const element = elements[index];
        
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null;
        };
            
        if (index === 0) {
            fiber.child = newFiber;
        } else {
            prevSibling.sibling = newFiber;
        }
        
        prevSibling = newFiber;
        index++;
    }
    
    if (fiber.child) {
        return fiber.child;
    }
    
    let nextFiber = fiber;
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling;
        }
        
        nextFiber = nextFiber.parent;
    }
}


Step5: render 和 commit 阶段

这有一个问题:

每次我们处理一个元素时, 都会向DOM添加一个新节点, 而且, 请记住,在我们完成渲染整个tree之前,浏览器可能会中断我们的工作. 在这种情况下, 用户将看到一个不完整的UI, 我们不希望这样.

function performUnitOfWork(fiber) {
    ...
    
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    
    ...
}

因此我们在函数中删除改变DOM的部分.

另外, 我们将继续跟踪fiber树的root,我们称其为正在进行的工作root或wipRoot.

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

let wipRoot = null;

一旦我们完成所有工作(我们知道它是因为没有找到下一个工作单元), 我们将整个fiber树交给DOM.

function commitRoot() {
    // TODO add nodes to dom
}

function workLoop(deadline) {
    ...
    
    if (!nextUnitOfWork && wipRoot) {
        commitRoot();
    }
    
    ...
}

我们在commitRoot函数中进行, 在这里, 我们递归地将所有的节点添加到Dom.

function commitRoot() {
    commitWork(wipRoot.child);
    wipRoot = null;
}

function commitWork(fiber) {
    if (!fiber) {
        return;
    }
        
    const domParent = fiber.parent.dom;
    domParent.appendChild(fiber.dom);
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

Step6: Reconciliation (协调)

当前为止, 我们只向Dom添加内容, 但是更新或删除node呢?

function commitRoot() {
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}

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

let currentRoot = null;

这就是我们现在要做的, 我们需要将我们在渲染函数上接收的element与我们提交给Dom的最后一个fiber树进行比较.

因此, 我们需要在完成commit后, 保存对我们commit给Dom的最后一个fiber树的引用, 我们称之为currentRoot.

我们还为每一个fiber添加了alternate属性, 此属性链接到旧的fiber, 旧的fiber是我们上一个阶段commit给Dom的.


function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    const elements = fiber.props.children
    let index = 0
    let prevSibling = null
    while (index < elements.length) {
        const element = elements[index]
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }
        if (index === 0) {
            fiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }

    if (fiber.child) {
        return fiber.child
    }
    
    let nextFiber = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
}

以上是此刻performUnitOfWork的完整函数, 现在让我们从 创建fiberperformUnitOfWork提取代码.

到新的reconcileChildren函数.

function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    
    // 提取到reconcileChildren
    const elements = fiber.props.children
    reconcileChildren(fiber, elements)
   
    if (fiber.child) {
        return fiber.child
    }
    
    let nextFiber = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
}

function reconcileChildren(wipFiber, elements) {
    let index = 0
    let prevSibling = null
    
    while (index < elements.length) {
        const element = elements[index]
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: wipFiber,
            dom: null,
        }
        if (index === 0) {
            wipFiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
}

在以上的reconcileChildren我们将协调旧的fiber和新的element.

function reconcileChildren(wipFiber, elements) {
    let index = 0
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child
    
    while(index < elements.length || oldFibler != null) {
        const element = elements[index];
        
        // TODO 将旧的fiber和新的element比较

    }
}

我们同时遍历旧的fiber(wipFiber.alternate)的children和我们想要协调的elements,

如果我们忽略遍历数组和链表所需的所有样板文件, 那么只剩下while中最重要的东西: oldFiberelement. element是我们要渲染到Dom的东西, oldFiber是我们上次渲染的东西.

function reconcileChildren(wipFiber, elements) {
    let index = 0
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child
    
    while(index < elements.length || oldFibler != null) {
        const element = elements[index];
        let newFiber = null;
        
        const sameType = oldFibler && element && element.type == oldFiber.type;
        
        if (sameType) {
            // TODO 更新node节点
        }
        
        if (element && !sameType) {
            // TODO 添加node节点
        }
        
        if (oldFibler && !sameType) {
            // TODO 删除oldFiber的node节点
        }
    }
}

我们需要比较它们, 以查看是否需要对Dom进行更改.

  • 如果oldFiber和新的element有相同的类型, 我们可以保留Dom节点, 并且用新的props更新它
  • 如果类型不同, 则意味着我们需要创建一个新的Dom节点
  • 如果类型不同且有oldFiber,则我们需要删除旧的节点

在这里React也使用keys, 这样可以更好的reconciliation, 例如, 它检测children何时更改元素数组中的位置.

// ...
    if (sameType) {
        newFiber = {
            type: oldFiber.type,
            props: element.props,
            dom: oldFiber.dom,
            parent: wipFiber,
            alternate: oldFiber,
            effectTag: "UPDATE"
        }
    }
// ...

当旧的fiberelement具有相同的类型, 我们创建一个新的fiber, 保留了旧的fiber的DOM节点和元素的props.

我们还向fiber添加了一个新的属性: effectTag, 我们稍后会在commit阶段使用这个属性.

// ...
    if (element && !sameType) {
        newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: "PLACEMENT"
        }
    }
// ...

然后, 另外一个case当元素需要一个新的DOM, 我们给一个新的fiber标记属性effectTag: "PLACEMENT".

// ...
    if (oldFibler && !sameType) {
        oldFiber.effectTag = "DELETION"
        deletions.push(oldFiber)
    }
// ...

对于我们需要删除node节点的case, 我们不需要一个新的fiber, 所以我们要给旧的fiber添加个属性effectTag: "DELETION".

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element],
        },
        alternate: currentRoot
    }
    
    // 新增开始
    deletions = [];
    // 新增结束
    
    nextUnitOfWork = wipRoot;
}

let currentRoot = null;
let deletions = [];

所以我们需要一个数组用来跟踪需要删除的节点nodes.

function commitRoot() {
    deletions.forEach(commitWork); // 新增

    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}

之后, 当我们提交变更到DOM时, 我们就会使用该fibers的数组.

接下来, 让我们更改commitWork函数去处理新的effectTags.

function commitWork(fiber) {
    if (!fiber) {
        return
    }
        
    const domParent = fiber.parent.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"){
        domParent.removeChild(fiber.dom)
    }    
    // 新增结束
    
    domParent.appendChild(fiber.dom);
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}
  1. 如果fiber有一个effectTag: "PLACEMENT", 我们和之前一样, 将 DOM 节点附加到父 fiber 的节点。
  2. 如果是DELETION, 我们将删除这个child
  3. 如果是UPDATE, 我们用props更新到现有的DOM节点中

我们将完成updateDom这个函数.

const isProperty = key => key !== "children"
const isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
 
    // 移除旧的properties
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })
        
    // 设置一个新的或者改变后的props
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })
}

我们将旧的fiberprops和新的fiber进行比较, 移除isGone(消失的)props, 并设置新的或者更改后的props.

我们需要一种新的propevent listeners(事件监听器),因此这个propon前缀开头,我们将要用不同的方式来处理它.

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) {
     
    // 移除旧的或者已经更改的事件处理程序
    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])
        })
 
    // 移除旧的properties
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })
        
    // 设置一个新的或者改变后的props
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })
    
    // 添加一个新的事件处理程序
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            const eventType = name.toLowerCase().substring(2)
            
            dom.addEventListener(eventType, nextProps[name])
        })
}
  1. 如果事件处理程序更改, 我们将从节点中移除
  2. 然后添加一个新的事件处理程序

Step7: Function Components (函数组件)

接下来, 我们需要添加的是对函数组件的支持.

首先我们换个例子。 我们将使用这个简单的函数组件,它返回一个 h1 元素。

请注意,如果我们将 jsx 转换为 js,它将是:

/** @jsx Didact.createElement */
function App(props) {
    return <h1>Hi {props.name}</h1>
}

const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

函数组件有两个不同:

  1. 函数组件的fiber没有Dom节点
  2. children运行函数, 而不是直接从props获取
function performUnitOfWork(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    
    // 提取到reconcileChildren
    const elements = fiber.props.children
    reconcileChildren(fiber, elements)
    
    ...
}

我们检查fiber的type是否是函数, 然后根据它转到不同的更新函数.

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

function updateFunctionComponent(fiber) {
    // TODO
}

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    
    // 提取到reconcileChildren
    const elements = fiber.props.children
    reconcileChildren(fiber, elements)
    
    ...
}

updateHostComponent我们和之前一样,

然后在updateFunctionComponent我们运行函数去获取children.

对于我们例子, fiber.type是一个App函数, 当运行它将返回一个h1元素. 然而, 一旦我们有了children, 这个reconciliation将以同样的方式工作, 我们不需要改变任何东西.

function updateFunctionComponent(fiber) {
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}

现在我们需要更改commitWork函数, 现在我们有了没有Dom节点的fiber, 我们需要做两件事.

首先, 我们需要找到DOM的父节点, 我们将沿着fiber树向上移动直到找到带有Dom的fiber.

function commitWork(fiber) {
    if (!fiber) {
        return
    }
    
    let domParentFiber = fiber.parent
    while(!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
    }
    
    const domParent = domParentFiber.dom
    
    ...
}

同样当移除一个节点时, 我们也需要一直向前, 直到找到具有DOM节点的子节点.

function commitWork(fiber) {
    if (!fiber) {
        return
    }
    
    let domParentFiber = fiber.parent
    while(!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
    }
    
    const domParent = domParentFiber.dom
    
    ...
    
    else if (fiber.effectTag === "DELETION") {
        commitDeletion(fiber, domParent)
    }
    
    ...
    
}

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

Step8: Hooks

最后一步, 现在我们有了function组件, 让我们为其添加状态.

让我们将示例更改为经典的计数器组件。 每次我们单击它时,它都会将状态递增 1。

请注意,我们正在使用 Didact.useState 来获取和更新计数器值。

const Didact = {
    createElement,
    render,
    useState
}

function Counter() {
    const [state, setState] = Didact.useState(1)
    
    return (
        <h1 onClick={() => setState(c => c + 1)}>
            Count: {state}
        </h1>
    )
}

const element = <Counter />

这是我们在例子中调用Counter的地方, 在该函数中我们调用了useState.

let wipFiber = null
let hookIndex = null

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

我们需要在初始化全局变量, 在调用函数组件之前, 以便我们可以在函数内部使用他们.

首先我们设置正在进行工作的fiber.

我们还在fiber中添加了一个hooks数组,以支持在同一个组件中多次调用useState, 我们会跟踪当前的hook索引.

function useState(initial) {
    const oldHook = wipFiber.alternate 
        && wipFiber.alternate.hooks 
        && wipFiber.alternate.hooks[hookIndex]

    const hook = {
        state: oldHook ? oldHook.state : initial
    }
    
    wipFiber.hooks.push(hook)
    hookIndex++
    return [hook.state]
}

当函数组件调用useState时, 我们检查是否有旧的hook, 我们使用hook的index索引去检查alternate的hook.

如果我们有一个旧的hook,我们将state从旧hook复制到新hook,如果我们没有,我们初始化state.

然后我们将新hook添加到fiber中, 将hook的index索引+1, 然后return这个state.

function useState(initial) {
    const oldHook = wipFiber.alternate 
        && wipFiber.alternate.hooks 
        && wipFiber.alternate.hooks[hookIndex]

    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: []
    }
    
    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]
}

useState还需要返回一个函数来更新这个state, 因此我们定义了一个setState来接收一个动作(对于Counter例子, 这个动作是递增1的函数).

我们将操作添加到hook的队列中.

然后我们做一些类似于在render函数中做的事情, 设置一个新的正在进行的工作根作为下一个工作单元,这样工作循环就可以开始一个新的render阶段.

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

但是我们还没有运行这个动作。

我们在下次render组件时这样做,我们从旧的hook队列中获取所有动作,然后将它们一个一个地应用到新的hook状态,所以当我们返回状态时它被更新了。

就这样。 我们构建了自己的 React 版本。


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);
    return (
        <h1 onClick={() => setState(c => c + 1)} style="user-select: none">
            Count: {state}
        </h1>
    );
}

const element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);