接上一篇 React Fiber原理
上一篇介绍了React Fiber的基本原理,本文会带你用React fiber实现react的首次渲染逻辑
新建项目
首先用create-react-app来创建一个react应用
- 删除src中的无用文件,保留
index.js
文件 - 检查
package
文件,react版本为17
及以上的话,需要将react版本降为16
- react17采用了
runtime
模式,在文件开头不用引入React
,会默认用项目中的react进行渲染,所以需要把版本降为16,用我们自己实现的react进行渲染 - 先用原生的react和react-dom来进行渲染
import React from 'react';
import ReactDOM from 'react-dom';
let style = { border: '3px solid red', margin: '5px' };
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'));
虚拟Dom
虚拟Dom介绍
- 项目打包时,jsx语法是通过babel来进行转换的,会将jsx语法替换成React.createElement()的方式,通过这个方法来创建虚拟dom
- 通过 babel官网,我们可以看到转换后的代码
- React.createElement()参数,第一个是节点的类型,第二个是节点的属性,后面的都是节点的children,会按照dom树结构,嵌套进行创建
实现createElement
方法
-
新建
react
文件夹,里面放我们自己写的react -
constants.js 放一些需要定义的常量
// type属性 表示这是一个文本元素 文本节点的fiber {tag:TAG_TEXT,type:ELEMENT_TEXT}
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
// tag属性
//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 TAG_CLASS = Symbol.for('TAG_CLASS');
//函数组件
export const TAG_FUNCTION_COMPONENT = Symbol.for('TAG_FUNCTION_COMPONENT');
// effectTag属性
//插入节点
export const PLACEMENT = Symbol.for('PLACEMENT');
//更新节点
export const UPDATE = Symbol.for('UPDATE');
//删除节点
export const DELETION = Symbol.for('DELETION');
- react.js对应原生的react库,导出
createElement
方法,主要负责创建虚拟dom
// react.js
import { ELEMENT_TEXT } from './constants';
/**
* 创建虚拟Dom的方法
* @param {*} type 元素类型 div span p
* @param {*} config 配置对象 元素属性
* @param {...any} children 所有的儿子,这里会做成一个数组
*/
function createElement(type, config, ...children) {
delete config.__self;
delete config.__source;
// 做了兼容处理,如果是文本类型,返回元素对象
const newChildren = children.map(child => {
// child是一个React.createElement返回的React元素
if (typeof child === 'object') return child;
// child是字符串
return {
type: ELEMENT_TEXT,
props: { text: child, children: [] },
};
});
return {
type,
props: {
...(config || {}), // 有可能为null
children: newChildren,
},
};
}
const React = {
createElement,
};
export default React;
- react.js对应原生的react-dom库,导出
render
方法 主要负责页面渲染
import { TAG_ROOT } from './constants';
import { scheduleRoot } from './scheduler';
/**
* 把一个元素渲染到容器内部
* @param {*} element 渲染的元素 虚拟Dom
* @param {*} container 挂载的节点
*/
function render(element, container) {
let rootFiber = {
tag: TAG_ROOT, // 根fiber
stateNode: container, // 真实dom节点
//props.children是一个数组,里面放的是React元素 虚拟DOM 后面会根据每个React元素创建 对应的Fiber
props: { children: [element] },
};
// 开始调度
scheduleRoot(rootFiber);
}
const ReactDOM = {
render,
};
export default ReactDOM;
实现调度核心逻辑
- scheduler.js核心调度逻辑,导出
scheduleRoot
方法,供render
方法调用,传入根fiber执行调度逻辑
import { TAG_HOST, TAG_ROOT, TAG_TEXT, PLACEMENT } from './constants';
import { reconcileChildren } from './reconcileChildren';
import { setProps } from './utils';
let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; //下一个工作单元
// 暴露给外部
export function scheduleRoot(rootFiber) {
workInProgressRoot = rootFiber;
nextUnitOfWork = workInProgressRoot;
}
// 1.工作循环,每帧结束都会执行
function workLoop(deadline) {
let shouldYield = false; // 是否暂停
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('调和阶段结束');
commitRoot();
}
//不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop,检查有没有要执行的任务
requestIdleCallback(workLoop, { timeout: 500 });
}
// 2.执行每个fiber的工作
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);
// 先遍历大儿子
if (currentFiber.child) {
return currentFiber.child;
}
while (currentFiber) {
// 没有子节点的先完成
completeUnitOfWork(currentFiber);
// 看看有没有弟弟,有的话遍历弟弟
if (currentFiber.sibling) {
return currentFiber.sibling;
}
// 子节点都完成了,让父亲完成,父亲是root节点,return是null, 跳出while循环
currentFiber = currentFiber.return;
}
}
// 3.开始工作
// 两个功能:1.创建真实DOM 2.创建子fiber
function beginWork(currentFiber) {
switch (currentFiber.tag) {
case TAG_ROOT:
updateHostRoot(currentFiber);
break;
case TAG_TEXT:
updateHostText(currentFiber);
break;
case TAG_HOST:
updateHost(currentFiber);
break;
default:
break;
}
}
// 根fiber, stateNode是外部传入的
function updateHostRoot(currentFiber) {
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
// 文本类型
function updateHostText(currentFiber) {
// 没有创建真实dom,进行创建
// 文本类型,不存在子节点,不需要执行reconcileChildren
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
}
// 原生类型
function updateHost(currentFiber) {
// 没有创建真实dom,进行创建
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
// 创建真实dom
function createDOM(currentFiber) {
if (currentFiber.tag === TAG_TEXT) {
return document.createTextNode(currentFiber.props.text);
} else if (currentFiber.tag === TAG_HOST) {
// span div 这些原生标签
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}
}
// 更新真实dom的属性
function updateDOM(stateNode, oldProps, newProps) {
// 存在,而且是个真实dom
if (stateNode && stateNode.setAttribute) {
setProps(stateNode, oldProps, newProps);
}
}
// 4.完成工作,收集有副作用的fiber,构建effect list
function completeUnitOfWork(currentFiber) {
let returnFiber = currentFiber.return;
if (!returnFiber) return;
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
// 让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
// 有effectTag,说明有副作用,需要收集
if (currentFiber.effectTag) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
// -----------commit阶段-----------
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
console.log(workInProgressRoot.firstEffect);
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
// 提交完成
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
const domReturn = returnFiber.stateNode;
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
//如果是新增DOM节点
domReturn.appendChild(currentFiber.stateNode);
}
currentFiber.effectTag = null;
}
requestIdleCallback(workLoop, { timeout: 500 });
performUnitOfWork
方法
- 主要控制节点的遍历顺序和完成顺序
- 遍历顺序:自己 -> 儿子 -> 弟弟
- 完成顺序:儿子 -> 自己 -> 弟弟 -> 父亲
function performUnitOfWork(currentFiber) {
// 1.先遍历自己
beginWork(currentFiber);
// 2.遍历大儿子
if (currentFiber.child) {
return currentFiber.child;
}
while (currentFiber) {
// 3.1 没有子节点的先完成
// 3.2 儿子和弟弟完成,让自己完成
completeUnitOfWork(currentFiber);
// 4. 看看有没有弟弟,有的话遍历弟弟
if (currentFiber.sibling) {
return currentFiber.sibling;
}
// 5. 子节点都完成了,让父亲完成,父亲是root节点,return是null, 跳出while循环
currentFiber = currentFiber.return;
}
}
beginWork
方法
- 1.根据不同类型,创建真实Dom,检查
stateNode
有没有值,没有的话就进行创建 - 2.执行
reconcileChildren
方法,根据子节点虚拟dom,创建fiber,构建fiber树
function beginWork(currentFiber) {
switch (currentFiber.tag) {
case TAG_ROOT:
// 根节点
updateHostRoot(currentFiber);
break;
case TAG_TEXT:
// 文本节点
updateHostText(currentFiber);
break;
case TAG_HOST:
// 原生标签
updateHost(currentFiber);
break;
default:
break;
}
}
// 根fiber, stateNode是外部传入的
function updateHostRoot(currentFiber) {
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
// 文本类型
function updateHostText(currentFiber) {
// 没有创建真实dom,进行创建
// 文本类型,不存在子节点,不需要执行reconcileChildren
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
}
// 原生类型
function updateHost(currentFiber) {
// 没有创建真实dom,进行创建
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
// 创建真实dom
function createDOM(currentFiber) {
if (currentFiber.tag === TAG_TEXT) {
return document.createTextNode(currentFiber.props.text);
} else if (currentFiber.tag === TAG_HOST) {
// span div 这些原生标签
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}
}
// 更新真实dom的属性
function updateDOM(stateNode, oldProps, newProps) {
// 存在,而且是个真实dom
if (stateNode && stateNode.setAttribute) {
setProps(stateNode, oldProps, newProps);
}
}
- 更新/设置dom属性
- 点击事件采用了原生方式进行设置,没有用到react合成事件
// utils.js,
// 给元素设置属性
export function setProps(dom, oldProps, newProps) {
for (let key in oldProps) {
if (key !== 'children') {
if (newProps.hasOwnProperty(key)) {
setProp(dom, key, newProps[key]); // 新老都有,则更新
} else {
dom.removeAttribute(key); //老props里有此属性,新 props没有,则删除
}
}
}
for (let key in newProps) {
if (key !== 'children') {
if (!oldProps.hasOwnProperty(key)) {
//老的没有,新的有,就添加此属性
setProp(dom, key, newProps[key]);
}
}
}
}
// 设置单个
function setProp(dom, key, value) {
if (/^on/.test(key)) {
//onClick
dom[key.toLowerCase()] = value; //没有用合成事件
} else if (key === 'style') {
if (value) {
for (let styleName in value) {
dom.style[styleName] = value[styleName];
}
}
} else {
dom.setAttribute(key, value);
}
}
reconcileChildren
方法
- 根据子节点虚拟dom,创建子节点fiber,构建fiber树
- 将大儿子挂在当前fiber的
child
上 - 将弟弟们挂在大儿子的
sibling
上 - 首次创建要将副作用标识
effectTag
设置为 PLACEMENT
import {
ELEMENT_TEXT,
TAG_TEXT,
TAG_HOST,
PLACEMENT,
} from './constants';
/**
* 创建fiber 构建fiber树
* @param {*} currentFiber 当前fiber
* @param {*} newChildren 当前节点的子节点,虚拟dom数组
*/
export function reconcileChildren(currentFiber, newChildren) {
let newChildIndex = 0; //新虚拟DOM数组中的索引
let prevSibling;
while (newChildIndex < newChildren.length) {
const newChild = newChildren[newChildIndex];
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT; //文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST; //原生DOM组件
}
// 创建fiber
let newFiber = {
tag, //原生DOM组件
type: newChild.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: null, //stateNode肯定是空的
return: currentFiber, //父Fiber
effectTag: PLACEMENT, //副作用标识
nextEffect: null,
};
// 构建fiber链表
if (newChildIndex === 0) {
currentFiber.child = newFiber; //第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber; //然后newFiber变成了上一个哥哥了
newChildIndex++;
}
}
收集Effect 构建Effect List
completeUnitOfWork
完成工作时,收集有副作用的fiber,构建副作用链表- 链表通过fiber的属性
firstEffect
nextEffect
lastEffect
进行构建
构建Effect List步骤
step1. 没有子节点的大儿子先完成
returnFiber和currentFiber 的firstEffect、lastEffect都为空
returnFiber.firstEffect = 大儿子
returnFiber.lastEffect = 大儿子
step2. 没有子节点的弟弟完成
returnFiber.firstEffect = 大儿子
returnFiber.lastEffect = 大儿子
currentFiber 的firstEffect、lastEffect为空
returnFiber.lastEffect.nextEffect = currentFiber = 弟弟;
returnFiber.lastEffect = currentFiber = 弟弟;
step3. 父节点完成
returnFiber的firstEffect、lastEffect为空
currentFiber.firstEffect = 大儿子
currentFiber.lastEffect = 弟弟
大儿子.nextEffect = 弟弟
returnFiber.firstEffect = currentFiber.firstEffect; (父节点的父节点.firstEffect = 大儿子)
returnFiber.lastEffect = currentFiber.lastEffect; (父节点的父节点.lastEffect = 弟弟)
将父节点挂在 弟弟 后面 returnFiber.lastEffect(弟弟).nextEffect = currentFiber(父节点);
父节点的父节点的lastEffect指向 父节点 returnFiber.lastEffect = currentFiber;
step4.下个单元完成
让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
function completeUnitOfWork(currentFiber) {
let returnFiber = currentFiber.return;
if (!returnFiber) return;
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
// 让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
// 有effectTag,说明有副作用,需要收集
if (currentFiber.effectTag) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
提交CommitRoot
- 节点遍历完成,effect list收集完毕后,进入提交阶段
- 提交阶段会根据收集的effect list,进行遍历,将要修改的内容应用到真实Dom上
- 这个过程是不可中断的
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
console.log(workInProgressRoot.firstEffect);
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
// 提交完成
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
const domReturn = returnFiber.stateNode;
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
//如果是新增DOM节点
domReturn.appendChild(currentFiber.stateNode);
}
currentFiber.effectTag = null;
}
实现首次渲染
- 将入口文件中的,react和react-dom库换成自己写的,启动项目
// 自己写的react
import React from './react/react';
import ReactDOM from './react/react-dom';
- 实现效果
代码奉上
commit
首次渲染逻辑
有漏洞和不足之处,还请大家指正