建立你自己的React

我们将一步一步,从头开始重写React。遵循真实的React代码中的体系结构,只是没有所有的优化和那些非必要的特性。

从头开始,我们将把所有这些东西一个一个添加到我们的React版本中:

Step 0: Review

但首先让我们回顾一下一些基本概念。如果您已经很好地了解了React、JSX和DOM元素是如何工作的,那么可以跳过这一步。

1
2
3
4
5
const element = <h1 title="foo">Hello</h1>

const container = document.getElementById("root")

ReactDOM.render(element, container)

我们将使用这个React app,只有三行代码。
第一行定义了React元素。
下一行从DOM获取一个节点。
最后一行将React元素呈现到容器中。

让我们删除所有特定于React的代码,并用普通的JavaScript替换它。

createElement

1
const element = <h1 title="foo">Hello</h1>

第一行是用JSX定义的元素。它甚至不是有效的JavaScript,所以为了用普通JS替换它,首先我们需要找到的有效JS。

JSX通过像Babel这样的构建工具转换为JS。转换通常很简单:用对 createElement 的调用替换标记内的代码,将tag namepropschildren作为参数传递。

1
2
3
4
5
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)

React.createElement根据其参数创建一个对象。除了一些验证,这就是它所做的一切。这样我们就可以安全地将函数调用替换为它的输出。

1
2
3
4
5
6
7
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}

这里的element,是一个有两个属性的对象:typeprops(其实,它有更多属性,但我们只关心这两个)。

type是一个字符串,它指定我们要创建的DOM节点的类型。当您想要创建一个HTML元素时,它是传递给document的标记名。它也可以是一个函数,但我们把它留到Step VII。

props是另一个对象,它拥有来自JSX属性的所有键和值。它还有一个特殊的属性:children

children在这里是一个字符串,但它通常是包含更多元素的数组。这就是为什么元素也是树。

render

我们需要替换的另一段React代码是对ReactDOM.render的调用。

1
ReactDOM.render(element, container)

render是React改变DOM的地方,所以让我们自己来做更新。

  • 首先,我们使用element type(在本例中为h1)创建一个node*。

  • 然后我们将element所有的props分配给那个node。这里只是title

1
2
3
4
5
6
7
8
9
10
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}

const node = document.createElement(element.type)
node["title"] = element.props.title

为了避免混淆,我将使用element表示React元素,使用node表示DOM元素。

  • 然后为children创建node。我们只有一个字符串作为子元素,因此我们创建一个text node。

    • 使用textNode而不是设置innerText将允许我们以后以相同的方式处理所有元素。还请注意我们是如何像设置h1 title一样设置nodeValue的,就好像这个字符串有props:{nodeValue: "hello"}
1
2
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
  • 最后,我们将textNode添加到h1,并将h1附加到容器。
1
2
3
4
5

const container = document.getElementById("root")

node.appendChild(text)
container.appendChild(node)

Step I: createElement 函数

让我们再次从另一个应用程序开始。这次我们将用我们自己版本的React替换React代码。

1
2
3
4
5
6
7
8
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

我们将从编写自己的createElement开始。

将JSX转换为JS,这样我们就可以看到createElement调用。

正如我们在前一步中看到的,element是具有typeprops的对象。我们的函数需要做的唯一一件事就是创建这个对象。

1
2
3
4
5
6
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)

我们为props使用spread操作符,为children prop使用rest参数语法,这样子道具将始终是一个数组。

1
2
3
4
5
6
7
8
9
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}

例如, createElement(“div”) 返回:

1
2
3
4
{
"type": "div",
"props": { "children": [] }
}

createElement(“div”, null, a) 返回:

1
2
3
4
{
"type": "div",
"props": { "children": [a] }
}

createElement(“div”, null, a, b) 返回:

1
2
3
4
{
"type": "div",
"props": { "children": [a, b] }
}

children数组还可以包含基本值,如字符串或数字。因此,我们将把所有不是对象的东西包装在它自己的元素中,并为它们创建一个特殊类型:TEXT_ELEMENT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}

function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}

当没有children时,React不会封装原语值或创建空数组,但我们这样做是因为它将简化代码,而且对于我们的库,我们更喜欢简单代码而不是性能代码。

我们仍然在使用React的createElement

1
2
3
4
5
6
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)

为了替换它,让我们给库起一个名字。我们需要一个听起来像React,但又暗示其说教目的的名字。

我们称它为Didact

1
2
3
4
5
6
7
8
9
10
const Didact = {
createElement,
}

const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)

但我们仍然希望在这里使用JSX。我们如何告诉babel使用Didact的createElement而不是React的?

如果我们有这样的注释,当babel转置JSX时,它将使用我们定义的函数。

1
2
3
4
5
6
7
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)

