实现DOM Diff的delete

大家好,我是万维读客的讲师曹欢欢。前面两节我们实现了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

参考

  1. DOM DIFF文档:https://zhuanlan.zhihu.com/p/362539108
  2. node contains方法:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/contains


请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部