1、前言

到目前为止,我们只实现了向DOM添加内容,所以接下来的目标我们实现更新和删除节点;

当执行更新时,我们要对比两棵fiber树,对有变化的DOM进行更新;

关于协调的原理篇请移步这里

2、实现步骤

2.1 新增变量

新增 currentRoot 变量,保存根节点更新前的fiber树,添加alternate属性到每一个fiber,关联老的fiber,老fiber是我们上一次提交阶段提交给DOM的fiber;

  1. // 更新前的根节点fiber树
  2. let currentRoot = null
  3. function render (element, container) {
  4. wipRoot = {
  5. // 省略
  6. alternate: currentRoot
  7. }
  8. // 省略
  9. }
  10. function commitRoot () {
  11. commitWork(wipRoot.child)
  12. currentRoot = wipRoot
  13. wipRoot = null
  14. }

2.2 新建reconcileChildren并提取performUnitOfWork中的逻辑

提取创建新fiber的代码到reconcileChildren中;

performUnitOfWork代码更改:

  1. /**
  2. * 处理工作单元,返回下一个单元事件
  3. * @param {*} fiber
  4. */
  5. function performUnitOfWork(fiber) {
  6. // 如果fiber上没有dom节点,为其创建一个
  7. if (!fiber.dom) {
  8. fiber.dom = createDom(fiber)
  9. }
  10. // 获取到当前fiber的孩子节点
  11. const elements = fiber.props.children
  12. // 协调
  13. reconcileChildren(fiber, elements)
  14. // 寻找下一个孩子节点,如果有返回
  15. if (fiber.child) {
  16. return fiber.child
  17. }
  18. let nextFiber = fiber
  19. while (nextFiber) {
  20. // 如果有兄弟节点,返回兄弟节点
  21. if (nextFiber.sibling) {
  22. return nextFiber.sibling
  23. }
  24. // 否则返回父节点
  25. nextFiber = nextFiber.parent
  26. }
  27. }

reconcileChildren代码:

  1. /**
  2. * 协调
  3. * @param {*} wipFiber
  4. * @param {*} elements
  5. */
  6. function reconcileChildren(wipFiber,elements){
  7. // 索引
  8. let index = 0
  9. // 上一个兄弟节点
  10. let prevSibling = null
  11. // 遍历孩子节点
  12. while (index < elements.length) {
  13. const element = elements[index]
  14. // 创建fiber
  15. const newFiber = {
  16. type: element.type,
  17. props: element.props,
  18. parent: wipFiber,
  19. dom: null,
  20. }
  21. // 将第一个孩子节点设置为 fiber 的子节点
  22. if (index === 0) {
  23. wipFiber.child = newFiber
  24. } else if (element) {
  25. // 第一个之外的子节点设置为第一个子节点的兄弟节点
  26. prevSibling.sibling = newFiber
  27. }
  28. prevSibling = newFiber
  29. index++
  30. }
  31. }

2.3 对比新旧fiber

添加循环条件oldFiber,将newFiber赋值为null;

  1. function reconcileChildren(wipFiber, elements) {
  2. // 省略
  3. // 上一次渲染的fiber
  4. let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  5. // 省略
  6. while (index < elements.length || oldFiber != null) {
  7. // 省略
  8. const newFiber = null
  9. // 省略
  10. }
  11. // 省略
  12. }

新旧fiber进行对比,看看是否需要对 DOM 应用进行更改;

  1. function reconcileChildren(wipFiber, elements) {
  2. // 省略
  3. // 上一次渲染的fiber
  4. let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  5. // 省略
  6. while (index < elements.length || oldFiber != null) {
  7. // 省略
  8. // 类型判断
  9. const sameType = oldFiber && element && element.type == oldFiber.type
  10. // 类型相同需要更新
  11. if (sameType) {
  12. // TODO update the node
  13. }
  14. // 新的存在并且类型和老的不同需要新增
  15. if (element && !sameType) {
  16. // TODO add this node
  17. }
  18. // 老的存在并且类型和新的不同需要移除
  19. if (oldFiber && !sameType) {
  20. // TODO delete the oldFiber's node
  21. }
  22. // 处理老fiber的兄弟节点
  23. if (oldFiber) {
  24. oldFiber = oldFiber.sibling
  25. }
  26. // 省略
  27. }
  28. // 省略
  29. }

