前言
接上文 # 创建自己的 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)
}
函数式组件
接下来添加对函数式组件的支持,添加 updateFunctionComponent
和 updateHostComponent
方法用来更新组件。在 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
函数用来更新组件状态,最后返回 state
和 setState
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>
)
}
参考
本文完,感谢阅读 🌹