Build your own React_6 调解器

Posted 一只前端小马甲

tags:

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

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

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

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

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

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

步骤六:解调器

到目前为止我们只是往DOM中添加节点,如果需要更新或者删除怎么办呢?

这正是这节会介绍的内容,我们需要将来自render函数的元素和最近一次提交的纤维树做对比。

因此,我们需要在提交后,保存指向“最近一次提交纤维树”的引用。我们把它叫做currentRoot。

我们还将给每个纤维添加alternate属性,它指向老的纤维,即我们最近一次提交给DOM的纤维。

function commitRoot()
	// commitWork(wipRoot.child)
	currentRoot = wipRoot
	// wipRoot = null


// function commitWork(fiber)
//	if(!fiber)
//		return
//	
//	cosnt 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

现在让我们提炼performUnitOfWork中创建新纤维的代码,创建一个新的函数reconcileChildren。

function performUnitOfWork(fiber)
	if(!fiber.dom)
		fiber.dom = createDom(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,
			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
	

这里我们将老的纤维关联新的元素。

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
	// prevSibling = null

	// while(index < elements.length)
	//		

我们同时遍历老的纤维(wipFiber.alternate)的所有子纤维,以及我们希望解调的元素数组。

如果我们忽略所有需要解调的元素数据和子纤维,我们将忽略了最重要的东西——老的纤维和元素。元素指我们希望渲染至DOM中的东西,老的纤维指我们最近一次渲染的东西。

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

		// if(oldFiber)
		//	oldFiber = oldFiber.sibling
		// 
	

我们需要对它们作比较,判断DOM是否需要变化。

我们通过类型进行比较:

  • 如果老的纤维和新的元素是类型相同,我们无需改变DOM节点,只需要使用新props更新属性即可
  • 如果类型发生了变化,并且此时有个新的元素加入,我们需要创建一个新的DOM节点
  • 如果类型发生了变化,并且此时有个老的纤维,我们需要移除老的DOM节点

React会通过使用keys保证更好的调解。它能监测出元素数组中的子元素什么时候发生变化。

// oldFiber != null
// )
//	const element = elements[index]
// let newFiber = null

const sameType = 
	oldFiber &&
	element &&
	element.type === oldFiber.type

if(sameType)
	// TODO update the node

if(element && !sameType)
	// TODO add the node

if(oldFiber && !sameType)
	// TODO delete the oldFiber's node


// if(oldFiber)
//	oldFiber = oldFiber.sibliing
// 

// if(index===0)

当老的纤维和元素具有相同的类型,我们创造一个新的纤维,把它跟老的纤维对应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"
	

// if(element && !sameType)
	// TODO add this node
// 
// if(oldFiber && !sameType)
	// TODO delete the oldFiber's node

当传来的元素需要增加一个新的DOM节点,我们给新的纤维一个"PLACEMENT"标签。

if(element && !sameType)
	newFiber = 
		type: element.type,
		props: element.props,
		dom: null,
		parent: wipFiber,
		alternate: null,
		effectTag: "PLACEMENT"
	

// if(oldFiber && !sameType)
	// TODO delete the oldFiber's node
// 

// if(oldFiber)
//	oldFiber = oldFiber.sibling
// 

当我们需要删除DOM节点时,我们不需要创建新的纤维,只要给老的纤维标签赋值。

但当我们提交当前的纤维树转化为DOM时,我们的渲染工作从当前渲染根节点(work in progress root)开始,但当前的纤维树并不包含老纤维。

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

// if(oldFiber)
//	oldFiber = oldFiber.sibling
// 

所以我们需要一个数组来保存希望移除的DOM节点。

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

// function workLoop(deadline)
//	let shouldYield = false

接着,我们将变化提交给DOM,我们会使用数组中的纤维。

function commitRoot()
	deletions.forEach(commitWork)
//	commitWork(wipRoot.child)
//	currentRoot = wipRoot
//	wipRoot = null


// function commitWork(fiber)
//	if(!fiber)return
//	cosnt domParent = fiber.parent.dom
//	domParent.appendChild(fiber.dom)

现在,配合新增的effectTags属性,我们来修改下commiitWork函数。

// function commitRoot()
//	deletions.forEach(commitWork)
//	commitWork(wipRoot.child)
//	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,

如果纤维的effectTag属性为"PLACEMENT",我们将DOM节点添加至父纤维的DOM结构中。

//function commitWork(fiber)
//	if(!fiber)
//		return
//	
//	const domParent = fiber.parent.dom
	if(
		fiber.effectTag === "PLACEMENT" &&
		fiber.dom != null
	)
		domParent.appendChild(fiber.dom)
	

//	commitWork(fiber.child)
//	commitWork(fiber.sibling)
//

//function render(element, container)
//	wipRoot = 

如果纤维的effectTag属性为"DELETION",我们会做相反的事情,将节点从父纤维的DOM中删除。

//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 === "DELETION")
		domParent.removeChild(fiber.dom)
	

//	commitWork(fiber.child)
//	commitWork(fiber.sibling)
//

//function render(element, container)
//	wipRoot = 

如果纤维的effectTag属性为"UPDATE",我们将根据传入的新props更新原来的DOM节点。

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

//function render(element, container)
//	wipRoot = 

更新节点的props我们在updateDom函数中实现。

//	.filter(isProperty)
//	.forEach(name =>
//		dom[name] = fiber.props[name]
//	)
//	return dom
//

function updateDom(dom, prevProps, nextProps)
	// TODO


//function commitRoot()
//	deletions.forEach(commitWork)
//	commitWork(wipRoot.child)
//	currentRoot = wipRoot
//	wipRoot = null
//

//function commitWork(fiber)

我们对比新老纤维的props属性,删掉已经移除的属性,修改变化的属性,新增新有的属性。

const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
	prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps)
	// remove old properties
	Object.keys(prevProps)
	.filter(isProperty)
	.filter(isGone(prevProps, nextProps))
	.forEach(name=>
		dom[name] = ""
	)

	Object.keys(nextProps)
	.filter(isProperty)
	.filter(isNew(prevProps, nextProps))
	.forEach(name=>
		dom[name] = nextProps[name]
	)

