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

本文完,感谢阅读 🌹

相关推荐
上官熊猫14 分钟前
nuxt3项目打包部署到服务器后配置端口号和开启https
前端·vue3·nuxt3
dal118网工任子仪2 小时前
61,【1】BUUCTF WEB BUU XSS COURSE 11
前端·数据库·xss
约定Da于配置4 小时前
uniapp封装websocket
前端·javascript·vue.js·websocket·网络协议·学习·uni-app
山楂树の4 小时前
xr-frame 模型摆放与手势控制,支持缩放旋转
前端·xr·图形渲染
LBJ辉5 小时前
1. 小众但非常实用的 CSS 属性
前端·css
milk_yan5 小时前
Docker集成onlyoffice实现预览功能
前端·笔记·docker
m0_748255026 小时前
头歌答案--爬虫实战
java·前端·爬虫
noravinsc7 小时前
python md5加密
前端·javascript·python
ac-er88888 小时前
Yii框架优化Web应用程序性能
开发语言·前端·php
cafehaus8 小时前
抛弃node和vscode,如何用记事本开发出一个完整的vue前端项目
前端·vue.js·vscode