在上一节 更新props 中,我们判断如果新旧dom的对应tag如果是一样的,name表示更新逻辑,如果不一样,则表示新建或删除。
从一个例子中去理解:
let show = true
const Counter = () => {
function handleClick() {
show = !show
React.update()
}
const Foo = () => <div>foo</div>
const Bar = () => <p>bar</p>
// 这里通过show变了来控制显示哪个组件
return (
<div>
count:
<div>{show ? <Foo></Foo> : <Bar></Bar>}</div>
<button onClick={handleClick}>click</button>
</div>
)
}
上面例子中对应的链表图示如下。
从新旧链表中可以知道,需要将旧的组件<Foo />删掉,并新建一个<Bar />组件
实现思路:
- 先将需要删除的dom(也就是新旧节点不一样的旧dom)收集起来。
- 在dom的挂载之前将需要删除的dom删掉,然后执行挂载操作。
1.收集需要删除的dom
let deletions = [] // 新建一个全局变量deletions,将需要删除的节点收集起来
function initChildren(fiber, children) {
let oldFiber = fiber.alternate?.child;
let prevChild = null;
children.forEach((child, index) => {
// 判断旧的tag与新tag是否相同
// true:更新节点
// false:创建、删除节点
const isSameTag = oldFiber && oldFiber.type === child.type;
let newFiber = null;
if (isSameTag) {
// 更新
// ...
} else {
// 新增
// 这里新建节点时会有一个边缘情况产生
if (child) {
newFiber = {
type: child.type,
props: child.props,
child: null,
sibling: null,
parent: fiber,
dom: null,
effectTag: "placement",
};
}
// 删除逻辑,将需要删除的节点收集起来
if (oldFiber) {
deletions.push(oldFiber);
}
}
// ...
});
// 如果旧的节点的子节点多于新节点的子节点,则需要把旧节点的子节点全部删除
// 所以循环调用,将sibling全部添加到deletions中
while (oldFiber) {
deletions.push(oldFiber);
oldFiber = oldFiber.sibling;
}
}
2.挂载之前删除dom
dom挂载环节是在链表创建完成后的 commitRoot() 函数中执行的,所以在挂载之前执行删除dom操作。
function commitRoot() {
deletions.forEach(commitDeletion); // 执行删除操作
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
deletions = []; // 删除完后清空
}
function commitDeletion(fiber) {
// step1.直接删除节点
// 问题:如果是函数组件的话,函数组件的dom是null
// 解决:递归查找子节点
// fiber.parent.dom.removeChild(fiber.dom)
// step2.递归查找子节点
// 问题:函数组件查找到子节点后,因为函数组件dom为null,执行删除时会发现fiber.parent.dom为null,同样有问题
// 解决:循环查找dom不为null的父节点
// if (fiber.dom) {
// fiber.parent.dom.removeChild(fiber.dom);
// } else {
// commitDeletion(fiber.child);
// }
// step3.循环查找dom不为null的父节点
if (fiber.dom) {
let fiberParent = fiber.parent;
while (!fiberParent.dom) {
fiberParent = fiberParent.parent;
}
fiberParent.dom.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child);
}
}
在新建节点时会有一个边缘情况 edge case:
我们经常会遇到一个情况,如以下代码所示,通过一个变量来控制一个组件是否显示。
let show = false
const Counter = () => {
function handleClick() {
show = !show
React.update()
}
const Foo = () => <div>foo</div>
return (
<div>
count
<div>{show && <Foo></Foo>}</div>
<button onClick={handleClick}>click</button>
</div>
)
}
这种情况下会导致我们的代码报错
分析过程:
首先是逻辑与&&的解释
逻辑与(
&&)运算符从左到右对操作数求值,遇到第一个假值操作数时立即返回;如果所有的操作数都是真值,则返回最后一个操作数的值。
由此可知:{show && <Foo></Foo>} 中,show为false则返回false,为true则返回<Foo></Foo>
所以,上述报错是由于返回了false导致的。
在react创建dom的方式是通过解析 jsx 后调用 createElement() 函数去创建dom
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map((child) => {
const isTextNode =
typeof child === "string" || typeof child === "number";
return isTextNode ? createTextEl(child) : child;
}),
},
};
};
所以从以上代码可以清晰的看到,如果child为Boolean类型的话,isTextNode判断为false,将直接返回child,也就是将变量给return回去了。
但由于这个变量并不是一个dom,这样我们在遍历Vdom创建链表的过程中会发现dom属性是undefined
然后就在创建dom的函数中,这里将会直接创建一个 <undefined></undefined> 的dom,导致了后面 updateProps 的报错。
function updateHostComponent(fiber) {
if (!fiber.dom) {
// 1.创建dom节点
const dom = (fiber.dom = createDom(fiber.type));
console.log("dom", dom); // <undefined></undefined>
// 2.设置props
updateProps(dom, fiber.props, {});
}
const children = fiber.props.children;
reconcileChildren(fiber, children);
}
解决问题:
在创建链表时,如果child的判断为false,则直接略过
function reconcileChildren(fiber, children) {
// ...
children.forEach((child, index) => {
const isSameTag = oldFiber && oldFiber.type === child.type;
let newFiber = null;
if (isSameTag) {
// ...
} else {
// 新增 / 删除
if (child) { // 判断child,为false则直接略过
newFiber = {
type: child.type,
props: child.props,
child: null,
sibling: null,
parent: fiber,
dom: null,
effectTag: "placement",
};
}
// ...
}
// ...
// 如果child为false,newFiber则为undefined,所以就不用再赋值了
if (newFiber) {
prevChild = newFiber;
}
});
}