我们还需要更新一种特殊的属性——事件监听函数,如果prop的属性以"on"前缀命名,我们把它当作事件监听函数来处理。

//		? document.createTextNode("")
//		: document.createElement(fiber.type)
//	updateDom(dom, , fiber.props)

//	retrun dom
//

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)
	// remove old properties
//	Object.keys(prevProps)
//	.filter(isProperty)
//	.filter(isGone(prevProps, nextProps))

如果事件监听函数变化,我们将它从节点上移除。

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

然后我们添加上新的监听函数。

		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]
			)
	)


//funciton commitRoot()

总结

步骤五中,作者改进了递归和并发模式——整个渲染任务被分成小的渲染单元,但是只有当整个纤维树完成创建节点时,才会整体添加至DOM中。

前面的章节都只负责向DOM中添加节点,并没有考虑更新和删除的情况,步骤六对此做了补充。修改和删除的前提是新老纤维的对比,作者介绍了对比函数,将新老节点的dom属性、props属性进行对比,根据不同的情况选择修改或是删除,特别的,还介绍了一种特殊的props属性——事件监听函数的修改和删除方法。到此为止,我们已经知道了React整个工作的流程,了解了如何新增、修改、删除节点和对应的属性,对React更加深入的了解有助于写出更好的程序。

上一篇传送门:Build your own React_5 渲染和提交阶段
下一篇传送门:Build your own React_7 函数组件

以上是关于Build your own React_6 调解器的主要内容,如果未能解决你的问题,请参考以下文章

Build your own React_4 理解React纤维

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 函数组件