拆分render和commit阶段

大家好,我是万维读客的讲师曹欢欢。

render和commit

关于渲染和提交大家可以看下官方文档基础的介绍:https://zh-hans.react.dev/learn/render-and-commit, 我们需要注意的流程是初次渲染和再次渲染: 初次渲染:

再次渲染:

其中再次渲染的时候,React 应该仅在渲染之间存在差异时才会更改 DOM 节点。但是我们前面的实现主要关注的是处理fiber节点的渲染,拆分每个子阶段的渲染:

|浏览器事件a|render阶段1|浏览器事件b|render阶段2|render阶段3|---commit阶段---|浏览器事件c|

所有的fiber都会被渲染一遍。这样其实是有问题,比如下面这个带有输入框的组件,如果用户输入了内容,再次重新渲染就会导致用户内容丢失,这样明显是不对的。

通过拆分render和commit阶段我们来处理这个问题,其实主要是增加DOM Diff的处理。

 --------                      --------
| render |    --------->      | commit | 
 --------                      --------

render阶段
- DOM diff 
- 生成effectTag 

commit阶段
- 基于effectTag变更DOM

编写测试用例

我们在上一节的基础上增加一个会产生动态DOM的测试用例,代码如下:

  it('render and commit', 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 className="button" onClick={()=>{setCount(count => count+1)}}>
          {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('.button').click();
      container.querySelector('.button').click();
    });
    console.log('container.innerHTML', container.innerHTML);
    expect(globalObj.count).toBe(2);
    expect(container.innerHTML).toBe('<div class="button">2<ol><li>0</li><li>1</li></ol></div>');
  });

查看控制台测试用例执行结果:

❯ treact06/jsx.test.jsx (5)
   ✓ async render Function Component (1)
     ✓ render with act
   ✓ fiber useState test (1)
     ✓ render useState
   ✓ fiber useReducer test (1)
     ✓ render useReducer
   ❯ event handler test (2)
     ✓ add event
     × render and commit

 FAIL  treact06/jsx.test.jsx > event handler test > render and commit
AssertionError: expected '<div class="button">0<ol><li>0</li><l…' to be '<div class="button">2<ol><li>0</li><l…' // Object.is equality

- Expected
+ Received

- <div class="button">2<ol><li>0</li><li>1</li></ol></div>
+ <div class="button">0<ol><li>0</li><li>1</li></ol></div>

 ❯ treact06/jsx.test.jsx:159:33
    157|     console.log('container.innerHTML', container.innerHTML);
    158|     expect(globalObj.count).toBe(2);
    159|     expect(container.innerHTML).toBe('<div class="button">2<ol><li>0</li><li>1</li></ol></div>');
       |                                 ^
    160|   });
    161| })

我们看报错,子节点是动态渲染出来了,但是count数据渲染的是不对的,说明之前的实现是有些问题的。

拆出commit

上面我们说commit阶段做DOM更新,所以我们要把DOM挂载的处理操作拆分到commit操作中。我们增加一个commitRoot方法,修改代码如下:

function commitRoot() {
    // 处理DOM挂载,来自performUnitOfWork
    if (fiber.return) {
        let tempParenNode = fiber.return;
        while (!tempParenNode.stateNode) {
            tempParenNode = tempParenNode.return;
        }
        tempParenNode.stateNode.appendChild(fiber.stateNode);
    }
    // fiber节点render之后,交换alternate; 来自workloop
    workInProgressRoot.current = workInProgressRoot.current.alternate;
    workInProgressRoot.current.alternate = null;
}

上面只是把代码挪过来,然后我们处理DOM挂载操作,跟处理fiber差不多,就是从根节点递归下来挂载DOM节点。增加commitDOM函数:

function commitRoot() {
    // 处理DOM挂载
    commitDOM(workInProgressRoot.current.alternate.child);
    // fiber节点render之后,交换alternate
    workInProgressRoot.current = workInProgressRoot.current.alternate;
    workInProgressRoot.current.alternate = null;
}
function commitDOM(fiber) {
    if (fiber.return && fiber.stateNode) {
        let tempParenNode = fiber.return;
        while (!tempParenNode.stateNode) {
            tempParenNode = tempParenNode.return;
        }
        try {
            tempParenNode.stateNode.appendChild(fiber.stateNode);
        } catch (e) {
            console.log(fiber.stateNode);
        }
    }

    if (fiber.child) {
        commitDOM(fiber.child);
    }
    if (fiber.sibling) {
        commitDOM(fiber.sibling);
    } 
}

拆出render

render部分主要是处理DOM Diff,给对应的fiber节点打上effectTag,这部分代码主要在performUnitOfWork中的节点处理,我们拆出来新的reconcil方法如下:

function performUnitOfWork(fiber) {
    // console.log('fiber props', fiber.props, fiber.props == null ? JSON.stringify(fiber.return.props.children) : '');
    const isFunctionComp = fiber.type instanceof Function;
    let needHandleProps = false;
    if (isFunctionComp) {
        currentHookFiber = fiber;
        currentHookFiber.memorizedState = [];
        currentHookFiberIndex = 0;
        fiber.props.children = [fiber.type(fiber.props)];
    } else {
        if (!fiber.stateNode) {
            fiber.stateNode = fiber.type === 'HostText' ? document.createTextNode('') : document.createElement(fiber.type);
            needHandleProps = true;
        }
    }
    reconcile(fiber, needHandleProps);
    return getNextFiber(fiber);
}
function reconcile(fiber, needHandleProps) {
    if (needHandleProps) {
        Object.keys(fiber.props).filter(filerProps).forEach(key => {
            fiber.stateNode[key] = fiber.props[key];
        })
        Object.keys(fiber.props).filter(isEvent).forEach(key => {
            const eventName = key.toLowerCase().substring(2);
            fiber.stateNode.addEventListener(eventName, fiber.props[key]);
        })
    }
    // 用链表处理child
    let preSibling = null;
    // mount时oldFiber是空,update阶段有数据
    let oldFiber = fiber.alternate?.child;
    fiber.props.children.forEach((child, idx) => {
        let newFiber = null;
        if (!oldFiber) {
            // mount
            newFiber = {
                type: child.type,
                stateNode: null,
                props: child.props,
                return: fiber,
                alternate: null,
                child: null,
                sibling: null,
            }
        } else {
            // update
            newFiber = {
                type: child.type,
                stateNode: oldFiber.stateNode,
                props: child.props,
                return: fiber,
                alternate: oldFiber,
                child: null,
                sibling: null,
            }
        }

        if (idx == 0) {
            fiber.child = newFiber;
        } else {
            preSibling.sibling = newFiber;
        }
        if (oldFiber) {
            oldFiber = oldFiber.sibling;
        }
        preSibling = newFiber;
    })
}

然后查看我们的测试用例,除了上面刚增加的用例,其他用例都能正常通过。

❯ treact06/jsx.test.jsx (5)
   ✓ async render Function Component (1)
     ✓ render with act
   ✓ fiber useState test (1)
     ✓ render useState
   ✓ fiber useReducer test (1)
     ✓ render useReducer
   ❯ event handler test (2)
     ✓ add event
     × render and commit

给大家留个作业,想一想为什么我们加的用例不通过, 下节课我们来具体实现render阶段的DOM diff处理。

参考

  1. render-and-commit文档:https://zh-hans.react.dev/learn/render-and-commit


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

扫一扫,反馈当前页面

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