「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
实现
我们将分为8步,一步一步的实现一个小型的React
- 实现createElement函数
- 实现render函数
- Currnet Mode模式
- fibers
- render和commit阶段
- 协调器
- function组件
- hooks
协调器reconciliation
在更新dom时,我们需要对新旧fiber节点进行对比,看看能不能复用,所以需要一个全局变量保存最后一次commit的fiber tree,我们命名为currentFiber
我们还需要对每个fiber进行连接,即新fiber有一个指针指向旧fiber,方便我们查找对比。
function commitRoot() {
commitWork(workInProcessFiber.child);
currentFiber = workInProcessFiber; // current tree与workInProcess tree互换
workInProcessFiber = 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) {
workInProcessFiber = {
dom: continer,
props: {
children: [element]
},
alternate: currentFiber, // 指针指向最后一次提交的fiber tree
}
nextUnitOfWork = workInProcessFiber
}
let currentFiber = null; // 保存最后一次提交的fiber tree
let nextUnitOfWork = null;
let workInProcessFiber = null;
下一步我们重构一下performUnitOfWork方法,将创建fiber tree的代码抽离出来成一个新函数reconcileChildren,我们会根据旧fiber来创建出新fiber来。
function performUnitOfWork(fiber) {
// 创建dom
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
// 省略。。。
}
function reconcileChildren(wipFiber, elements) {
// TODO
}
在reconcileChildren里我们可以通过alternate获取旧fiber的子节点,拿新元素跟旧fiber节点对比:
- 如果旧fiber和子元素具有相同的type,仅需要更新它props,标记为更新
if (sameType) {
// 如果类型相同,可以复用旧dom节点,标记为更新
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
- 如果type不同并且是新元素,意味着我们需要创建新的dom节点,我们给这个fiber加上新建的标记
if (element && !sameType) {
// 如果类型不同且有新节点,标记为替换
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
- 如果type不同并且是旧fiber,则需要删除旧fiber
if (oldFiber && !sameType) {
// 如果类型不同且存在旧节点,标记删除
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
这里还需要新增一个全局变量,来记录需要删除的fiber
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = [];
nextUnitOfWork = wipRoot
}
let currentFiber = null; // 保存最后一次提交的fiber tree
let nextUnitOfWork = null;
let workInProcessFiber = null;
let deletions = null; // 存储需要删除的fiber
来看下完整的代码
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;
const sameType =
oldFiber &&
element &&
element.type === oldFiber.type;
if (sameType) {
// 如果类型相同,可以复用旧dom节点,标记为更新
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
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 (index === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber;
index++
}
}
前面我们对新旧fiber进行了对比,并给fiber添加标记,标记完之后会在commit阶段,根据标记一次性完成dom操作,下面我们来改造commitWork方法,根据effectTag来完成对应的操作。
function commitRoot() {
deletions.forEach(commitWork)
commitWork(workInProcessFiber.child)
currentFiber = workInProcessFiber
workInProcessFiber = null
}
function updateDom(dom, prevProps, nextProps) {
// TODO
}
function commitWork(fiber) {
if (!fiber) {
return
}
let domParent = fiber.parent.dom;
if (fiber.effectTag === 'PALCEMENT' &&
fiber.dom != null
) {
// 如果标记是PALCEMENT并且dom存在,直接复用dom
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === 'UPDATE' &&
fiber.dom != null
) {
// 如果标记是UPDATE并且dom不存在,更新dom
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === 'DELETION') {
// 如果标记是DELETION,我们直接删除dom
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
如果标记是UPDATE并且dom不存在,我们会更新dom的props,具体是对比新旧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) {
// 删除旧的props
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 加入新的props或者更新同名props
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
有一类props需要特殊处理的,那就是on开头的监听事件
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
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) {
// 删除旧的监听事件
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]
)
})
// 删除旧的props
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 加入新的props
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// 加入新的监听事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
总结
-
在render阶段,也就是reconciliation中,会根据render传入的dom和最后一次提交的fiber tree进行对比,给fiber添加标记
-
react中的reconciliation会使用diff算法进行优化,后面单独梳理
-
在commit阶段,会根据fiber的标记,对dom进行增删改