Article
从零实现 mini-react(4)-vdom 更新&update children
DOM 事件绑定
- 对 DOM 事件进行绑定,需要使用
addEventListener方法。 - 获取绑定事件的字段,然后使用
addEventListener方法进行绑定。 - 注册事件也是同
props传下来,因此只需要在处理props的时候去判断是否为注册事件,然后进行事件的注册即可。 - 更新注册的时候,需要注意将旧的事件删除掉,避免每次更新都加一个新事件。
const updateProps = (dom, props) => { Object.keys(props).forEach((attr) => { const isEvent = attr.startsWith("on"); if (isEvent) { const eventType = +attr.slice(2).toLocaleLowerCase(); dom.addEventListener(eventType, props[attr]); } else { if (attr !== "children") { dom[attr] = props[attr]; } } });};实现更新 props
更新props的时候,我们需要判断新旧props的差异,然后进行更新。
- 生成新Fiber树
- 通过render函数来生成Fiber树
- 使用update的函数在用户交互时调用,并生成新的Fiber树
- 通过render函数来生成Fiber树
- 获取旧Fiber树
- 使用
currentRoot在每次统一提交时保存旧的根节点 - 然后再次更新时,将新的根节点的
alternate指向旧节点 - 更新调和
children时,即从alternate.child拿到对应需要对比的第一个旧子节点 - 此后以此通过
sibiling拿到后续的旧节点比较 (关键)
- 使用
- 对比新旧Fiber树,并进行处理
- 在
initChildren中判断新旧child的type是否一样- 如果一样effectTag为更新update
- 不一样则处理为新增加placement
- 这里updateProps有几种情况要添加处理
- 旧fiber有props ,新fiber没有props=>需删除
- 旧fiber没有props ,新fiber有props=>需添加
- 旧fiber没有props ,新fiber没有props=>需更新
- 在
function updateProps(dom, nextProps, prevProps) { // Object.keys(props).forEach((key) => { // if (key !== "children") { // if (key.startsWith("on")) { // const eventType = key.slice(2).toLocaleLowerCase(); // dom.addEventListener(eventType, props[key]); // } else { // dom[key] = props[key]; // } // } // }); // - 3、新老节点对比 // - - 1、重构 updateProps // - - - 1、old 有 new 没有 删除 Object.keys(prevProps).forEach((key) => { if (key !== "children") { if (!(key in nextProps)) { dom.removeAttribute(key); } } }); // - - - 2、new 有 old 没有 添加 // - - - 3、new 有 old 有 修改 Object.keys(nextProps).forEach((key) => { if (key !== "children") { if (nextProps[key] !== prevProps[key]) { if (key.startsWith("on")) { const eventType = key.slice(2).toLocaleLowerCase(); dom.removeEventListener(eventType, prevProps[key]); dom.addEventListener(eventType, nextProps[key]); } else { dom[key] = nextProps[key]; } } } });}function reconcileChildren(fiber, children) { let oldFiber = fiber.alternate?.child; let prevChild = null; children.forEach((child, index) => { const isSameType = oldFiber && oldFiber.type === child.type;
if (isSameType) { // 更新 newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: oldFiber.dom, effectTag: "update", alternate: oldFiber, }; } else { newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: null, effectTag: "placement", }; }
if (oldFiber) { oldFiber = oldFiber.sibling; }
if (index === 0) { fiber.child = newFiber; } else { prevChild.sibling = newFiber; } prevChild = newFiber; });}
function updateFunctionComponent(fiber) { const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children);}
function updateHostComponent(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber.type); } updateProps(fiber.dom, fiber.props, {});
const children = fiber.props.children; // 转换链表 设置指针 reconcileChildren(fiber, children);}// 更新commitWork 统一提交逻辑function commitWork(fiber) { if (!fiber) return;
let fiberParent = fiber.parent; while (!fiberParent.dom) { fiberParent = fiberParent.parent; }
if (fiber.effectTag === "update") { updateProps(fiber.dom, fiber.props, fiber.alternate?.props); } else if (fiber.effectTag === "placement") { if (fiber.dom) { fiberParent.dom.append(fiber.dom); } }
commitWork(fiber.child); commitWork(fiber.sibling);}双缓存机制- 为了减少页面闪烁和提高渲染效率的一种逻辑。 在传统的渲染中,直接在显示屏上进行 dom 的绘制和更新操作,可能会导致用户看不到完整的画面,从而产生闪烁或者撕裂效果。 双缓存的话是通过一个缓冲区进行绘制和修改,然后将更新后的缓冲区与显示的缓冲区进行交换,避免上述问题。
update children & 边界处理
- 通过对比新旧节点,判断isSameType为false时创建新节点
- 没有对旧的节点进行删除
- 判断isSameType时,对旧节点进行收集
- commitRoot时,统一对旧节点进行删除
- 这样的情况只在新节点不会少于旧节点的数量时才能正常显示
- 以新节点进行遍历,没有对在新节点中不存在的兄弟节点进行处理
- 所以需要判断oldFiber并依次遍历并进行收集删除
const isSameType = oldFiber && oldFiber.type === child.type; let newFiber = null;
if (isSameType) { newFiber = { type: child.type, props: child.props, parent: fiber, sibling: null, child: null, dom: oldFiber.dom, alternate: oldFiber, effectTag: "update", }; } else { newFiber = { type: child.type, props: child.props, parent: fiber, sibling: null, child: null, dom: null, effectTag: "placement", };
if (oldFiber) { deletions.push(oldFiber); } }function commitDeletions(fiber) { let fiberParent = fiber.parent;
while (!fiberParent.dom) { fiberParent = fiberParent.parent; }
if (fiber.dom) { fiberParent.dom.removeChild(fiber.dom); } else { commitDeletions(fiber.child) }}边界处理
当使用如下表达式-
function App() { return (show & <p>hello world</p>)}这时候会得到一个 false 的节点,因此在处理的时候,我们要跳过这个节点。
如果 false 是第一个节点,那么下面这行代码旧有问题了,更新时就无法更新该 fiber 的child了,child 为 null 了
当节点为false时的处理如-1 === 2 &&
123
, 在这种情况下该节点的值为false,也应在链式结构中被忽略,不进行创建,所以需要处理newFiber没有值时的情况,并对prevFiber跳过赋值。
- 节点为false的情况中存在三种类型
- 空节点在children开始
- 空节点在children中间
- 空节点在children结束
if (!fiber.child) fiber.child = newFiber
function update() { // 记录当前的 const currentFiber = wipFiber
return () => { wipRoot = { ...currentFiber, // reconcileChildren时重新收集 child child: null, alternate: currentFiber, }
nextWorkOfUnit = wipRoot requestIdleCallback(loop) }}