乐趣区

关于react.js:Fiber架构的简单理解与实现

一、简介

本文次要了解 fiber 的基本原理。为了可能更好的了解 fiber 原理,咱们会从零开始构建一个简略的 react,并在其中引入 fiber 以及 useState hookDOM-DIFF 等。

二、React 根底

① JSX 语法
JSX 是 JavaScript 的一种 语法扩大 ,它和模板语言很靠近,然而它充沛具备 JavaScript 的能力。所以 无奈像 JavaScript 一样被间接执行

// JSX
let age = 18;
let element = (
    <div>
        <h1>age: {age}</h1>
        <button onClick={() => alert(18)}> 批改 age 的值 </button>
    </div>
);

下面这段 JSX看起来十分像 HTML,然而它又不仅仅是 HTML,其 具备肯定的 JavaScript 能力 ,比方能够通过{} 去读取变量,以及进行一些运算等等。
下面这段 JSX 会被放到.js 文件中,如果咱们间接执行,那么会报错。而在 React 中之所以可能执行,是因为 借助了 Babel 的转换,最终会被解析成 JS 成一段 JS。下面这段代码会被解析为,如下所示

let age = 18;
let element = React.createElement("div", {}, 
    React.createElement("h1", {}, "age:", age),
    React.createElement("button", {onClick: function onClick() {return alert(18);
        }
    }, "批改 age 的值"));

所以 React 只有提供 createElement() 办法 就能够正确解析下面这段 JSX 了。

② 实现 createElement()办法
createElement() 办法次要负责接管 JSX 被 Babel 解析后传递过去的参数,而其次要作用就是 解析参数 并最终返回一个虚构 DOM 对象

function createElement(type, config, ...children) { // Babel 解析后传递的 config 必定是一个对象,不论 JSX 中有没有设置属性
    delete config.__source; // ——souce 属性和__self 属性太简单间接移除
    delete config.__self;
    const props = {...config};
    props.children = children.map((child) => { // 遍历子节点,次要解决一些纯文本
        return typeof child === "object" ? child : createTextElement(child)
    });
    return { // 结构一个虚构 DOM 节点
        type,
        props
    }
}

function createTextElement(text) { // 专门解决纯文本,将纯文本转化成一个 React 虚构 DOM 节点
    return {
        type: "TEXT",
        props: {
            nodeValue: text,
            children: []}
    }
}

通过 createElement()办法的解决,就能将 JSX 转换为对应的虚构 DOM 节点。

③ 实现 render()办法
render(vdom, container) 办法会接管一个 虚构 DOM 对象 和一个 实在的容器 DOM 作为虚构 DOM 渲染实现后的挂载点 。其次要作用就是 将虚构 DOM 渲染为实在 DOM 并挂载到容器下

function render(vdom, container) {
    let dom;
    let props = vdom.props;
    if (vdom.type === "TEXT") { // 是文本节点
        dom = document.createTextNode("");
    } else {dom = document.createElement(vdom.type);
    }
    updateProperties(dom, props); // 更新 DOM 属性
    vdom.props.children.forEach((child) => {// 遍历子节点,递归调用 render()办法将子节点渲染进去
        render(child, dom);
    });
    container.appendChild(dom); // 整个虚构 DOM 渲染实现后将其退出到容器中进行挂载到页面上
}

function updateProperties(dom, props) {for (let key in props) { // 遍历属性并更新到 DOM 节点上
        if (key !== "children") {if (key.slice(0, 2) === "on") {dom.addEventListener(key.slice(2).toLowerCase(), props[key], false);
            } else {dom[key] = props[key];
            }
        }
    }
}

三、Fiber 简介

