05【React再造之旅】从零实现一个React(下)

写在前面

本文继续上一节文章,来介绍下剩余的知识,如下:

  • Render和Commit阶段
  • 调和过程
  • 函数组件
  • Hooks

我们接下来的部分就依次介绍下这些知识点。

代码获取

本文涉及到的代码全部上传至码云,需要的同学请在下面地址中获取:

https://gitee.com/XuQianWen/zerocreate_react

Render和Commit阶段

我们在之前完成的代码中其实有一个问题,在workLook()中每次循环调用performUnitOfWork()方法时,我们都会往fiber父节点中添加一个新的dom元素,就像下面的代码:

之前我们也介绍过,自从react引入fiber之后,我们的渲染任务是会被分割成若干个小的任务单元的,每次这些小的任务单元完成后如果有优先级高的任务,浏览器就会打断这些任务单元的执行,而是去执行优先级高的任务,等执行完之后再回来继续从头开始执行这些小的任务单元,所以在浏览器打断的这个过程中,我们在前端页面有时候会看到页面渲染空白、不完整等这样的情况,所以我们接下来优化一下我们之前的代码。

我们先在performUnitOfWork()方法中删除添加新dom元素到fiber上的这段代码,如下:

然后在render()方法中,我们给root fiber去一个别名,叫它wipRoot,然后将其赋值给nextUnitOfWork,代码如下:

let nextUnitOfWork = null;
let wipRoot = null;

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

接下来,如果"下一个任务单元"没有任何指向的时候就说明我们完成了所有的工作,所以在此时我们将整个fiber树提交给DOM,这就是渲染和提交阶段的一个简单介绍,代码如下:

let nextUnitOfWork = null;
let wipRoot = null;

function commitRoot() {

}

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        }
    };
    nextUnitOfWork = wipRoot;
}
function workLoop(deadline) {
    let shouldYield = false;
    while(nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }

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

    requestIdleCallback(workLoop);
}

接下来完善一下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);
}

这样一来我们就将开始时所描述的那种情形得到了优化,我们最终会将一整棵fiber树递归到添加到dom中,所以这就避免了渲染中被浏览器打断从而出现页面不完整的问题。

调和过程

到目前为止的话我们仅仅实现了DOM元素的渲染和添加这些过程,如果我们的元素要删除、更新的话应该怎么做呢,这就是接下来要介绍的,也就是调和过程。在此过程中我们需要对比两棵fiber树:render()方法接收的新fiber树和我们最后提交到DOM的旧fiber树。

所以在开始之前我们需要一个引用,用来存放最后一个提交的fiber树,而且还要为每一个fiber元素添加alternate属性,用来连接到旧的fiber上,代码如下:

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

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

接下来我们提取performUnitOfWork()方法中创建新fiber的代码片段到一个新的函数中,这个新的函数叫做reconcileChildren()方法,最后这两个方法中的代码如下所示:

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

    // if(fiber.parent) {
    //     fiber.parent.dom.appendChild(fiber.dom);
    // }

    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: fiber,
            dom: null,
        }

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

        prevSibling = newFiber;
        index++;
    }
}

在reconcileChildren()方法中实现旧fiber和新元素的调和过程,不过在此处我们目前的reconcileChildren()方法是不能直接运行的,接下来还要优化。

我们同时遍历旧fiber的children(wipFiber.alternate)和要协调的元素数组。在此过程中我们忽略掉一些其他的信息之后,其实仅仅关心oldFiber和element。element是我们要添加到DOM的元素,oldFiber是我们最后一次提交渲染过的fiber,通过比较我们可以了解到是否需要对DOM进行更改,所以reconcileChildren()方法中的代码可以暂时优化成这样:

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和新元素

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

以上的代码中并没有添加对比的过程,所以接下来我们按如下规则添加对比的代码片段:

  • 如果旧fiber和新元素有相同的类型,我们只需要用新的属性去更新这个dom即可;
  • 如果类型不同,就说明它是一个新元素,所以我们要增加这个新的dom节点;
  • 如果类型不同并且它是一个旧fiber,我们需要删除这个旧的dom节点。

按照上述的规则,我们来编写代码,如下:

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) {
            //更新dom
        }
        if(element && !sameType) {
            //添加dom
        }
        if(oldFiber && !sameType) {
            //删除dom
        }

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

在上述过程中,react中同时也用了key,以便有一个更好地调和过程,但在本文中为了简单,我们不做介绍。

对于要更新的dom节点,我们可以这样来做:通过旧的fiber创建一个新的fiber,它的props属性从新的element元素赋值,并且为这个新的fiber添加一个effectTag属性,我们在后期提交阶段来使用,代码如下:

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

