随笔:关于Fiber架构的一点点理解
Posted 我是真的不会前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了随笔:关于Fiber架构的一点点理解相关的知识,希望对你有一定的参考价值。
什么是fiber:react16以后的一个虚拟dom思想
fiber:纤维 :很小很小絮状物
react 16版本以前的虚拟dom
其实react16以前对于虚拟dom和vue2.0是很相似的。
vue2.0通过snabdom生成虚拟dom ,diff运算 而vue3则做了优化
推荐一个开源作者租户做的一个react的网址(还有教程)
https://pomb.us/build-your-own-react/
一切从React.createElement开始
我们都知道其实jsx是语法糖。真正的react理论上只能读懂下列这种语法
const element =React.creatElement(
'h1',
titile:'foo',
'hello'
)
创建了一个对象 包含这个dom的参数。接着给改改
const element =React.creatElement(
type:'h1',
props:
title:'foo',
children:'hello'
,
)
这个type表示我们将要创建的dom结构。用document.createElement这个原生方法创建节点。我们也可以把他构造成函数
props你可以理解成就是一个对象。children可以是个字符串也可以是数组。可以放置更多的element
这里的element可以指react的元素,
这就是为什么说elelments同样也可以是一棵树。
ReactDom.render()他到底渲染了什么
- 我们需要替换并调用这个方法。当dom改变时,可以触发并重新调用render方法更新。再通过diff运算 并最终触发commit提交。
- 首先,我们用type字段创建了一个node在h1。
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)
- 如果还有孩子,再用text创建一个文本节点。并用append进去。再创建根节点containier。再把h1放进去。这样未用react即创建了一个节点树。
createElement函数
function createElement(type, props, ...children)
return
type,
props:
...props,
children,
,
元素是一个带有type和的对象props(入参)。我们的函数唯一需要做的就是创建那个对象。
我们使用扩展运算符来结构该props,其他用restful参数语法交给children,这样的children(restful参数,一般放最后一个)道具将永远是一个数组。
言简意赅点说:
childern是个对象,用restful参数接收,放进函数中
这个children数组可以包含原始数据类型。还可以是jsx。如果这个chilren是个基础数据类型,并为他们创造一种特殊类型: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: [],
,
总结:
一个用于创建文本节点,一个用于创建真实的reac元素
render方法
我们知道render方法会触发虚拟dom进行diff运算。
现在我们可以试着写一个自己的render方法看看其原理。
- 首先 我们现在添加一个方法到dom上。我们需要用到更新和删除相关方法。
- 我们使用元素类型创建 DOM 节点,然后将新节点附加到容器中,也可以理解react中的app。
function render(element, container)
const dom = document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
- 创建一个函数,入参第一个是元素,第二个根容器。从根节点一层一层追加进去,有孩子就遍历。递归地为每个孩子做同样的事情。
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
- 我们还需要处理文本元素,如果元素类型是TEXT_ELEMENT我们创建一个文本节点而不是常规节点。
- 我们需要再加上元素属性。
- 我们对Key名遍历。我们把children放在props中。把children先过滤掉。对其他属性进行遍历,把所有属性都添加到dom属性上。
- 现在做成了一个库可以支持jsx了
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name =>
dom[name] = element.props[name]
)
并发模式
但是问题来了 如果render很复杂层级复杂
就会很卡
这个时候我们引入一个并发模式,进行重构
一旦开始渲染,递归是无法停止。直到吧所有的元素树都渲染上去。如果元素树很大,很有可能会阻塞浏览器的主线程。浏览器需要做一些高优先级任务的时候时,解决用户输入或者动画问题时。这将导致render不得不等待高优先级任务渲染完成。(说白了就会变得很卡)
unit
尝试重构时。引入一个unit概念,把大的render任务切割成小任务。当我们执行每一个单元,我们让浏览器中断渲染。(
把递归改造成一个一个小任务,允许浏览器中断,可以让浏览器执行优先级更高的任务)
我们使用requestIdleCallback 去做任务的切割,实现一个循环,你可以认为这个方法就是一个settimeout延时器,但是实际上运行时,浏览器会调用他的callback方法。当我们用这个方法执行任务时。
var t =requestIdleCallbac(Fn)
当我们直接调用fn()时,执行同步执行
如果按照上面代码这么执行,等于等待ts时间再来执行这个方法。t可以不确定,根据浏览器线程来决定。当主线程空闲的时候 ,我就执行他。请求空闲时间执行Fn。
window.requestIdleCallback(callback)的callback中会接收到默认参数 deadline ,其中包含了以下两个属性:
timeRamining 返回当前帧还剩多少时间供用户使用
didTimeout 返回 callback 任务是否超时
window.requestIdleCallback(callback)的callback中会接收到默认参数 deadline。其中包含了以下两个属性:
timeRamining 返回当前帧还剩多少时间供用户使用 didTimeout 返回 callback 任务是否超时
schedule
react源码中自带的包
现在的react已经不使用requestIdleCallback了。现在使用调度程序包。但是对于这个用例,它在概念上是相同的
requestIdleCallback还给了我们一个截止日期参数。我们可以使用它来检查在浏览器需要再次控制之前还有多少时间。
执行单元工作
while (nextUnitOfWork)
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
- 下一个单元任务,把整个render过程变成unitwork,用这些api不断执行下一个单元工作,每次执行完返回一个新的单元工作。进行一个循环,而每次循环都要返回一个新工作。
- 要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork函数,该函数不仅执行工作而且返回下一个工作单元。
- 总结一下:
封装一个执行任务,执行一个后返回一个下一个
通过requestIdlerequest切割成一个一个小单元
fiber的到来
Fiber 可以理解为一个执行单元,每次执行完一个执行单元,react 就会检查现在还剩多少时间,如果没有时间则将控制权让出去。
首先 React 向浏览器请求调度,浏览器在一帧中如果还有空闲时间,会去判断是否存在待执行任务,不存在就直接将控制权交给浏览器,如果存在就会执行对应的任务,执行完成后会判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则就会将控制权交给浏览器。
组织一个单元我们需要一个数据结构,用传统的虚拟dom很难再切割。我们现在构造一个新的结构。我们称之为fiberTree
每一个react元素都可以被创建成一个fiber。并且每一个fiber都是一个unitwork
假设我们现在要渲染一个tree
React.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
app
)
React Fiber 就是采用链表实现的。每个 Virtual DOM 都可以表示为一个 fiber,如下图所示,每个节点都是一个 fiber。一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于以下的数据结构
为什么选择链表,因为链表可以使每个数据指针有所指引。更方便找到下一个节点
Fiber 是 React 进行重构的核心算法,fiber 是指数据结构中的每一个节点
render我们将创建根fiber并将其设置为nextUnitOfWork. 其余的工作将发生在performUnitOfWork函数上,我们将为每个fiber做三件事:
- 将元素添加到 DOM
- 为元素的子元素创建fiber
- 选择下一个工作单元
在设计这个数据结构的目标是使得找到下一个数据单元更加容易。
这种数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都有一个链接到它的第一个子节点、下一个兄弟节点和它的父节点。
当我们完成对光纤的工作时,如果它有Fiber,则child该光纤将是下一个工作单元。
在我们的示例中,当我们完成div Fiber的工作时,下一个工作单元将是h1 Fiber。
如果Fiber没有child,我们将使用sibling作为下一个工作单元。
此外,如果parent没有 a sibling,我们会继续向上遍历parents 直到找到一个带有 a sibling或直到我们到达根。如果我们已经到了根,就意味着我们已经完成了为此的所有工作render。
Fiber节点设计
Fiber 的拆分单位是 fiber(fiber tree上的一个节点),实际上就是按虚拟DOM节点拆,我们需要根据虚拟dom去生成 Fiber 树。下文中我们把每一个节点叫做 fiber 。
简单点说逻辑
- 每个Fiber都有三个指针指向他的第一个儿子,他的下一个兄弟,和他的父亲
- 当unit工作完成,要完成下一个单元时,先完成第一个子Fiber。孩子优先级更高
- 没孩子找直接兄弟
- 没孩子没直接兄弟找叔叔
- 没有叔叔继续往上找爸爸的叔叔
- 一直找,找到root就结束
代码实现
首先,让我们从render函数中删除这段代码。
我们将创建 DOM 节点的部分保留在自己的函数中,稍后我们将使用它。
在render函数中我们设置nextUnitOfWork为FIBER Tree的根。
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)
nextUnitOfWork =
dom: container,
props:
children: [element],
,
然后,当浏览器准备好时,它会调用我们的workLoop,我们将开始在根节点上工作。
首先,我们创建一个新节点并将其附加到 DOM。
我们在fiber.dom属性中跟踪 DOM 节点。
然后我们为每个孩子创造一个新的纤维。
我们将它添加到纤维树中,将其设置为孩子或兄弟姐妹,具体取决于它是否是第一个孩子。
最后我们寻找下一个工作单元。我们首先尝试与孩子,然后是兄弟姐妹,然后是叔叔,依此类推。
这就是我们的performUnitOfWork.
function workLoop(deadline)
let shouldYield = false
while (nextUnitOfWork && !shouldYield)
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
requestIdleCallback(workLoop)
requestIdleCallback(workLoop)
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
在完成整个树的渲染时候,浏览器可能会中断我们的操作。因此我们需要remove那些修改代码的地方。在循环的时候不断在做dom操作,在render中不断生成fiber 不断生成dom。被中断之后把修改dom的代码删掉,减少dom操作。
dom操作在commit阶段
render 阶段:这个阶段是可中断的,会找出所有节点的变更
commit 阶段:这个阶段是不可中断的,会执行所有的变更
当创建好fiber Tree时 再一次性的提交所有的fiber到dom中。render可以说是异步的,render的方法不会霸占浏览器主线程。把一个个单元做完,返回一个大的fiberTree。
但commit不能中断,从根节点一层层追加。以此可以看到一个完整UI,commit会霸占完整的线程。
收集effect list
知道了遍历方法之后,接下来需要做的工作就是在遍历过程中,收集所有节点的变更产出effect list,注意其中只包含了需要变更的节点。通过每个节点更新结束时向上归并effect list来收集任务结果,最后根节点的effect list里就记录了包括了所有需要变更的结果。
收集effect list的具体步骤为:
- 如果当前节点需要更新,则打tag更新当前节点状态(props, state, context等)
- 为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect
list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点 - 如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点
- 如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。
到目前为止,我们只向DOM添加了东西,但是更新或删除节点呢?
这就是我们现在要做的,我们需要将在render函数上接收到的元素与我们提交给 DOM 的最后一个 Fiber 树进行比较。
因此,我们需要在完成提交后保存对“我们提交给 DOM 的最后一个FiberTree”的引用。我们称之为currentRoot。
将alternate属性添加到每个fiber中。该属性是旧fiber的链接,即我们在前一个提交阶段提交给 DOM 的纤程。
现在让我们从中提取performUnitOfWork创建新纤程的代码……
…到一个新的reconcileChildren功能。
协调旧fiber与新element(diff运算)
同时迭代旧纤程 ( wipFiber.alternate) 的子元素和需要协调的元素数组。
如果我们忽略同时迭代数组和链表所需的所有样板文件,那么我们只剩下 while:oldFiber和 中最重要的内容element。这element是我们想要渲染到 DOM 的oldFiber东西,也是我们上次渲染的东西。
我们需要比较它们以查看是否需要对 DOM 应用任何更改。
为了比较它们,我们使用以下类型:
- 如果旧的 Fiber 和新的元素具有相同的类型,我们可以保留 DOM 节点并使用新的 props 更新它
- 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点(不做dom操作了)
- 如果类型不同并且有旧Fiber,我们需要删除旧节点
这里 React 也使用了Key(如果有key,更好的diff),这可以更好地协调。例如,它检测子元素何时更改元素数组中的位置。
如果有多个孩子,位置会发生变化,有利于更好的diff运算,也可以说是更好的协调
当旧纤程和元素具有相同的类型时,我们创建一个新fiber,保留旧fiber中的 DOM 节点和元素中的 props。
我们还为纤程添加了一个新属性:effectTag. 我们稍后会在提交阶段使用这个属性。
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",
元素需要一个新的 DOM 节点的情况,我们用PLACEMENTeffect 标签来标记新的 Fiber。(新Fiber是由旧Fiber计算而来,并且给新Fiber添加标记)
对于我们需要删除节点的情况,我们没有新的Fiber,所以我们将效果标签添加到旧光纤。
但是当我们将纤程树提交到 DOM 时,我们是从正在进行的工作根中完成的,它没有旧fiber。
所以需要一个数组来跟踪我们想要删除的节点。
然后,当我们将更改提交到 DOM 时,我们也会使用该数组中的Fiber。
现在,让我们更改commitWork函数来处理新的effectTags.
如果纤程有PLACEMENT效果标签,我们和之前一样,将 DOM 节点附加到来自父纤程的节点。
如果是DELETION,我们做相反的事情,移除孩子。
如果是UPDATE,我们需要使用更改的 props 更新现有的 DOM 节点。
UpdateDom
这个updateDom函数中做到真实的dom操作。
我们将旧光纤中的道具与新光纤的道具进行比较,将消失的属性移除,并设置新的或更改的属性。
我们需要更新的一种特殊道具是事件侦听器,因此如果道具名称以“on”前缀开头,我们将以不同的方式处理它们。
先删旧的事件处理器,再添加新的事件处理器
如果事件处理程序发生变化,我们将其从节点中删除。
然后我们添加新的处理程序。
总结
新fiber一定从旧fiber来
生成新fiber一定要依据旧fiber计算而来
每一个新fiber都要跟旧fiber做一个指针链接
添加effcetTag
1.更新:dom节点不同删除只更新属性
2.删除:删掉这个节点
3.插入:插入新节点
这个最终交给commitWork来使用,根据标签来操作。进行diff/协调阶段
在函数式组件中
使用这个简单的函数组件,它返回一个h1元素。
function App(props)
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
const element = Didact.createElement(App,
name: "foo",
)
函数式组件在两个方面有所不同:
函数组件的 Fiber 没有 DOM 节点 并且子节点们来自调用该函数而不是直接从 props
我们检查Fiber类型是否是一个函数,并根据它转到不同的更新函数。
在updateHostComponent我们做的和以前一样。
在updateFunctionComponent我们运行函数来获取子节点。
对于我们的示例,这里fiber.type是App函数,当我们运行它时,它返回h1元素。
然后,一旦我们有了孩子,和解就以同样的方式进行,我们不需要在那里改变任何东西。
我们需要改变的是commitWork函数。
现在我们有了没有 DOM 节点的纤程,我们需要改变两件事。
首先,为了找到一个 DOM 节点的父节点,我们需要沿着纤程树向上移动,直到找到一个带有 DOM 节点的纤程。
当删除一个节点时,我们还需要继续下去,直到找到一个具有 DOM 节点的子节点。
以上是关于随笔:关于Fiber架构的一点点理解的主要内容,如果未能解决你的问题,请参考以下文章