拆分render和commit阶段
On this page
大家好,我是万维读客的讲师曹欢欢。
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处理。
参考
- render-and-commit文档:https://zh-hans.react.dev/learn/render-and-commit