创建自己的 React (下)

前言

接上文 # 创建自己的 React (中) 目前添加 DOM 节点完成了,但是还不能更新或者删除节,因此需要对 Fiber 树进行更新和删除。

节点更新与删除

添加 currentRoot 变量,用来存储上一次提交的 Fiber 树。给每个 Fiber 元素添加 alternate 属性,用来保存旧的 Fiber 状态。添加 deletedFibers 数组用来存放被删除的 Fiber。

ts 复制代码
/// hypereact.ts

let currentRoot = null
let deletedFibers = []

新增 reconcileChildren 方法,在这个方法里对 Fiber 树进行更新。判断 wipFiber 是否有 alternate 以及新旧 Fiber 的 type 属性是否相同来判断对这个 Fiber 要进行什么操作。

为每个 Fiber 添加 effectTag 属性,有三种操作,添加,更新和删除。

  • 如果旧的 Fiber 和新的元素具有相同的类型,保留 DOM 节点并用新的 props 更新它
  • 如果类型不同并且有新元素,则需要创建一个新的 DOM 节点
  • 如果类型不同并且存在旧 Fiber,则需要删除旧节点
ts 复制代码
/// hypereact.ts

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while (index < elements.length || oldFiber != null) {
    const element = elements[index]
    let newFiber = null

    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: EffectTag.UPDATE,
      }
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: EffectTag.PLACEMENT,
      }
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = EffectTag.DELETION
      deletions.push(oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}

commitWork 方法里根据每个 Fiber 的 effectTag 属性进行对应的操作,在 commitRoot 执行删除 DOM 操作。

ts 复制代码
/// hypereact.ts

function commitRoot() {
  deletedFibers.forEach(commitWork)
  /// ...
}

function commitWork(fiber) {
  if (!fiber) {
    return
  }

  const domParent = fiber.parent.dom
  if (fiber.effectTag === EffectTag.PLACEMENT && fiber.dom != null) {
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === EffectTag.UPDATE && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  } else if (fiber.effectTag === EffectTag.DELETION) {
    domParent.removeChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

updateDom 方法是更新 DOM 节点的具体方法,将旧 Fiber 的 props 与新 Fiber 的 props 进行比较,删除消失的属性,并设置新的或更改属性。

ts 复制代码
/// hypereact.ts

function createDom(fiber) {
  const dom =
    fiber.type == TEXT_ELEMENT
      ? document.createTextNode("")
      : document.createElement(fiber.type)

  updateDom(dom, {}, fiber.props)

  return dom
}

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: HTMLElement, 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 => {
      if (dom.removeAttribute) {
        dom.removeAttribute(name)
      } else {
        dom[name] = ""
      }
    })

  // 设置新的属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      if (dom.setAttribute) {
        dom.setAttribute(name, nextProps[name])
      } else {
        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])
    })
}

现在能够更新组件了

tsx 复制代码
let value = "Hypereact"

const updateValue = e => {
  value = e.target.value
  renderApp()
}


export const renderApp = () => {
  /** @jsx Hypereact.createElement */
  const App = (
    <div>
      /// ...
      <h1>Hello {value}!</h1>
      <input onInput={updateValue} value={value} />
    </div>
  )

  const container = document.getElementById("app")
  Hypereact.render(App, container)
}

函数式组件

接下来添加对函数式组件的支持,添加 updateFunctionComponentupdateHostComponent 方法用来更新组件。在 performUnitOfWork 方法判断组件是否是函数式组件,是的话就执行 updateFunctionComponent 否则执行 updateHostComponent 方法。

updateFunctionComponent 方法中 因为 Fiber 的 type 属性是一个函数,所以将它的 props 属性传给 type 生成 Fiber 的子元素

ts 复制代码
/// hypereact.ts

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function

  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  /// ...
}

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

然后需要更新 commitWork 方法,为了找到有 DOM 节点的父节点,我们需要沿着 Fiber 树向上查找,直到找到具有 DOM 节点的 Fiber。

ts 复制代码
/// hypereact.ts

function commitWork(fiber: Fiber) {
  if (!fiber) {
    return
  }

  let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

  /// ...
}

这样就能使用函数式组件了

javascript 复制代码
const HelloFunctional = () => {
  return (
    <div>
      <h2>Hello, Functional Component</h2>
    </div>
  )
}

/// ...

<HelloFunctional />

Hooks

最后添加组件的状态, 为函数式组件添加 hooks 属性用来存储 hook,使用 wipFiber 存储正在工作的 Fiber,hookIndex 表示钩子函数索引值。

添加 useState 方法,先判断是否有旧的 hook,执行 hook 的函数更新组件状态,添加 setState 函数用来更新组件状态,最后返回 statesetState

ts 复制代码
/// hypereact.ts

/// ...

let wipFiber = null
let hookIndex = null

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

export function useState(initial: any) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  }

  const actions = oldHook ? oldHook.queue : []

  actions.forEach(action => {
    hook.state = action(hook.state)
  })

  const setState = action => {
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletedFibers = []
  }

  wipFiber.hooks.push(hook)
  hookIndex++

  return [hook.state, setState]
}
ts 复制代码
const HelloFunctional = () => {
  const [count, setCounter] = useState(1)

  const handleClick = () => {
    setCounter(() => count + 1)
  }

  return (
    <div>
      <h2>Hello, Functional Component</h2>
      <p>Counter: {count}</p>
      <button onClick={handleClick}>Plus 1</button>
    </div>
  )
}

参考

本文代码

# 创建自己的 React (中)

Build your own React

本文完,感谢阅读 🌹

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax