如何自己实现一个React Fiber
准备工作
目录结构:
(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结构。
//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结构)
未完待续~~