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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值