因为我们是在Fiber树完全构建后再执行的commit,而且有一个变量workInProgressRoot指向了Fiber的根节点,所以我们可以直接把workInProgressRoot拿过来递归渲染就行了:
// 统一操作DOM function commitRoot() { commitRootImpl(workInProgressRoot.child); // 开启递归 workInProgressRoot = null; // 操作完后将workInProgressRoot重置 } function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; parentDom.appendChild(fiber.dom); // 递归操作子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); } reconcile调和reconcile其实就是虚拟DOM树的diff操作,需要删除不需要的节点,更新修改过的节点,添加新的节点。为了在中断后能回到工作位置,我们还需要一个变量currentRoot,然后在fiber节点里面添加一个属性alternate,这个属性指向上一次运行的根节点,也就是currentRoot。currentRoot会在第一次render后的commit阶段赋值,也就是每次计算完后都会把当次状态记录在alternate上,后面更新了就可以把alternate拿出来跟新的状态做diff。然后performUnitOfWork里面需要添加调和子元素的代码,可以新增一个函数reconcileChildren。这个函数里面不能简单的创建新节点了,而是要将老节点跟新节点拿来对比,对比逻辑如下:
如果新老节点类型一样,复用老节点DOM,更新props
如果类型不一样,而且新的节点存在,创建新节点替换老节点
如果类型不一样,没有新节点,有老节点,删除老节点
注意删除老节点的操作是直接将oldFiber加上一个删除标记就行,同时用一个全局变量deletions记录所有需要删除的节点:
// 对比oldFiber和当前element const sameType = oldFiber && element && oldFiber.type === element.type; //检测类型是不是一样 // 先比较元素类型 if(sameType) { // 如果类型一样,复用节点,更新props newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, return: workInProgressFiber, alternate: oldFiber, // 记录下上次状态 effectTag: 'UPDATE' // 添加一个操作标记 } } else if(!sameType && element) { // 如果类型不一样,有新的节点,创建新节点替换老节点 newFiber = { type: element.type, props: element.props, dom: null, // 构建fiber时没有dom,下次perform这个节点是才创建dom return: workInProgressFiber, alternate: null, // 新增的没有老状态 effectTag: 'REPLACEMENT' // 添加一个操作标记 } } else if(!sameType && oldFiber) { // 如果类型不一样,没有新节点,有老节点,删除老节点 oldFiber.effectTag = 'DELETION'; // 添加删除标记 deletions.push(oldFiber); // 一个数组收集所有需要删除的节点 }然后就是在commit阶段处理真正的DOM操作,具体的操作是根据我们的effectTag来判断的:
function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom); } else if(fiber.effectTag === 'DELETION') { parentDom.removeChild(fiber.dom); } else if(fiber.effectTag === 'UPDATE' && fiber.dom) { // 更新DOM属性 updateDom(fiber.dom, fiber.alternate.props, fiber.props); } // 递归操作子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); }替换和删除的DOM操作都比较简单,更新属性的会稍微麻烦点,需要再写一个辅助函数updateDom来实现:
// 更新DOM的操作 function updateDom(dom, prevProps, nextProps) { // 1. 过滤children属性 // 2. 老的存在,新的没了,取消 // 3. 新的存在,老的没有,新增 Object.keys(prevProps) .filter(name => name !== 'children') .filter(name => !(name in nextProps)) .forEach(name => { if(name.indexOf('on') === 0) { dom.removeEventListener(name.substr(2).toLowerCase(), prevProps[name], false); } else { dom[name] = ''; } }); Object.keys(nextProps) .filter(name => name !== 'children') .forEach(name => { if(name.indexOf('on') === 0) { dom.addEventListener(name.substr(2).toLowerCase(), nextProps[name], false); } else { dom[name] = nextProps[name]; } }); }