Build your own React
Build your own React 的学习笔记
预览
Build your own React CN
前言
重写 React, 遵循 React 代码中的架构, 然而没有进行优化。基于 React16.8, 应用 hook 并删除了所有与类相干的代码。
零: review
首先回顾一些 React 的概念,上面是一个简略的 React 应用程序。一共三行代码,第一行定义了一个 React 元素, 第二行获取了 DOM 节点, 最初一行将 React 元素渲染到容器中。
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
第一行中,咱们应用了 JSX, JSX 不是无效的 JavaScript,咱们应用原生 js 替换它。通常通过 Babel 等构建工具,JSX 转换为 JS。应用 createElement
替换 JSX 标记,并将标签名,props,子级作为参数。
const element = React.createElement(
"h1",
{title: "foo"},
"Hello"
);
React.createElement
, 会依据参数创立一个对象。除了一些验证外,这就是 React.createElement
所做的全副。咱们能够间接 React.createElement
函数替换成它的输入。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
一个一般的 JavaScript 对象, 次要有两个属性 type
和props
。type
属性是一个字符串,示意咱们创立的 DOM 节点的类型。它也能够是一个函数,然而咱们留在前面说。props
是一个对象, props
中有一个非凡的属性 children
。在以后的状况children
是字符串,然而通常状况下它是蕴含更多元素的数组。接下来咱们须要替换ReactDOM.render
。
首先应用 type
属性,创立一个节点。咱们将 element
的所有 props
调配给该节点,目前只有 title
属性。而后咱们为子节点创立节点。咱们的 children
是一个字符串,因而咱们创立一个文本节点。
为什么应用
createTextNode
而不是innerText
呢?因为在之后都会以雷同的形式解决所有元素。
最初将 textNode 增加到 h1 中,h1 增加到 container 中。
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)
目前咱们领有了和之前一样的程序,然而没有应用 React。
一: createElement
咱们从一个新的程序开始,这次咱们应用本人的 React 替换原来的 React 代码。
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
咱们从编写本人的 createElement
开始。
const element = createElement(
"div",
{id: "foo"},
createElement("a", null, "bar"),
createElement("b")
)
const container = document.getElementById("root")
render(element, container)
createElement
须要做的就是创立一个 type
和props
的对象。createElement
函数中, children
参数应用 rest
运算符, children
始终就会为数组。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
};
例如, createElement("div", null, a, b)
会返回:
{
"type": "div",
"props": {"children": [a, b] }
}
目前 children
数组中会蕴含原始值,比方字符串和数字。咱们须要对它们进行包装。咱们创立一个非凡的类型TEXT_ELEMENT
。
在 React 源码中,不会包装原始值, 或者在没有子级的状况下创立空的数组。咱们这样做的目标是为了简化咱们的代码.
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],},
}
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
咱们如何让 Babel
在编译的过程中,应用咱们本人创立的 createElement
呢?咱们在配置 babel
的@babel/preset-react
插件时自定义 pragma
参数
二: render
接下来咱们须要编写本人的ReactDOM.render
。
目前咱们只关怀向 DOM 中增加内容,稍后解决更新和删除
咱们首先应用元素的类型创立 DOM
节点,而后将新节点增加到容器中
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)
}
之前增加了文本元素的节点,所以在创立节点时须要判断元素的类型
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 的库。
三: 并发模式
在这之前,咱们须要重构代码。
递归渲染存在问题,一旦开始渲染就无奈进行,直到咱们渲染实现整个树。如果树很大,会阻塞主线程过长的工夫。
????️: React Fiber 架构应用了链表树实现了可中断渲染,如果大家有趣味能够参考这篇文章
因而咱们须要把工作分解成几个小单元,在咱们实现每个单元后,有重要的事件要做,咱们中断渲染。
咱们应用 requestIdleCallback
实现循环, 浏览器会在闲暇时,执行 requestIdleCallback
的回调。React 的外部并不应用 requestIdleCallback
, React 外部应用 scheduler package, 通过requestIdleCallback
咱们还能够取得咱们还有多少可用工夫用于渲染。
????️: 对于 requestIdleCallback 的更多细节能够查看这篇文章,详解 requestIdleCallback
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}
????️: nextUnitOfWork 变量放弃了 Fiber 中须要工作节点援用或者为 null, 如果是 null 示意没有工作。
要开始咱们的 workLoop
, 咱们须要第一个工作单元(Fiber 节点),而后编写performUnitOfWork
函数,performUnitOfWork
函数执行工作,并返回下一个须要工作的节点。
四: Fibers
咱们须要一个数据结构 Fiber 树(链表树)。每一个元素都有对应的 Fiber 节点, 每一个 Fiber 是一个工作单元。
假如咱们须要渲染这样的一颗树:
render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
在 render
中,创立 Fiber,并将根节点的 Fiber 调配给 nextUnitOfWork
变量。余下的工作在 performUnitOfWork
函数进行,须要做三件事:
- 将元素增加到 DOM
- 为子节创立 Fiber
- 返回下一个工作单元
Fiber 树是一个链表树,每一个 Fiber 节点有 child
, parent
, sibling
属性
child
, 第一个子级的援用sibling
, 第一个同级的援用parent
,父级的援用
????️: 在 React 的 Fiber 节点中,应用
return
字段保留了对父 Fiber 节点的援用
遍历 Fiber 树 (链表树) 时应用了深度优先遍历,说一下遍历的过程:
- 从根节点 root 获取第一个子节点
- 如果 root 有子节点,将以后指针设置为第一个子节点,并进入下一次迭代。(深度优先遍历)
- 如果 root 的第一个子节点,没有子节点,则尝试获取它的第一个兄弟节点。
- 如果有兄弟节点,将以后指针设置为第一个子节点,而后兄弟节点进入深度优先遍历。
- 如果没有兄弟节点,则返回根节点 root。尝试获取父节点的兄弟节点。
- 如果父节点没有兄弟节点,则返回根节点 root。最初完结遍历。
好,接下来咱们开始增加代码, 将创立的 DOM 的代码独自抽离出, 稍后应用它
function createDom(fiber) {
const dom = fiber.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]
})
return dom
}
在 render
函数中,将 nextUnitOfWork
变量设置为 Fiber 节点树的根
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {children: [element],
},
}
}
当浏览器准备就绪,调用 workLoop,开始解决根节点
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(fiber) {
// 增加 DOM 节点
// 创立 Fiber
// 获取下一个解决工作的 Fiber 节点
}
首先创立 DOM, 并增加到 Fiber 节点的 dom
字段中,咱们在 dom
字段中保留对 dom
的援用
function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
}
if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
}
}
????️: 在 React 的 Fiber 节点中,
stateNode
字段,保留对 class 组件实例的援用, DOM 节点或其余与 Fiber 节点相关联的 React 元素类实例的援用。
接下来为每一个子元素创立 Fiber 节点。同时因为 Fiber 树是一个链表树,所以咱们须要为 Fiber 节点增加 child
, parent
, sibling
字段
function performUnitOfWork(nextUnitOfWork) {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, // 父 Fiber 节点的援用
dom: null,
}
if (index === 0) {
// 父 Fiber 节点增加 child 字段
fiber.child = newFiber
} else {
// 同级的 Fiber 节点增加 sibling 字段
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
在实现的以后节点的工作后,咱们须要返回下一个节点。因为是深度优先遍历,首先尝试遍历 child
,而后是sibling
, 最初回溯到parent
, 尝试遍历parent
的sibling
function performUnitOfWork(nextUnitOfWork) {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 字段
fiber.child = newFiber
} else {
// 同级的 Fiber 节点增加 sibling 字段
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
}
}
五: render 和 commit
目前存在的问题,在遍历 Fiber 树的时候,咱们目前会在这里向 DOM 中增加新节点,因为咱们应用requestIdleCallback
, 浏览器可能会中断咱们的渲染,用户会看到不残缺的 UI。这违反了一致性的准则。
????️: React 的外围准则之一是 ” 一致性 ”, 它总是一次性更新 DOM, 不会显示局部后果。
????️: 在 React 的源码中, React 分为两个阶段执行工作,
render
阶段和commit
阶段。render
阶段的工作是能够异步执行的,React 依据可用工夫解决一个或者多个 Fiber 节点。当产生一些更重要的事件时,React 会进行并保留已实现的工作。等重要的事件解决实现后,React 从中断处持续实现工作。然而有时可能会放弃曾经实现的工作,从顶层从新开始。此阶段执行的工作是对用户是不可见的,因而能够实现暂停。然而在commit
阶段始终是同步的它会产生用户可见的变动, 例如 DOM 的批改. 这就是 React 须要一次性实现它们的起因。
咱们须要删除 performUnitOfWork
函数中更改 DOM 的代码。
function performUnitOfWork(nextUnitOfWork) {if (!fiber.dom) {fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
// ...
咱们须要保留 Fiber 树根的援用, 咱们称其为正在工作的 root 或wipRoot
。
????️: 在 React 中 Fiber 树的根被称为
HostRoot
。咱们能够在通过容器的 DOM 节点获取,容器 DOM._reactRootContainer._internalRoot.current
。????️:
wipRoot
相似 React 源码中workInProgress tree
的根节点,在 React 应用程序中,咱们能够通过容器 DOM._reactRootContainer._internalRoot.current.alternate
, 获取workInProgress tree
的根节点。
let wipRoot = null
function render(element, container) {
wipRoot = {
dom: container,
props: {children: [element],
},
}
nextUnitOfWork = wipRoot
}
实现了所有的工作。咱们须要把整个 Fiber 树更新到 DOM 上。咱们须要在 commitRoot
函数中实现这个性能。
function commitWork(fiber) {if (!fiber) {return}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 递归子节点
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitRoot() {commitWork(wipRoot.child)
wipRoot = null
}
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1}
// 如果 nextUnitOfWork 为假, 阐明所有的工作都曾经做完了, 咱们须要进入 commit 阶段
if (!nextUnitOfWork && wipRoot) {
// 增加 dom
commitRoot()}
}
????️: 在 React 的源码中
commit
阶段从completeRoot
函数开始,在开始任何工作前,它将FiberRoot
的finishedWork
属性设置为 null。
六: 协调
目前为止,咱们仅仅向 DOM 中增加了内容,然而更新和删除呢?咱们须要将 render 函数接管到元素和提交到 DOM 上的最初的 Fiber 树进行比照。
因而在 commit
咱们须要保留最初的 Fiber 树的援用,咱们称之为 currentRoot
。咱们还将alternate
字段增加到每一个 Fiber 节点上,alternate
字段上保留了 currentRoot
的援用。
????️: 在 React 源码中,在第一次渲染实现后,React 会生成一个 Fiber 树。该树映射了应用程序的状态,这颗树被称为
current tree
。当应用程序开始更新时,React 会构建一个workInProgress tree
,workInProgress tree
映射了将来的状态。????️: 所有的工作都是在
workInProgress tree
上的 Fiber 上进行的。当 React 开始遍历 Fiber 时,它会为每一个现有的 Fiber 节点创立一个备份, 在alternate
字段中,备份形成了workInProgress tree
。
let nextUnitOfWork = null
let wipRoot = null
let currentRoot = null
function commitWork(fiber) {if (!fiber) {return}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 递归子节点
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitRoot() {commitWork(wipRoot.child)
// 保留最近一次输入到页面上的 Fiber 树
currentRoot = wipRoot
wipRoot = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
接下来咱们须要从 performUnitOfWork
函数中将创立 Fiber 的代码提取进去,一个新的 reconcileChildren
函数。在这里咱们将对 currentRoot
(当前页面对应的 Fiber 树) 与新元素进行协调。
function reconcileChildren(wipFiber, elements) {
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 字段,child 指向了第一个子节点
wipFiber.child = newFiber
} else {
// 同级的 Fiber 节点增加 sibling 字段
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(wipFiber, elements)
// 首先尝试子节点
if (fiber.child) {return fiber.child}
let nextFiber = fiber
while (nextFiber) {
// 尝试同级节点
if (nextFiber.sibling) {return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
咱们同时遍历旧的 Fiber 树,既 wipFiber.alternate
,和须要协调的新的元素。如果咱们疏忽遍历链表和数组的模版代码。那么在while
循环中,最重要的就是 oldFiber
和element
。element
是咱们须要渲染的 DOM, oldFiber
是上次渲染的 Fiber。咱们须要比拟它们,以确定 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
// ....
if (oldFiber) {oldFiber = oldFiber.sibling}
if (index === 0) {
// 父 Fiber 节点增加 child 字段,child 指向了第一个子节点
wipFiber.child = newFiber
} else {
// 同级的 Fiber 节点增加 sibling 字段
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
为了比拟它们咱们应用以下的规定:
- 如果
oldFiber
和element
具备雷同的类型,咱们保留 DOM 节点,并应用新的 props 更新 - 如果类型不同,并且有新元素。咱们须要创立一个新的 DOM 节点。
- 如果类型不同,存在之前的 Fiber,咱们须要移除旧节点
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) {// 更新节点}
if (!sameType && element) {// 新增节点}
if (!sameType && oldFiber) {// 删除节点}
if (oldFiber) {oldFiber = oldFiber.sibling}
if (index === 0) {
// 父 Fiber 节点增加 child 字段,child 指向了第一个子节点
wipFiber.child = newFiber
} else {
// 同级的 Fiber 节点增加 sibling 字段
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
在 React 中,React 应用了 key
, 能够更好的进行协调,应用key
能够检测元素在列表中地位是否扭转,更好的复用节点。
当之前的 Fiber 和新元素具备雷同的类型时,咱们创立一个新的 Fiber 节点,保留旧 Fiber 的 DOM 节点和元素的 props。
并且为 Fiber 增加了一个新的属性 effectTag
, 稍后在commit
阶段应用
????️: 在 React 源码中
effectTag
,effectTag
编码的是与 Fiber 节点相干的effects
(副作用)。React 中effectTag
应用了数字的模式存储,应用了按位或结构了一个属性集。更多内容请查看
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 (!sameType && element) {// 新增节点}
if (!sameType && oldFiber) {// 删除节点}
// ...
}
}
对于新增的节点,咱们在 effectTag
属性上,应用 PLACEMENT
标记进行标记。
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 && element) {
// 新增节点
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (!sameType && oldFiber) {// 删除节点}
// ...
}
}
对于须要删除节点,咱们不创立新的 Fiber,而是将 effectTag
设置为DELETION
, 并增加到旧的 Fiber 节点上。
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 && oldFiber) {
// 删除节点
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ...
}
}
当咱们在 commit
时, 咱们从新构建的 Fiber 节点树开始遍历,因为没有须要保留删除的旧节点。所以咱们须要额定应用一个数组 deletions
保留须要删除的旧节点
????️: 在 React 的源码,
workInProgress tree
的 Fiber 节点领有current tree
对应节点的援用。反之亦然。????️: 在 React 的源码中,会把所有须要在
commit
阶段,执行副作用的 Fiber 节点,构建为线性列表,以不便疾速迭代。迭代线性列表要比迭代树快的多,因为不须要迭代没有 side-effects 的节点。
let deletions = null
function render(element, container) {
wipRoot = {
dom: container,
props: {children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
当咱们进入 commit
阶段时,应用该数组中的 Fiber
function commitRoot() {deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
当初让我批改 commitWork
函数以解决新的 effectTag
字段
如果 effectTag
是PLACEMENT
, 与之前一样,将 DOM 增加增加到父节点上
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)
}
如果 effectTag
是DELETION
, 咱们从父节点上删除节点
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 === "DELETION") {
// 对于删除节点的解决
domParent.removeChild(fiber.dom)
}
// 递归解决子节点
commitWork(fiber.child)
commitWork(fiber.sibling)
}
如果 effectTag
是UPDATE
, 咱们应用新的 props
更新当初的 DOM
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 === "DELETION") {
// 对于删除节点的解决
domParent.removeChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
// 对于须要更新节点的解决
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
// 递归解决子节点
commitWork(fiber.child)
commitWork(fiber.sibling)
}
接下来须要实现 updateDom
函数
function updateDom(dom, prevProps, nextProps) {// TODO}
咱们应用旧的 Fiber 的 props 和新的 Fiber 的 props 进行比拟,移除删除的的 props,增加或更新已更改的 props
// 用于排除 children 属性
const isProperty = key => key !== "children"
// 用于判断是否更新了属性
const isNew = (prev, next) => key => prev[key] !== next[key]
// 用于判断在新的 props 上是否有属性
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// 删除之前的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {dom[name] = ""
})
// 增加或者更新属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {dom[name] = nextProps[name]
})
}
咱们须要对事件监听器做非凡的解决,如果 props 以 on
结尾, 咱们应用不同的形式去解决它们
// 判断 props 是否是 on 结尾
const isEvent = key => key.startsWith("on")
// 用于排除 children 属性,和 on 结尾的属性
const isProperty = key => key !== "children" && !isEvent(key)
如果事件处理程序产生了更改,咱们须要首先删除,而后增加新的处理程序
????️: 间接在 DOM 上增加事件处理程序的形式,有点相似
preact
中的解决形式
function updateDom(dom, prevProps, nextProps) {Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
// 如果事件处理程序产生了更新,获取新的 props 上没有
// 须要先删除之前的处理程序
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// 删除之前的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {dom[name] = ""
})
// 增加或者更新属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {dom[name] = nextProps[name]
})
// 增加事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
七: Function 组件
咱们须要增加的下一件事是对 Function 组件的反对。咱们批改下咱们的例子。
function App(props) {return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
render(element, container)
咱们将 jsx 转换为 js
function App(props) {
return createElement(
"h1",
null,
"Hi",
props.name
)
}
const element = createElement(App, {name: "foo",})
Function 组件和 DOM 次要有两个不同
- Function 组件的 Fiber 没有 DOM 节点
- children 来自 Function, 而不是间接从 DOM 中间接获取
咱们查看 Fiber 的类型是否为函数,并依据类型由不同的函数进行解决,如果是不同的 DOM,传入updateHostComponent
function performUnitOfWork(fiber) {
// 判断是不是函数组件
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {updateFunctionComponent(fiber)
} else {updateHostComponent(fiber)
}
// 接下来返回下一个须要解决的 Fiber 节点,因为是深度优先遍历,优先从子节点开始
if (fiber.child) {return fiber.child}
let nextFiber = fiber
while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
updateHostComponent
和咱们之前做的一样
function updateHostComponent () {if (!fiber.dom) {
// 创立 dom 节点
fiber.dom = createDom(fiber)
}
// 子元素
const elements = fiber.props.children
// 子元素与旧的 Fiber 进行子协调
reconcileChildren(wipFiber, elements)
}
updateFunctionComponent
运行函数组件获取children
。在咱们的例子中 App 会返回 h1 元素。一旦有了children
, 协调就能够依照之前的形式进行了。不须要进行任何批改。
function updateFunctionComponent () {
// 获取 Function 组件的 children
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
上面咱们须要批改 commitWork
函数。因为咱们的 Function 组件的 Fiber 节点没有 DOM 节点。咱们须要批改两件事。
首先如果要找到 DOM 节点的父节点,咱们须要顺次向上查找,找到带有 DOM 节点的 Fiber
function commitWork(fiber) {if (!fiber) {return}
// 父级 Fiber
let domParentFiber = fiber.parent
// 直到找到含有 dom 的 Fiber 节点
while (!domParentFiber.dom) {domParentFiber = domParentFiber.parent}
const domParent = domParentFiber.dom
// ...
}
在删除节点时,咱们须要向下直到找到含有 DOM 节点的 Fiber
function commitDeletion (fiber, domParent) {if (fiber.dom) {domParent.removeChild(fiber.dom)
} else {commitDeletion(fiber.child, domParent)
}
}
function commitWork(fiber) {
// ...
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
// 解决新增
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "DELETION") {
// 解决删除
commitDeletion(fiber, domParent)
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// 解决更新
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
// ...
}
八: hooks
最初一步。目前咱们有了 Function 组件,当初让咱们增加状态。上面是一个计数器的例子
function Counter() {const [state, setState] = Didact.useState(1)
return (<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
onst container = document.getElementById("root")
render(element, container)
咱们应用 useState
获取和更新计数器的值。在调用函数组件前,咱们须要初始化一些全局变量,以便在 useState
函数中应用它们。
首先获取正在工作的 Fiber,咱们在 Fiber 节点中增加 hooks
数组,应用数组的目标是为了反对多个 useState
。并且援用以后hooks
的索引。
// 以后正在工作的 Fiber
let wipFiber = null
// 以后 Fiber 的 hooks 的索引
let hookIndex = null
function updateFunctionComponent () {
// 正在工作的 Fiber
wipFiber = fiber
// 以后 hooks 的索引默认为 0
hookIndex = 0
// hooks 的汇合
wipFiber.hooks = []
// 获取 Function 组件的 children
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
当组件调用 useState
时,首先咱们查看是否之前是否有 hook,如果存在旧的 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
用于接管 action
, 用于更新状态。setState
会将 action
推入到 hook
的队列上。
而后咱们执行与 render
函数中相似的操作,咱们设置 nextUnitOfWork
开始进行新的渲染阶段。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
// 判断之前是否有状态
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [], // 更新队列}
const setState = (action) => {
// action 增加到队列中
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
// 当 nextUnitOfWork 不为空时,就会进入渲染阶段
nextUnitOfWork = wipRoot
deletions = []}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
????️: 这里简化了 setState, setState 只接管函数作为参数。
然而目前咱们还没有更新state
。在下次渲染组件时,咱们从旧的队列中获取所有action
。而后将它们逐个利用到新的 hook state 上。当咱们返回状态时,state 会被更新。
????️: 调用 setState,不会立即更新 state。而是在进入
render
阶段后更新 state,而后useState
会返回新的状态。
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) => {
// action 增加到队列中
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
// 当 nextUnitOfWork 不为空时,就会进入渲染阶段
nextUnitOfWork = wipRoot
deletions = []}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
咱们曾经建设了好了本人的 React。
结语
除了帮忙你了解 react 是工作原理外,本文的另一个目标是让你在后续可能更轻松深刻 React。所以咱们屡次应用了和 react 源码中一样的函数名以及变量名。
咱们省略了很多了 React 的优化
- 在
render
阶段遍历整棵树,然而 React 中会跳过没有任何更改的子树。 commit
阶段,React 会进行线性遍历- 目前咱们会每次都创立一个新的 Fiber,而 React 中会复用之前的 Fiber 节点
还有很多 …
咱们还能够持续增加性能,比方:
- 增加 key
- 增加 useEffect
- 应用对象作为款式的 props
- children 扁平化
参考
- Build your own React(基于 hooks 实现)
- Didact: a DIY guide to build your own React(基于 class 实现)
- didact