创建自己的 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

本文完,感谢阅读 🌹

相关推荐
GISer_Jing38 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪2 小时前
CSS复习
前端·css
咖啡の猫4 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲6 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路7 小时前
GeoTools 读取影像元数据
前端
ssshooter7 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry8 小时前
Jetpack Compose 中的状态
前端
dae bal9 小时前
关于RSA和AES加密
前端·vue.js
柳杉9 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化