Step II: render 函数

接下来,我们需要编写我们版本的ReactDOM.render函数。

1
ReactDOM.render(element, container)

目前,我们只关心向DOM添加内容。稍后我们将处理更新和删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

首先使用元素类型创建DOM节点,然后将新节点附加到容器中

1
2
3
4
5
function render(element, container) {
const dom = document.createElement(element.type)

container.appendChild(dom)
}

我们递归地对每个子节点执行相同的操作。

1
2
3
4
5
6
7
8
9
function render(element, container) {
const dom = document.createElement(element.type)

element.props.children.forEach(child =>
render(child, dom)
)

container.appendChild(dom)
}

我们还需要处理文本元素,如果元素类型是TEXT_ELEMENT,则创建文本节点而不是常规节点。

1
2
3
4
5
6
7
8
9
10
11
12
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分配给节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

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)

试一下codesandbox

Step III: 并发模式

但是,在开始添加更多代码之前,我们需要一个重构器。

这个递归调用有一个问题。

1
2
3
4
5
6
7
function render(element, container) {
...
element.props.children.forEach(child =>
render(child, dom)
)
...
}

一旦我们开始渲染,我们不会停止,直到我们已经渲染了完整的元素树。如果元素树很大,它可能长时间阻塞主线程。如果浏览器需要做一些高优先级的事情,比如处理用户输入或者保持动画的平滑,它将不得不等待渲染完成。

所以我们将把工作分解成小单元,在我们完成每个单元后,如果有任何其他需要完成的事情,我们将让浏览器中断渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。现在它使用scheduler包。但对于这个用例,它在概念上是相同的。

requestIdleCallback还提供了一个deadline参数。我们可以使用它来检查在浏览器需要再次控制之前我们还有多少时间。

截止到2019年11月,Concurrent模式在React中还不稳定。稳定版本的循环看起来更像这样:

1
2
3
4
5
while (nextUnitOfWork) {    
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}

要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork函数,它不仅执行工作,还返回下一个工作单元。

1
2
3
4
5
6
7
8
9
10
11
12
let nextUnitOfWork = null

...
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
...


function performUnitOfWork(nextUnitOfWork) {
// TODO
}

Step IV: Fibers

为了组织工作单元,我们需要一个数据结构: fiber树。

每个元素都有一个fiber,每个fiber都是一个工作单位。

让我给你们看一个例子。

假设我们想渲染一个这样的元素树:

1
2
3
4
5
6
7
8
9
10
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)

render中,我们将创建root fiber并将其设置为nextUnitOfWork。其余的工作将发生在performUnitOfWork函数里,我们将为每个fiber做三件事:

  1. 将元素添加到DOM中
  2. 为元素的子元素创建fibers
  3. 选择下一个工作单元

fiber tree

此数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都有一个连接到它的第一个子节点,它的下一个兄弟节点和它的父节点。

当我们在一个fiber上完成工作时,如果它有一个子节点,那么fiber将是下一个工作单元。

在我们的示例中,当我们完成div fiber的工作时,下一个工作单元将是h1 fiber

如果fiber没有子节点,我们将使用同级节点作为下一个工作单元。

例如,p fiber没有子fiber,所以我们在完成后转移到a fiber

如果fiber没有子节点或兄弟节点,我们会找到叔叔:父节点的兄弟节点。比如例子中的ah2 fiber。

而且,如果父结点没有兄弟结点,我们就会一直通过父结点,直到找到一个有兄弟结点或者到达根结点。如果我们到达了根节点,这意味着我们已经完成了render的所有工作。

现在让我们把它放到代码中。

首先,让我们从render函数中删除这段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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节点的部分保留在自己的函数中,稍后将使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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树的根。

1
2
3
4
5
6
7
8
9
10
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}

let nextUnitOfWork = null

然后,当浏览器准备就绪时,它将调用我们的workLoop,我们将开始在root上工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}

首先,我们创建一个新节点并将其附加到DOM中。

1
2
3
4
5
6
7
8
9
10
11
12
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
}

我们在fiber.dom属性中跟踪DOM节点。

然后我们为每一个child创造一种新的fiber。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  // create new fibers
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,
}
}

// TODO return next unit of work

和将它添加到fiber树中设置它为child或sibling,这取决于它是否是第一个child。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    ...

if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}

prevSibling = newFiber
index++
}

// TODO return next unit of work
}

最后,我们寻找下一个工作单元。我们首先尝试子节点,然后尝试兄弟节点,然后尝试叔叔节点,等等。

1
2
3
4
5
6
7
8
9
10
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}

这就是我们的performUnitOfWork。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 V: 渲染和提交阶段

我们还有一个问题。