对于要添加dom的情况,更上述类似,直接看代码:

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

对于要删除的dom节点,我们没有必要再创建一个新的fiber,我们只需要给原来的fiber添加一个effectTag标记即可,但是当我们将fiber树提交给dom的时候它是从正在工作的root fiber中进行的,root fiber并没有旧的fiber,所以我们需要一个数组去存这些要删除的dom节点,所以还需要定义一个数组,代码如下:

        if(oldFiber && !sameType) {
            //删除dom
            oldFiber.effectTag = 'DELETION';
            deletions.push(oldFiber);
        }
let deletions = null;

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

然后,我们将变化后的fiber提交至dom时,我们也要用这个数组中的fiber,所以还需要优化一下commitRoot()方法,代码如下:

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

接下来我们在commitWork()方法中处理一下我们新增的effectTag标签。如果effectTag标签标记的是增加dom,我们的操作还是和原来一样,将这个dom节点添加至父fiber中,代码如下:

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

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }

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

如果标记为删除dom,我们就将这个dom从它的父fiber中删除,代码如下:

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 === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }

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

如果标记为更新dom,我们就要用目前的元素属性去更新现有的dom节点,所以我们在此处直接调用一个更新节点的函数,这个函数我们稍后定义,代码如下:

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 === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

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

接下来定义updateDom()这个方法,要实现这个方法,我们其实是在做新旧fiber的对比操作,进而去删除没用的属性或者更新、设置改变后的属性。所以我们在定义updateDom()方法之前还要定以几个额外的方法来辅助我们进行判断,进而在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) {
    //删除旧属性
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //设置新属性或者改变属性
    Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
        dom[name] = nextProps[name];
    });
}

上述代码在处理属性的时候,我们其实还遗漏了节点上挂载的事件,所以我们要继续优化一下前面几个辅助判断的方法,对前缀是"on"的属性我们要做特殊处理,代码如下:

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

进而,我们还需要在updateDom()方法中做一下优化,最后代码如下所示:

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

    //删除旧属性
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //设置新属性或者改变属性
    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]);
    })
}

到此为止,我们就完成了调和过程的介绍,其实调和就是在做dom节点的更新和删除等操作,对应到我们的代码中的话,它其实就是对新旧fiber进行的操作,我们现在保存代码在前端查看时,可以看到原来的输出,代码也并没有任何报错。我们改变一下之前的JSX编写的组件,为其添加一个href属性,我们在前端页面可以看到它是相应的进行了更新,并且这个超链接也是工作正常的,如下:

/** @jsx XbcbLib.createElement */
const element = (
    <div id='xbcb'>
        <a href="http://www.xbcb.top">X北辰北</a>
        <br />
    </div>
);

到目前为止,所有的index.js文件代码如下,大家可以参考对比一下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map( value => {
                //typeof value == "object" ? value : createTextElement(value)
                if(typeof value == 'object') {
                    return value;
                }else {
                    return createTextElement(value)
                }
            })
        }
    }
}
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);

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

    return dom;

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

    // container.appendChild(dom);
}

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

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

    //删除旧属性
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //设置新属性或者改变属性
    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]);
    })
}

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

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 === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

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

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

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) {
    if(!fiber.dom) {
        fiber.dom = createDom(fiber);
    }

    // if(fiber.parent) {
    //     fiber.parent.dom.appendChild(fiber.dom);
    // }

    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 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) {
            //更新dom
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: 'UPDATE',
            }
        }
        if(element && !sameType) {
            //添加dom
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: 'PLACEMENT',
            }
        }
        if(oldFiber && !sameType) {
            //删除dom
            oldFiber.effectTag = 'DELETION';
            deletions.push(oldFiber);
        }

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

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

        prevSibling = newFiber;
        index++;
    }
}

const XbcbLib = {
    createElement,
    render
};

/** @jsx XbcbLib.createElement */
const element = (
    <div id='xbcb'>
        <a href="http://www.xbcb.top">X北辰北</a>
        <br />
    </div>
);

const container = document.getElementById('root');
XbcbLib.render(element, container);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

函数组件

介绍完以上部分之后,我们这部分内容介绍一下函数组件。因为目前我们添加的JSX语法组件都是正常的HTML标记,并不是自定义的组件,所以接下来我们继续优化我们的项目,使其能够支持函数组件。

我们先改写原来编写的element组件代码,让它变成一个函数组件,如下:

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

const element = <App name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

我们这时候直接保存代码的时候,前端页面会报错,因为目前代码中并不支持函数组件渲染。但是我们知道,如果此时将这个函数组件的JSX向JS转换的时候,它应该会做以下的转变:

/** @jsx XbcbLib.createElement */
function App(props) {
    return XbcbLib.createElement(
        'h1',
        null,
        'Hi',
        props.name
    )
}

const element = XbcbLib.createElement(App, {
    name: 'X北辰北',
});

在开始之前我们要知道两点:

  • 函数组件的fiber没有DOM节点
  • children属性并不是直接来自于props,而是来自于函数的调用

所以我们要对之前的performUnitOfWork()方法做一个优化,在它里面对fiber的类型做一下判断,然后再决定使用不同的更新方法来进行fiber的更新调和操作。如果是函数组件,我们使用函数组件的更新方法,如果不是函数组件,我们使用原来的更新方法,代码如下:

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

function updateFunctionComponent(fiber) {

}

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

    const elements = fiber.props.children;
    reconcileChildren(fiber, elements);
}

在函数组件的更新方法中,我们主要是去获取children属性。比如在我们的示例代码中,它的fiber.type就是一个App函数,所以我们调用它之后会返回一个h1的dom元素。如果我们拿到了children属性,那接下来的过程就是调和了,调和的过程跟我们之前的代码是没有任何差别的,代码如下:

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

因为有了没有dom节点的fiber树,所以我们要更改一下commitWork()方法。在这里我们主要改两部分,第一部分就是我们首先要找到dom节点的父节点,我们需要沿着fiber树一直往上找,直到找到带有dom节点的fiber为止,所以要修改的第一部分代码如下:

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 === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

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

第二部分就是节点删除部分,我们需要找到具有dom节点的子节点为止,代码如下:

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 === 'DELETION') {
        //domParent.removeChild(fiber.dom);
        commitDeletion(fiber, domParent);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

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

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

至此为止我们就完成了函数组件的支持,我们定义一个组件,然后将其渲染到页面上,如下:

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
    return <h1>Hi, {props.name}</h1>;
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

Hooks

我们自己的react目前已经支持函数组件,但是还缺少state的支持,所以接下来我们看看如何添加state的支持。在此处我们使用hooks来维护函数组件中的state。所以我们先改写一下示例代码,就用最经典的计数器例子,每次点击的时候它的次数会增加1,代码如下:

const XbcbLib = {
    createElement,
    render,
    useState,
};

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
    const [state, setState] = XbcbLib.useState(1);
    return (
        <h1 onClick={() => setState(c => c + 1)}>
            H1, {props.name}。你点击的次数为{state}。
        </h1>
    )
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

然后定义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);
}

function useState(initial) {

}

上述代码中我们将work设置为进行中的fiber,同时还向fiber增加了一个hooks数组,以便于支持在同一组件中多次调用useState()。同时我们跟踪当前的hook索引。

当函数组件调用useState()时我们检查它是否有旧的hook。用hook索引去检查fiber的alternate属性。如果有旧的hook,我们将state从旧的hook复制到新的hook,否则我们将初始化state。然后将新的hook添加到fiber,并且将hook索引增加1之后返回state,代码如下:

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()中还需要返回一个函数去更新state,所以我们要在此方法里面定义一个setState()函数来接收一个操作,我们将这个操作放到一个队列中,然后就执行与渲染过程中类似的操作,将新的进行中的工作单元设置为下一个工作单元,以便可以循环进行新的渲染阶段,代码如下:

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

但是目前我们还不能运行上述代码中的action操作,我们是在下一次渲染组件时运行这些的,首先是从旧的hook队列中拿到所有的action,然后将它们逐一应用到新的hook中的state上,所以我们会在它更新完成后返回state,代码如下:

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

至此为止,我们就完成了自己的react,点击效果如下:

结尾

这篇文章仅仅是帮助我们了解react的工作流程,同时也是为我们后期阅读react源码做了铺垫,所以在我们的代码里使用了和react中同样名称的变量和方法。但是在我们的代码中没有包括很多React的功能和优化。例如,我们可以看看react中有些操作它是怎么做的:

  • 在XbcbLib中,我们在渲染阶段遍历整棵树。相反,React遵循一些提示和试探法,以跳过没有任何更改的整个子树。
  • 我们还在提交阶段遍历整棵树。React仅保留有影响的fiber并仅访问那些fiber的链表。
  • 每次我们建立一个新的进行中的工作树时,都会为每个fiber创建新的对象。React回收了先前树中的fiber。
  • 当XbcbLib在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根开始重新进行。React使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。
  • 类似的还有很多…

你自己也可以添加如下的功能:

  • 使用对象作为样式属性
  • 展平子数组
  • useEffect hook
  • 密钥对帐

猜你喜欢

转载自blog.csdn.net/qq_35117024/article/details/107364905
今日推荐