截止到目前,我们的 react 已经可以完成首次渲染,但还不能响应式更新和删除,下面我们来实现一下。
保存 old fiber 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // ... function render(element, container) { // 虽然后面会给这个对象添加更多属性,但这里是第一个 fiber wipRoot = { dom: container, props: { children: [element], }, + alternate: currentRoot, } nextUnitOfWork = wipRoot } function commitRoot() { commitWork(wipRoot.child) + // commit 后,新 fiber 就变成了旧 fiber,更新一下旧 fiber + currentRoot = wipRoot wipRoot = null } // ... let nextUnitOfWork = null + // 当有新 fiber root 后,会拿它跟当前 root fiber 做对比,所以需要缓存当前 root fiber + let currentRoot = null let wipRoot = null //...
缓存当前的root fiber
,以便有了新的root fiber
后可以进行diff
给每一个 fiber 都新增一个alternate
属性,用于存放旧 fiber
提取 diff 部分并进行封装 之前我们处理 diff 部分是在performUnitOfWork
方法里,现在将其提出来,封装到新方法reconcileChildren
里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children + reconcileChildren(fiber, elements) - let index = 0 - let prevSibling = null - // 1. 遍历当前fiber的children - // 2. 给children里的每个child指定3个指针,分别指向其 父、子、兄弟三个节点 - while (index < elements.length) { - const element = elements[index] - const newFiber = { - type: element.type, - props: element.props, - parent: fiber, - dom: null, - } - if (index === 0) { - fiber.child = newFiber - } else { - prevSibling.sibling = newFiber - } - prevSibling = newFiber - index++ - } // 下面的操作是返回下一个单元——nextUnitOfWork // 1. 优先找child // 2. 没有child找兄弟 // 3. 没有兄弟,找叔叔,也就是递归到父元素的兄弟 // 4. 没有叔叔就一直往上递归... if (fiber.child) { return fiber.child } // ... } + function reconcileChildren(wipFiber, elements) { + let index = 0 + let prevSibling = null + ... + }
在 reconcileChildren
方法中,把 new fiber
和 old fiber
表示出来 (便于 TODO 部分进行对比),并将old fiber
的变化也加入到while
迭代中来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function reconcileChildren(wipFiber, elements) { let index = 0 + // 从 alternate 找到旧父fiber的第一个child,作为第一个要对比的old fiber + let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null // 1. 遍历当前fiber的children // 2. 给children里的每个child指定3个指针,分别指向其 父、子、兄弟三个节点 - while (index < elements.length) { + while (index < elements.length || oldFiber != null) { const element = elements[index] + let newFiber = null - const newFiber = { - type: element.type, - props: element.props, - parent: wipFiber, - dom: null, - } + // TODO diff部分将在这里实现 + if (oldFiber) { + oldFiber = oldFiber.sibling + } if (index wipFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } }
下面我们来完成 reconcileChildren
方法里的TODO 部分,也就是 diff
diff 这里的 diff 主要是更新 fiber 的属性,还没有到真实的操作 dom
对比的策略
新、老 fiber 的 type 相同:保留 dom,更新属性
新、老 fiber 的 type 不同:创建新 fiber,删除旧 fiber
下面写出大体框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null + const sameType = + oldFiber && + element && + element.type == oldFiber.type + if (sameType) { // TODO update the node + } + if (element && !sameType) { // TODO add this node + } + if (oldFiber && !sameType) { // TODO delete the oldFiber's node + } if (oldFiber) { oldFiber = oldFiber.sibling } if (index wipFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber index++ }
对比旧 fiber,创建新 fiber 下面我们来完成上面 3 个 TODO 部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type : oldFiber.type , props : element.props , dom : oldFiber.dom , parent : wipFiber, alternate : oldFiber, effectTag : "UPDATE" , } } if (element && !sameType) { newFiber = { type : element.type , props : element.props , dom : null , parent : wipFiber, alternate : null , effectTag : "PLACEMENT" , } } if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push (oldFiber) }
给每个 fiber 新增了effectTag
属性,后面统一处理的时候,就知道是更新
、删除
还是插入
新增了deletions
数组,存放所有待删除的fiber
,后面统一删除里面的dom
上面的代码已经完成了迭代所有旧 fiber,并将其更新为了新 fiber
处理 deletions 数组 清空deletions
数组将在 commit
这个阶段进行处理,而我们会将包括删除在内的所有更新操作都放到commitWork
方法里去做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function render(element, container) { // 虽然后面会给这个对象添加更多属性,但这里是第一个 fiber wipRoot = { dom: container, props: { children: [element], }, alternate: currentRoot, } + deletions = [] nextUnitOfWork = wipRoot } function commitRoot() { + deletions.forEach(commitWork) commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null } let nextUnitOfWork = null let currentRoot = null let wipRoot = null + let deletions = null
commitWork 下面我们来完善 commitWork
方法,commitWork
除了插入,还有删除和更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function commitWork(fiber) { if (!fiber) return const domParent = fiber.parent.dom - domParent.appendChild(fiber.dom) + if ( fiber.effectTag === "PLACEMENT" && fiber.dom != null) { + // 插入新dom + domParent.appendChild(fiber.dom) + } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { + // 更新dom属性 + updateDom( + fiber.dom, + fiber.alternate.props, + fiber.props + ) + } else if (fiber.effectTag === "DELETION") { + // 删除dom + domParent.removeChild(fiber.dom) + } commitWork(fiber.child) commitWork(fiber.sibling) } + function updateDom(dom, prevProps, nextProps) { + // TODO + }
updateDom 上面新增了一个 updateDom
方法,updateDom
会将所有的 diff 真实反应到的 dom 上,现在我们来实现它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const isEvent = key => key.startsWith ("on" )const isProperty = key => key !== "children" && !isEvent (key) const isNew = (prev, next ) => key => prev[key] !== next[key] const isGone = (prev, next ) => key => !(key in next)function updateDom (dom, prevProps, nextProps ) { Object .keys (prevProps) .filter (isEvent) .filter ( key => !(key in nextProps) || isNew (prevProps, nextProps)(key) ) .forEach (name => { const eventType = name .toLowerCase () .substring (2 ) dom.removeEventListener ( eventType, prevProps[name] ) }) Object .keys (prevProps) .filter (isProperty) .filter (isGone (prevProps, nextProps)) .forEach (name => { dom[name] = "" }) Object .keys (nextProps) .filter (isProperty) .filter (isNew (prevProps, nextProps)) .forEach (name => { dom[name] = nextProps[name] }) Object .keys (nextProps) .filter (isEvent) .filter (isNew (prevProps, nextProps)) .forEach (name => { const eventType = name .toLowerCase () .substring (2 ) dom.addEventListener ( eventType, nextProps[name] ) }) }
实现很简单粗暴:删除旧属性,创建新属性
最后将 createDom
里的 dom 更新,也改为使用 updateDom
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function createDom(fiber) { const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type) + updateDom(dom, {}, fiber.props); - // children 被放到了 props 属性里,这里过滤掉 children - const isProperty = key => key !== "children" - Object.keys(fiber.props) - .filter(isProperty) - // 设置 dom 元素的属性,这里是简化版意思一下,直接赋值 - .forEach(name => dom[name] = fiber.props[name]) return dom }
现在,我们的 diff 基本实现
本章源码