1
2
3
4
5
6
7
function performUnitOfWork(fiber) {
...

if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
...

每次处理一个元素时,我们都会向DOM添加一个新节点。而且,请记住,浏览器可能会在我们完成整个树的渲染之前中断我们的工作。在这种情况下,用户将看到一个不完整的UI。我们不希望这样。

因此,我们需要从这里删除改变DOM的部分。

相反,我们将跟踪fiber tree的root。我们称它为正在进行的工作root或wipRoot。

1
2
3
4
5
6
7
8
9
10
11
12
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}

let nextUnitOfWork = null
let wipRoot = null

一旦我们完成了所有的工作(我们知道它是因为没有下一个工作单元),我们就将整个fiber tree 提交给DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function commitRoot() {
// TODO add nodes to dom
}


function workLoop(deadline) {
...

if (!nextUnitOfWork && wipRoot) {
commitRoot()
}

...
}

我们完成commitRoot函数中。在这里,我们递归地将所有节点追加到dom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 VI: 调解 (Reconciliation)

到目前为止,我们只向DOM添加了一些东西,那么更新或删除节点呢?

我们现在要做的,是将render函数接收到的元素与提交给DOM的最后一个fiber树进行比较。

因此,在完成提交之后,我们需要保存对“最后一个提交到DOM的fiber树”的引用。我们称之为currentRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function commitRoot() {
...
currentRoot = wipRoot
...
}


function render(element, container) {
wipRoot = {
...
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}

...
let currentRoot = null

我们还为每个fiber添加了alternate(备用)特性。这个属性是到旧fiber的链接,即我们在上一次提交阶段提交给DOM的fiber

现在让我们从创建新fibers的performUnitOfWork中提取代码……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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函数

1
2
3
4
5
6
7
8
9
10
function performUnitOfWork(fiber) {
...

const elements = fiber.props.children
reconcileChildren(fiber, elements)

...
}

function reconcileChildren(wipFiber, elements) {

在这里,我们将使用新元素调和旧fibers:

1
function reconcileChildren(wipFiber, elements)

我们同时遍历旧fiber(wipFiber.alternate)的子元素和我们想要调和的元素数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

如果我们忽略同时遍历数组和链表所需的所有样板文件,那么我们只剩下while中最重要的部分:oldFiberelementelement是我们想要渲染到DOM的东西,而oldFiber是我们上次渲染的东西。

我们需要比较它们,看看是否需要对DOM进行更改。

为了比较它们,我们使用type:

  • 如果旧的fiber和新element具有相同的类型,我们可以保留DOM节点,并使用新的props更新它
  • 如果类型不同,并且有一个新element,这意味着我们需要创建一个新的DOM节点
  • 如果类型不同,并且有一个旧的fiber,我们需要删除旧的节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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也使用keys,这样可以更好的调解。例如,它检测子元素在元素数组中的位置发生了变化。

当旧fiber和element具有相同的type时,我们创建一个fiber,使DOM节点与旧fiber保持一致,使props与element保持一致。

1
2
3
4
5
6
7
8
9
10
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}

我们还向fiber添加了一个新属性:effectTag。我们将在稍后的提交阶段使用这个属性。

然后,对于元素需要一个新DOM节点的情况,我们用PLACEMENT effect tag标记新建的fiber。

1
2
3
4
5
6
7
8
9
10
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}

对于需要删除节点的情况,我们没有新建的fiber,所以我们在旧fiber上添加effect tag。

1
2
3
4
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}

但是当我们将fiber树提交到DOM时,我们从正在进行的root执行,它没有旧的fibers。

所以我们需要一个数组来跟踪要删除的节点

1
2
3
4
5
6
7
function render(element, container) {
...
deletions = []
}

...
let deletions = null

然后,当我们将更改提交到DOM时,我们还使用该数组的fibers

1
2
3
4
function commitRoot() {
deletions.forEach(commitWork)
...
}

现在,让我们修改 commitWork函数来处理新的effectTags

1
2
3
4
5
6
7
8
9
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}

如果fiber有一个PLACEMENT effect tag,我们就像前面一样,从父fiber将DOM节点附加到节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}

commitWork(fiber.child)
commitWork(fiber.sibling)
}

如果是DELETION,则做相反的操作,删除子元素。

1
2
3
4
5
6
7
8
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}

如果是UPDATE,则需要使用更改后的props更新现有DOM节点。

1
2
3
4
5
6
7
8
9
10
11
12
} 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函数

1
2
3
function updateDom(dom, prevProps, nextProps) {
// TODO
}

我们将旧fiber中的props与新fiber中的props进行比较,去掉该去掉的props,设置新建的或更换的props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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是事件监听器,因此如果道具名称以on前缀开头,我们将以不同的方式处理它们。

1
2
3
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)

如果事件处理程序发生了更改,我们将其从节点中删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]
)
})

