React第六章(实现vdom,fiber,diff)
React第六章(实现vdom,fiber,diff)

React第六章(实现vdom,fiber,diff)

虚拟DOM (Virtual DOM)Virtual DOM 就是用JavaScript对象去描述一个DOM结构,虚拟DOM不是直接操作浏览器的真实DOM,而是首先对 UI 的更新在虚拟 DOM 中进行,再将变更高效地同步到真实 DOM 中。

优点
性能优化:直接操作真实 DOM 是比较昂贵的,尤其是当涉及到大量节点更新时。虚拟 DOM 通过减少不必要的 DOM 操作,显著提高了性能。

跨平台性:虚拟 DOM 是一个与平台无关的概念,它可以映射到不同的渲染目标,比如浏览器的 DOM 或者移动端(React Native)的原生 UI。

实现简易虚拟DOM
const App = () => {
return (

小满zs)
}

上面这段代码会通过babel或者swc转换成

const App = () => {
return React.createElement(‘div’, { id: 2 },
React.createElement(‘span’, null, ‘小满zs’)
);
};

接着我们就来实现React.createElement

React.createElement
用于生成虚拟 DOM 树,返回一个包含 type(元素类型)和 props(属性和子元素)的对象。
children 可以是文本或其他虚拟 DOM 对象。
React.createTextElement:

用于处理文本节点,将字符串封装成虚拟 DOM 对象。
React.render:

将虚拟 DOM 转化为实际 DOM 元素。
使用递归的方式渲染所有子元素。
最后将生成的 DOM 节点插入到指定的容器中

const React = {
createElement(type, props = {}, …children) {
return {
type,
props: {
…props,
children: children.map(child =>
typeof child === ‘object’
? child // 如果子元素是对象(嵌套元素),返回对象
: this.createTextElement(child) // 否则将字符串转换为文本元素
)
}
};
},

createTextElement(text) {
    // 文本是没有props children什么的 这样做只是为了结构统一方便遍历
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: []
        }
    };
}

};

React Fiber
Fiber 是 React 16 引入的一种新的协调引擎,用于解决和优化 React 应对复杂 UI 渲染时的性能问题

Fiber 的作用
为了解决React15在大组件更新时产生的卡顿现象,React团队提出了 Fiber 架构,并在 React16 发布,将 同步递归无法中断的更新 重构为 异步的可中断更新

它实现了4个具体目标

可中断的渲染:Fiber 允许将大的渲染任务拆分成多个小的工作单元(Unit of Work),使得 React 可以在空闲时间执行这些小任务。当浏览器需要处理更高优先级的任务时(如用户输入、动画),可以暂停渲染,先处理这些任务,然后再恢复未完成的渲染工作。

优先级调度:在 Fiber 架构下,React 可以根据不同任务的优先级决定何时更新哪些部分。React 会优先更新用户可感知的部分(如动画、用户输入),而低优先级的任务(如数据加载后的界面更新)可以延后执行。

双缓存树(Fiber Tree):Fiber 架构中有两棵 Fiber 树——current fiber tree(当前正在渲染的 Fiber 树)和 work in progress fiber tree(正在处理的 Fiber 树)。React 使用这两棵树来保存更新前后的状态,从而更高效地进行比较和更新。

任务切片:在浏览器的空闲时间内(利用 requestIdleCallback思想),React 可以将渲染任务拆分成多个小片段,逐步完成 Fiber 树的构建,避免一次性完成所有渲染任务导致的阻塞。

双缓存
react内部有两颗树维护着两个状态:一个是fiber tree,一个是work in progress fiber tree

fiber tree:表示当前正在渲染的fiber树
work in progress fiber tree:表示更新过程中新生成的fiber树,也就是渲染的下一次UI状态
举个例子:

当我们用 canvas 绘制动画时,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面,如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

深入理解任务切片
要先理解切片得先理解浏览器一帧做些什么

浏览器一帧做些什么
处理时间的回调click…事件
处理计时器的回调
开始帧
执行requestAnimationFrame 动画的回调
计算机页面布局计算 合并到主线程
绘制
如果此时还有空闲时间,执行requestIdleCallback
例如要更新10000条dom数据
我们可以分成三个小任务进行更新

