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纤维