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 渲染和提交阶段

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值