英文文档地址:pomb.us/build-your-…
将按照以下步骤,翻译文档
- Step I: The
createElementFunction - Step II: The
renderFunction - Step III: Concurrent Mode
- Step IV: Fibers
- Step V: Render and Commit Phases
- Step VI: Reconciliation
- Step VII: Function Components
- Step VIII: Hooks
Step Zero: Review
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
But first let’s review some basic concepts. You can skip this step if you already have a good idea of how React, JSX and DOM elements work.
首先让我们复习一些基本概念。如果您已经对React、JSX和DOM元素的工作原理有了很好的了解,那么可以跳过这一步。
We’ll use this React app, just three lines of code. The first one defines a React element. The next one gets a node from the DOM. The last one renders the React element into the container.
我们即将实现的这个React应用程序,只需要三行代码。第一行定义了一个React元素。第二行从DOM上获取一个节点。最后一行则将React元素渲染到容器中。
Let’s remove all the React specific code and replace it with vanilla JavaScript.
让我们移除所有与React相关的代码,并用普通的JavaScript替换它
On the first line we have the element, defined with JSX. It isn’t even valid JavaScript, so in order to replace it with vanilla JS, first we need to replace it with valid JS.
第一行是用JSX定义的元素。它不是有效的JavaScript,所以为了用普通JS替换它,首先我们需要用有效的JS替换它。(本人翻译,就是把jsx想办法转换为js)
JSX is transformed to JS by build tools like Babel. The transformation is usually simple: replace the code inside the tags with a call to createElement, passing the tag name, the props and the children as parameters.
JSX通过像Babel这样的构建工具转换为JS。转换通常很简单:调用' createElement '方法替换标签内部的代码,将标签名称、属性和其子元素作为参数传递。
React.createElement方法详情参阅:zh-hans.reactjs.org/docs/react-…
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
React.createElement creates an object from its arguments. Besides some validations, that’s all it does. So we can safely replace the function call with its output.
React.createElement 方法通过其参数创建一个对象。除了一些验证之外,这就是它所做的一切。因此,我们可以安全地用函数的输出替换函数调用。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
ReactDOM.render(element, container)
And this is what an element is, an object with two properties: type and props (well, it has more, but we only care about these two).
这就是一个有两个属性的对象元素:类型和属性(它有更多的属性,但我们只关心这两个)。
The type is a string that specifies the type of the DOM node we want to create, it’s the tagName you pass to document.createElement when you want to create an HTML element. It can also be a function, but we’ll leave that for Step VII.
这个type字段的数据类型是字符串,是指定我们想要创建的DOM节点类型,你调用document.createElement方法时,传递的tagName字段,其实就是你想创建的HTML元素。它也可以是一个函数,但我们把它留到第七步。
props is another object, it has all the keys and values from the JSX attributes. It also has a special property: children.
props是另一个对象,它具有来自JSX属性的所有键和值。它还有一个特殊的属性:children。
children in this case is a string, but it’s usually an array with more elements. That’s why elements are also trees.
这里的children字段类型是字符串,但它通常是一个包含更多元素的数组。这就是为什么元素通常以树的形状展现。
onst element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
ReactDOM.render(element, container)
The other piece of React code we need to replace is the call to ReactDOM.render.
我们需要替换的另一段React代码是对ReactDOM.render的调用。
render is where React changes the DOM, so let’s do the updates ourselves.
render是React更改DOM的地方(视图做渲染),所以让我们自己做更新。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
First we create a node* using the element type, in this case h1.
第一步,我们利用上述代码中定义好的element对象中type属性值创建一个node节点,在本例中就是h1标签。
Then we assign all the element props to that node. Here it’s just the title.
然后我们把element中的所有属性赋值给上述的node节点,这里指的是title属性(译者注:children不属于属性,是他的子节点,往下看翻译就知道了)
To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*
为了避免混淆,我将使用element指代React元素,node指代DOM元素(译者注:老外的思维很活跃,因此我们不能太局限)
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
Then we create the nodes for the children. We only have a string as a child so we create a text node.
然后为子节点创建节点。我们只有一个字符串作为子节点,所以我们创建一个文本节点。
Using textNode instead of setting innerText will allow us to treat all elements in the same way later. Note also how we set the nodeValue like we did it with the h1 title, it’s almost as if the string had props: {nodeValue: "hello"}.
使用textNode而不是设置innerText将允许我们以后以相同的方式处理所有元素。还要注意我们如何设置nodeValue,就像我们设置h1标题一样,设置字符串的属性:{nodeValue: "hello"}。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
Finally, we append the textNode to the h1 and the h1 to the container.
最后,我们将textNode这个文本节点添加到h1,并将h1添加到container这个元素节点。
And now we have the same app as before, but without using React.
现在我们就有了一个跟之前一样的应用程序,但没有再使用React了。
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
Step I: The createElement Function
Let’s start again with another app. This time we’ll replace React code with our own version of React.
让我们从另一个应用程序开始。这次我们将用我们自己的React版本替换React代码。
We’ll start by writing our own createElement.
我们将从编写自己的createElement方法开始。
Let’s transform the JSX to JS so we can see the createElement calls.
让我们将JSX转换为JS,这样我们就可以看到createElement调用。
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
As we saw in the previous step, an element is an object with type and props. The only thing that our function needs to do is create that object.
正如我们在上一步中看到的,元素是一个带有类型和属性的对象。我们的函数需要做的唯一一件事就是创建那个对象。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
We use the spread operator for the props and the rest parameter syntax for the children, this way the children prop will always be an array.
我们对属性使用spread操作符,对子元素使用rest形参语法,这样子元素将始终是一个数组。
For example, createElement("div") returns:
{
"type": "div",
"props": { "children": [] }
}
createElement("div", null, a) returns:
{
"type": "div",
"props": { "children": [a] }
}
and createElement("div", null, a, b) returns:
{
"type": "div",
"props": { "children": [a, b] }
}
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: [],
},
}
}
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
The children array could also contain primitive values like strings or numbers. So we’ll wrap everything that isn’t an object inside its own element and create a special type for them: TEXT_ELEMENT.
子元素数组也可以包含原始值,如字符串或数字。因此,我们将把所有非对象的内容都包装到它自己的元素中,并为它们创建一个特殊类型:TEXT_ELEMENT。
React doesn’t wrap primitive values or create empty arrays when there aren’t children, but we do it because it will simplify our code, and for our library we prefer simple code than performant code.
当没有子元素时,React不会封装原语值或创建空数组,但我们这样做是因为这会简化我们的代码,对于我们的库来说,我们更喜欢简单的代码而不是性能更好的代码。
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
We are still using React’s createElement.
我们还是使用React.createElement方法
In order to replace it, let’s give a name to our library. We need a name that sounds like React but also hints its didactic purpose.
为了替换它,让我们为库指定一个名称。我们需要一个听起来像React但又暗示其教学目的的名字。
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
We’ll call it Didact.
我们可以叫他 Didact
But we still want to use JSX here. How do we tell babel to use Didact’s createElement instead of React’s?
但是我们仍然希望在这里使用JSX。我们如何告诉babel使用Didact的' createElement '而不是React的?
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
If we have a comment like this one, when babel transpiles the JSX it will use the function we define.
我们如果有这样一个注释,当babel转换jsx的时候就会使用我们自己定义的函数。
以下是第二部分
ReactDOM.render(element, container)
Step II: The render Function
Next, we need to write our version of the ReactDOM.render function.
接下来,我们需要编写我们的ReactDOM版本的渲染函数。
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
Didact.render(element, container)
For now, we only care about adding stuff to the DOM. We’ll handle updating and deleting later.
现在,我们只关心向DOM添加内容。稍后我们将处理更新和删除。
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
We start by creating the DOM node using the element type, and then append the new node to the container.
我们首先使用元素类型创建DOM节点,然后将新节点附加到container中。
function render(element, container) {
const dom = document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
We recursively do the same for each child.
我们递归的对每一个子元素做同样的操作。
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)
}
We also need to handle text elements, if the element type is TEXT_ELEMENT we create a text node instead of a regular node.
我们还需要处理文本元素,如果元素类型是TEXT_ELEMENT,则创建文本节点而不是常规节点。
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
The last thing we need to do here is assign the element props to the node.
这里我们需要做的最后一件事是将元素props赋值给节点。
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 = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
Didact.render(element, container)
And that’s it. We now have a library that can render JSX to the DOM.
就是这样。现在我们有了一个库,可以将JSX呈现给DOM。
Give it a try on codesandbox.
来试试吧。
Step III: Concurrent Mode
But… before we start adding more code we need a refactor.
但是……在我们开始添加更多代码之前,我们需要一个重构。
element.props.children.forEach(child =>
render(child, dom)
)
There’s a problem with this recursive call.
这个递归调用有个问题。
Once we start rendering, we won’t stop until we have rendered the complete element tree. If the element tree is big, it may block the main thread for too long. And if the browser needs to do high priority stuff like handling user input or keeping an animation smooth, it will have to wait until the render finishes.
一旦开始渲染,直到渲染完完整的元素树才会停止。如果元素树很大,它可能会阻塞主线程很久。如果浏览器需要做高优先级的事情,比如处理用户输入或保持动画流畅,它将不得不等到渲染完成再去做。
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
}
So we are going to break the work into small units, and after we finish each unit we’ll let the browser interrupt the rendering if there’s anything else that needs to be done.
因此,我们将把工作分成小单元,在完成每个单元后,如果还有其他需要完成的事情,我们将让浏览器中断渲染。
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
We use requestIdleCallback to make a loop. You can think of requestIdleCallback as a setTimeout, but instead of us telling it when to run, the browser will run the callback when the main thread is idle.
我们使用requestIdleCallback来创建循环。你可以把requestIdleCallback看作是一个setTimeout,但不是我们告诉它什么时候运行,而是浏览器在主线程空闲时运行这个回调。
React doesn’t use requestIdleCallback anymore. Now it uses the scheduler package. But for this use case it’s conceptually the same.
React不再使用requestIdleCallback。现在它使用调度程序包。但是对于这个用例,概念上是一样的。
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback also gives us a deadline parameter. We can use it to check how much time we have until the browser needs to take control again.
requestIdleCallback还为我们提供了一个截止日期参数。我们可以用它来检查在浏览器需要再次控制之前我们还有多少时间。
As of November 2019, Concurrent Mode isn’t stable in React yet. The stable version of the loop looks more like this:
截至2019年11月,React中的并发模式还不稳定。稳定版本的循环看起来更像这样:
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
To start using the loop we’ll need to set the first unit of work, and then write a performUnitOfWork function that not only performs the work but also returns the next unit of work.
要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork函数,它不仅执行工作,而且还返回下一个工作单元。
Step IV: Fibers
To organize the units of work we’ll need a data structure: a fiber tree.
为了组织工作单元,我们需要一个数据结构:fiber树。
We’ll have one fiber for each element and each fiber will be a unit of work.
每个元素都有fiber,每个fiber都是一个单元。
Let me show you with an example.
让我举个例子。
Suppose we want to render an element tree like this one:
假设我们想要渲染一个像这样的元素树:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
In the render we’ll create the root fiber and set it as the nextUnitOfWork. The rest of the work will happen on the performUnitOfWork function, there we will do three things for each fiber:
在渲染中,我们将创建根fiber,并将其设置为nextUnitOfWork。剩下的工作将在performUnitOfWork函数中进行,我们将为每个fiber做三件事:
- add the element to the DOM——————>将元素添加到DOM中
- create the fibers for the element’s children————————>为元素的子元素创建fiber
- select the next unit of work————————>选择下一个工作单元
One of the goals of this data structure is to make it easy to find the next unit of work. That’s why each fiber has a link to its first child, its next sibling and its parent.
这种数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都有一个箭头指到它的第一个子节点,它的下一个兄弟节点和它的父节点。
When we finish performing work on a fiber, if it has a child that fiber will be the next unit of work.
当我们完成对一个fiber的工作时,如果它有一个子节点,那么该fiber将是下一个工作单元。
From our example, when we finish working on the div fiber the next unit of work will be the h1 fiber.
在我们的示例中,当我们完成div fiber的工作时,下一个工作单元将是h1 fiber。
If the fiber doesn’t have a child, we use the sibling as the next unit of work.
如果fiber没有子节点,则使用兄弟节点作为下一个工作单元。
For example, the p fiber doesn’t have a child so we move to the a fiber after finishing it.
例如,p fiber没有子节点,所以我们在完成它之后再移动到a fiber。
And if the fiber doesn’t have a child nor a sibling we go to the “uncle”: the sibling of the parent. Like a and h2 fibers from the example.
如果fiber没有子节点或兄弟节点,我们就找“叔叔”节点:父母的兄弟节点。例如示例中的a和h2 fiber。
Also, if the parent doesn’t have a sibling, we keep going up through the parents until we find one with a sibling or until we reach the root. If we have reached the root, it means we have finished performing all the work for this 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
Now let’s put it into code.
现在让我们把它放到代码中。
First, let’s remove this code from the render function.
首先,让我们从渲染函数中删除这段代码。
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
We keep the part that creates a DOM node in its own function, we are going to use it later.
我们将创建DOM节点的部分保留在它自己的函数中,我们稍后将使用它。
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
In the render function we set nextUnitOfWork to the root of the fiber tree.
在渲染函数中,我们将nextUnitOfWork设置为fiber树的根。
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
}
Then, when the browser is ready,it will call our workLoop and we’ll start working on the root.
然后,当浏览器准备好时,它将调用我们的workLoop,我们将从根节点开始工作。
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
First, we create a new node and append it to the DOM.
首先,我们创建一个新节点并将其附加到DOM中。
We keep track of the DOM node in the fiber.dom property.
我们跟踪fiber.dom属性中的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,
}
}
Then for each child we create a new fiber.
然后我们为每个子节点创造一个新fiber。
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
And we add it to the fiber tree setting it either as a child or as a sibling, depending on whether it’s the first child or not.
我们把它添加到fiber树中,把它设为子结点或兄弟结点,这取决于它是不是第一个子结点。
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
Finally we search for the next unit of work. We first try with the child, then with the sibling, then with the uncle, and so on.
最后我们寻找下一个工作单元。我们先试子节点,然后是兄弟结点,然后是叔叔节点,等等。
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
}
}
And that’s our performUnitOfWork.
这就是我们的performUnitOfWork方法。
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
Step V: Render and Commit Phases 渲染和提交阶段
We have another problem here.
我们还有一个问题。
We are adding a new node to the DOM each time we work on an element. And, remember, the browser could interrupt our work before we finish rendering the whole tree. In that case, the user will see an incomplete UI. And we don’t want that.
每次处理元素时,都会向DOM添加一个新节点。请记住,浏览器可能会在我们完成整个树的渲染之前中断我们的工作。在这种情况下,用户将看到一个不完整的UI。我们不希望那样。
So we need to remove the part that mutates the DOM from here.
所以我们需要从这里移除改变DOM的部分。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
Instead, we’ll keep track of the root of the fiber tree. We call it the work in progress root or wipRoot.
相反,我们将跟踪fiber树的根。我们称它为正在进行的工作根或wipRoot。
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
And once we finish all the work (we know it because there isn’t a next unit of work) we commit the whole fiber tree to the DOM.
一旦我们完成了所有的工作(我们知道这一点,因为没有下一个工作单元),我们就将整个fiber树提交给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)
}
We do it in the commitRoot function. Here we recursively append all the nodes to the dom.
我们在commitRoot函数中完成。这里,我们递归地将所有节点附加到dom中。
Step VI: Reconciliation
So far we only added stuff to the DOM, but what about updating or deleting nodes?
到目前为止,我们只向DOM添加了一些东西,但是更新或删除节点呢?
That’s what we are going to do now, we need to compare the elements we receive on the render function to the last fiber tree we committed to the DOM.
这就是我们现在要做的,我们需要将渲染函数接收到的元素与提交给DOM的最后一个fiber树进行比较。
So we need to save a reference to that “last fiber tree we committed to the DOM” after we finish the commit. We call it currentRoot.
因此,在完成提交之后,我们需要保存对“最后提交给DOM的fiber树”的引用。我们称之为currenroot。
We also add the alternate property to every fiber. This property is a link to the old fiber, the fiber that we committed to the DOM in the previous commit phase.
我们还为每一个fiber添加了可选属性。此属性是到旧fiber的指向,即我们在前一个提交阶段提交给DOM的fiber。
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
}
}
Now let’s extract the code from performUnitOfWork that creates the new fibers…
现在让我们从performUnitOfWork中提取创建新fiber的代码…
…to a new reconcileChildren function.
一个新的reconcileChildren功能
Here we will reconcile the old fibers with the new elements.
在这里,我们将把旧的fiber与新fiber协调起来。
We iterate at the same time over the children of the old fiber (wipFiber.alternate) and the array of elements we want to reconcile.
我们同时遍历旧fiber(wipFiber.alternate)的子元素和我们想要协调调度的元素数组。
If we ignore all the boilerplate needed to iterate over an array and a linked list at the same time, we are left with what matters most inside this while: oldFiber and element. The element is the thing we want to render to the DOM and the oldFiber is what we rendered the last time.
如果我们忽略所有需要同时遍历数组和链表的样板文件,我们就只剩下最重要的部分:oldFiber和element。元素是我们想要呈现给DOM的,而oldFiber是我们上次呈现的。
We need to compare them to see if there’s any change we need to apply to the DOM.
我们需要比较它们,看看是否有需要应用于DOM的更改。
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
}
To compare them we use the type:
为了比较它们,我们使用类型:
- if the old fiber and the new element have the same type, we can keep the DOM node and just update it with the new props
如果旧的fiber和新元素具有相同的类型,我们可以保留DOM节点,只是用新的props更新它。
- if the type is different and there is a new element, it means we need to create a new DOM node
如果类型不同且有新元素,则意味着需要创建新的DOM节点
- and if the types are different and there is an old fiber, we need to remove the old node
如果类型不同,并且有旧的fiber,我们需要移除旧的节点
Here React also uses keys, that makes a better reconciliation. For example, it detects when children change places in the element array.
这里React也使用了键,这使得和解效果更好。例如,它检测子元素在元素数组中的位置何时发生变化。
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",
}
}
When the old fiber and the element have the same type, we create a new fiber keeping the DOM node from the old fiber and the props from the element.
当旧fiber和元素具有相同的类型时,我们创建一个新的fiber,使DOM节点与旧fiber保持一致,而属性与元素保持一致。
We also add a new property to the fiber: the effectTag. We’ll use this property later, during the commit phase.
我们还向fiber添加了一个新属性:effectTag。我们将在稍后的提交阶段使用这个属性。
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
Then for the case where the element needs a new DOM node we tag the new fiber with the PLACEMENT effect tag.
然后,如果元素需要一个新的DOM节点,我们用“PLACEMENT”效果标签标记新fiber。
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
And for the case where we need to delete the node, we don’t have a new fiber so we add the effect tag to the old fiber.
对于需要删除节点的情况,我们没有新的fiber,所以我们将效果标签添加到旧fiber。
But when we commit the fiber tree to the DOM we do it from the work in progress root, which doesn’t have the old fibers.
但是,当我们将fiber树提交给DOM时,我们是从没有旧fiber的working in progress根执行的。(不好翻译,待议)
So we need an array to keep track of the nodes we want to remove.
所以我们需要一个数组来跟踪我们想要删除的节点。
And then, when we are commiting the changes to the DOM, we also use the fibers from that array.
然后,当我们将更改提交给DOM时,我们也使用了来自该数组的fiber。
Now, let’s change the commitWork function to handle the new effectTags.
现在,让我们改变' commitWork '函数来处理新的' effectTags '。
If the fiber has a PLACEMENT effect tag we do the same as before, append the DOM node to the node from the parent fiber.
如果fiber有PLACEMENT标签,我们就像前面一样,将DOM节点附加到父fiber的节点上。
If it’s a DELETION, we do the opposite, remove the child.
如果是一个delete,则相反,删除子元素。
And if it’s an UPDATE, we need to update the existing DOM node with the props that changed.
如果它是一个UPDATE,我们需要用变化的props属性来更新现有的DOM节点。
We’ll do it in this updateDom function.
我们将在这个updateDom函数中完成。
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]
})
}
We compare the props from the old fiber to the props of the new fiber, remove the props that are gone, and set the props that are new or changed.
我们将旧fiber的props与新fiber的props进行比较,移除已经消失的props,设置新的或改变过的props。
One special kind of prop that we need to update are event listeners, so if the prop name starts with the “on” prefix we’ll handle them differently.
我们需要更新的一种特殊类型的道具是事件监听器,所以如果props名称以“on”前缀开始,我们将以不同的方式处理它们。
If the event handler changed we remove it from the node.
如果事件处理程序发生了变化,则将其从节点中移除。
And then we add the new handler.
然后我们添加新的处理程序。
Try the version with reconciliation on codesandbox.
在codesandbox上尝试这个协调版本。
/** @jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
Step VII: Function Components
The next thing we need to add is support for function components.
我们需要添加的下一件事是对函数组件的支持。
First let’s change the example. We’ll use this simple function component, that returns an h1 element.
首先让我们改变这个例子。我们将使用这个简单的函数组件,它返回一个' h1 '元素。
Note that if we transform the jsx to js, it will be:
注意,如果我们将jsx转换为js,它将是:
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
Function components are differents in two ways:
函数式组件有两种不同:
- the fiber from a function component doesn’t have a DOM node
来自函数组件的fiber没有DOM节点
- and the children come from running the function instead of getting them directly from the
props
子元素子组件来自于运行时函数而不是直接从" props "中获得
We check if the fiber type is a function, and depending on that we go to a different update function.
我们检查fiber类型是否是一个函数,并根据这个我们转到一个不同的更新函数。
In updateHostComponent we do the same as before.
在' updateHostComponent '中,我们与之前做的一样。
And in updateFunctionComponent we run the function to get the children.
在' updateFunctionComponent '中,我们运行函数来获取子组件。
For our example, here the fiber.type is the App function and when we run it, it returns the h1 element.
在我们的例子中,这里是fiber.type是' App '函数,当我们运行它时,它返回' h1 '元素。
Then, once we have the children, the reconciliation works in the same way, we don’t need to change anything there.
然后,一旦我们有了子组件,调度就会以同样的方式起作用,我们不需要做任何改变。
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)
}
What we need to change is the commitWork function.
我们需要更改的是commitWork功能。
Now that we have fibers without DOM nodes we need to change two things.
现在我们有了没有DOM节点的fiber,我们需要更改两件事。
First, to find the parent of a DOM node we’ll need to go up the fiber tree until we find a fiber with a DOM node.
首先,要找到DOM节点的父节点,我们需要沿着fiber树往上走,直到找到带有DOM节点的fiber为止。
And when removing a node we also need to keep going until we find a child with a DOM node.
当删除一个节点时,我们还需要一直找,直到我们找到一个具有子节点的DOM。
/** @jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
Step VIII: Hooks
Last step. Now that we have function components let’s also add state.
最后一步。既然我们有了函数组件,让我们也加上状态。
Let’s change our example to the classic counter component. Each time we click it, it increments the state by one.
让我们将示例更改为经典的计数器组件。每次点击它,状态值就增加1。
Note that we are using Didact.useState to get and update the counter value.
注意,我们使用的是Didact.useState获取和更新计数器的值。
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
Here is where we call the Counter function from the example. And inside that function we call useState.
这里是我们调用例子中的Counter函数的地方。在这个函数中,我们调用useState。
We need to initialize some global variables before calling the function component so we can use them inside of the useState function.
在调用函数组件之前,需要初始化一些全局变量,以便在useState函数中使用它们。
First we set the work in progress fiber.
首先我们设置正在进行的工作fiber。
We also add a hooks array to the fiber to support calling useState several times in the same component. And we keep track of the current hook index.
我们还向fiber添加了一个钩子数组,以支持在同一个组件中多次调用useState。我们跟踪当前的钩子索引。
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]
}
When the function component calls useState, we check if we have an old hook. We check in the alternate of the fiber using the hook index.
当函数组件调用useState时,我们检查是否有一个旧的钩子。我们使用钩子索引检查fiber的交替。
If we have an old hook, we copy the state from the old hook to the new hook, if we don’t we initialize the state.
如果我们有一个旧的钩子,我们将状态从旧钩子复制到新钩子,如果没有,则初始化状态。
Then we add the new hook to the fiber, increment the hook index by one, and return the state.
我们将新钩子添加到fiber中,将钩子索引增加1,并返回状态。
useState should also return a function to update the state, so we define a setState function that receives an action (for the Counter example this action is the function that increments the state by one).
useState还应该返回一个函数来更新状态,因此我们定义了一个setState函数来接收一个action(在Counter示例中,这个action是将状态加1的函数)。
We push that action to a queue we added to the hook.
我们将该action推送到添加到钩子的队列中。
And then we do something similar to what we did in the render function, set a new work in progress root as the next unit of work so the work loop can start a new render phase.
然后我们做一些类似于我们在渲染函数中所做的事情,设置一个新的工作根作为下一个工作单元,这样工作循环就可以开始一个新的渲染阶段。
But we haven’t run the action yet.
但我们还没开始运行这个action。
We do it the next time we are rendering the component, we get all the actions from the old hook queue, and then apply them one by one to the new hook state, so when we return the state it’s updated.
我们在下次呈现组件的时候做这个操作,我们从旧的钩子队列中获取所有的action,然后把它们一个一个地应用到新的钩子状态,这样当我们返回状态时,它就更新了。
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 (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
And that’s all. We’ve built our own version of React.
这是全部代码。我们已经建立了自己的React版本。
You can play with it on codesandbox or github.
你也可以上面两个地方练习这个代码
Epilogue 结语
Besides helping you understand how React works, one of the goals of this post is to make it easier for you to dive deeper in the React codebase. That’s why we used the same variable and function names almost everywhere.
除了帮助你理解React是如何工作的,这篇文章的目标之一就是让你更容易深入地了解React的代码库。这就是为什么我们几乎在所有地方都使用相同的变量和函数名。
For example, if you add a breakpoint in one of your function components in a real React app, the call stack should show you:
例如,如果你在一个真实的React应用的函数组件中添加了一个断点,调用栈应该显示给你:
workLoopperformUnitOfWorkupdateFunctionComponent
We didn’t include a lot of React features and optimizations. For example, these are a few things that React does differently:
我们没有包含很多React的特性和优化。例如,以下是React所做的一些不同的事情:
- In Didact, we are walking the whole tree during the render phase. React instead follows some hints and heuristics to skip entire sub-trees where nothing changed.
在Didact中,我们在渲染阶段遍历整棵树。相反,React遵循一些提示和启发式,在没有任何改变的情况下跳过整个子树。
- We are also walking the whole tree in the commit phase. React keeps a linked list with just the fibers that have effects and only visit those fibers.
我们还在提交阶段遍历整个树。React保留了一个只有有效果的fiber的链表,并且只访问那些fiber。
- Every time we build a new work in progress tree, we create new objects for each fiber. React recycles the fibers from the previous trees.
每次我们构建一个新的工作进程树时,我们都为每个fiber创建新的对象。React会回收以前fiber树。
- When Didact receives a new update during the render phase, it throws away the work in progress tree and starts again from the root. React tags each update with an expiration timestamp and uses it to decide which update has a higher priority.
当Didact在渲染阶段接收到一个新的更新时,它会丢弃正在进行的工作树,并从根开始重新开始。React用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。
- And many more…
There are also a few features that you can add easily:
还有一些功能,你可以很容易地添加:
- use an object for the style prop
- flatten children arrays
- useEffect hook
- reconciliation by key
If you add any of these or other features to Didact send a pull request to the GitHub repo, so others can see it.
如果你添加任何这些或其他功能的Didact发送一个请求到GitHub,其他人也可以看到它。
Thanks for reading!
谢谢阅读