一、这段代码怎么个逻辑
const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);
babel帮我们转化(react17以后,react内置了转化工具)
const element = <h1 title="foo">Hello</h1>;
// 会转成
const element = React.createElement("h1", { title: "foo" }, "Hello");
最终转成对象
- type是dom节点的类型, 它是通过document.createElement 创建的标签名称
- type也可能是个函数,sep II 会讲到
children在这里是个字符串, 但是它通常是个数组,包含多个元素.
// 最终转成对象 const element = { type: "h1", props: { title: "foo", children: "Hello", }, };
二、react render干了啥
为了避免争论, 我用element标识react element, 用node标识Dom元素
const node = document.createElement(element.type)
node["title"] = element.props.title
// 不要直接抄作dom,方便最后一起操作
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
三、createElement函数
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
// to
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
直接放代码
function createElement(type, props, ...children) { // 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: [],
},
}
}
四、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)
}
五、并发模式
首先我们要先重构,应为render不能停止,可能会block主线程,导致用户input或者动画不流畅
首先我们要分成更小的单元,我们完成每个单元后,如果有其它事情要做,我们就可以让浏览器终端渲染
react用的是sheduler package. 但是概念上是一样的
let nextUnitOfWork = null
// requestIdleCallback给我们提供了deadline参数
// 我们可以知道到浏览器下一次控制渲染进程还剩多少时间
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
要开始使用循环,我们需要设置第一个工作单元,然后编写一个 performUnitOfWork 函数,该函数不仅执行工作,还返回下一个工作单元
关于requestIdleCallback和requestAnimationFrame 请看 https://segmentfault.com/a/11...
六、Fibers
为了组织工作单元,我们需要一个数据结构:纤维树。
我们将为每个元素使用一个fiber,每个fiber都是一个工作单元。
假如我们想渲染下面的树
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
在渲染中,我们将创建root fiber并将其设置为 nextUnitOfWork。其余的工作将在 performUnitOfWork 函数上进行,我们将为每个光纤做三件事:
- 将元素添加到dom中
- 将该元素的children创建fibers
- 选择nextUnitOfWork
看下方数据结构,这种数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都有一个链接到它的第一个child、下一个sibling和它的parent。
当我们完成对fiber的工作时,如果它有子fiber,则该fiber将是nextUnitOfWork。
在我们的示例中,当我们完成 div fiber的工作时,nextUnitOfWork将是 h1 fiber。
方式就是爸爸找儿子,儿子找弟弟,弟弟找叔叔的步骤
如果fiber既没有child也没有sibling,我们就去找“uncle”:也就是parent的sibling。就像示例中的 a 和 h2 fiber一样。
此外,如果parent没有sibling,我们会继续通过parent往上找,直到我们找到有sibling的父母,或直到我们到达根。如果我们已经到了根,就意味着我们已经完成了这个渲染的所有工作
现在我们开始写代码
这是原来的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)
}
我们将创建dom节点部分,放到自己的函数中,后面会用
// fiber
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
// 更新属性可换成updateDom(dom,{},fiber.props)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
在render函数中我们设置 nextUnitOfWork为root fiber
function render(element, container) {
// TODO set next unit of work
nextUnitOfWork = {
dom: container, // 跟节点
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
然后,当浏览器准备好时,它将调用我们的 workLoop,我们将开始在根上工作。
首先,我们创建一个新节点并将其附加到 DOM。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// 然后每个子节点,我们创建一个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, // props包含children和属性
parent: fiber,
dom: null,
}
// 我们将newFiber添加到当前fiber树中,将其设置为child或sibling,具体取决于它是否是第一个孩子。
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 最后我们寻找nextUnitOfWork。我们首先是child,然后sibling,然后是叔叔uncle。
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
七、渲染和提交 commit
有一个问题,每次处理元素时,我们都会向 DOM 添加一个新节点。而且,请记住,浏览器可能会在我们完成渲染整个树之前中断我们的工作。在这种情况下,用户将看到不完整的 UI。我们不希望那样
所以我们需要从这里移除改变 DOM 的部分,看注释1
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 1. 移除改变 DOM 的部分
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
...
}
代替,我们将跟踪fiber tree的根。我们称其为正在进行的工作 root 或 wipRoot。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
一旦我们完成了所有的工作(我们知道这是因为没有nextUnitOfWork)我们将整个fiber tree提交给 DOM。
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
// 在这里,我们递归地将所有节点附加到 dom。
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
八、Reconciliation 调和
到目前为止,我们只向 DOM 添加了东西,但是更新或删除节点呢?
这就是我们现在要做的,我们需要将我们在render函数上收到的element与我们提交给 DOM 的最后一个fiber树进行比较
因此,我们需要在完成提交后, 保存对“我们提交给 DOM 的最后一个fiber树”的引用。我们称之为currentRoot。
我们还为每个fiber添加了alternate属性。该属性是old filber的链接,即我们在前一个提交阶段提交给 DOM 的fiber
function commitRoot() {
commitWork(wipRoot.child)
// 新加的引用
currentRoot = wipRoot
wipRoot = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
现在让我们从 performUnitOfWork 中提取创建新filber的代码......
到一个新的 reconcileChildren 函数
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 新加
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// 寻找下一个
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
在reconcileChildren中,我们将协调旧fiber与新elements。
我们同时迭代旧fiber (wipFiber.alternate) 的子children和我们想要协调的elements数组。
如果我们忽略同时迭代数组和链表所需的所有样板文件,那么我们只剩下这个 while 中最重要的东西:oldFiber 和 element
元素是我们想要渲染到 DOM 的东西,而 oldFiber 是我们上次渲染的东西。
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber = wipFiber.alternate && wipFiber.alternate.child // 新加
let prevSibling = null
while (index < elements.length || oldFiber != null) { // oldFiber
const element = elements[index]
let newFiber = null
// 我们需要比较它们, 以查看是否需要对DOM做改变。
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
// 这里 React 也使用了key,这可以更好地协调。例如,它检测子元素何时更改元素数组中的位置
if (sameType) {
// update the node 如果旧的 Fiber 和新的元素具有相同的类型,我们可以保留 DOM 节点并使用新的 props 更新它
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", // 我们稍后会在commit阶段使用这个属性
}
}
if (element && !sameType) {
// add this node 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT", // PLACEMENT 标签标记新的fibr。
}
}
// *******但是当我们将fiber树提交到 DOM 时,是在work in progress root完成的,是没有old fiber的
// 所以我们需要一个数组来跟踪我们想要删除的节点。看下方的render
if (oldFiber && !sameType) {
// delete the oldFiber's node 如果类型不同并且有fiber,我们需要删除旧节点
oldFiber.effectTag = "DELETION" // 我们没有新的 Fiber,因此我们将效果标签添加到旧的 Fiber
deletions.push(oldFiber)
}
if (oldFiber) { // 如果是假,下一个oldFiber还是假
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
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
然后,当我们将更改提交到 DOM 时,我们也会使用该数组中的fiber。
function commitRoot() {
deletions.forEach(commitWork) //
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
现在,让我们更改 commitWork 函数来处理新的 effectTags。
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
) {
// 如果是 UPDATE,我们需要用改变的 props 更新现有的 DOM 节点。
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
updateDom
一个特殊的props是事件监听,如果props以on开头,我们需要处理不同的
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) {
// TODO 主要就是更新props
//Remove 旧的或者已经改变的 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]
)
})
// 添加新的或者已经改变的props
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[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]
})
}
到这里可以测试一下上面的代码了
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const container = document.getElementById("root")
const updateValue = e => {
rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
Didact.render(element, container)
}
rerender("World")
系列
重学react——slot
重学react——state和生命周期
重学react——redux
重学react——hooks以及原理
重学react——context/reducer
重学react——router
重学react——高阶组件
build your own react
React——fiber
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。