Fiber 在英文中的意思为“纤维化 ”,即 细化 ,将工作进行细化。咱们能够把一个耗时长的工作分成很多小片, 每一个小片的运行工夫很短 ,尽管总工夫仍然很长,然而在每个小片执行完之后,都给其余工作一个执行的机会,这样惟一的线程就不会被独占,其余工作仍然有运行的机会。React 中的 Fiber 就把 整个更新过程碎片化
下面咱们的 render()办法在更新的时候是 进行递归操作的 ,如果在更新的过程中有大量的节点须要更新,就会呈现长时间占用 JS 主线程的状况,并且 整个递归过程是无奈被打断的,因为JS 线程和 GUI 线程是互斥的,所以可能会看到 UI 卡顿。

所以要实现 Fiber 架构,必须要解决两个问题:
a. 保障工作在浏览器闲暇的时候执行;
b. 将工作进行碎片化;

① requestIdleCallback
requestIdleCallback(callback)是实验性 API,能够传入一个回调函数,回调函数可能收到一个 deadline 对象,通过该对象的 timeRemaining() 办法能够获取到以后浏览器的闲暇工夫,如果有闲暇工夫,那么就执行一小段工作,如果 工夫有余 了,则 持续 requestIdleCallback,等到浏览器又有闲暇工夫的时候再接着执行。

② 链表构造
目前的虚构 DOM 是树结构,当工作被打断后,树结构无奈复原之前的工作继续执行,所以须要一种新的数据结构,即 链表,链表能够蕴含多个指针,能够轻易找到下一个节点,继而复原工作的执行。Fiber 采纳的链表中蕴含三个指针,parent 指向其父 Fiber 节点child 指向其子 Fiber 节点sibling 指向其兄弟 Fiber 节点。一个 Fiber 节点对应一个工作节点。

四、实现 Fiber

① 保障工作在浏览器闲暇的时候执行
定义一个工作循环回调函数,并通过 requestIdleCallback 注册,通过判断 以后 Fiber 是否存在 以及 浏览器是否有闲暇工夫 来判断是否须要打断工作执行。

// 新增如下代码
let currentFiber; // 保留当前工作的 Fiber 节点
let rootFiber; // 根 Fiber 节点,render()办法内会进行初始化
function commitRoot() {}