然后添加新的处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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]
)
})

codesandbox上尝试reconciliation版本。

Step VII: 函数组件

接下来需要添加的是对函数组件的支持

首先,让我们更改示例。我们将使用这个返回h1元素的简单函数组件

1
2
3
4
5
6
7
/** @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)

注意,如果我们将jsx转换为js,它将是:

1
2
3
4
5
6
7
8
9
10
11
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})

函数组件在两个地方不同:

  • 来自函数组件的fiber没有DOM节点

  • children来自于运行函数而不是直接从props中获得

1
2
3
4
5
6
7
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}

const elements = fiber.props.children
reconcileChildren(fiber, elements)

我们检查fiber类型是否是一个函数,然后依据它,我们需要不同的更新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
}

updateHostComponent中,我们执行与前面相同的操作

updateFunctionComponent中,我们运行函数来获取子元素

1
2
3
4
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}

以我们为例,这里是fiber.type是App函数,当我们运行它时,它会返回h1 element。

然后,一旦我们有了children,reconciliation就会以同样的方式进行,我们不需要改变任何东西。

我们需要改变commitWork函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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节点的fibers,我们需要更改两件事

首先,要找到一个DOM节点的父节点,我们需要沿着fiber树向上走,直到找到一个带有DOM节点的fiber

1
2
3
4
5
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom

在删除节点时,我们还需要继续操作,直到找到带有DOM节点的子节点为止

1
2
3
4
5
6
7
8
9
10
11
12
13
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 VIII: Hooks

最后一步。现在我们有了函数组件,让我们也添加状态

1
2
3
4
5
6
7
/** @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)

让我们将示例更改为经典的counter组件。每次单击它,它都会将状态增加1。

1
2
3
4
5
6
7
8
9
10
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />

注意,我们使用的是Didact.useState获取并更新计数器值

这里是我们调用例子中的Counter函数的地方。在这个函数中,我们调用useState

1
2
3
4
5
6
7
8
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
...
}

function useState(initial) {
// TODO
}

在调用函数组件之前,我们需要初始化一些全局变量,这样我们就可以在useState函数中使用它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let wipFiber = null
let hookIndex = null

function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
...
}

function useState(initial) {
// TODO
}

首先,我们将正在进行的工作设置为fiber

我们还向fiber添加了一个hooks数组,以支持在同一个组件中多次调用useState。我们跟踪当前hook索引。

当函数组件调用useState时,我们检查是否有一个旧hook。我们使用hook index检入fiberalternate

1
2
3
4
5
6
7
8
9
10
11
12
13
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]
}

如果我们有一个旧hook,我们把状态从旧hook复制到新hook,如果没有,我们就初始化状态。

然后我们将新的hook添加到fiber中,将hook索引增加1,并返回状态。

useState还应该返回一个函数来更新状态,因此我们定义了一个setState函数来接收一个操作(对于Counter示例,该操作是将状态递增1的函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  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]
}

我们将该action推到添加到hook的队列中

然后我们做一些类似于我们在render函数中所做的事情,设置一个新的正在进行的root工作作为下一个工作单元,这样工作循环就可以开始一个新的渲染阶段

但我们还没有运行这个action

1
2
3
4
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})

我们在下一次渲染组件时做这个,我们从旧的hook队列中获得所有的action,然后一个接一个地应用它们到新的hook状态,所以当我们返回状态时,它被更新了

这是所有。我们已经建立了自己版本的React。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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)

你可以在codesandboxgithub上使用它。

后记

除了帮助你理解React是如何工作的,这篇文章的目标之一就是让你更容易深入了解React代码库。这就是为什么我们在几乎所有地方使用相同的变量和函数名。

例如,如果你在一个真实的React应用程序中在你的函数组件中添加一个断点,那么调用堆栈会显示:

  • workLoop
  • performUnitOfWork
  • updateFunctionComponent

我们没有包括很多React特性和优化。例如,React有几个不同的功能:

  • 在Didact中,我们在渲染阶段遍历整棵树。React会遵循一些提示和启发式操作,跳过没有任何变化的子树
  • 我们还在提交阶段遍历整个树。React保留了一个链表,其中只包含有effects的fibers,并且只访问那些fibers
  • 每次构建一个新的工作进程树时,我们都会为每个fiber创建新的对象。React回收了以前树上的fibers
  • 当Didact在渲染阶段收到一个新的更新时,它会丢弃正在进行的工作树,并重新从根开始。React为每个更新标记一个过期时间戳,并使用它来决定哪个更新具有更高的优先级
  • 和更多的……

也有一些功能,你可以轻松添加:

  • 使用一个对象作为style prop
  • 扁平化children数组
  • useEffect hook
  • reconciliation使用key

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :