本文翻译自:https://pomb.us/build-your-ow...
创建一个自己的 React 版本
从头开始,下面这些是我们将要添加到我们的 React 版本中的内容。
- Step 1:
createElement
函数 - Step 2:
render
函数 - Step 3:
Concurrent Mode
并发模式 - Step 4:
Fibers
- Step 5:
Render and Commit Phases
渲染和提交阶段 - Step 6:
Reconciliation
协调 - Step 7:
Function Components
函数组件 - Step 8:
Hooks
Step 0 : 回顾
首先让我们回顾一些基本概念。如果您已经对 React
,JSX
和 DOM
元素的工作方式有了很好的了解,则可以跳过此步骤。
我们将使用此 React 应用程序
只需三行代码。第一行定义一个 React
元素。第二行从 DOM
获取一个节点。最后一行将 React
元素渲染到容器中。
让我们删除所有 React
特定的代码,然后将其替换为原始 JavaScript
。
// 第一行定义一个React元素
const element = Hello
;
// 第二行从DOM获取一个节点
const container = document.getElementById("root");
// 最后一行将React元素渲染到容器中
ReactDOM.render(element, container);
在第一行中,我们具有用JSX
定义的元素。它甚至不是有效的JavaScript
,因此要用标准JS
取代它,首先我们需要用有效JS
取代它。
JSX
通过Babel
等构建工具转换为JS
。转换通常很简单:使用对createElement
的调用来替换标签内的代码,并将标签名称,道具和子代作为参数传递。
React.createElement
根据其参数创建一个对象。除了进行一些验证之外,这就是它所做全部工作。因此,我们可以安全地将函数调用替换为其输出。
const element = React.createElement("h1", { title: "foo" }, "Hello");
这就是一个元素,一个具有两个属性的对象:type
和 props
(它有更多的属性,但是我们只关心这两个属性):
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello"
}
};
类型type
是一个字符串,用于指定我们要创建的DOM
节点的类型,它是您要创建 HTML
元素时传递给document.createElement
的tagName
。它也可以是一个函数,但我们将其留给步骤 7。
props
是另一个对象,它具有JSX
属性中的所有键和值。它还有一个特殊的属性:children
。
在这种情况下,children
是字符串,但通常是包含更多元素的数组。这就是为什么元素也是树的原因。
我们需要替换的另一部分React
代码是对ReactDOM.render
的调用。
render
是React
更改DOM
的地方,所以让我们自己进行更新。
首先,我们使用元素类型(在本例中为h1
)创建一个node
*。
然后,我们将所有元素props
分配给该节点。这里只是标题。
*为避免混淆,我将使用“element
”来指代React elements
,并使用“node
”来指代DOM elements
。
// 首先,我们使用元素类型(在本例中为`h1`)创建一个`node` *。
const node = document.createElement(element.type);
// 然后,我们将所有元素`props`分配给该节点。这里只是标题。
node["title"] = element.props.title;
然后,我们为孩子创建节点。我们只有一个字符串作为孩子,所以我们创建一个文本节点。
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
使用textNode
而不是设置innerText
将使我们以后以相同的方式对待所有元素。还请注意我们是如何像设置h1
标题一样设置nodeValue
,就像字符串中带有props
一样:{nodeValue:“ hello”}
。
最后,我们将textNode
附加到h1
并将h1
附加到container
。
node.appendChild(text);
container.appendChild(node);
现在,我们拥有与以前相同的应用程序,但是没有使用React
。
// 1. 创建一个元素,这个元素是具有两个属性的对象:type和props(它有更多的属性,但是我们只关心这两个属性)
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello"
}
};
// 2. 从DOM获取一个节点
const container = document.getElementById("root");
// 3. 使用 type 创建一个`node` 。然后,我们将所有元素`props`分配给该节点。这里只是标题。
const node = document.createElement(element.type);
node["title"] = elememt.props.title;
// 4. 创建子节点(使用`textNode`而不是设置`innerText`将使我们以后以相同的方式对待所有元素)
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
// 5. 将`textNode`附加到`h1`并将`h1`附加到`container`
node.appendChild(text);
container.appendChild(node);
Step 1: createElement
函数
让我们从另一个应用程序开始。这次,我们将用自己的React
版本替换React
代码。
我们将从编写自己的createElement
开始。
让我们将JSX
转换为JS
,以便可以看到createElement
的调用。
const element = (
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
正如我们在上一步Step 0
中看到的,元素是具有type
和props
的对象。我们的函数唯一需要做的就是创建该对象。
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
);
我们对props
使用spread operator
,对children
使用rest parameter
语法,这样children
属性将始终是数组。
扩展运算符回顾:
// 此处使用扩展运算符可以直接将数组作为参数传入
function foo(name, ...arr) {
return {
name,
arr
};
}
const testArr = [1, 3, 5, 6, 2, 5];
foo("Bob", ...testArr);
/*
结果:
{
name: "Bob",
arr: [1,3,5,6,2,5]
}
*/
例如:createElement("div")
返回:
{
"type": "div",
"props": { "children": [] }
}
createElement("div", null, a)
返回:
{
"type": "div",
"props": { "children": [a] }
}
createElement("div", null, a, b)
返回:
{
"type": "div",
"props": { "children": [a, b] }
}
那么:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children
}
};
}
children
组也可以包含原始值,例如字符串或数字。因此,我们会将不是对象的所有内容包装在其自己的元素中,并为其创建特殊类型:TEXT_ELEMENT
。
当没有children
时,React
不会包装原始值或创建空数组,但是我们这样做是因为它可以简化我们的代码,对于我们的库,我们更喜欢简单代码而不是高性能代码。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
)
}
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
};
}
我们仍然在使用React
的createElement
。
为了代替它,我们给我们自己的库取一个名字。我们需要一个听起来像React
,但又不同的名字。
我们叫它Didact
。
但是我们仍然想在这里使用 JSX
。我们如何告诉babel
使用 Didact
的 createElement
代替 React
的?
const Didact = {
createElement
};
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
);
如果我们有这样的注解,当babel
转译JSX
时,它将使用我们定义的函数。
/** @jsx Didact.createElement */
// 得到:
const element = (
);
Step 2: render
函数
接下来,我们需要编写我们的ReactDOM.render
函数版本。
ReactDOM.render(element, container);
目前,我们只关心向 DOM
添加内容。我们稍后将处理更新和删除。
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const element = (
)
const container = document.getElementById("root")
Didact.render(element, container)
我们首先使用元素类型创建DOM
节点,然后将新节点附加到容器container
中。
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
我们递归地为每个children
做同样的事情。
function render(element, container) {
const dom = document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
我们还需要处理文本元素,如果元素类型为TEXT_ELEMENT
,我们将创建文本节点而不是常规节点。
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
我们在这里要做的最后一件事是将元素props
分配给节点。
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
就是这样。现在,我们有了一个可以将JSX
呈现到DOM
的库。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const element = (
)
const container = document.getElementById("root")
Didact.render(element, container)
Step 3: Concurrent Mode(并发模式)
但是……在开始添加更多代码之前,我们需要重构。
此递归调用存在问题。
function render(element, container) {
element.props.children.forEach(child => render(child, dom));
}
一旦开始渲染后,直到渲染完完整的元素树后,我们才会停止。 如果元素树很大,则可能会阻塞主线程太长时间。 而且,如果浏览器需要执行诸如处理用户输入或使动画保持平滑等高优先级的工作,则它必须等到渲染完成为止。
因此,我们将工作分成几个小单元,在完成每个单元后,如果需要执行其他任何操作,我们将让浏览器中断渲染。
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
我们使用requestIdleCallback
进行循环。您可以将requestIdleCallback
视为setTimeout
,但浏览器将在主线程空闲时运行回调,而不是告诉我们何时运行。
React
不再使用requestIdleCallback
。 现在,它使用调度程序包。但是对于此用例,它在概念上是相同的。
requestIdleCallback
还为我们提供了截止日期参数。我们可以使用它来检查我们有多少时间,直到浏览器需要再次控制。
截至 2019 年 11 月,并发模式在React
中还不稳定。循环的稳定版本看起来像这样:
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork
函数,该函数不仅执行工作,还返回下一个工作单元。
Step 4: Fibers
要组织工作单元,我们需要一个数据结构:一棵构造树(fiber tree
)。
我们将为每个元素分配一根fiber
,并且每根fiber
将成为一个工作单元。
举一个例子:
假设我们要渲染一个像这样的元素树:
Didact.render(
,
container
);
在渲染中,我们将创建root fiber
并将其设置为nextUnitOfWork
。剩下的工作将在performUnitOfWork
函数上进行,我们将为每根fiber
做三件事:
该数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber
都链接到其第一个子节点,下一个兄弟姐妹和父节点。
当我们在一个fiber
上完成了工作,如果这个fiber
有一个child
,那么这个fiber
将会是下一个工作单元(the next unit of work)。
在我们的示例中,当完成 div
fiber 上的工作,下一个工作单元将是h1
fiber。
如果这个fiber
没有child
, 用 sibling
(兄弟) 作为下一个工作单元。
比如,p
fiber 没有子节点·,那么在p
fiber 完成后,移动到a
fiber。
如果fiber
既没有孩子child
也没有兄弟姐妹sibling
,那么我们去“叔叔”:父母的兄弟姐妹。就像示例中的a
和h2
fiber 一样。
另外,如果父母没有兄弟姐妹,我们会不断检查父母,直到找到有兄弟姐妹的父母,或者直到找到根。如果到达根目录,则意味着我们已经完成了此渲染的所有工作。
现在,将其放入代码中。
首先,让我们从 render
函数中删除此代码。
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
let nextUnitOfWork = null
我们将创建DOM
节点的部分保留在其自身的功能中,稍后我们将使用它。
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
function render(element, container) {
// TODO set next unit of work
}
let nextUnitOfWork = null
在 render
函数中,我们将nextUnitOfWork
设置为 fiber tree
的根。
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
然后,当浏览器准备就绪时,它将调用我们的workLoop
,我们将开始在根上工作。
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
首先,我们创建一个新节点 node
并将其添加到 DOM
。
我们在 fibre.dom
属性中跟踪 DOM
节点。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO create new fibers
// TODO return next unit of work
}
然后,为每个孩子 child
创建一个新的 fiber
。
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
}
然后将其添加到 fiber tree
中,将其设置为子代 child
或者兄弟 sibling
,具体取决于它是否是第一个子代 child
。
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
最后,我们搜索下一个工作单元。我们首先尝试与孩子,然后与兄弟姐妹,然后与叔叔,依此类推。
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
这就是我们的 performUnitOfWork
。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Step 5: Render and Commit Phases
渲染和提交阶段
我们这里还有另一个问题。
每次处理元素时,我们都会向 DOM
添加一个新节点。 而且,请记住,在完成渲染整个树之前,浏览器可能会中断我们的工作。 在这种情况下,用户将看到不完整的 UI
。 我们不想要那样。
function performUnitOfWork(fiber) {
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
因此,我们需要从此处删除更改 DOM
的部分。
function performUnitOfWork(fiber) {
}
相反,我们将跟踪 fiber tree
的根 root
。我们称其为进行中的根或 wipRoot
。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
一旦完成所有工作(因为没有下一个工作单元),我们便将整个 fiber tree
提交给 DOM
。
function commitRoot() {
// TODO add nodes to dom
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
我们在 commitRoot
函数中做到这一点。在这里,我们将所有节点递归附加到 Dom
。
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Step 6: Reconciliation
到目前为止,我们仅向 DOM
添加了内容,但是更新或删除节点又如何呢?
这就是我们现在要做的,我们需要将在 render
函数上得到的元素与我们提交给 DOM
的最后一棵 fiber tree
进行比较。
因此,在完成提交之后,我们需要保存对“我们提交给 DOM
的最后一棵 fiber tree
”的引用。 我们称它为 currentRoot
。
我们还将 alternate
属性添加到每根 fiber
。 此属性是到旧 fiber
的链接,旧 fiber
是我们在上一个提交阶段向 DOM
提交的 fiber
。
function commitRoot() {
commitWork(wipRoot.child)
currentRoot = wipRoot // new code
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function render(element, container) {
wipRoot = { // new code
dom: container,
props: {
children: [element],
},
alternate: currentRoot, // new code
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null // new code
let currentRoot = null
现在,让我们从 performUnitOfWork
中提取代码来创建新的 fibers
...
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
到新的 reconcileChildren
函数:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function reconcileChildren(wipFiber, elements) {}
在这里,我们将旧 fibers
与新元素 elements
进行协调
function reconcileChildren(wipFiber, elements) {}
我们同时遍历 old fiber
的 children
(wipFiber.alternate
)和要协调的元素数组。
如果我们忽略了同时迭代数组和链接列表所需的所有样板,那么在此期间,我们剩下的最重要的是: oldFiber
和 element
。 element
是我们要渲染到 DOM
的东西,而 oldFiber
是我们上次渲染的东西。
我们需要对它们进行比较,以了解是否需要对 DOM
进行任何更改。
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
// TODO compare oldFiber to element
}
}
为了比较它们,我们使用以下类型:
- 如果
old fiber
和new element
具有相同的类型type
,我们可以保留DOM
节点,仅使用新的props
进行更新。 - 如果类型
type
不同并且有一个新元素new element
,则意味着我们需要创建一个新的DOM
节点。 - 如果类型
types
不同且有old fiber
,则需要删除旧节点。
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
// TODO update the node
}
if (element && !sameType) {
// TODO add this node
}
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
}
在这里,React
也使用密钥,这样可以实现更好的协调。例如,它检测子元素何时更改元素数组中的位置。
当 old fiber
和 new element
具有相同的类型 type
时,我们创建新的 fiber
,以使保持 DOM
节点远离 old fiber
,保持 props
远离元素 element
。
我们还向 fiber 添加了一个新属性:effectTag
。 稍后,我们将在提交阶段使用此属性。
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
节点的情况,我们使用 PLACEMENT
效果标签 effectTag
来标记新的 fiber
。
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
对于需要删除节点的情况,我们没有新的 fiber ,因此我们将效果标签 effectTag
添加到oldFiber
。
但是,当我们将 fiber tree
提交给 DOM
时,我们是从正在进行的根 root
开始的,它没有 oldFiber。
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
因此,我们需要一个数组来跟踪要删除的节点。
function render(element, container) {
// ...
deletions = []
}
// ...
let deletions = null
然后,当我们将更改提交到 DOM
时,我们也使用该数组中的 fiber
。
function commitRoot() {
deletions.forEach(commitWork)
// ...
}
现在,让我们更改 commitWork
函数以处理新的 effectTags
。
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
如果 fiber
具有 PLACEMENT
效果标签,则与之前相同,将 DOM
节点附加到父 fiber
的节点上。如果是 DELETION
,则执行相反的操作,删除该子项。如果是 UPDATE
,我们需要使用更改的 props
来更新现有的 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)
}
我们将在此 updateDom
函数中进行操作。
function updateDom(dom, prevProps, nextProps) {
// TODO
}
我们将 oldFiber
中的 props
与 newFiber
中的 props
进行比较,删除不再使用的 props
,并设置新的或更改的 props
。
const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
我们需要更新的一种特殊 prop
是事件侦听器,因此,如果 prop 名称以“ on
”前缀开头,我们将以不同的方式处理它们。
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
如果事件处理程序发生更改,我们将从节点中将其删除。
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
然后,我们添加新的处理程序。
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
Step 7: Function Components 函数组件
我们需要添加的下一件事是对函数组件的支持。
首先,让我们更改示例。我们将使用此简单的函数组件,该组件将返回 h1
元素。
/** @jsx Didact.createElement */
function App(props) {
return Hi {props.name}
}
const element =
const container = document.getElementById("root")
Didact.render(element, container)
请注意,如果我们将 jsx
转换为 js
,它将是:
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
函数组件在两个方面有所不同:
- 功能组件中的
fiber
没有DOM
节点 -
children
来自于函数的调用,而不是直接来自于props
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
我们检查 fiber
类型是否为函数,并根据此函数使用其他更新函数。
在 updateHostComponent
中,我们执行与以前相同的操作。
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// ...
}
function updateFunctionComponent(fiber) {
// TODO
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
在 updateFunctionComponent
中,我们运行该函数以获取子代 children
。
对于我们的示例,这里的 fibre.type
是 App
函数,当我们运行它时,它返回 h1
元素。
然后,一旦有了 children
,对帐便以相同的方式进行,我们不需要在那里进行任何更改。
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
我们需要更改的是 commitWork
函数。
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)
}
现在我们有了没有 DOM
节点的 fiber
,我们需要更改两件事。
首先,要找到 DOM
节点的父节点,我们需要沿着 fiber tree
向上移动,直到找到带有 DOM
节点的 fiber
。
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
在删除节点时,我们还需要继续操作,直到找到具有 DOM
节点的子节点为止。
else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Step 8: Hooks
最后一步。现在我们有了函数组件,我们还要添加状态 state
。
/** @jsx Didact.createElement */
function App(props) {
return Hi {props.name}
}
const element =
const container = document.getElementById("root")
Didact.render(element, container)
让我们将示例更改为经典计数器组件。每次单击它,状态都会增加一。
请注意,我们正在使用 Didact.useState
获取和更新计数器值。
const Didact = {
createElement,
render,
useState,
}
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
setState(c => c + 1)}>
Count: {state}
)
}
const element =
const container = document.getElementById("root")
Didact.render(element, container)
在这里,我们从示例中调用 Counter
函数。在该函数内部,我们称为 useState
。
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
我们需要在调用函数组件之前初始化一些全局变量,以便可以在 useState
函数中使用它们。
首先,我们将 work
设置在进行中的 fiber
。
我们还向 fiber
添加了一个 hooks
数组,以支持在同一组件中多次调用 useState
。并且我们跟踪当前的钩子索引。
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
当函数组件调用 useState
时,我们检查是否有旧的钩子。我们使用钩子索引检查 fiber
的替代项 alternate
。
如果我们有旧的 hook
,则将状态从旧的 hook
复制到新的 hook
,否则,我们将初始化状态。
然后,将新的 hook
添加到 fiber
,将 hook
索引加1,然后返回状态。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
useState
还应该返回一个更新状态的函数,因此我们定义了一个 setState
函数,该函数接收一个动作(对于 Counter
示例,该动作是将状态加1的函数)。
我们将该动作推送到添加到挂钩中的队列中。
然后,我们执行与渲染功能中类似的操作,将新的进行中的工作根设置为下一个工作单元,以便工作循环可以开始新的渲染阶段。
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
但是我们尚未执行该操作。
下次渲染组件时,我们会从旧的挂钩队列中获取所有动作,然后将它们逐一应用于新的挂钩状态,因此当我们返回更新后的状态。
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
就这样。我们已经构建了自己的React版本。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.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") {
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
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
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",
}
}
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 (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
useState,
}
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
setState(c => c + 1)}>
Count: {state}
)
}
const element =
const container = document.getElementById("root")
Didact.render(element, container)
结语
除了帮助您了解 React
的工作原理外,本文的目的之一是使您更轻松地深入 React
代码库。 这就是为什么我们几乎在所有地方都使用相同的变量和函数名称的原因。
例如,如果您在真正的 React
应用程序的功能组件之一中添加断点,则调用堆栈应显示:
workLoop
performUnitOfWork
updateFunctionComponent
我们没有包括很多 React
功能和优化。例如,以下是 React
可以做的一些事情:
- 在
Didact
中,我们在渲染阶段遍历整棵树。相反,React
遵循一些提示和试探法,以跳过没有任何更改的整个子树。 - 我们还在提交阶段遍历整棵树。
React
仅保留有影响的fiber
并仅访问那些fiber
的链表。 - 每次我们构建一个新的进行中的工作树时,都会为每根
fiber
创建新的对象。React
回收了先前树木中的fiber
。 - 当
Didact
在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根重新开始。React
使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。 - ......