虚拟dom、fiber、渲染dom、dom-diff
- 三者之间相互关联,且按照先后顺序进行依赖
- 在虚拟dom与渲染dom之间还存在一个步骤,创建fiber
- react的整体渲染流程:
- 1.拿到虚拟dom(一个object)
- 2.根据虚拟dom创建fiber结构
- 3.通过fiber来进行渲染dom
- 4.再次更新的时候,已存在旧dom和旧fiber,重新执行1,在2阶段查找可复用fiber,然后在3阶段渲染
虚拟dom
- 将jsx转换成createElement或jsx包裹的函数,函数会返回一个包含标签特征的object
- jsx转换完成的时候是一整个大的object,前面文章有写,但不详细,补充说明下
- 示例:
let element = (
<h1 className="a" key={"1"}>
<p>hello<span>word</span></p>
</h1>
)
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
let element = _jsx("h1", {
className: "a",
children: _jsxs("p", {
children: ["hello", _jsx("span", {
children: "word"
})]
})
}, "1");
let element = {
$$typeof: Symbol(react.element),
key: "1",
props: {
className: 'a',
children: {
$$typeof: Symbol(react.element),
key: null,
props: {
children: [
'hello',
{
$$typeof: Symbol(react.element),
type: 'span',
key: null,
ref: null,
props: {children: 'word'}
}
]
},
ref: null,
type: "p"
}
ref: null,
type: "h1"
}
}
- 可以看到这个大的object,是层层嵌套的
单个子节点,children为object
多个子节点,children为array
- children放在props里,className、id等属性也在里面
fiber
- 那fiber是什么,为什么要有fiber,它起什么作用
产生原因
- 浏览器的渲染与js执行是互斥的,那么当js执行时间过长,会导致浏览器长时间无法渲染,造成页面上的效果就是ui交互的卡顿
- 屏幕刷新频率一般在60hz,那么将1秒分为60份,就是16.6毫秒,那么我们可以做一个策略,将一个大的js任务,处理成多个小任务,在浏览器空闲的时候执行小任务,执行完成后,查看当前帧剩余空闲时间,如果剩余时间大于某个值,我们继续执行下一个任务,否则将控制权给到浏览器渲染
- 好处:会减少浏览器绘制的阻塞
- 补充:虽然可以判断空闲时间,但是无法在任务执行前判断执行耗时,所以当任务耗时过旧,依然会阻塞,但相比一次执行完毕会好很多
- 函数: requestIdleCallback
- 告知浏览器有待执行的任务,有空的时候会执行
- 会将剩余耗时实例(
deadline
)传递给函数,可以通过(deadline.timeRemaining()
)判断剩余的毫秒值
fiber结构
- 也是object,且是
通过虚拟dom生成
fiber的结构都一致,但一些属性在不同的类型上作用不同
- 看一下fiber的所有属性
export function FiberNode(tag, pendingProps, key) {
this.tag = tag;
this.key = key;
this.type = null;
this.stateNode = null;
this.return = null;
this.child = null;
this.sibling = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
this.updateQueue = null;
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.alternate = null;
this.index = 0;
this.deletions = null;
}
- 有几个需要注意的点
stateNode始终指向的是自己的真实dom
,但根fiber的state指向{current: 当前根fiber}
- alternate指向老fiber,可能直接理解老fiber不太合适,也可以理解为
新旧两个fiber通过alternate互相引用
函数fiber的memoizedState指向hooks链表
创建fiber
初始化
createRoot传递
进来了一个根dom,通常为div#root
- 初始化FiberRoot实例,
{containerInfo:div#root}
,存储下根dom
- 初始化根fiber实例, 可以理解为
new FiberNode
,根fiber的tag指向HostRoot(3)
,后续会根据这个值判断是否为根fiber
- 根fiber的stateNode指向FiberRoot,FiberRoot的current指向当前根fiber
- 初始化fiber的更新队列,里面啥也没有
- 初始化root实例并返回,
{_internalRoot: FiberRoot实例}
更新队列
- 简述更新队列
- fiber的updateQueue指向更新队列
- shared里面的pending始终指向最后一个更新
- 更新之间形成一个单向循环链表,那么我们就可以通过最后一个更细,拿到所有的更新
- 链表结构简述:3-1-2-3
- 拿到3的next,然后将3的next置为null,可以断开链表,避免遍历死循环
function initialUpdateQueue(fiber) {
const queue = {
shared: {
pending: null
}
}
fiber.updateQueue = queue;
}
function createUpdate() {
return {};
}
function enqueueUpdate(fiber, update) {
const updateQueue = fiber.updateQueue;
const shared = updateQueue.shared;
const pending = shared.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
updateQueue.shared.pending = update;
}
function processUpdateQueue(fiber) {
const queue = fiber.updateQueue;
const pending = queue.shared.pending;
if (pending !== null) {
queue.shared.pending = null;
const lastPendingUpdate = pending;
const firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
let newState = fiber.memoizedState;
let update = firstPendingUpdate;
while (update) {
newState = getStateFromUpdate(update, newState);
update = update.next;
}
fiber.memoizedState = newState;
}
}
function getStateFromUpdate(update, newState) {
return Object.assign({}, newState, update.payload);
}
let fiber = { memoizedState: { id: 1 } };
initialUpdateQueue(fiber);
let update1 = createUpdate();
update1.payload = { name: 'zhufeng' }
enqueueUpdate(fiber, update1)
let update2 = createUpdate();
update2.payload = { age: 14 }
enqueueUpdate(fiber, update2)
processUpdateQueue(fiber);
console.log(fiber.memoizedState);
渲染dom
- 渲染dom分为两部分:生成fiber树,渲染
- 这部分可以理解为是在requestIdleCallback中执行
render
- 在生成root实例后,我们会调用render
- 在页面上的表现为dom直接更新到页面上了,其实在插入dom前还有一步,创建fiber树
- 创建fiber树依赖于虚拟dom,先将虚拟dom入队列,存到根fiber的updateQueue里
构建fiber树
- 开始渲染dom的第一步,构建fiber树
- 在构建前,有一个小步骤
判断当前根fiber是否存在对应的老fiber,没有就创建一个,有就拷贝点旧值,然后使用alternate互相指向
- 将这个新(或拷贝,后边统一称为新)的根fiber,放到全局中进行while循环
- 变量为:workInProgress
let workInProgress = null;
function prepareFreshStack(root) {
workInProgress = createWorkInProgress(root.current, null);
}
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function renderRootSync(root) {
prepareFreshStack(root);
workLoopSync();
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
completeWork(current, completedWork);
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
- 以真实dom进行举例,走一下它的执行顺序
- beginWork的作用为获取fiber的子节点,将它处理成fiber后与父级关联,然后返回首个子fiber
- 示例:
()=>(
<div>
hello
<p>world</p>
</div>
)
export function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostRoot:
bubbleProperties(workInProgress);
break;
case HostComponent:
const { type } = workInProgress;
if (current !== null && workInProgress.stateNode !== null) {
updateHostComponent(current, workInProgress, type, newProps);
} else {
const instance = createInstance(type, newProps, workInProgress);
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
finalizeInitialChildren(instance, type, newProps);
}
bubbleProperties(workInProgress);
break;
case FunctionComponent:
bubbleProperties(workInProgress);
break;
case HostText:
const newText = newProps;
workInProgress.stateNode = createTextInstance(newText);
bubbleProperties(workInProgress);
break;
}
}
function bubbleProperties(completedWork) {
let subtreeFlags = NoFlags;
let child = completedWork.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child = child.sibling;
}
completedWork.subtreeFlags = subtreeFlags;
}
- 首先
新根fiber是存在老fiber
的,虽然老fiber里没有什么内容
- 走进beginWork,会按照tag类型进行处理
- 因为是
HostRoot
,所以它的子节点一定不存在,但是在更新队列中存在虚拟dom
- 将队列更新,从memoizedState中拿到虚拟dom
- 将虚拟dom生成fiber节点,return指向
新根fiber
,新根fiber的child指向这个节点
- 因为有父级有老fiber,但子级别没有,所以这是一个准备插入的节点,那么给fiber节点的flags累加副作用
Placement
,然后返回fiber节点
- 然后将
fiber当做新的workInProgress
,会再次触发performUnitOfWork
Functionfiber
没有老fiber,且没有子,那么需要将函数执行,拿到子
,将首个子节点处理成fiber,关联下关系返回
divFiber
继续触发performUnitOfWork
- 没有老fiber,没有子,在
pendingProps里有多个子节点
,遍历数组,将子节点以sibling关联,且加上return,divFiber的child指向首个childFiber,也就是hello文本fiber
,然后返回
TextFiber
继续触发performUnitOfWork
- 没子节点了,走到
completeUnitOfWork
函数
- 一样是按照tag类型分别处理
- 创建文本节点真实dom,将
真实dom给到TextFiber的stateNode
,然后将子的副作用累加到自身的subtreeFlags
- 有sibling,将
sibling给到workInProgress,再次触发performUnitOfWork
- 只有一个子,且是文本,可以理解为没有儿子,那么又走到
completeUnitOfWork
- 创建P的真实dom,是
原生标签,那么将所有儿子真实dom插入到自身
,把所有儿子的副作用累加到自身的subtreeFlags
- 没sibling了,把父级给到workInProgress,自身也有个while,没return走自身
- 创建dom,添加儿子dom到自身,累加副作用
- 没sibling,找父级,自身while,
是Function,只累加副作用
- 没sibling,找父级,自身while,到
根Fiber了,也只累加副作用
,
- 没sibling,也没父了,结束while,这时候
workInProgress也为null了
,后续再次处理fiber树也不会受到影响
- fiber树创建完毕
首次渲染
- 拿到根fiber的current.alternate,也就是刚才创建的fiber树
- 给到finishedWork,开始渲染
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
commitRoot(root);
- 根节点上就是个树尖,没有dom,所以我们先通过subtreeFlags来判断是否有要增删改查的副作用
- 刚才在创建阶段,我们给FunctionFiber加上了插入副作用,所以要插入
先处理要删除的节点
,但现在的deletions没有,所以不需要删除
- 然后
递归处理后代节点的副作用
- 首次
只有FunctionFiber有副作用
,但是它是个函数,不许要插入,我们要插入的是它的子节点div
- 然后减去自身副作用,渲染完成
将root.current变为本次构建fiber树的根fiber,以前的根下次更新使用
,形成轮替效果
domdiff
- diff的前提是已经渲染过一次dom了
- 触发条件,dom要重新渲染,比如以下情况
() => {
const [count, setCount] = React.useState(0);
return (
<div
onClick={() => {
setCount(count + 1);
}}
>
hello
<p>{count === 0 ? "word" : count}</p>
</div>
);
}
- 当前的
根fiber存在alternate
,那么可以复用这个fiber根的老节点
,但需要改些值
- flags、subtreeFlags、deletions给初始化,会重新统计
- child拷贝一下
- 将更新
{setCount(count+1)}
入到自身的队列
开始构建fiber树
- 还是workLoopSync函数,依然是从根fiber开始
- 区别:这次
Function有老节点,但是没老fiber
(这块逻辑跟首次创建根一样)
function reconcileChildren(current, workInProgress, nextChildren) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
} else {
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren);
}
}
- 判断
新老虚拟dom的type和key是否一致
- 一致就复用当前节点,然后将
老fiber中的的sibling放入新fiber的deletions中,表示要删除
,但Function没有sibling,所以不放
- 只有节点不行啊,我们要创建fiber,那么
拿到旧的fiber,创建新的fiber,然后alternate相互指向
- 然后初始化sibling和index,因为要重新赋值
- 然后return指向父fiber
- 父fiber的child指向当前fiber
- 因为本次function没改动,且老fiber存在,所以不是插入,不添加副作用
- 执行
FunctionFiber的performUnitOfWork
- 刚才给它关联了老fiber,
- 而且新的fiber是复用了老fiber部分值的,所以新fiber存在memoizedState,这里面存的是hook的链表
- 那么执行函数,函数内部执行hook,hook根据链表拿到对应的更新队列,依次执行,得到新的值,根据新的值生成虚拟dom,然后返回
- 现在要把子节点变成fiber节点,还是走
reconcileChildFibers
函数
- 老节点存在,且key和type一样,且没有sibling,所以不删除sibling,依据老divFiber创建新的divFiber,两者关联,加上return索引
- div的老fiber也有,所以也不累加副作用,返回新divFiber
- 执行
divFiber的的performUnitOfWork
- 有多个子节点,那么依次处理
- 有三轮循环:
- 第一轮:从第一个节点开始遍历,遇到不能复用的break,记录当前索引
- 第2轮:如果老节点遍历完了,新节点还没有,证明需要插入节点,依次插入
- 第3轮:
将老节点存入map结构中
,遍历新节点,有key或index相同的就复用,并删除map中对应的值,如果没有相同的就插入新节点
,遍历完成后,还留在map中的就是要删除的
- 后边的fiber创建重复以上步骤,最终直到创建完成
- 可以看到是老节点的type和key来判断是否复用的
- 而且在fiber树创建阶段:复用的内容是虚拟dom,和老fiber,并不是真实dom,真实dom还是会重新创建一遍
提交阶段
- 与初次执行步骤一致,但区别在于这次只有一个更新,那么只更新p即可