react18 学习(二)
在上一篇创建了根容器和根fiber,在根fiber加了一个空的更新队列。
构建轮替的根fiber
fiber是怎么运作的
为什么要轮替在上一篇已经说过了,这一篇写一下fiber的单项循环链表。
假如我们有一个jsx的dom结构
let element = (
<div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
)
在以前没有用fiber渲染是这样的,这个渲染方式是递归渲染如果数据很多就可能会卡顿。
let vDom = {
"type": "div",
"key": "A1",
"props": {
"id": "A1",
"children": [
{
"type": "div",
"key": "B1",
"props": {
"id": "B1",
"children": [
{
"type": "div",
"key": "C1",
"props": { "id": "C1"},
},
{
"type": "div",
"key": "C2",
"props": {"id": "C2"},
}
]
},
},
{
"type": "div",
"key": "B2",
"props": {"id": "B2"},
}
]
},
}
// 把vDom一气呵成渲染到页面
function render(element, container) {
// 把虚拟DOM创建成真实DOM
let dom = document.createElement(element.type);
// 遍历属性
Object.keys(element.props).filter(key => key !== 'children').forEach(key => {
dom[key] = element.props[key];
});
// 把子节点渲染到父节点上
if(Array.isArray(element.props.children)){
element.props.children.forEach(child=>render(child,dom));
}
// 把真实节点挂载到容器
container.appendChild(dom);
}
render(element, document.getElementById('root'));
下面是fiber的渲染方式,可以中断、暂停、恢复渲染。深度优先
// 把虚拟DOM构建成Fiber树
let A1 = { type: 'div', props:{id: 'A1'} };
let B1 = { type: 'div', props:{id: 'B1'}, return: A1 };
let B2 = { type: 'div', props:{id: 'B2'}, return: A1 };
let C1 = { type: 'div', props:{id: 'C1'}, return: B1 };
let C2 = { type: 'div', props:{id: 'C2'}, return: B1 };
// A1的子节点是B1
A1.child = B1;
// B1的第一个弟弟是B2
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
//下一个工作单元
let nextUnitOfWork = null;
function hasRemainingTime() {
// 模拟有时间
return true
}
//render工作循环
function workLoop() {
// 有下一个fiber任务并且有时间的话
// 每一个任务执行完都可以放弃,让浏览器执行更高优先级的任务
while (nextUnitOfWork && hasRemainingTime()) {
//执行一个任务并返回下一个任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
console.log('render阶段结束');
//render阶段结束
}
function performUnitOfWork(fiber) {
// 执行渲染
let child = beginWork(fiber);
if(child){
return child;
}
while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(fiber);//可以结束此fiber的渲染了
if (fiber.sibling) {//如果它有弟弟就返回弟弟
return fiber.sibling;
}
fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.props.id);
// 工作完之后返回第一个子节点
return fiber.child;
}
function completeUnitOfWork(fiber) {
// 标记完成这个fiber节点
console.log('completeUnitOfWork', fiber.props.id);
}
nextUnitOfWork = A1;
workLoop();
队列的单向链表
预热完成,现在正式开始写构建的代码,先在main.jsx添加一行熟悉的代码
root.render(element)
继续上一篇文章,找到ReactDOMRoot.js
文件,给ReactDOMRoot
函数对象的原型上添加一个render
方法
然后给fiber创建渲染的队列
ReactDOMRoot.js
import { createContainer, updateContainer } from "react-reconciler/src/ReactFiberReconciler";
ReactDOMRoot.prototype.render = function (children) {
// 1. 获取根容器
const root = this._internalRoot;
updateContainer(children, root);
};
ReactFiberReconciler.js
import { createUpdate, enqueueUpdate } from "./ReactFiberClassUpdateQueue";
/**
* 更新容器, 把虚拟DOM变成真实DOM 插入到container容器中
* @param {*} element 虚拟DOM
* @param {*} container 容器 FiberRootNode
*/
export function updateContainer(element, container) {
// 获取根fiber
const current = container.current;
// 2. 创建更新任务
const update = createUpdate();
// 需要更新的虚拟DOM
// 这个更新任务是整个虚拟dom
update.payload = {
element,
};
// 3. 把此更新任务对象添加到current这个根Fiber的更新队列里
let root = enqueueUpdate(current, update);
console.log(root);
}
ReactFiberClassUpdateQueue.js
import { markUpdateLaneFromFiberToRoot } from "./ReactFiberConcurrentUpdate";
// 更新状态
export const UpdateState = 0;
// 创建更新任务
export function createUpdate() {
const update = { tag: UpdateState };
return update;
}
export function enqueueUpdate(fiber, update) {
// 获取根fiber的更新队列 (上一篇最后加的)
const updateQueue = fiber.updateQueue;
// 获取等待执行的任务 (初始化时没有)
const pending = updateQueue.shared.pending;
// 说明是初始化的状态
if (pending === null) {
// 让下一个更新任务指向自己
update.next = update;
} else {
// 让当前更新的 next 指向下一个更新
update.next = pending.next;
// pending 永远指向最后一个更新
pending.next = update;
}
// 让等待更新指向当前 update 开始更新
updateQueue.shared.pending = update;
// 4. 从当前的fiber到返回找到并返回根节点
return markUpdateLaneFromFiberToRoot(fiber);
}
单向循环链表
冒泡获取根节点容器
ReactFiberConcurrentUpdate.js
import { HostRoot } from "./ReactWorkTags";
/**
* 本来此文件要处理更新优先级问题,把不同的fiber优先级冒泡一路标记到根节点。
* 目前现在值实现向上冒泡找到根节点
* @param {*} sourceFiber
*/
export function markUpdateLaneFromFiberToRoot(sourceFiber) {
// 当前fiber
let node = sourceFiber;
// 父fiber
let parent = sourceFiber.return;
while (parent !== null) {
// 如果有父fiber
node = parent;
parent = parent.return;
}
// 一直找到parent为null
if (node.tag === HostRoot) {
console.log(node.stateNode);
return node.stateNode;
}
return null;
}
调度更新
更新对象已经添加到根fiber的更新队列
上,现在要开始更新了。
ReactFiberReconciler.js
import { scheduleUpdateOnFiber } from "./ReactFiberWorkLoop";
export function updateContainer(element, container) {
// ...
// 调度更新
scheduleUpdateOnFiber(root);
}
ReactFiberWorkLoop.js
import { scheduleCallback } from "scheduler";
// 计划更新root
export function scheduleUpdateOnFiber(root) {
// 确保调度执行root上的更新
ensureRootIsScheduled(root);
}
function ensureRootIsScheduled(root) {
// 告诉浏览器要执行performConcurrentWorkOnRoot 参数定死为root
scheduleCallback(performConcurrentWorkOnRoot.bind(null, root));
}
function performConcurrentWorkOnRoot(root) {
console.log(root, 'performConcurrentWorkOnRoot');
}
src/scheduler/index.js
export * from "./src/forks/Scheduler";
forks/Scheduler.js
// 此处后面会实现优先级队列
export function scheduleCallback(callback) {
requestIdleCallback(callback);
}
工作循环
引用上一章提到的一张图片,在上一章已经创建好一个根节点容器和一个空的根fiber(黑色部分),在图中看到还有一个正在构建中的
根fiber
。
根节点的current指的是当前的
根fiber,是会和构建中
的根fiber轮替
工作(双缓冲),现在需要构建一个新的根fiber并且把fiber树写在里面。
一个是表示当前页面已经渲染完成
的fiber树,一个是正在构建中
还没有生效、更没有更新
到页面的fiber树
建立新的hostRootFiber
ReactFiberWorkLoop.js
import { creatWorkInProgress } from "./ReactFiber";
// 正在进行中的工作
let workInProgress = null
//...
/**
* (被告知浏览器确保执行的函数)
* 根据当前的fiber节点构建fiber树, 创建真实的dom节点, 插入到容器
* @param {*} root
*/
function performConcurrentWorkOnRoot(root) {
// 1. 初次渲染的时候以同步方式渲染根节点, 因为要尽快展示 (初始化)
renderRootSync(root);
}
function prepareFreshStack(root) {
// 5. 根据老fiber构建新fiber (初始化)
workInProgress = creatWorkInProgress(root.current);
}
function renderRootSync(root) {
// 2. 先构建了一个空的栈
prepareFreshStack(root);
}
ReactFiber.js
/**
* 根据老fiber和新的属性构建新fiber
* @param {*} current 老fiber
* @param {*} pendingProps 新的属性
*/
export function creatWorkInProgress(current, pendingProps) {
// 3. 拿到老fiber的轮替 第一次没有 (初始化)
let workInProgress = current.alternate;
if (workInProgress === null) {
// 4. 如果没有, 创建一个新的根fiber (初始化)
workInProgress = createFiber(current.tag, pendingProps, current.key);
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 双向指针 互相轮替
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 如果有, 说明是更新, 只更改属性就可以复用
workInProgress.pendingProps = current.pendingProps;
workInProgress.type = current.type;
// 清空副作用标识
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
// 更新就不用双向指针了
}
// 复制属性
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
workInProgress.sibling = current.sibling;
workInProgress.index = current.index
return workInProgress
}
执行工作单元
然后在新的根fiber里构建更新fiber树
ReactFiberWorkLoop.js
import { beginWork } from "./ReactFiberBeginWork";
// ...
function renderRootSync(root) {
// 先构建了一个空的栈
prepareFreshStack(root);
// 1. 现在的 workInProgress 是新的根fiber
workLoopSync();
}
// 工作同步循环
function workLoopSync() {
while (workInProgress !== null) {
// 2. 执行工作单元
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
// 3. 通过新fiber获取老fiber
const current = unitOfWork.alternate;
// 4. 开始工作, 完成当前fiber的子fiber链表构建后
const next = beginWork(current, unitOfWork);
// 把等待生效的属性变成已经生效的
unitOfWork.memoizeProps = unitOfWork.pendingProps;
if (next === null) {
// 说明已经完成
// 完成工作单元
// completeUnitOfWork(); // 这个方法之后写 先模拟一下完成工作
workInProgress = null;
} else {
// 如果有子节点就成为下一个工作单元
workInProgress = next;
}
}
ReactFiberBeginWork.js
import { HostComponent, HostRoot, HostText } from "./ReactWorkTags";
import { processUpdateQueue } from "./ReactFiberClassUpdateQueue";
function updateHostRoot(current, workInProgress) {
// 需要知道它的子虚拟dom, 知道它的儿子的虚拟dom信息
// 之前在根fiber的更新队列加的虚拟dom, 可以在这获取
processUpdateQueue(workInProgress); // workInProgress.memoizedState = { element }
const nextState = workInProgress.memoizedState;
// 拿到虚拟dom
const nextChildren = nextState.element;
// 协调子节点 (DOM-DIFF)
reconcileChildren(current, workInProgress, nextChildren);
// 返回根据子节点创建的子fiber节点返回出去
return workInProgress.child; // { tag: 5, type: 'h1' }
}
function updateHostComponents(current, workInProgress) {}
/**
* 5. 根据 `新的` 虚拟dom去构建 `新的` fiber链表
* @param {*} current 老fiber
* @param {*} workInProgress 新fiber
* @returns 下一个工作单元
*/
export function beginWork(current, workInProgress) {
console.log("beginWork", workInProgress);
// 6 判断类型不同处理方式返回子节点或者弟弟
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress);
case HostComponent:
return updateHostComponents(current, workInProgress);
// 纯文本没有子节点
case HostText:
return null;
default:
return null;
}
}
获取更新队列的虚拟dom
写上一步引入的processUpdateQueue
方法
ReactFiberClassUpdateQueue.js
import { assign } from "shared/assign";
/**
* 根据老状态和更新队列的更新计算最新的状态
* @param {*} workInProgress 要计算的fiber
*/
export function processUpdateQueue(workInProgress) {
// 拿到更新队列
const queue = workInProgress.updateQueue;
// 等待生效的队列
const pendingQueue = queue.shared.pending;
// 如果有更新, 或者更新队列里有内容
if (pendingQueue !== null) {
// 清除等待生效的更新 因为在这就要使用了可以清除了
queue.shared.pending = null;
// 获取最后一个等待生效的更新 // update = { payload: { element: 'h1' }}
const lastPendingUpdate = pendingQueue;
// 第一个等待生效的更新
const firstPendingUpdate = pendingQueue.next;
// 把更新链表剪开, 变成单向链表
lastPendingUpdate.next = null;
// 获取老状态 (会不停更新和计算赋值新状态, 所以起名newState)
let newState = workInProgress.memoizedState;
let update = firstPendingUpdate;
while (update) {
// 根据老状态和更新计算新状态
newState = getStateFromUpdate(update, newState);
update = update.next;
}
// 把最终计算到的状态赋值给 memoizedState
workInProgress.memoizedState = newState;
}
}
/**
* 根据老状态和更新, 计算新状态
* @param {*} update 更新
* @param {*} prevState 上一个状态
* @returns 新状态
*/
function getStateFromUpdate(update, prevState) {
switch (update.tag) {
case UpdateState:
const { payload } = update;
return assign({}, prevState, payload);
}
}
根据子虚拟dom创建子fiber节点
上上步还有一个reconcileChildren
没有定义
ReactFiberBeginWork.js
import { mountChildFibers, reconcileChildFibers } from "./ReactChildFiber";
/**
* 根据新的虚拟dom生成新的fiber链表
* @param {*} current 老的父fiber
* @param {*} workInProgress 新的父fiber
* @param {*} nextChildren 新的子虚拟dom
*/
function reconcileChildren(current, workInProgress, nextChildren) {
// 如果此新fiber没有老fiber, 说明是新创建的
if (current === null) {
// 挂在子fiber
workInProgress.child = mountChildFibers(workInProgress, null, next);
} else {
// 更新: 协调子fiber列表 需要做DOM-DIFF (初始化时的根fiber是有老fiber的(一开始创建的))
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren
);
}
}
ReactChildFiber.js
import { REACT_ELEMENT_TYPE } from "shared/ReactSymbols";
/**
*
* @param {*} shouldTrackSideEffect 是否跟踪副作用
* @returns
*/
function createChildReconciler(shouldTrackSideEffect) {
function reconcileSingleElement(returnFiber, currentFirstFiber, element) {
// 因为我们实现的是初次挂载, 老节点currentFirstFiber是没有的, 所以可以直接根据虚拟dom创建fiber节点
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}
/**
* 比较子fiber (DOM-DIFF) 就是用老的fiber链表和新的虚拟dom进行比较
* @param {*} returnFiber 新父fiber
* @param {*} currentFirstFiber 当前的第一个子fiber(老fiber的第一个儿子)
* @param {*} newChild 新的子虚拟dom
*/
function reconcileChildFibers(returnFiber, currentFirstFiber, newChild) {
// 现在暂时只考虑新的节点只有一个的情况
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return reconcileSingleElement(
returnFiber,
currentFirstFiber,
newChild
);
default:
break;
}
}
}
return reconcileChildFibers;
}
// 有老父fiber 更新
export const reconcileChildFibers = createChildReconciler(true);
// 没有老的父fiber 更新
export const mountChildFibers = createChildReconciler(false);