React
前沿
因为公司的这次分享主要与大家分享React源码相关的知识,我之前稍微理解过创建一个简单的React是一个什么样的过程,可是随着时间的过去,我的记忆逐渐淡忘了,然后我又去反复看了好几遍,想简单给大家介绍一下。
目录
- createElement函数
- render函数
- 并发渲染与调度
- Fibers
- Render 与 Commit 两大阶段(Phases)
- 调和算法 Reconciliation
回顾
//定义一个React元素
const element = (
<h1 title="foo">Hello</h1>
);
//从DOM中获取一个DOM node
const container = document.getElementById("root");
//将React元素渲染到DOM node上
ReactDOM.render(element, container)
我们将使用以上这个仅包含三行代码的React App,接下来,让我们把React代码替换成原生的JavaScript代码。
JSX转换成JS代码的过程是由Babel之类的构建工具来完成的。转换过程通常很简单:使用creatElement函数的调用来替换Tag内的代码,并将Tag名、props和children作为参数传入:
const element = React.createElement(
'h1',
{title: 'foo'},
'Hello, world!'
);
React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
title: 'foo',
children: 'Hello, world!'
}
};
type是一个字符串,用于指定我们要创建的DOM node的类型,它就是HTML元素;props是另一个对象,它具有JSX attributes中的所有键值对,它还有一个特殊的属性:children
我们需要替换的另一部分 React 代码是对ReacDOM.render 的调用。
render 函数是 React 改变 DOM 的地方,所以在这里我们手动实现一下 DOM 的更新:
const node = document.createElement(element.type)
node["title"] = element.props.title
1、首先我们使用element type属性创建一个DOM节点,在这个例子是h1。
2、然后我们将所有的element props分配到这个DOM节点中,在这里只有一个title。
3、接下来我们为children创建DOM节点。我们只有一个字符串作为children,所以我们将创建一个文本节点。
const text = document.creatTextNode("");
text["nodeValue"] = element.props.children;
4、最后,我们将textNode添加至h1,将h1添加至container。
node.appendChild(text);
container.appendChild(node);
完整代码如下:
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)
一、createElement函数
JSX转化成JS
我们从另外一个稍微复杂的例子来开始:
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
在之前的步骤中我们可以看到,一个element是一个含有type和props的对象。我们这个函数唯一要做的就是创建这个对象
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
);
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
children数组也可以包含字符串和数字这样的原始类型。所以我们为所有不是对象的内容创建一个独立的元素,并为其创建一个特殊的类型:TEXT_ELEMENT
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: [],
},
}
}
二、render
创建DOM node
接下来我们要编写简化版的ReactDOM.render 函数。
function render(element, container) {
//使用element type创建DOM节点
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
//递归地为每个 child 做同样的事,同时将element props分配给DOM node
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
container.appendChild(dom)
}
三、并发渲染与调度
在我们开始实现功能之前我们需要一次重构。
element.props.children.forEach(child =>
render(child, dom)
)
这里的递归调用便是症结所在。
一旦我们开始渲染,在整棵element tree渲染完成之前程序是不会停止的,如果这棵element tree过于庞大,它有可能会阻塞主进程太长时间,如果浏览器需要做类似于用户输入、拖拽保持动画流畅这样的高优先级任务,则必须等到渲染完成为止。
因此,我们将渲染工作分成几个小部分,在完成每个单元后,如果需要执行其他操作,我们将让浏览器中断渲染。
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 ,但在这里浏览器将在主线程空闲时进行回调,而不是指定回调何时运行。
requestIdleCallback 还为我们提供了 deadline 参数。我们可以用它来检查在浏览器需要再次控制之前我们有多少时间。
调度
1、任务优先级
UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接联系,不同事件产生的更新,它们的优先级是有差异的,所以更新优先级的根源在于事件的优先级。一个更新的产生可直接导致React生成一个更新任务,最终这个任务被Scheduler调度。
所以在React中,人为地将事件划分了等级,最终目的是决定调度任务的轻重缓急,因此,React有一套从事件到调度的优先级机制。
我们将围绕事件优先级、更新优先级、任务优先级、调度优先级,重点梳理它们之间的转化关系。
- 事件优先级:按照用户时间的交互紧急程度,划分的优先级
- 更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)
- 任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级
- 调度优先级: Scheduler依据React去更新任务生成一个调度任务,这个调度任务所持有的优先级
前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制,虽然与React有所区别,但等级的划分基本一致。
2、时间片
四、Fibers
为了组织各个工作单元,我们需要一个数据结构:fiber tree。
我们将为每一个 element 分配一个 fiber,而每个 fiber 将成为一个工作单元。
Fiber 的执行
举个例子,假设我们像渲染这样一个 element tree:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
在 render 函数中我们将会创建 root fiber,将其设置为 nextUnitOfWork。剩下的工作将在 performUnitOfWork 中进行,在那里我们将为每个 fiber 做三件事:
- 将 element 添加至 DOM
- 为 element 的 children 创建 fiber
- 选出下一个工作单元
//1、首先我们删除 render 函数中的原有代码。将创建 DOM node 的部分代码抽离处理,稍后进行填充
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
//2、在 render 函数中,我们将 nextUnitOfWork 设置为 Fiber Tree 的根节点:
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
//3、接下来,当浏览器准备好的时候,它将会调用我们的 workLoop 函数,从根节点开始执行 performUnitOfWork
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
}
//4、首先,我们创建一个 node 节点然后将其添加至 DOM,将这个 DOM node 保存在 fiber.dom 属性中以持续跟踪
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
}
//5、接着,为每一个chid创建一个新的 fiber
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,
}
}
// TODO return next unit of work
}
//6、将其添加到 Fiber Tree 中,它是 child 还是 sibling ,取决于它是否是第一个 child
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++
}
// TODO return next unit of work
}
//7、最后,我们选出下一个工作单元。首先寻找 child ,其次 sibling ,然后是 uncle ( parent 的 sibling)
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
}
}
五、Render 与 Commit 两大阶段(Phases)
为什么要分阶段?
每当我们在处理一个React element时,我们都会添加一个新的节点到DOM中,而浏览器在渲染完成整个数之前可能会中断我们的工作。在这种情况下,用户将看不到完整的UI。
如何分阶段
首先我们需要删除那部分对 DOM 进行修改的代码:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
//...
}
相反地,我们会跟踪 Fiber Tree 的根节点。我们称它为「进行中的 root」—— wipRoot。
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)
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = 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)
我们将这个步骤在 commitRoot 函数中完成。在这里我们将所有节点递归附加到 DOM 中。
六、协调
更新和删除节点的过程,我们称为协调。
目前我们只做了添加节点至DOM这个动作,那么更新和删除节点是怎么实现的呢?
我们需要将在render函数上接收到的elements与我们提交给DOM的最后一棵Fiber Tree进行比较。
保存当前渲染的Fiber Tree
因此,在完成commit之后,我们需要对最后一次commit到DOM的一颗Fiber Tree的引用进行保存。我们称它为currentRoot。同时我们也对那个Fiber添加了一个alternate属性。这个属性是对旧Fiber的链接,这个旧Fiber是我们在上个commit阶段向DOM commit的Fiber。
function commitRoot() {
commitWork(wipRoot.child)
// currentRoot: 最后一次 commit 到 DOM 的一棵 Fiber Tree
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
Reconcile
Reconcile的过程会在执行工作单元时完成。
现在我们把performUnitOfWork中用来创建新Fiber的部分代码抽离成一个新的reconcileChildren函数。
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++
}
}
在这里我们将用旧的Fibers与新的elements进行调和:
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
....
}
}
我们使用type属性对它们进行比较:
如果老的Fiber和新的element拥有相同的type,我们可以保留DOM节点并仅使用新的props进行更新。这里我们会创建一个新的Fiber来使DOM节点与旧的Fiber保持一致,而props与新的element保持一致。
- 我们还向Fiber中添加了一个新的属性effectTag,这里的值为UPDATE。为稍后我们将在commit阶段使用这个属性。
如果两者的type不一样并且有一个新的element,这意味着我们需要创建一个新的DOM节点。
- 在这种情况下,我们会用PLACEMENT effect tag来标记新的Fiber。
如果两者的type不一样,并且有一个旧的Fiber,我们需要删除旧节点。
- 在这种情况下,我们没有新的Fiber,所以我们把DELETION effect tag添加到旧Fiber中。
// 对于新旧 element 的处理
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) // 这里使用了一个数组来追踪我们想要删除的 node
}
变更commitWork以处理不同类型的变化
接下来,当我们要 commit 这些变更到 DOM 时,我们就会用到 deletions 这个数组中的 fibers。
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
为了处理前面定义的各种 effectTags,我们也需要对 commitWork 函数进行变更:
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)
}
- PLACEMENT: 这个DOM节点添加到父Fiber的节点上
- DELETION:删除这个child
UPDATE:使用最新的props来更新现有的DOM节点
- 这部分动作将由updateDOM函数来完成: 我们将旧Fiber的props与新Fiber的props进行比较,删除旧的props,并设置新的或者变更之后的props
- 针对event listener这种特殊的prop,我们将以不同的方式处理:如果event listener发生了变更我们会把它从node中移除,然后设置一个新的event listener
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]
)
})
}
最后更新于 2022-09-11 03:24:04 并被添加「」标签,已有 1932 位童鞋阅读过。
此处评论已关闭