react源码中全局变量
// 下一个要处理的fiber节点
let nextUnitOfWork = null;
// 正在构建的fiber树
let workInProgressRoot = null; // rootFiber 应用的根
// 已经完成构建的fiber树
let currentRoot = null;
// 需要删除的dom的fiber
let deletions = [];
// 正在构建的fiber节点
let workInProgressFiber = null;
// 函数组件中正在执行的第几个hook
// useState不能放if中的原因就在这里
let hookIndex = 0;
fiber结构
graph TD
root --current--> container
container --child--> app
app --child--> fiber1
fiber1 --simbling--> fiber2
fiber2 --simbling--> fiber3
fiber1 -.-> app
fiber2 -.-> app
fiber3 -.-> app
app -.-> container
container --stateNode--> root
root --dom--> div#app
fiber树长什么样
当子节点为文本时
- 如果只用一个子节点
- 并且该子节点是文本节点
- 那么不会创建fiber节点
<div>
<h1>React</h1>
</div>;
fiber 树🌲
graph LR
fiber:div --child--> fiber:h1
当没有文本子节点时
- 当只有一个子节点
- 并且该子节点不是文本节点
- 那么会创建fiber节点
<div>
<h1>
<i class='icon icon-home' />
</h1>
</div>;
graph LR
fiber:div --child--> fiber:h1 --child--> fiber:i
当有多个子节点
- 当有多个子节点
- 就算子节点为文本, 依然会创建fiber
<h2>
<span>what</span>
the
<p>fk</p>
</h2>
graph LR
fiber:h2 --child--> fiber:span --simbling-->fiber:txt --simbling--> fiber:p
root和container的关系
graph TD
root --current--> container
container --stateNode--> root
root --containerInfo--> 真实根节点div#root
container --child--> app
函数组件的特点
- 对应的虚拟dom没有 children:
vdom.children=null - 对应的 fiber 没有dom实例
fiber.stateNode = null - 函数组件的return 一定是个单节点
- 函数组件的child 一定没有simbling
- 函数组件的child 一定不是fnFiber
schedule
翻译: schedule: 进度表
双缓存
function render(fiber: Fiber) {
// 第一次渲染
// fiber.type是'div'
if (!Fiber.curRoot) {
Fiber.wipRoot = fiber
}
// 第二次渲染
if (Fiber.curRoot && !Fiber.curRoot.alternate) {
Fiber.wipRoot = {
...fiber,
alternate: fiber
}
}
// 第三次渲染(双缓存)
if (Fiber.curRoot && Fiber.curRoot.alternate) {
// 修改Fiber.curRoot.alternate, 将其变得和fiber一样
Fiber.curRoot.alternate.type = fiber.type
Fiber.curRoot.alternate.props = fiber.props
Fiber.wipRoot = Fiber.curRoot.alternate
Fiber.wipRoot.alternate = fiber
}
Fiber.unitWork = Fiber.wipRoot
}
增量渲染
什么是增量渲染:
Fiber增量渲染是指把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面,实现任务的 "可中断"、"可恢复",并给不同的任务赋予不同的"优先级",最终达成更加顺滑的用户体验。
// (浏览器每渲染一帧) 并且 (浏览器有空), 就执行一次performUnitOfWork
function workLoop(deadline) {
let shouldYield = false; // shouldYield === false 表示有空
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 拿到下一个fiber,如果浏览器有空闲就执行
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot(); // 每一次提交root都会将wipRoot设置为null
console.log("commitRoot")
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);// 约等于 workLoop()
举一反三
- 往页面中添加30万个div
- 利用时间切片性能优化
<div id="app">
<button class="btn">add</button>
</div>
const app = document.querySelector('#app')
const btn: any = document.querySelector('.btn')
btn.onclick = () => {
const add = (index: number) => {
const div: any = document.createElement('div')
div.innerText = index
app.appendChild(div)
}
// 执行 add 三十万次
performChunk(300000, add)
}
// 默认往requestIdleCallback队列添加异步任务
function performChunk(times: number, add: Function) {
let i = 0;
function unitWork(idle) {
const hasTime = () => idle.timeRemaining() > 0
while(hasTime() && i<=times) {
add(`这是第${i}个`)
i++
}
if(i<=times) {
requestIdleCallback(unitWork)
}
}
requestIdleCallback(unitWork)
}
reconcile
翻译: reconcile: 使一致
reconcile的工作内容:
- 创建子fiber(diff算法发生在这里)
- 把父fiber和子fiber串起来
graph TD
fiber --child--> fiber1
fiber1 --simbling--> fiber2
fiber2 --simbling--> fiber3
fiber1 -.-> fiber
fiber2 -.-> fiber
fiber3 -.-> fiber
优先级
fiber=performUnitOfWork(fiber)- performUnitOfWork:
- 1: 优先返回child
- 2: 没有chidl就返回sibling
- 3: 既没有child, 也没有sibling,
- 则返fiber.parent.sibling
- 或者fiber.parent.parent.sibling
- 或者fiber.parent.parent.parent.sibling
- 直到找到为止
- 如果fiber.parent是rootFiber, rootFiber.parent不存在 => 循环结束
function performUnitOfWork(currentFiber) {
beginWork(currentFiber); // 这里构建fiber树,就是把父fiber和子fiber串起来
if (currentFiber.child) {
return currentFiber.child;
}
while (currentFiber) {
completeUnitOfWork(currentFiber); // 这里构建effect链
if (currentFiber.sibling) {
return currentFiber.sibling;
}
currentFiber = currentFiber.return;
}
}
递: beginWork
- 链接 child sibling return
- diff 算法: 给不能复用的节打标记
function beginWork(currentFiber) {
// 如果 currentFiber 是根fiber
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {
updateHost(currentFiber); // 原生DOM节点
} else if (currentFiber.tag === TAG_FUNCTION_COMPONENT) { //处理函数组件
updateFunctionComponent(currentFiber);
}
}
处理根fiber
// 更新根组件
function updateHostRoot(currentFiber) {
//更新根rootFiber
let newChildren = currentFiber.props.children;
// 调和 子节点
reconcileChildren(currentFiber, newChildren);
}
处理函数组件的fiber
let wipFiber = null;// 正在构建的fiber节点
let hookIndex = null; // 确保useState一一对应
function updateFunctionComponent(fiber) {
wipFiber = fiber; // 保存下当前正在处理的fiber,这样做的目的是可以任务中断再开始后可以快速恢复之前的状态
hookIndex = 0; // useState每执行一次, hookIndex+1
wipFiber.hooks = []; // useState每执行一次, wipFiber.hooks.push(hook)
const children = [fiber.type(fiber.props)]; // 如果fiber是函数组件,那就执行type方法,把props传过去,返回值又是一个jsx转createElement
reconcileChildren(fiber, children); // 继续调用协调器来处理父fiber和子fiber之间的关系
}
处理原生节点的fiber
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren( // 如果fiber的dom有东西,就调用协调器处理该dom的子节点,当前例子子节点是一个函数(组件)
fiber,
(fiber.props && fiber.props.children) ? fiber.props.children : []
);
}
reconcileChildren
// 通过虚拟dom elements 生成 子fiber
// 将父fiber和子fiber串起来
// diff算法也在这里进行
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) { // 如果
const element = elements[index];
let newFiber = null;
// 这里是简单diff, 就是从左往右无脑对比, 没有key
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", // 如果更新的子dom的类型和oldFiber的一样,那就是触发了组件更新
};
}
if (element && !sameType) {
newFiber = { // 如果第一次创建也就是mount的话,sameType肯定null,必然构建一个PLACEMENT的tag,而不是UPDATE或其他
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
// oldFiber存在而element不存在->需要删除
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber); //
}
// 更新oldFiber准备好下一轮比较
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber; // 所有的element都会被包装成fiber,然后wipFiber的child指向当前newFiber
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
归: completeUnitOfWork
- mount阶段: 创建真实dom
- 给可以复用的节点打上更新标记
- flags冒泡
- 以前用的effectList
- 使用subTreeFlags的好处: 能确定某一个fiber的子孙有没有副作用
- 只要subTreeFlags不等于0, 它的子孙节点中就有副作用
let NoFlags = ob00000000
let Update = ob00000001
...
let subFlags = NoFlags
subFlags |= child.subFlags
subFlags |= child.flags
fiber.subFlags = subFlags
diff算法
let lastPlacedIndex
示例
- 考虑一个列表从 [A, B, C] 变为 [C, A, B] 的情况:
- 处理 C 时,它的 oldIndex = 2,lastPlacedIndex = 0,因为 2 >= 0,不需要移动,且更新 lastPlacedIndex = 2。
- 处理 A 时,它的 oldIndex = 0,lastPlacedIndex = 2,因为 0 < 2,需要移动(标记为 Placement)。
- 处理 B 时,它的 oldIndex = 1,lastPlacedIndex = 2,因为 1 < 2,需要移动(标记为 Placement)。
- 最终只有 A 和 B 需要移动,而 C 不需要移动,从而减少了 DOM 操作。
flag |= Placement
- 发生在beginWork阶段
- fiber.alternate 不存在时标记📌
- fiber 发生移动时标记📌
flag |= Update
- 发生在 complete阶段
- fiber.stateNode 可以复用时标记📌
commit
- 找出fiber tree 上面有副作用的节点
- 根据flags进行: 新增, 更新, 位移
- flags不为0就有副作用
- subFlags为0表示子孙节点没有副作用, 不必往下遍历了
- subFlags的值是: 所有子孙节点
逻辑或操作
00000001
00000000
00000000
00000000
--------
00000001 // 二进制位运算: 只要有一个的值不为0,结果都不可能为0