实现DOM Diff的delete
On this page
大家好,我是万维读客的讲师曹欢欢。前面两节我们实现了DOM节点的新增和更新的diff操作,本节我们增加delete的操作。
增加测试用例
我们新增一个测试用例,增加节点删除的情况。代码如下:
it('update and delete', async () => {
const globalObj = {};
function App() {
const [count, setCount] = Treact.useState(0);
globalObj.count = count;
const getLi = ()=>{
let arr=[];
for(let i=0; i< count; i++){
arr.push(<li>{i}</li>);
}
return arr
}
return (
<div >
<span className="add" onClick={()=>{setCount(count=>count+1)}}>+</span>
<span className="del" onClick={()=>{setCount(count=>count-1)}}>-</span>
{count}
<ol>
{getLi()}
</ol>
</div>
);
}
const container = document.createElement('div');
const root = Treact.createRoot(container);
await Treact.act(() => {
root.render(<App />);
expect(container.innerHTML).toBe('');
});
await Treact.act(() => {
container.querySelector('.add').click();
container.querySelector('.add').click();
});
console.log('container.innerHTML', container.innerHTML);
expect(globalObj.count).toBe(2);
expect(container.innerHTML).toBe('<div><span class="add">+</span><span class="del">-</span>2<ol><li>0</li><li>1</li></ol></div>');
await Treact.act(() => {
container.querySelector('.del').click();
container.querySelector('.del').click();
});
console.log('container.innerHTML', container.innerHTML);
expect(globalObj.count).toBe(0);
expect(container.innerHTML).toBe('<div><span class="add">+</span><span class="del">-</span>0<ol></ol></div>');
});
实现delete操作
我们先补全reconcile中的delete场景的代码。delete场景是没有newFiber节点的,所以只能把effectTag放在oldFiber节点上。
这时候我们还需要有个地方存储这些需要删除的fiber,我们增加一个deleteFibers参数到workInProgressRoot上,代码如下:
function reconcile(fiber, needHandleProps) {
// 用链表处理child
let preSibling = null;
// dom diff: mount/update/delete
let oldFiber = fiber.alternate?.child;
// fiber.props?.children.forEach((child, idx) => {
let idx = 0;
while (idx < fiber.props?.children.length || oldFiber) {
const child = fiber.props?.children[idx];
let newFiber = null;
let isSameType = oldFiber && child && oldFiber.type == child.type;
if (child && (!oldFiber || !isSameType)) {
// mount
newFiber = {
type: child.type,
stateNode: null,
props: child.props,
return: fiber,
alternate: null,
child: null,
sibling: null,
effectTag: 'PLACEMENT'
}
} else if (isSameType && oldFiber) {
// update
newFiber = {
type: child.type,
stateNode: oldFiber.stateNode,
props: child.props,
return: fiber,
alternate: oldFiber,
child: null,
sibling: null,
effectTag: 'UPDATE'
}
} else if (!isSameType && oldFiber) {
// delete
oldFiber.effectTag = 'DELETE';
if (!workInProgressRoot.deleteFibers) workInProgressRoot.deleteFibers = [];
workInProgressRoot.deleteFibers.push(oldFiber);
}
if (idx == 0) {
fiber.child = newFiber;
} else {
preSibling && (preSibling.sibling = newFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
preSibling = newFiber;
idx++;
}
}
然后我们处理commit中的阐述逻辑。
修改commit
我们先复用之前的逻辑,处理deleteFibers中的数据,在commitDOM中增加删除的操作。
前面实现函数式组件的时候,我们其实也提到了对于这种是没有stateNode的,children直接是往上一级节点挂载的,所以删除的时候我们需要做特殊处理。增加deleteDOM操作,兼容这种情况。
function commitRoot() {
// 删除节点
workInProgressRoot?.deleteFibers?.forEach(commitDOM);
// 处理DOM挂载
commitDOM(workInProgressRoot.current.alternate.child);
// fiber节点render之后,交换alternate
workInProgressRoot.current = workInProgressRoot.current.alternate;
workInProgressRoot.current.alternate = null;
}
function commitDOM(fiber) {
let tempParenNode = null;
if (fiber.return && fiber.stateNode) {
tempParenNode = fiber.return;
while (!tempParenNode.stateNode) {
tempParenNode = tempParenNode.return;
}
}
if (tempParenNode && fiber.effectTag === 'PLACEMENT') {
updateDom({ props: {} }, fiber);
tempParenNode.stateNode.appendChild(fiber.stateNode);
} else if (tempParenNode && fiber.effectTag === 'UPDATE') {
updateDom(fiber.alternate, fiber);
} else if (tempParenNode && fiber.effectTag === 'DELETE') {
deleteDOM(tempParenNode, fiber);
}
if (fiber.child) {
commitDOM(fiber.child);
}
if (fiber.sibling) {
commitDOM(fiber.sibling);
}
}
function deleteDOM(parent, fiber) {
if (fiber.stateNode) {
// 函数式
if (parent.stateNode.contains(fiber.stateNode)) {
parent.stateNode.removeChild(fiber.stateNode);
}
} else {
deleteDOM(parent, fiber.child);
}
}
查看我们的测试用例全部通过。
✓ treact06/jsx.test.jsx (6)
✓ async render Function Component (1)
✓ render with act
✓ fiber useState test (1)
✓ render useState
✓ fiber useReducer test (1)
✓ render useReducer
✓ event handler test (3)
✓ add event
✓ render and commit
✓ update and delete
Test Files 1 passed (1)
Tests 6 passed (6)
Start at 14:34:04
Duration 58ms
参考
- DOM DIFF文档:https://zhuanlan.zhihu.com/p/362539108
- node contains方法:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/contains