摘要
在上一篇中,实现了多节点的渲染。但是之前写得diff算法,只能适用于单节点的情况,例如这种情况:
<div><p><span></span></p>
</div>
如果对于多节点的情况:
<ul><li></li><li></li><li></li>
</ul>
之前实现的diff算法就不会有效果了,所以在这一篇中,我们主要实现针对于多节点的diff算法。
实现之前,我们先将index.js修改一下:
function App() {const [num, setNum] = useState(100)const click1 = () => {console.log(num);setNum(num + 1)}return num % 2 > 0 ? jsx("ul", {onClick: click1,key: 'ul',children: [jsx("li", {children: "1",key: "1"}), jsx("li", {children: "2",key: "2"}), jsx("li", {children: "3",key: "3"})]}): jsx("ul", {onClick: click1,key: 'ul',children: [jsx("li", {children: "2",key: "2"}), jsx("li", {children: "1",key: "1"}), jsx("li", {children: "3",key: "3"})]});
}ReactDOM.createRoot(root).render(<App />)
1.修改beginWork流程
在reconcileChildren方法里面,我们判断了如果element为数组的情况,就是多节点。所以我们需要在这里进行diff算法的处理。
function reconcileChildren(parent,element) {//其他代码。。。。}else if(Array.isArray(element) && element.length > 0) {const newChild = diffReconcileManyChildren(parent, element);if(newChild) {return newChild}//其他代码。。。。
所以我们的diff算法那主要是在diffReconcileManyChildren方法里面实现。
对于多节点的Diff,我们需要进行以下步骤。
- 创建变量lastIndex,用来标记索引
- 将旧的filberNode列表,转换为map结构,key为filberNode的key,value为filberNode
- 遍历新的element数组。
- 如果element.key可以在map中找到,lastIndex记录为找到的filberNode的index
- 如果找不到,创建新的FilberNode
- 继续遍历,如果又在map中找到filberNode,比较fiberNode的index和lastIndex.
- 如果index < lastIndex,给filberNode打上移动的标志
基于上面的步骤,实现diffReconcileManyChildren方法
function diffReconcileManyChildren(filberNode, element) {let firstChild = filberNode.child;if(!firstChild) {return;}const head = {sibling: null};const oldChildren = []while(firstChild) {oldChildren.push(firstChild);firstChild = firstChild.sibling;}const oldMap = new Map();oldChildren.forEach((item,index) => {item.index = indexif(item.key) {oldMap.set(item.key, item)}else{oldMap.set(index, item)}})let lastIndex = 0;let empty = headfor(let i=0; i<element.length; i++) {if(!element[i].key){continue;}const useFilber = oldMap.get(element[i].key);useFilber.sibling = null;if(useFilber) {if(useFilber.index < lastIndex) {useFilber.flags = 'insert'}useFilber.memoizedProps = element[i]lastIndex = useFilber.index;empty.sibling = useFilber;empty = empty.sibling;oldMap.delete(element[i].key)}else{const filberNode = new FilberNode(HostComponent, element[i].props, element[i].key) filberNode.type = element[i].typeempty.sibling = filberNode;empty = empty.sibling;}}return head.sibling;
}
经过上面的处理,beginWork流程结束,可复用的filberNode就不会重复创建。
2.修改completeWork流程
在beginWork中,可复用的节点已经被打上了insert的标志,所以在updateCompleteHsotComponent中,我们要判断是不是insert的标志,如果是,就不能无脑创建,而是通过移动DOM的位置来复用DOM。
同时,也要对同级的sibling进行递归处理。
function updateCompleteHostComponent(filberNode) {//其他代码。。。。if(element.key === filberNode.key && element.type === filberNode.type) {addPropsToDOM(filberNode.stateNode, filberNode.pendingProps);if(filberNode.flags === 'insert') {const parent = filberNode.return;parent.stateNode.insertBefore(filberNode.stateNode, filberNode.sibling?.stateNode)}//其他代码if(filberNode.sibling) {completeWork(filberNode.sibling)}
}
在对HostText的处理中,也要考虑,当前的操作是更新还是替换。
function completeHostText(filberNode) {//其他代码。。。。。if(parent && parent.stateNode && parent.tag === HostComponent) {if(!parent.stateNode) {parent.stateNode.appendChild(element);}else{parent.stateNode.replaceChildren(element);}}//其他代码。。。。
}