原文链接:Build your own React
- Step I: The createElement Function ✅
- Step II: The render Function ✅
- Step III: Concurrent Mode 并发模式 ✅
- Step IV: Fibers ✅
- Step V: Render and Commit Phases ✅
- Step VI: Reconciliation ✅
- Step VII: Function Components
- Step VIII: Hooks
** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*
前言
前文我们提到过,每次处理一个元素时,都会向DOM添加一个新节点,但是记住,我们使用了RIC,所以浏览器可能会在我们完成渲染整个树之前中断我们的工作。在这种情况下,用户将看到一个不完整的 UI。在这节,我们将提交(commit)阶段修改为同步执行来解决此问题。
Render and Commit Phases
首先移除之前渲染DOM的代码:
// react/render.js
function performUnitOfWork(fiber) {
// ...
- if (fiber.parent) {
- fiber.parent.dom.append(fiber.dom);
- }
}
接下来思考下,在上节中,我们仅仅记录了每个DOM节点对应的fiber节点,如果要提交阶段改成同步执行,需要以下几个步骤:
- 首先必须获取到一颗完整的Fiber树,记录了整个DOM节点的信息。我们将这颗完整的Fiber树命名wipRoot(work in progress root)
- render阶段完成所有的工作后,一次性提交整颗Fiber树渲染界面
开始修改代码:
// react/render.js
function render(element, container) {
// 初始化下一个工作单元(根节点)
+ wipRoot = {
+ dom: container,
+ props: {
+ children: [element],
+ },
+ sibling: null,
+ child: null,
+ parent: null,
+ };
+ nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null;
// 记录整颗Fiber树
+ let wipRoot = null;
// commit阶段
+ function commitRoot() {
+ commitWork(wipRoot.child);
+ wipRoot = null;
+ }
+ function commitWork(fiber) {
+ if (!fiber) {
+ return;
+ }
+ const parentDOM = fiber.parent.dom;
+ parentDOM.append(fiber.dom);
+ commitWork(fiber.child);
+ commitWork(fiber.sibling);
+ }
function workLoop(deadline) {
// 剩余时间是否足够执行任务的标识
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
// 处理当前任务并返回下一个待执行的任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
// commit阶段 保证异步渲染 同步提交
// 此时可以理解为已完成全部工作
+ if (!nextUnitOfWork && wipRoot) {
+ commitRoot();
+ }
// 本次没有足够空闲时间 请求下一次浏览器空闲的时候执行
requestIdleCallback(workLoop);
}
至今为止,我们只完成了添加DOM节点的工作,接下来让我们看看如何更新以及删除节点。
Reconciliation(Diff算法)
我们需要比较上一次render函数中处理的最后一课Fiber树(commit阶段的wipRoot),所以首先需要定义一个变量存储上一次commit的Fiber树:currentRoot,同时在每一颗fiber树上添加alternate属性(a link to the old fiber),记录上一次此节点对应的fiber。
// react/render.js
function render(element, container) {
// 初始化下一个工作单元(根节点)
wipRoot = {
dom: container,
props: {
children: [element],
},
sibling: null,
child: null,
parent: null,
+ alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
}
function commitRoot() {
commitWork(wipRoot.child)
+ currentRoot = wipRoot
wipRoot = null
}
// 记录上一次Fiber树
+ let currentRoot = null;
在之前performUnitOfWork函数的作用是每一次都会为dom节点生成对应的fiber树,现在我们需要在生成新fiber树的同时对比旧的fiber树,这里也是我们此处diffing算法的核心,接下来需要重构我们的performUnitOfWork函数:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.append(fiber.dom);
}
const elements = fiber.props.children;
+ reconcileChildren(fiber, elements);
- let prevSibling = null;
- for (let i = 0; i < elements.length; i++) {
- const newFiber = {
- type: elements[i].type,
- props: elements[i].props,
- parent: fiber,
- dom: null,
- child: null,
- sibling: null,
- };
-
- if (i === 0) {
- fiber.child = newFiber;
- } else {
- prevSibling.sibling = newFiber;
- }
- prevSibling = newFiber;
- }
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
可以看到我们加了一个新的reconcileChildred函数,此函数接收两个参数:即将被处理的fiber(wipFiber)以及它的子元素(elements)。首先需清晰明白此函数的意义:
-
同时遍历旧fiber以及当前待处理的子元素
-
对两者进行对比(开始diffing),根据type相同与否分为以下三种情况处理
- type类型相同:直接继承DOM,不需要重新生成新DOM,并使用自己的新属性进行更新,并为此fiber添加标识UPDATE
- type类型不同,存在子元素(elements):新建此元素的fiber,添加标识PLACEMENT
- type类型不同,存在旧fiber:删除旧fiber,添加标识DELETION
// react/render.js
function render(element, container) {
// 初始化下一个工作单元(根节点)
wipRoot = {
dom: container,
props: {
children: [element],
},
sibling: null,
child: null,
parent: null,
// 记录上一次此节点对应的fiber
alternate: currentRoot,
};
+ deletions = [];
nextUnitOfWork = wipRoot;
}
// diffing中记录要删除的节点
+ let deletions = null;
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber) {
const element = elements[index];
const sameType = element && oldFiber && element.type === oldFiber.type;
let newFiber = null;
if (sameType) {
// 更新
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
// 新建
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
// 删除
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (index === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
index++;
}
}
接下来处理commit阶段:
// react/render.js
function commitRoot() {
+ deletions.forEach(commitWork);
commitWork(wipRoot.child);
// 记录上一次Fiber树
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
/**
* 新建 删除操作比较容易,直接控制父节点的添加删除即可
* 更新操作需要对比前后props
*/
const parentDOM = fiber.parent.dom;
- parentDOM.append(fiber.dom);
+ if (fiber.effectTag === "PLACEMENT" && fiber.dom) {
+ parentDOM.append(fiber.dom);
+ } else if (fiber.effectTag === "DELETION" && fiber.dom) {
+ parentDOM.removeChild(fiber.dom);
+ } else if (fiber.effectTag === "UPDATE" && fiber.dom) {
+ updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
+ }
commitWork(fiber.child);
commitWork(fiber.sibling);
}
// 新建updateDOM函数处理更新情况
function updateDOM(dom, prevProps, nextProps) {
// 删除已经没有的props
Object.keys(prevProps)
.filter((key) => key !== "children")
.filter((key) => !(key in nextProps))
.forEach((name) => (dom[name] = ""));
// 赋予新的或者改变的props
Object.keys(nextProps)
.filter((key) => key !== "children")
.filter((key) => !(key in prevProps) || prevProps[key] !== nextProps[key])
.forEach((name) => (dom[name] = nextProps[name]));
// 删除已经没有的或者发生变化的事件处理函数
Object.keys(prevProps)
.filter((key) => key.startsWith("on"))
.filter((key) => !(key in nextProps) || prevProps[key] !== nextProps[key])
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// 添加新的事件处理函数
Object.keys(nextProps)
.filter((key) => key.startsWith("on"))
.filter((key) => prevProps[key] !== nextProps[key])
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, prevProps[name]);
});
}
至此,我们的diffing算法就已经基本完成了,在通过示例验证之前,先来总结下至今为止从进入render函数之后的函数调用栈,方便大家理解。
调用栈
进入render函数,初始化rootFiber,同时设为nextUnitOfWork
初始化执行workLoop
=> performUnitOfWork
=> reconcileChildren(diffing阶段)构建fiber
第一次diffing阶段时,由于不存在oldFiber,所以所有节点都属于新增节点,都会被打上PLACEMENT标记
后续diffing时,根据type相同与否,分别对应不同的处理
=> 出reconcileChildren,此DOM对应的fiber构建完成
=> 出performUnitOfWork,返回下一个fiber
循环直至整颗Fiber Tree构成
=> 进入commitRoot
-
先处理deletion中记录的待删除的dom,直接从DOM树中移除
-
处理子节点
- PLACEMENT新增,直接append进DOM树中
- UPDATE更新,进入updateDOM函数
=> 记录currentRoot(赋值为当前的渲染,currentRoot = wipRoot)
=> 初始化wipRoot为null,等待下一次渲染
通过示例加深Diffing算法
我分别提供两种示例供大家加深理解,第一种主要是UPDATE更新操作:
// main.js
/** diffing示例 update*/
const handleInput = (e) => {
renderer(e.target.value);
};
const renderer = (value) => {
const container = document.querySelector("#root");
const element = createElement(
"div",
null,
createElement("input", { oninput: (e) => handleInput(e) }, null),
createElement("h1", null, value)
);
render(element, container);
};
renderer("hello");
现象:h1标签值根据input输入框实时变化
diffing过程:
- 第一次diffing,不存在oldFiber,所有节点属于新增
- input输入框输入值,触发下一次的diffing
- 前后对比,发现前后节点类型type相同,只是属性值有变化,所有标签被打上UPDATE标识
第二个实例涉及到删除操作:
// main.js
/** diff示例 delete */
const container = document.querySelector("#root");
const handleDel = () => {
renderer("new");
};
const renderer = (type) => {
const initElement = createElement(
"div",
null,
createElement("button", { onclick: () => handleDel() }, "delete h1"),
createElement("h1", null, "hello h1")
);
const newElement = createElement(
"div",
null,
createElement("button", { onclick: () => handleDel() }, "delete h1"),
createElement("h2", null, "hello h2")
);
render(type === "new" ? newElement : initElement, container);
};
renderer("init");
现象:点击button按钮,删除h1标签,添加h2标签
diffing过程:
- 第一次diffing,不存在oldFiber,所有节点属于新增
- 点击button按钮,前后对比,h1标签进入deletions删除队列中,并打上DELETION标记
- h2标签添加上PLACEMENT,属于新增节点
- 其余button节点,外层div节点type类型没有变化,属于UPDATE操作
总结
至此,我们已经完成了react中重要的Reconciliation阶段,如果单独看代码理解起来可能不是很容易,建议大家根据我提供的两个示例,一步一步debugger,根据其内部函数调用栈了解diffing阶段是如何工作的。在此系列完结的最后一章中,我会将代码更新到Github。