并且把每一段任务插入requestIdleCallback 如图

diff算法
比如有A B C D四个节点

那么首先react会把这个节点变成链表结构也就是

root
|
child

A -> B -> C -> D


然后我们更新了节点 A C B E

那么diff算法

{A B C D} 他会从map里面去找能够复用的节点也就是 A C B 进行复用
如果{A B C D} 这个结构没有出现E那么说明是新增了创建新的fiber结构
如果{A B C D} 旧节点存在 { A C B E} 新节点没有存在那么说明是删除了

代码实现 vDom Fiber Diff 完整版
//vdom
const React = {
createElement(type, props = {}, …children) {
return {
type,
props: {
…props,
children: children.map(child =>
typeof child === ‘object’
? child
: React.createTextElement(child)
),
},
};
},

createTextElement(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: [],
        },
    };
},

};

// const vdom = React.createElement(‘div’, { id: 1 }, React.createElement(‘span’, null, ‘小满zs’));

// console.log(vdom)

//Fiber 是 React 16 引入的一种新的协调引擎
let nextUnitOfWork = null; // 下一个工作单元
let currentRoot = null; // 当前 Fiber 树的根
let wipRoot = null; // 正在工作的 Fiber 树
let deletions = null; // 存储需要删除的 Fiber

// Fiber 渲染入口
function render(element, container) {
//wipRoot 表示“正在进行的工作根”,它是 Fiber 架构中渲染任务的起点
wipRoot = {
dom: container, //渲染目标的 DOM 容器
props: {
children: [element], //要渲染的元素(例如 React 元素)
},
alternate: currentRoot,
//alternate 是 React Fiber 树中的一个关键概念,用于双缓冲机制(双缓冲 Fiber Tree)。currentRoot 是之前已经渲染过的 Fiber 树的根,wipRoot 是新一轮更新的根 Fiber 节点。
//它们通过 alternate 属性相互关联
//旧的fiber树
};
nextUnitOfWork = wipRoot;
//nextUnitOfWork 是下一个要执行的工作单元(即 Fiber 节点)。在这里,将其设置为 wipRoot,表示渲染工作从根节点开始
deletions = [];
//专门用于存放在更新过程中需要删除的节点。在 Fiber 更新机制中,如果某些节点不再需要,就会将它们放入 deletions,
//最后在 commitRoot 阶段将它们从 DOM 中删除
}

// 创建 Fiber 节点
function createFiber(element, parent) {
return {
type: element.type,
props: element.props,
parent,
dom: null, // 关联的 DOM 节点
child: null, // 子节点
sibling: null, // 兄弟节点
alternate: null, // 对应的前一次 Fiber 节点
effectTag: null, // ‘PLACEMENT’, ‘UPDATE’, ‘DELETION’
};
}

// 创建 DOM 节点
function createDom(fiber) {
const dom =
fiber.type === ‘TEXT_ELEMENT’
? document.createTextNode(”)
: document.createElement(fiber.type);

updateDom(dom, {}, fiber.props);
return dom;

}

// 更新 DOM 节点属性
function updateDom(dom, prevProps, nextProps) {
// 移除旧属性
Object.keys(prevProps)
.filter(name => name !== ‘children’)
.forEach(name => {
dom[name] = ”;
});

// 添加新属性
Object.keys(nextProps)
    .filter(name => name !== 'children')
    .filter(name => prevProps[name] !== nextProps[name])
    .forEach(name => {
        dom[name] = nextProps[name];
    });

}

// Fiber 调度器
// 实现将耗时任务拆分成多个小的工作单元
function workLoop(deadline) {
//deadline 表示浏览器空闲时间
let shouldYield = false;
//是一个标志,用来指示是否需要让出控制权给浏览器。如果时间快用完了,则设为 true,以便及时暂停任务,避免阻塞主线程

while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    //performUnitOfWork 是一个函数,它处理当前的工作单元,并返回下一个要执行的工作单元。每次循环会更新 nextUnitOfWork 为下一个工作单元
    shouldYield = deadline.timeRemaining() < 1;
    //使用 deadline.timeRemaining() 来检查剩余的空闲时间。如果时间少于 1 毫秒,就设置 shouldYield 为 true,表示没有空闲时间了,就让出控制权
}

