Build your own React_4 理解React纤维

Posted 一只前端小马甲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Build your own React_4 理解React纤维相关的知识,希望对你有一定的参考价值。

前端工程师的要求越来越高,仅懂得“三大马车”和调用框架API,已经远不能满足岗位的能力要求。因此增强自身的底层能力,了解框架的内部原理非常重要。本系列文章,翻译自Rodrigo Pombo的《Build your own React》一文,同时每篇文章最后,都会加入自己的理解,一方面记录自己初探React框架原理的过程,另一方面也是想与各位大牛多多交流,以出真知。

我们打算从零开始重写一个React框架,在遵循源码架构的基础上,省略了一些优化和非核心功能代码。

假设你阅读过我之前的文章《build your own React》,那篇文章是基于React 16.8版本,当时还不能使用hooks来替代class。

你可以在Didact仓库找到那篇文章和对应的代码。这里还有个相同主题的视频,跟文章的内容有些区别的,但是可以参考观看。

从零开始重写React框架,我们需要遵循以下步骤:

步骤四:理解React纤维

为了组织渲染任务单元,我们需要一种数据结构:纤维树。

每个React元素都跟唯一纤维对应,纤维也表示一个渲染任务单元。

这里有个例子,假如你想渲染的元素树如下:

Didact.render(
	<div>
		<h1>
			<p />
			<a />
		</h1>
		<h2></h2>
	</div>,
	container
)

在render函数中我们创建根纤维并将其设置为下个渲染任务单元(nextUnitOfWork)。剩下的工作都会在performUnitOfWork函数中进行,对于任意纤维,我们需要做三件事情:

  1. 将元素添加至DOM
  2. 创建元素的子元素的纤维
  3. 选择下个渲染任务单元


使用这种数据结构的目的在于:轻松地找到下个渲染任务单元。这也是为什么每个纤维跟它相邻的纤维都有连接,这个相邻的纤维可能是第一个子纤维,第一个子纤维的兄弟纤维或者是父纤维。


当完成了一个纤维的渲染工作,它的子纤维会成为下个渲染任务单元。

在我们的例子中,当我们完成了div纤维的渲染工作,接下来要对h1进行渲染。

如果纤维没有子纤维,我们把它的兄弟纤维作为下个渲染任务单元。

例如下图,p元素对应纤维没有子纤维,因此当p渲染完成之后我们会渲染a纤维。

如果纤维既没有子纤维,又没有兄弟纤维,我们则考虑它的“叔叔”纤维——父纤维的兄弟纤维。例如上图中的a和h2。

同时,如果父纤维没有兄弟纤维,我们就继续往上找,直至找到“叔叔”纤维或者根纤维。如果到达了根纤维,表示整个渲染工作已经完成。

接下来,我们来看看代码。

首先,我们将步骤二中的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)


let nextUnitOfWork = null

我们先将创造DOM节点的部分保存在createDom函数中,我们之后再用它。

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

在render函数中,我们将纤维树的根纤维设置为下个渲染任务单元。

function render(element, container)
	nextUnitOfWork = 
		dom: container,
		props: 
			children: [element],
		,
	

然后,当浏览器准备好后,它会调用我们的workLoop函数,我们将从根元素开始渲染。

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

首先,我们创建一个新的节点,并将它添加至DOM中。

我们通过fiber的dom属性来获取相应的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
// 

然后,对纤维的每个子元素创造相应的纤维。

// 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

接着,我们将新生成的纤维加入纤维树,它可能是个子纤维或者是兄弟纤维,取决于他是否是父纤维的第一个子纤维。

// 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

最后,我们来找到下个渲染任务单元。我们首先尝试下子纤维,其次是兄弟纤维,然后是”叔叔“纤维,以此类推。

 // prevSibling = newFiber
 // index++
 // 

if(fiber.child)
	return fiber.child


let nextFiber = fiber
while(nextFiber)
	if(nextFiber.sibling)
		return nextFiber.sibling
	
	nextFiber = nextFiber.parent

这就是我们的performUnitOfWork函数。

function performUnitOfWork(fiber)
	// create a new dom node
	if(!fiber.dom)
		fiber.dom = createDom(fiber)
	

	if(fiber.parent)
		fiber.parent.dom.appendChild(fiber.dom)
	

	// create new fibers
	const elements = fiber.props.children
	let index = 0
	let prevSibliing = 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++
	
	// return next unit of work
	if(fiber.child)
		return fiber.child
	
	let nextFiber = fiber
	while(nextFiber)
		if(nextFiber.sibling)
			return nextFiber.sibling
		
		nextFiber = nextFiber.parent
	

总结

步骤三中,作者提出了通过把整个渲染任务拆成小单元,来解决传统渲染无法中止问题的思路。

步骤四则详细介绍了把任务拆成小单元的实现,并给小单元了个新的名字——纤维。纤维是一种自定义的数据结构,反映了原始React元素的信息,同时也能获取到最终渲染的DOM节点,React并不是将元素树直接渲染为DOM,而是将元素树转化为fiber树的同时渲染DOM。我们在步骤三并发模式中知道了performUnitOfWork会获取下个渲染任务单元,实际上就是获取下个fiber,因此performUnitOfWork的实现非常重要。获取下个fiber需要三步:首先根据当前对象生成DOM,并添加至父节点;其次将当前对象的所有子元素遍历,生成相应的fiber;最后返回下个fiber。

上一篇传送门:Build your own React_3 并发模式
下一篇传送门:Build your own React_5 渲染和提交阶段

以上是关于Build your own React_4 理解React纤维的主要内容,如果未能解决你的问题,请参考以下文章

Build your own React_0 总述

Build your own React_0 总述

Build your own React_8 Hooks

Build your own React_7 函数组件

Build your own React_8 Hooks

Build your own React_7 函数组件