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

本文完,感谢阅读 🌹

相关推荐
IT_陈寒13 分钟前
JavaScript开发者必知的5个性能杀手,你踩了几个坑?
前端·人工智能·后端
跟着珅聪学java17 分钟前
Electron 精美菜单设计
运维·前端·数据库
日光倾17 分钟前
【Vue.js 入门笔记】闭包和对象引用
前端·vue.js·笔记
一只程序熊23 分钟前
UniappX 未找到 “video“ 组件,已自动当做 “view“ 组件处理。请确保代码正确,或重新生成自定义基座后再试。
前端
林小帅25 分钟前
【笔记】xxx 技术分享文档模板
前端
雾岛心情30 分钟前
【HTML&CSS】HTML为文字添加格式和内容
前端·css·html
心.c39 分钟前
如何在项目中减少 XSS 攻击
前端·xss
Rsun045511 小时前
Vue相关面试题
前端·javascript·vue.js
TON_G-T1 小时前
前端包管理器(npm、yarn、pnpm)
前端
卤炖阑尾炎1 小时前
Web 技术基础与 Nginx 网站环境部署全解析
前端·nginx·microsoft