关闭

Fiber架构的简单理解与实现

时间: 2020-09-29阅读: 96标签: 架构

一、简介

本文主要理解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"));
站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/9789

关闭

网站架构优化性能

最初业务量不大,访问量小,此时的架构,应用程序、数据库、文件都部署在一台服务器上,有些甚至仅仅是租用主机空间, 将应用程序、数据库、文件各自部署在独立的服务器上

成为一个顶尖架构师

架构师的一个重要职责是,确保团队有共同的技术愿景,以帮助我们向客户交付他们想要的系统。在某些场景下,架构师只需要和一个团队一起工作,这时他们等同于技术引领者。在其他情况下,他们要对整个项目的技术愿景负责,通常需要协调多个团队之间,甚至是整个组织内的工作。

C/S和B/S两种架构区别与优缺点分析

C/S 架构是一种典型的两层架构,其全程是Client/Server,即客户端服务器端架构,其客户端包含一个或多个在用户的电脑上运行的程序,而服务器端有两种,一种是数据库服务器端,客户端通过数据库连接访问服务器端的数据

软件架构师之路

软件架构师是一名软件开发专家,他可以进行高层设计选择并决定技术标准,包括软件编码标准,工具和平台。软件架构可以被抽象的分为几个层次,不同的层次对技能的要求不同。对层次有很多不同的划分

网站的架构

有人说过,大型网站是根据业务需求逐步演化而来的,而不是设计出来的,下面就是一个大型网站的进化过程。在初始阶段,访问量并不大,所以应用程序、数据库、文件等所有的资源都在一台服务器上。

微内核架构在大型前端系统中的应用

架构和框架是独立的,本文仅仅是提出一种架构思路,而且这个架构也在百度的某款用户量很大的复杂前端产品中得以应用。基于这一套弹性架构并结合Vue/React的现代化开发理念,可以很好的完成高复杂度的前端系统。

微前端架构:如何由内而外取代单体架构

如何利用微前端技术实现单体应用程序的现代化改造?在本篇教程中,我们将探讨如何将前端从单体架构当中剥离出来,并快速完成微前端架构迁移。本文作者将结合个人项目实践经验为大家介绍心得。

大型网站系统架构的演化

一个成熟的大型网站(如淘宝、京东等)的系统架构并不是开始设计就具备完整的高性能、高可用、安全等特性,它总是随着用户量的增加,业务功能的扩展逐渐演变完善的,在这个过程中,开发模式、技术架构

架构/构建高可用的网站

目的为保证服务器硬件故障时依然可用,数据依然保持并能够访问,手段:数据和服务的冗余备份以及失效转移机制,有状态 :在服务端保留之前的请求信息,用以处理当前请求(例如:session)无状态 :没有特殊状态的服务

Vue实战_从目录结构谈可扩展项目架构设计

很多人都会用项目脚手架,也会跑hello world,然后再写写简单的todolist。但是再往下深入就难了。比如很多教程和老师都会说,大家要多问一个为什么。其实我想说多问你妹啊。我都不知道问为什么怎么多问?

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!