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

本文完,感谢阅读 🌹

相关推荐
余生H5 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍7 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai12 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默24 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_8572979134 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_44 分钟前
meta标签作用/SEO优化
前端·javascript·html
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_1 小时前
说说你对es6中promise的理解?
前端·ecmascript·es6
Promise5201 小时前
总结汇总小工具
前端·javascript