共计 10310 个字符,预计需要花费 26 分钟才能阅读完成。
一、这段代码怎么个逻辑
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") |