核心思路及思想
- 利用requestIdleCallback实现每帧空余时间处理单个fiber,提升render阶段的性能。
- render阶段以fiber为处理单元,收集effect(生成真实dom)形成单链表,如果有大儿子先完成大儿子,然后是大儿子的弟弟,然后是大儿子的父亲。然后以父亲重新作为起点继续遍历。直到回到根节点,根节点完成后,将持有完成的effect list(单链表)。
- 最终将单链表进行commit,将effect逐层挂载到父级的真实dom上。
- 补充:
4.1 fiber之所以比react15版本性能有很大提升,得益于fiber的数据结构包含了大儿子、大儿子弟弟、大儿子的父亲。
4.2 这样当帧时间不够,暂停运行时,全局的nextWorkFiber保存了上下级关系,可以随时找到下一个需要处理的fiber。
4.3 根fiber的props.children中挂载了整个dom树,每次render调和生成fiber时,会在fiber的.props.children中挂载子dom树。
4.4 因此fiber可以暂停,并持续完成手机effect list的工作
以下代码可直接全部拷贝正常执行
// 表示一个文本元素
export const ELEMENT_TEXT = Symbol("ELEMENT_TEXT");
// 根节点
export const TAG_ROOT = Symbol("TAG_ROOT");
// 原生节点 div span class 函数
export const TAG_HOST = Symbol("TAG_HOST");
// 文本节点
export const TAG_TEXT = Symbol("TAG_TEXT");
// 插入节点
export const PLACEMENT = Symbol("PLACEMENT");
// 更新节点
export const UPDATE = Symbol("UPDATE");
// 删除节点
export const DELETETION = Symbol("DELETETION");
import React from './react';
import ReactDOM from './react-dom';
const style = {
border: '1px solid red',
margin: '20px',
}
let el = (
<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(el, document.getElementById('root'));
import { ELEMENT_TEXT } from './constants';
function createElement(type, config, ...children) {
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;
import { TAG_ROOT } from './constants';
import { scheduleRootFiber } from './schedule';
/**
* render 是把一个元素渲染到一个容器内部
*/
function render (element, container) { // container = root Dom节点
let rootFiber = {
tag: TAG_ROOT, // 每个fiber都有一个tag属性标识此元素的类型
stateNode: container, // 一般情况下,此元素如果是host节点,stateNode将指向真实dom元素
// props.children是一个数组,里面存放的是要被渲染的react元素(虚拟dom),后面会把每个react元素创建对应的fiber
props: { // 虚拟dom和fiber节点的属性区别:虚拟dom有type和props,fiber有tag、stateNode、props、type、return、child、sibling...等更丰富的属性
children: [ element ] // element为要被渲染的虚拟dom
}
}
scheduleRootFiber(rootFiber);
}
const ReactDOM = {
render
}
export default ReactDOM;
export function setProps (dom, oldProps, newProps) {
for(let key in oldProps) {
}
for(let key in newProps) {
if(key !== 'children') {
setProp(dom, key, newProps[key])
}
}
}
function setProp(dom, key, value) {
if(/^on/.test(key)) { // onClick onChange
dom[key.toLowerCase()] = value;
} else if( key === 'style' ) {
for(let key in value) {
dom.style[key] = value[key];
}
} else if( key === 'className' ) {
dom.class = value;
} else {
dom.setAttribute(key, value);
}
}
/**
* react工作的两个重要阶段:diff阶段、commit阶段
* diff阶段(render阶段):
* 1. 对比新旧的虚拟dom,进行增量更新或创建,这个
* 阶段可能比较花时间,所以我们对任务要进行拆分,拆分的维度为虚拟dom节点,此阶段可以暂停(此帧时间不够了,优先执行下一帧)。
* 2. diff(render)阶段的目的是收集哪些节点发生了变化,最终生成一个effectlist(单链表)
* 3. diff阶段有两个任务:1.根据虚拟dom生成fiber树 2.收集effectList
*
* commit阶段:进行dom更新创建阶段,此阶段不能暂停,要一气呵成(这样做页面不会出现卡顿)
*/
/**
* render阶段代码处理逻辑
* 1. 将virtual dom tree mount 到rootFiber上
* 2. 将rootFiber上的virtual dom进行循环遍历 创建fiber
*/
import { setProps } from "./utils";
import { ELEMENT_TEXT, TAG_TEXT, TAG_HOST, PLACEMENT, TAG_ROOT } from "./constants";
let nextUnitOfWork = null; // 下一个执行单元
let workInProgressRoot = null; // RootFiber应用的根,用于单链表的火车头?
export function scheduleRootFiber(rootFiber) { // { tag: TAG_ROOT, stateNode: container, props: { children: [ element ] } }
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
// react告诉浏览器,在每帧的空余时间,执行workLoop
requestIdleCallback(workLoop, { timeout: 500 });
// 循环执行工作 nextUnitOfWork
function workLoop(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) { // 有任务,并且时间充裕或已超时,则继续执行
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork) {
let firstEffect = workInProgressRoot.firstEffect;
commit(firstEffect);
console.log('render阶段结束');
}
requestIdleCallback(workLoop, { timeout: 500 }); // 如果时间片到期后,无论是否还有nextUnitOfWork未处理完,都执行一遍requestIdleCallback
}
function commit(currentEffect) {
while (currentEffect) {
doCommit(currentEffect)
currentEffect = currentEffect.nextEffect;
}
}
function doCommit(currentEffect) {
if (!currentEffect) return;
let returnFiber = currentEffect.return;
let returnDom = returnFiber.stateNode;
if(currentEffect.effectTag === PLACEMENT) {
returnDom.appendChild(currentEffect.stateNode);
}
currentEffect.effectTag = null;
}
/**
* 处理一个执行单元
* 遍历子节点同时收集effect
* 首先通过beginWork处理根节点,然后处理>>大儿子(大儿子的弟弟完成)>>大儿子的弟弟(大儿子的弟弟完成、大儿子的父亲完成)>>大儿子的叔叔(大儿子的叔叔完成)>>...>>(根节点完成)
* 根据当前fiber的类型进行对应的处理
* 处理之后返回其大儿子或者弟弟或者叔叔
* @param {Object} currentFiber
*/
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);
if (currentFiber.child) {
return currentFiber.child;
}
while (currentFiber) { // 此处while循环、currentFiber的的作用是让最后一个儿子可以一直溯源到自己的父亲及祖先,同时让父亲可以找到自己的弟弟
completeUnitOfWork(currentFiber); // 所有的儿子完成,自己则完成,最终rootFiber持有整个effect list
if (currentFiber.sibling) {
return currentFiber.sibling; // 有弟弟则返回弟弟
}
currentFiber = currentFiber.return; // 没有弟弟则找自己的叔叔,如果没有父亲则本次workLoop结束。如果有父亲,则找叔叔,没有叔叔接着向上找父亲,直到没有父亲没为止。
}
}
/**
* 处理不同类型的fiber
* 0. 根据虚拟dom树创建fiber树
* 1. 创建真实dom stateNode
* @param {Object} currentFiber 不同类型的fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) { // 处理根fiber,将根节点的每个第一层子节点创建fiber
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) { // 处理文本fiber,如果是文本节点,创建真实dom
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) { // 处理host fiber,创建真实dom
updateHost(currentFiber);
}
}
/**
* 收集子fiber的effect,由第一个完成的fiber形成单链表的头部,并将firstEffect和lastEffect都交给父亲,当父亲完成的时候,将first和last交给爷爷。最终祖先将持有单链表的头部first
* 当前节点完成,收集有副作用的fiber,然后组成effect list
* 每个fiber都有自己的firstEffect和lastEeffect, firstEffect指向第一个有副作用的子fiber,lastEffect指向最后一个有副作用的子fiber
* 中间的用nextEffect做一个单链表,firstEffect为大儿子,firstEffect.nextEffect指向二儿子、firstEffect.nextEffect.nextEffect为三儿子。
* @param {Object} currentFiber
*/
function completeUnitOfWork(currentFiber) {
const returnFiber = currentFiber.return;
if (returnFiber) {
const effectTag = currentFiber.effectTag;
if (effectTag) { // 自己有副作用
if (!returnFiber.firstEffect) { // 2 当第一个节点的父亲完成时,爷爷如果没有firstEffect,则把爸爸的firstEffect给到爷爷的firstEffect
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (currentFiber.lastEffect) { // 此行判断应该可以省略??
if (returnFiber.lastEffect) { // 4 把叔叔的第一个effect交给爷爷的lastEffect的nextEffect
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect; // 3 把父亲的lastEffect交给爷爷
}
if (returnFiber.lastEffect) { // 1. 处理第一个优先完成的节点
returnFiber.lastEffect.nextEffect = currentFiber; // 此处的currentFiber为前面儿子的弟弟
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}
/**
* 更新根fiber,将根fiber下的第一层virtual dom转成fiber
* @param {Obeject} currentFiber 根节点
*/
function updateHostRoot(currentFiber) { // 将fiber下的props的children取出来,取出来的是虚拟dom
let newChildren = currentFiber.props.children; // [element]
reconcileChildren(currentFiber, newChildren);
}
/**
* 更新文本fiber的statenode
* 创建真实dom
* @param {Object} currentFiber 文本节点
*/
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
}
/**
* 更新host fiber的stateNode
* 1. 创建真实dom
* 2. 将属性添加到真实dom上
* @param {Object} currentFiber
*/
function updateHost(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
/**
* 根据当前fiber创建第一层虚拟dom的fiber
* @param {Object} currentFiber
* @param {Array} newChildren 存放着虚拟dom
*/
function reconcileChildren(currentFiber, newChildren) { // 将虚拟dom转成fiber, newChildren为virtual dom,转换为fiber后,将生成的fiber全部挂载到当前fiber上
let prevSibling; // 上一个新的fiber
newChildren.forEach((newChild, index) => {
let tag;
if (newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT;
} else if (typeof newChild.type === 'string') {
tag = TAG_HOST;
}
let newFiber = {
tag, // fiber 节点的类型, 根节点、属性节点、文本节点
type: newChild.type, // fiber对应的虚拟dom的标签类型 div span等
props: newChild.props, // 将虚拟dom的props传给fiber
stateNode: null, // 此时的host元素还没有对应的dom元素
return: currentFiber, // 父fiber
effectTag: PLACEMENT, // 重要: 副作用标识 render阶段我们会收集副作用 增加 删除 更新
nextEffect: null, // 相当于单链表中的next
}
if (index === 0) { // 将第一个child挂载在父亲上
currentFiber.child = newFiber;
} else { // 如果是非首个child,将其挂载到哥哥的sibling上
prevSibling.sibling = newFiber;
}
prevSibling = newFiber; // 将当前的fiber缓存下来,供后面如果有弟弟时,让弟弟可以找到哥哥
});
}
/**
* 用于将文本fiber 或host fiber创建为真实dom
* @param {Object} currentFiber
*/
function createDOM(currentFiber) {
switch (currentFiber.tag) {
case TAG_TEXT:
return document.createTextNode(currentFiber.props.text);
case TAG_HOST: {
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}
}
}
/**
* 更新dom
* @param {HTMLDivElement} stateNode
* @param {*} oldProps
* @param {*} newProps
*/
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
网友评论