function workLoop(deadline) {while(currentFiber && deadline.timeRemaining() > 0) {currentFiber = getNextFiber(currentFiber);
    }
    if (!currentFiber && rootFiber) { // 如果没有工作了,则一次性提交整个根 fiber,渲染出界面
        commitRoot(); // 提交根 Fiber 节点开始渲染到界面}
    requestIdleCallback(workLoop); // 如果还有工作, 然而闲暇工夫不够了,则持续注册回调,等浏览器闲暇之后再复原执行
}
requestIdleCallback(workLoop);

function getNextFiber(fiber) {}

② 启动工作
目前咱们注册了 workLoop 回调,其执行条件是 currentFiber 必须存在,所以要启动工作,必须初始化 currentFiber,render() 办法中将不再递归遍历虚构 DOM,创立实在 DOM 并退出到容器中 ,咱们须要 在 render()中初始化一个 rootFiber并把 rootFiber 当作是 currentFiber

function render(vdom, container) {
    rootFiber = { // 渲染的时候创立一个根 fiber
        type: "root",
        props: {children: [vdom] // children 存储对应的虚构 DOM 节点
        },
        dom: container, // rootFiber 节点对应的 dom 为容器
        parent: null, // 指向父 Fiber
        child: null, // 指向子 Fiber
        sibling: null // 指向兄弟 Fiber
    };
    currentFiber = rootFiber; // 将根 fiber 节点标记为 currentFiber,开始一个一个节点进行渲染
}

Fiber 节点和虚构 DOM 节点十分类似,只是比虚构 DOM 多了一些属性

③ 实现获取下一个 Fiber 的逻辑
a. 首先 看一下 fiber 对应的 dom 有没有创立进去 ,如果没有则创立出对应的实在 DOM
b. 取出 fiber 的虚构子节点并遍历 ,遍历的过程中开始 层层构建 fiber 链表 将第一个子节点作为以后 fiber 的 child将第二个子节点作为第一个子节点的 sibling,以此类推,将第三个子节点作为第二个子节点的 sibling,最终构建出下一层 Fiber 链表。
c. 下一层 Fiber 链表构建实现后,就能够找到下一个 Fiber 节点,即下一个工作了,获取下一个工作的时候,首先会 把以后 Fiber 的 child 指向的 fiber 作为下一个工作 ,如果 以后 Fiber 没有 child,那么就 获取其 sibling,如果 以后 Fiber 也没有 sibling 了 ,那么 就找到其 parentFiber再通过 parentFiber 找到其 sibling 作为下一个工作

// 这里须要给 Fiber 节点创立出对应的实在 DOM,所以将创立 DOM 的办法抽取进去
function createDOM(vdom) {
    let dom;
    if (vdom.type === "TEXT") {dom = document.createTextNode("");
    } else {dom = document.createElement(vdom.type);
    }
    updateProperties(dom, vdom.props);
    return dom;
}
function getNextFiber(fiber) {if (!fiber.dom) { // 刚开渲染的时候,只有根 fiber 才有对应的 dom,即容器
        fiber.dom = createDOM(fiber);
    }
    const vchildren = (fiber.props && fiber.props.children) || []; // 取出以后 fiber 的子节点,即子虚构 DOM
    createFiberLinkedList(fiber, vchildren);
    if (fiber.child) { // 如果以后 fiber 存在 child 则取出其 child 指向的 Fiber 节点作为下一个工作
        return fiber.child;
    }
    // 当遍历到最底层的时候,就会呈现没有 child 的状况,此时就要开始找其兄弟节点了
    let currentFiber = fiber;
    while(currentFiber) {if (currentFiber.sibling) { // 如果存在兄弟节点
            return currentFiber.sibling; // 返回其兄弟节点
        }
        // 当遍历到最初一个子节点的时候,会呈现 sibling 兄弟节点不存在的状况
        currentFiber = currentFiber.parent; // 找到以后节点的父节点
    }
}

function createFiberLinkedList(fiber, vchildren) {
    let index = 0;
    let prevSibling = null; // 兄弟节点的上一个节点
    while(index < vchildren.length) { // 遍历子节点,进行层层构建
        let vchild = vchildren[index];
        let newFiber = { // 依据子节点结构出对应的 Fiber 节点
            type: vchild.type, // 以后节点类型 h1
            props: vchild.props, // 以后节点属性
            parent: fiber, // 指向父节点
            dom: null,
            child: null
        }
        if (index === 0) { // 如果是第一个子节点,则作为以后 fiber 的子 fiber
            fiber.child = newFiber;
        } else {prevSibling.sibling = newFiber; // 将第二个以及之后的子节点作为上一个兄弟节点的兄弟节点}
        prevSibling = newFiber; // 将以后创立的 fiber 保留为其兄弟节点的上一个节点
        index++;
    }
}

④ 提交整个根 Fiber 并渲染出界面
当初根 Fiber 曾经构建实现,接下来就是 将整个根 Fiber 进行提交 ,而后渲染出界面,提交的时候 须要将整个根 Fiber 重置为 null防止屡次提交 。提交的时候首先 从根 Fiber 的 child 开始 找到其父 Fiber 对应的 DOM,而后 将子 Fiber 对应的 DOM 退出到父 Fiber 对应的 DOM 中 ,接着 反复该过程递归将以后 Fiber 的 child 和 sibling 进行提交

function commitRoot() {commitWorker(rootFiber.child); // 将根 fiber 的子 fiber 传入
    rootFiber = null;
}
function commitWorker(fiber) {if (!fiber) {return;}
    const parentDOM = fiber.parent.dom; // 拿到以后 fiber 的父 fiber 对应的 dom
    parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    commitWorker(fiber.child); // 递归将以后 fiber 的子 fiber 提交
    commitWorker(fiber.sibling); // 递归将以后 fiber 的下一个兄弟 fiber 提交
}

五、反对函数组件

函数组件的 type 比拟非凡 是一个函数 ,无奈像 HTML 标签一样,间接通过 document.createElement() 办法创立出对应的 DOM 元素,所以在 getNextFiber()的时候,必须先判断是否是函数组件 ,如果是函数组件,那么 不创立 DOM,而是 执行函数组件获取到其返回的虚构节点作为函数组件对应 Fiber 的子节点

function doFunctionComponent(fiber) {const vchildren = [fiber.type(fiber.props)]; // 执行函数组件拿到对应的虚构 DOM 作为函数组件的虚构子节点
    createFiberLinkedList(fiber, vchildren);
}

function doNativeDOMComponent(fiber) {if (!fiber.dom) { // 刚开渲染的时候,只有根 fiber 才有对应的 dom,即容器
        fiber.dom = createDOM(fiber);
    }
    const vchildren = (fiber.props && fiber.props.children) || []; // 子节点就是虚构 DOM
    createFiberLinkedList(fiber, vchildren);
}

function getNextFiber(fiber) {
    const isFunctionComponent = typeof fiber.type === "function";
    if (isFunctionComponent) { // 处理函数组件
        doFunctionComponent(fiber);
    } else { // 解决 DOM
        doNativeDOMComponent(fiber);
    }
    ......
}

新增了函数组件对应的 Fiber 节点后,在提交的时候会存在一些问题:
a. 因为 函数组件对应的 Fiber 没有对应的实在 DOM,所以 无奈间接通过其父 Fiber 的 DOM 将其退出
b. 因为 函数组件对应的 Fiber 没有对应的实在 DOM,所以 其子 Fiber 节点也无奈通过其父 Fiber(函数组件对应的 Fiber)获取到 DOM 并退出子 Fiber 节点对应的 DOM

function commitWorker(fiber) {
    ......
    // const parentDOM = fiber.parent.dom; // 拿到以后 fiber 的父 fiber 对应的 dom
    let parentFiber = fiber.parent; // 获取到以后 Fiber 的父 Fiber
    while (!parentFiber.dom) { // 看看父 Fiber 有没有 DOM,如果没有阐明是函数组件对应的 Fiber,须要持续向上获取其父 Fiber 节点,直到其父 Fiber 有 DOM 为止
        parentFiber = parentFiber.parent;
    }
    const parentDOM = parentFiber.dom; // 获取到父 Fiber 对应的实在 DOM
    // parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    if (fiber.dom) { // 如果以后 fiber 存在 dom 则退出到父节点中, 函数组件没有对应 dom
        parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    }
    ......
}

六、实现 useState hook

useState 能够让咱们的函数组件领有本人的状态 。useState 的实现非常简单,首先在创立函数组件对应 Fiber 的时候, 会给其增加一个 hooks 数组 ,用于 存储以后函数组件内创立的 hook。每当执行 useState()函数的时候,外部会创立一个 hook 对象 ,该 hook 对象蕴含了以后的 state 和批改时传入的最新状态数组 queue,同时返回一个 setState() 函数,而后 将创立的 hook 放到函数组件对应 Fiber 的 hooks 数组内 ,当 setState(newState) 函数被调用的时候,传入的最新状态就会被放到 hook 的 queue 数组中 ,同时 更新 rootFiber 和 currentFiber,以便 让 workLoop 能够继续执行 ,此时整个组件将会被从新渲染,当 函数组件从新渲染 的时候,useState()也会从新执行 ,此时会 通过上一次渲染时函数组件对应的 Fiber 拿到上一次的 hook,继而从 hook 的 queue 中取出调用 setState 时候传入的最新状态数据,而后更新为以后 hook 的状态,从而使状态失去更新。

let functionComponentFiber; // 保留函数组件对应的 Fiber
let hookIndex; // 可能一个组件中应用到多个 hook,记录 hook 的索引
let oldRootFiber; // rootFiber 提交完后保留为旧的 rootFiber 即上一次渲染的 rootFiber
function doFunctionComponent(fiber) {
    functionComponentFiber = fiber; // 保留函数组件对应的 Fiber 节点
    hookIndex = 0; // 初始化 hook 索引为 0
    functionComponentFiber.hooks = []; // 并在函数组件对应的 fiber 上增加一个 hooks 数组,每次从新渲染都会从新初始化为空数组
    ......
}
function useState(init) {
    // 从上一次渲染实现的函数组件对应的 fiber 的 hooks 数组中依据索引获取到对应的 hook
    const oldHook = functionComponentFiber.base && functionComponentFiber.base.hooks && functionComponentFiber.base.hooks[hookIndex];
    const hook = { // 创立一个新的 hook,state 从上次中获取
        state: oldHook? oldHook.state: init,
        queue: []};
    const newStates = oldHook ? oldHook.queue : []; // 从上次 hook 中获取最新的状态
    newStates.forEach(newState => {hook.state = newState; // 更新 hook});
    const setState = (newState) => {hook.queue.push(newState); // 将新的状态放到 hook 的 queue 数组中
        rootFiber = {
            dom: oldRootFiber.dom,
            props: oldRootFiber.props,
            base: oldRootFiber
        }
        currentFiber = rootFiber;
    }
    functionComponentFiber.hooks.push(hook); // 将以后 hook 保留到函数组件对应的 fiber 节点的 hooks 数组中
    hookIndex++; // 可能会有多个状态
    return [hook.state, setState];
}

在 useState()中 须要获取上一次的 hook,所以 须要给 Fiber 节点减少一个 base 属性用于保留上一次的 Fiber 节点

function createFiberLinkedList(fiber, vchildren) {
    ......
    let oldFiber = fiber.base && fiber.base.child; // 取出第一个子节点对应的 oldFiber
    ......
    while(index < vchildren.length) { // 遍历子节点,进行层层构建
        ......
        let newFiber = { // 依据子节点结构出对应的 Fiber 节点
            type: vchild.type, // 以后节点类型 h1
            props: vchild.props, // 以后节点属性
            parent: fiber, // 指向父节点
            dom: null,
            child: null,
            base: oldFiber // 保留上一次的 Fiber
        }
        // 如果比拟的时候有多个子节点,须要更新 oldFiber
        if (oldFiber) {oldFiber = oldFiber.sibling;}
        ......
    }
}

此时还有一个问题,就是从新渲染的时候,必须将容器 DOM 中上一次渲染的 DOM 清空,否则会从新创立一份 DOM 追加到容器 DOM 中。

function commitRoot() {
    let rootDOM = rootFiber.dom; // 取出容器 DOM
    while(rootDOM.hasChildNodes()) {rootDOM.removeChild(rootDOM.firstChild); // 清空容器 DOM 中的子节点
    }
    ......
}

七、实现 DOM-DIFF

DOM-DIFF 次要就是 比拟两个 Fiber 节点的 type 是否统一 ,如果统一则进行 复用上一次渲染的 DOM 节点,而后更新 DOM 的属性即可。

function createFiberLinkedList(fiber, vchildren) {
    ......
    while(index < vchildren.length) { // 遍历子节点,进行层层构建
        let newFiber;
        const sameType = oldFiber && vchild && oldFiber.type === vchild.type; // 比拟新旧 Fiber 的 type 是否统一
        if (sameType) { // 示意是更新
            newFiber = {
                type: oldFiber.type,
                props: vchild.props, // 从最新的虚构节点中获取最新的属性
                dom: oldFiber.dom, // 应用上次创立的 dom 即可
                parent: fiber,
                child: null,
                base: oldFiber,
                effectTag: "UPDATE" // 标识为更新
            }
        }
        if (!sameType && vchild) { // 如果类型不同且存在虚构子节点则示意新增
            newFiber = {
                type: vchild.type, // 以后节点类型 如 h1
                props: vchild.props, // 以后节点属性
                parent: fiber, // 指向父节点
                dom: null,
                child: null,
                base: null,
                effectTag: "ADD" // 标识为新增
            }
        }
    }
}

因为进行了 DOM 的复用,所以在提交 DOM 的时候,就不必先将容器 DOM 的所有子节点清空了,如:

function commitRoot() {
    // 不须要先清空容器 DOM 的所有子节点了
    // let rootDOM = rootFiber.dom;
    // while(rootDOM.hasChildNodes()) {//     rootDOM.removeChild(rootDOM.firstChild);
    // }
    ......
}
function commitWorker(fiber) {if (fiber.effectTag === "ADD" && fiber.dom != null) { // 新增
        parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    } else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) { // 更新
        updateProperties(fiber.dom, fiber.base.props, fiber.props);
    }
}

复用之前的 DOM 时,须要更新 DOM 上的属性,所以须要批改 updateProperties()办法,传入新的和旧的属性,如:

function updateProperties(dom, oldProps, newProps) {for (let key in oldProps) {if (!newProps[key]) {dom.removeAttribute(key);
        }
    }
    for (let key in newProps) { // 遍历属性并更新到 DOM 节点上
        if (key !== "children") {if (key.slice(0, 2) === "on") {dom.addEventListener(key.slice(2).toLowerCase(), newProps[key], false);
            } else {if (oldProps[key] !== newProps[key]) { // 如果属性值发生变化则进行更新
                    dom[key] = newProps[key];
                }
            }
        }
    }
}

八、残缺代码如下

react.js 代码如下:

let currentFiber; // 保留当前工作的 Fiber 节点
let rootFiber; // 根 Fiber 节点,render()办法内会进行初始化
let functionComponentFiber; // 保留函数组件对应的 Fiber
let hookIndex; // 可能一个组件中应用到多个 hook,记录 hook 的索引
let oldRootFiber; // rootFiber 提交完后保留为旧的 rootFiber 即上一次渲染的 rootFiber
function createElement(type, config, ...children) { // Babel 解析后传递的 config 必定是一个对象,不论 JSX 中有没有设置属性
    delete config.__source; // ——souce 属性和__self 属性太简单间接移除
    delete config.__self;
    const props = {...config};
    props.children = children.map((child) => { // 遍历子节点,次要解决一些纯文本
        return typeof child === "object" ? child : createTextElement(child)
    });
    return { // 结构一个虚构 DOM 节点
        type,
        props
    }
}

function createTextElement(text) { // 专门解决纯文本,将纯文本转化成一个 React 虚构 DOM 节点
    return {
        type: "TEXT",
        props: {
            nodeValue: text,
            children: []}
    }
}

function render(vdom, container) {
    // let dom;
    // let props = vdom.props;
    // if (vdom.type === "TEXT") { // 是文本节点
    //     dom = document.createTextNode("");
    // } else {//     dom = document.createElement(vdom.type);
    // }
    // updateProperties(dom, props); // 更新 DOM 属性
    // vdom.props.children.forEach((child) => {// 遍历子节点,递归调用 render()办法将子节点渲染进去
    //     render(child, dom);
    // });
    // container.appendChild(dom); // 整个虚构 DOM 渲染实现后将其退出到容器中进行挂载到页面上
    rootFiber = { // 渲染的时候创立一个根 fiber
        type: "root",
        props: {children: [vdom] // children 存储对应的虚构 DOM 节点
        },
        dom: container, // rootFiber 节点对应的 dom 为容器
        parent: null, // 指向父 Fiber
        child: null, // 指向子 Fiber
        sibling: null // 指向兄弟 Fiber
    };
    currentFiber = rootFiber; // 将根 fiber 节点标记为 currentFiber,开始一个一个节点进行渲染
}

function createDOM(vdom) {
    let dom;
    if (vdom.type === "TEXT") {dom = document.createTextNode("");
    } else {dom = document.createElement(vdom.type);
    }
    updateProperties(dom, {}, vdom.props);
    return dom;
}

function updateProperties(dom, oldProps, newProps) {for (let key in oldProps) {if (!newProps[key]) {dom.removeAttribute(key);
        }
    }
    for (let key in newProps) { // 遍历属性并更新到 DOM 节点上
        if (key !== "children") {if (key.slice(0, 2) === "on") {dom.addEventListener(key.slice(2).toLowerCase(), newProps[key], false);
            } else {if (oldProps[key] !== newProps[key]) { // 如果属性值发生变化则进行更新
                    dom[key] = newProps[key];
                }
            }
        }
    }
}

function commitRoot() {
    // 不须要先清空容器 DOM 的所有子节点了
    // let rootDOM = rootFiber.dom;
    // while(rootDOM.hasChildNodes()) {//     rootDOM.removeChild(rootDOM.firstChild);
    // }
    commitWorker(rootFiber.child); // 将根 fiber 的子 fiber 传入
    oldRootFiber = rootFiber; // 提交后将最终的 rootFiber 保留为 oldRootFiber
    rootFiber = null;
}

function commitWorker(fiber) {if (!fiber) {return;}
    let parentFiber = fiber.parent;
    while (!parentFiber.dom) {parentFiber = parentFiber.parent;}
    const parentDOM = parentFiber.dom;
    // const parentDOM = fiber.parent.dom; // 拿到以后 fiber 的父 fiber 对应的 dom
    // if (fiber.dom) { // 如果以后 fiber 存在 dom 则退出到父节点中, 函数组件没有对应 dom
    //     parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    // }
    if (fiber.effectTag === "ADD" && fiber.dom != null) {parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    } else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) {updateProperties(fiber.dom, fiber.base.props, fiber.props);
    }
    // parentDOM.appendChild(fiber.dom); // 将以后 fiber 对应的 dom 退出父 dom 中
    commitWorker(fiber.child); // 递归将以后 fiber 的子 fiber 提交
    commitWorker(fiber.sibling); // 递归将以后 fiber 的下一个兄弟 fiber 提交
}

function workLoop(deadline) {while(currentFiber && deadline.timeRemaining() > 0) {currentFiber = getNextFiber(currentFiber);
    }
    if (!currentFiber && rootFiber) { // 如果没有工作了,则一次性提交整个根 fiber,渲染出界面
        commitRoot(); // 提交根 Fiber 节点开始渲染到界面}
    requestIdleCallback(workLoop); // 如果还有工作, 然而闲暇工夫不够了,则持续注册回调,等浏览器闲暇之后再复原执行
}
requestIdleCallback(workLoop);

function doNativeDOMComponent(fiber) {if (!fiber.dom) { // 刚开渲染的时候,只有根 fiber 才有对应的 dom,即容器
        fiber.dom = createDOM(fiber);
    }
    const vchildren = (fiber.props && fiber.props.children) || []; // 子节点就是虚构 DOM
    createFiberLinkedList(fiber, vchildren);
}

function doFunctionComponent(fiber) {
    functionComponentFiber = fiber;
    hookIndex = 0;
    functionComponentFiber.hooks = []; // 并在函数组件对应的 fiber 上增加一个 hooks 数组,每次从新渲染都会从新初始化为空数组
    const vchildren = [fiber.type(fiber.props)]; // 执行函数组件拿到对应的虚构 DOM 作为函数组件的虚构子节点
    createFiberLinkedList(fiber, vchildren);
}

function getNextFiber(fiber) {
    const isFunctionComponent = typeof fiber.type === "function";
    if (isFunctionComponent) {doFunctionComponent(fiber);
    } else {doNativeDOMComponent(fiber);
    }

    if (fiber.child) { // 如果以后 fiber 存在 child 则取出其 child 指向的 Fiber 节点作为下一个工作
        return fiber.child;
    }
    // 当遍历到最底层的时候,就会呈现没有 child 的状况,此时就要开始找其兄弟节点了
    let currentFiber = fiber;
    while(currentFiber) {if (currentFiber.sibling) { // 如果存在兄弟节点
            return currentFiber.sibling; // 返回其兄弟节点
        }
        // 当遍历到最初一个子节点的时候,会呈现 sibling 兄弟节点不存在的状况
        currentFiber = currentFiber.parent; // 找到以后节点的父节点
    }
}

function createFiberLinkedList(fiber, vchildren) {
    let index = 0;
    let oldFiber = fiber.base && fiber.base.child; // 取出第一个子节点对应的 oldFiber
    let prevSibling = null; // 兄弟节点的上一个节点
    while(index < vchildren.length) { // 遍历子节点,进行层层构建
        let vchild = vchildren[index];
        let newFiber;
        const sameType = oldFiber && vchild && oldFiber.type === vchild.type; // 比拟新旧 Fiber 的 type 是否统一
        if (sameType) { // 示意是更新
            newFiber = {
                type: oldFiber.type,
                props: vchild.props, // 从最新的虚构节点中获取最新的属性
                dom: oldFiber.dom, // 应用上次创立的 dom 即可
                parent: fiber,
                child: null,
                base: oldFiber,
                effectTag: "UPDATE" // 标识为更新
            }
        }
        if (!sameType && vchild) { // 如果类型不同且存在虚构子节点则示意新增
            newFiber = {
                type: vchild.type, // 以后节点类型 如 h1
                props: vchild.props, // 以后节点属性
                parent: fiber, // 指向父节点
                dom: null,
                child: null,
                base: null,
                effectTag: "ADD" // 标识为新增
            }
        }
        
        // 如果比拟的时候有多个子节点,须要更新 oldFiber
        if (oldFiber) {oldFiber = oldFiber.sibling;}
        if (index === 0) { // 如果是第一个子节点,则作为以后 fiber 的子 fiber
            fiber.child = newFiber;
        } else {prevSibling.sibling = newFiber; // 将第二个以及之后的子节点作为上一个兄弟节点的兄弟节点}
        prevSibling = newFiber; // 将以后创立的 fiber 保留为其兄弟节点的上一个节点
        index++;
    }
}

function useState(init) {
    // 从上一次渲染实现的函数组件对应的 fiber 的 hooks 数组中依据索引获取到对应的 hook
    const oldHook = functionComponentFiber.base && functionComponentFiber.base.hooks && functionComponentFiber.base.hooks[hookIndex];
    const hook = { // 创立一个新的 hook,state 从上次中获取
        state: oldHook? oldHook.state: init,
        queue: []};
    const newStates = oldHook ? oldHook.queue : []; // 从上次 hook 中获取最新的状态
    newStates.forEach(newState => {hook.state = newState; // 更新 hook});
    const setState = (newState) => {hook.queue.push(newState); // 将新的状态放到 hook 的 queue 数组中
        rootFiber = {
            dom: oldRootFiber.dom,
            props: oldRootFiber.props,
            base: oldRootFiber
        }
        currentFiber = rootFiber;
    }
    functionComponentFiber.hooks.push(hook); // 将以后 hook 保留到函数组件对应的 fiber 节点的 hooks 数组中
    hookIndex++; // 可能会有多个状态
    return [hook.state, setState];
}
export default {
    createElement,
    render,
    useState
}

利用代码如下:

import React from "./react/index";
let ReactDOM = React;
function App(props) {const [minAge, setMinAge] = React.useState(1);
    const [maxAge, setMaxAge] = React.useState(100);
    return (
        <div>
            <h1>minAge: {minAge}</h1>
            <button onClick={() => setMinAge(minAge + 1)}> 加 </button>
            <h1>maxAge: {maxAge}</h1>
            <button onClick={() => setMaxAge(maxAge - 1)}> 减 </button>
        </div>
    );
}
ReactDOM.render(<App/>, document.getElementById("root"));
退出移动版