React Fiber架构 (三)--具体实现

240 阅读3分钟

如何自己实现一个React Fiber

准备工作

目录结构:

屏幕截图 2022-04-04 084104.png

(1)在src当前目录创建constants.js

//constants.js
//表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');

//React 应用需要一个根 Fiber
export const TAG_ROOT = Symbol.for('TAG_ROOT');

//原生的节点 span div p 函数组件 类组件
export const TAG_HOST = Symbol.for('TAG_HOST');

//这是文本节点
export const TAG_TEXT = Symbol.for('TAG_TEXT');

//这是插入节点
export const PLACEMENT = Symbol.for('PLACEMENT');

//更新节点
export const UPDATE = Symbol.for('UPDATE');

//删除节点
export const DELETION = Symbol.for('DELETION');

Symbol.for 表示创建一个唯一的 Symbol值。.for会搜索全局是否有当前key的值,有则复用。类似单例模式。 同时后续用到type:UPDATE唯一,方便做判断。

(2)在index.html里

<body>
    <div id="root"></div>
</body>

(3)index.js

//index.js
import React from './react';
import ReactDOM from './react-dom';

let style = { border: '3px solid red', margin: '5px' };

//jsx 通过babel编译成js
let element = (     //还是拿上一篇的结构举例
    <div id="A1" style={style}>
        A1
        <div id='B1' style={style}>
            B1
            <div id="c1" style={style}>
                C1
            </div>
            <div id="c2" style={style}>
                C2
            </div>
        </div>
        <div id="B2" style={style}>B2</div>
    </div>
)

ReactDOM.render(
    element,  //节点
    document.getElementById('root')  //根节点
)

(4)react.js文件,主要用来处理上面element 被babel 编译成 React.createElement的组合对象的,重写createElement,生成虚拟Dom

上述element在传进render方法前,已被处理成,这种虚拟Dom结构。

屏幕截图 2022-04-04 084104.png

//react.js
import { ELEMENT_TEXT } from "./constants";

function createElement(type, config, ...children) {
    //重写方法 生成虚拟dom,也就是描述dom的对象。
    delete config.__self;
    delete config.__source;
    return {
        type,
        props: {
            ...config,
            children: children.map(child => {
                return typeof child === 'object' ? child : {
                    type: ELEMENT_TEXT,
                    props: { text: child, children: [] }
                }
            })
        }
    }
}

const React = {
    createElement
}
export default React;

(5)react-dom.js

//react-dom.js
import {TAG_ROOT} from './constants';
import {scheduleRoot} from './schedule'
/**
 * render 是要把一个元素渲染到一个容器内部
 **/
function render(element, container) {  // container = rootDOM节点
    let rootFiber = {
        tag: TAG_ROOT,
        stateNode: container, //一般情况下,如果这个元素是一个原生节点的话,stateNode指向真实dom元素。
        //props.children 是一个数组,里面放的是React元素 虚拟DOM
        props: { children: [element] } // 这个fiber的属性对象 children 属性,里面放的是要渲染的元素。
    }
    scheduleRoot(rootFiber);
}

const ReactDOM = {
    render
}

export default ReactDOM;

这里在创建一个rootFiber根节点。这里实际定义了一个父级的rootFiber也就是头Fiber 节点,里面props.children 装了 element也就是虚拟Dom 等待后续处理,后续所有虚拟dom节点都要转换成Fiber。

(6)深入包含scheduleRoot的schedule文件:

//schedule.js
  ...
    let nextUnitOfWork = null;      //下一个工作单元
    let workInProgressRoot = null;   //保存根节点
    export function scheduleRoot(rootFiber) {
        workInProgressRoot = rootFiber;
        nextUnitOfWork = rootFiber;
    }
    ...
//schedule.js
...
//循环执行工作
function workLoop(deadline) {
    let shouldYield = false;  //是否要让出时间片
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);  //执行完一个任务之后
        shouldYield = deadline.timeRemaining() < 1; //没有时间的话,就要让出控制权。
        //这里的意思就是 没有时间的话 退出 while 走下一帧 
    }
    //直到nextUnitOfWork 执行完毕
    if (!nextUnitOfWork) {
        console.log('render阶段结束')
        //如果时间片到期后任务没有完成就需要用浏览器再次调度
    }
    //每一帧都需要执行
    requestIdleCallback(workLoop, { timeout: 500 })
}


//告诉浏览器 我现在有任务 ,请你在空闲的时候,执行。
requestIdleCallback(workLoop, { timeout: 500 })
...

这段代码主要是利用requestIdleCallback在每一帧执行之后空余时间执行performUnitOfWork。

(7) 深入performUnitOfWork方法

// performUnitOfWork
  function performUnitOfWork(currentFiber) {
    beginWork(currentFiber);
   ...
}

function beginWork(currentFiber) {
    if (currentFiber.tag == TAG_ROOT) {
    //如果是根节点 就执行updateHostRoot 
        updateHostRoot(currentFiber)
    }
}

//准备从根节点开始处理虚拟dom,虚dom-> Fiber

function updateHostRoot(currentFiber) {
    let newChildren = currentFiber.props.children;
    //2.创建子fiber ,第一次没有老元素进行比较,直接创建
    reconcileChildren(currentFiber, newChildren);
}

(8)reconcileChildren方法

function reconcileChildren(currentFiber, newChildren) {
    let newChildIndex = 0; //新子节点索引
    let prevSibling; //上一个新的子fiber
    //遍历我们子虚拟dom元素,为每一个虚拟dom 创建子Fiber
    while (newChildIndex < newChildren.length) {
        let tag;
        let newChild = newChildren[newChildIndex]; //取出虚拟DOM节点
        if (newChild.type == ELEMENT_TEXT) {
            tag = TAG_TEXT;
        } else if (typeof newChild.type == 'string') {
            tag = TAG_HOST //如果type是字符串,那么这是一个原生dom节点
        }
        let newFiber = {
            tag,                   //TAG_HOST
            type: newChild.type,   //div
            props: newChild.props, // {id='A1' style ={style}  }
            stateNode: null,       //div 还没有创建DOM元素
            return: currentFiber,  //父Fiber returnFiber
            effectTag: PLACEMENT,  //副作用标识  render我们会收集副作用  增加 删除 更新
            nextEffect: null       //effect list也是一个单链表
            // effect list 顺序和完成顺序是一样的  但是节点只放那些出钱的人的fiber 节点
        }
        //最小的儿子是没有弟弟
        if (newFiber) {
            if (newChildIndex == 0) {
                //如果当前索引为0 说明这是太子
                currentFiber.child = newFiber;
            } else {
                prevSibling.sibling = newFiber;
                //让太子的sibling 弟弟指向二皇子
            }
            prevSibling = newFiber;
        }
        newChildIndex++;
    }
}

这里主要是在遍历子虚拟dom节点,把父中子虚拟Dom节点获取出来再通过 child 和sibling进行连接。

| (child)

子 --(sibling)--弟弟--(sibling)--弟弟

(也就是上图的 子元素 和弟弟的虚拟Dom转化成fiber结构)

未完待续~~