if (!nextUnitOfWork && wipRoot) {
    //当没有下一个工作单元时(nextUnitOfWork 为 null),并且有一个待提交的“工作根”(wipRoot),就会调用 commitRoot() 将最终的结果应用到 DOM 中
    commitRoot();
}

requestIdleCallback(workLoop);
//使用 requestIdleCallback 来安排下一个空闲时间段继续执行 workLoop,让任务在浏览器空闲时继续进行

}
//requestIdleCallback 浏览器绘制一帧16ms 空闲的时间去执行的函数 浏览器自动执行
//浏览器一帧做些什么
//1.处理时间的回调click…事件
//2.处理计时器的回调
//3.开始帧
//4.执行requestAnimationFrame 动画的回调
//5.计算机页面布局计算 合并到主线程
//6.绘制
//7.如果此时还有空闲时间,执行requestIdleCallback
requestIdleCallback(workLoop);

// 执行一个工作单元
function performUnitOfWork(fiber) {
// 如果没有 DOM 节点,为当前 Fiber 创建 DOM 节点
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
//确保每个 Fiber 节点都在内存中有一个对应的 DOM 节点准备好,以便后续在提交阶段更新到实际的 DOM 树中

// 创建子节点的 Fiber
// const vdom = React.createElement('div', { id: 1 }, React.createElement('span', null, '小满zs'));
// 子节点在children中
const elements = fiber.props.children;
reconcileChildren(fiber, elements);

// 返回下一个工作单元(child, sibling, or parent)
if (fiber.child) {
    return fiber.child;
}

let nextFiber = fiber;
while (nextFiber) {
    if (nextFiber.sibling) {
        return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
}
return null;

}

// Diff 算法: 将子节点与之前的 Fiber 树进行比较
function reconcileChildren(wipFiber, elements) {
let index = 0;//
let oldFiber = wipFiber.alternate && wipFiber.alternate.child; // 旧的 Fiber 树
let prevSibling = null;

while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;

    // 比较旧 Fiber 和新元素
    const sameType = oldFiber && element && element.type === oldFiber.type

    //如果是同类型的节点,复用
    if (sameType) {
        newFiber = {
            type: oldFiber.type,
            props: element.props,
            dom: oldFiber.dom,
            parent: wipFiber,
            alternate: oldFiber,
            effectTag: 'UPDATE',
        };

    }

    //如果新节点存在,但类型不同,新增fiber节点
    if (element && !sameType) {
        newFiber = createFiber(element, wipFiber);
        newFiber.effectTag = 'PLACEMENT';
    }

    //如果旧节点存在,但新节点不存在,删除旧节点
    if (oldFiber && !sameType) {
        oldFiber.effectTag = 'DELETION';
        deletions.push(oldFiber);
    }

    //移动旧fiber指针到下一个兄弟节点
    if (oldFiber) {
        oldFiber = oldFiber.sibling;
    }

    // 将新fiber节点插入到DOM树中
    if (index === 0) {
        //将第一个子节点设置为父节点的子节点
        wipFiber.child = newFiber;
    } else if (element) {
        //将后续子节点作为前一个兄弟节点的兄弟
        prevSibling.sibling = newFiber;
    }

    //更新兄弟节点
    prevSibling = newFiber;
    index++;
}

}

// 提交更新到 DOM
function commitRoot() {
deletions.forEach(commitWork); // 删除需要删除的 Fiber 节点
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}

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

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

}

//测试

// render(React.createElement(‘h1’, null, ‘hello world’), document.getElementById(‘root’));

// 测试用例diff

render(React.createElement(‘div’, { id: 1 }, React.createElement(‘span’, null, ‘hello 11’)), document.getElementById(‘root’));

render(React.createElement(‘div’, { id: 1 }, React.createElement(‘span’, null, ‘hello 22’)), document.getElementById(‘root’));
————————————————

                    

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注