当类型相同时,创建一个新的fiber,保留旧的fiber的dom节点,更新props,此外还加入一个effectTag属性来标识当前执行状态;

  1. function reconcileChildren(wipFiber, elements) {
  2. while (index < elements.length || oldFiber != null) {
  3. // 省略
  4. // 类型相同只更新props
  5. if (sameType) {
  6. newFiber = {
  7. type: oldFiber.type,
  8. props: element.props,
  9. dom: oldFiber.dom,
  10. parent: wipFiber,
  11. alternate: oldFiber,
  12. effectTag: "UPDATE",
  13. }
  14. }
  15. // 省略
  16. }

对于元需要一个新的 DOM 节点的情况,我们用 PLACEMENT effect 标签标记新的fiber;

  1. function reconcileChildren(wipFiber, elements) {
  2. while (index < elements.length || oldFiber != null) {
  3. // 省略
  4. // 新的存在并且类型和老的不同需要新增
  5. if (element && !sameType) {
  6. newFiber = {
  7. type: element.type,
  8. props: element.props,
  9. dom: null,
  10. parent: wipFiber,
  11. alternate: null,
  12. effectTag: "PLACEMENT",
  13. }
  14. }
  15. // 省略
  16. }

对于需要删除节点的情况,没有新fiber,将 effect 标签添加到旧的fiber中,删除旧的fiber;

  1. function reconcileChildren(wipFiber, elements) {
  2. while (index < elements.length || oldFiber != null) {
  3. // 省略
  4. // 老的存在并且类型和新的不同需要移除
  5. if (oldFiber && !sameType) {
  6. oldFiber.effectTag = "DELETION"
  7. deletions.push(oldFiber)
  8. }
  9. // 省略
  10. }

设置一个数组来存储需要删除的节点;

  1. let deletions = null
  2. function render(element, container) {
  3. // 省略
  4. deletions = []
  5. // 省略
  6. }

渲染DOM时,遍历删除旧节点;

  1. function commitRoot() {
  2. deletions.forEach(commitWork)
  3. // 省略
  4. }

修改commitWork处理effectTag标记,处理新增节点(PLACEMENT);

  1. function commitWork(fiber) {
  2. // 省略
  3. if (
  4. fiber.effectTag === "PLACEMENT" &&
  5. fiber.dom != null
  6. ) {
  7. domParent.appendChild(fiber.dom)
  8. }
  9. // 省略
  10. }

处理删除节点标记;

  1. function commitWork(fiber) {
  2. // 省略
  3. // 处理删除节点标记
  4. else if (fiber.effectTag === "DELETION") {
  5. domParent.removeChild(fiber.dom)
  6. }
  7. // 省略
  8. }

处理更新节点,加入updateDom方法,更新props属性;

  1. function updateDom(){
  2. }
  3. function commitWork(fiber) {
  4. // 省略
  5. // 处理删除节点标记
  6. else if (
  7. fiber.effectTag === "UPDATE" &&
  8. fiber.dom != null
  9. ) {
  10. updateDom(
  11. fiber.dom,
  12. fiber.alternate.props,
  13. fiber.props
  14. )
  15. }
  16. // 省略
  17. }

updateDom方法根据不同的更新类型,对props更新;

  1. const isProperty = key => key !== "children"
  2. // 是否有新属性
  3. const isNew = (prev, next) => key =>
  4. prev[key] !== next[key]
  5. // 是否是旧属性
  6. const isGone = (prev, next) => key => !(key in next)
  7. /**
  8. * 更新dom属性
  9. * @param {*} dom
  10. * @param {*} prevProps 老属性
  11. * @param {*} nextProps 新属性
  12. */
  13. function updateDom(dom, prevProps, nextProps) {
  14. // 移除老的属性
  15. Object.keys(prevProps)
  16. .filter(isProperty)
  17. .filter(isGone(prevProps, nextProps))
  18. .forEach(name => {
  19. dom[name] = ""
  20. })
  21. // 设置新的属性
  22. Object.keys(nextProps)
  23. .filter(isProperty)
  24. .filter(isNew(prevProps, nextProps))
  25. .forEach(name => {
  26. dom[name] = nextProps[name]
  27. })
  28. }

修改一下createDom方法,将更新属性逻辑修改为updateDom方法调用;

  1. function createDom(fiber) {
  2. const dom =
  3. fiber.type == "TEXT_ELEMENT"
  4. ? document.createTextNode("")
  5. : document.createElement(fiber.type)
  6. updateDom(dom, {}, fiber.props)
  7. return dom
  8. }

添加是否为事件监听,以on开头,并修改isProperty方法;

  1. const isEvent = key => key.startsWith("on")
  2. const isProperty = key =>
  3. key !== "children" && !isEvent(key)

修改updateDom方法,处理事件监听,并从节点移除;

  1. function updateDom(dom, prevProps, nextProps) {
  2. // 移除老的事件监听
  3. Object.keys(prevProps)
  4. .filter(isEvent)
  5. .filter(
  6. key =>
  7. !(key in nextProps) ||
  8. isNew(prevProps, nextProps)(key)
  9. )
  10. .forEach(name => {
  11. const eventType = name
  12. .toLowerCase()
  13. .substring(2)
  14. dom.removeEventListener(
  15. eventType,
  16. prevProps[name]
  17. )
  18. })
  19. // 省略
  20. }

添加新的事件监听;

  1. function updateDom(dom, prevProps, nextProps) {
  2. // 添加新的事件处理
  3. Object.keys(nextProps)
  4. .filter(isEvent)
  5. .filter(isNew(prevProps, nextProps))
  6. .forEach(name => {
  7. const eventType = name
  8. .toLowerCase()
  9. .substring(2)
  10. dom.addEventListener(
  11. eventType,
  12. nextProps[name]
  13. )
  14. })
  15. // 省略
  16. }

3、实现效果

修改src/index.js代码:

  1. // src/index
  2. import React from '../react';
  3. const container = document.getElementById("root")
  4. const updateValue = e => {
  5. rerender(e.target.value)
  6. }
  7. const rerender = value => {
  8. const element = (
  9. <div>
  10. <input onInput={updateValue} value={value} />
  11. <h2>Hello {value}</h2>
  12. </div>
  13. )
  14. React.render(element, container)
  15. }
  16. rerender("World")

运行:
image.png

4、本节代码

代码地址:https://github.com/linhexs/mini-react/tree/6.reconcileChildren