截止目前,我们已经可以渲染 html 标签组件了,但还不支持 react 的函数组件,我们替换一下试试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| - const profile = ( - <div className="profile"> - <span className="profile-title">title</span> - <h3 className="profile-content">content</h3> - 我是一段文本 - </div> - );
+ function App(props) { + return <h1>Hi {props.name}</h1> + } + const profile = <App name="foo" />
const container = document.getElementById("root") Didact.render(profile, container)
|
会发现报错了,因为函数组件要执行一下,才会返回 jsx
支持函数组件
函数组件有两个地方不同:
- 函数组件的 fiber 没有 dom 节点
- 执行一下函数组件,才有 children
判断是否是函数组件
所以在 performUnitOfWork
方法中,我们要先检测组件是否是函数组件,然后将分别处理的逻辑提取到两个函数 updateHostComponent
和 updateFunctionComponent
内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| function performUnitOfWork(fiber) { - if (!fiber.dom) { - fiber.dom = createDom(fiber) - }
- const elements = fiber.props.children - reconcileChildren(fiber, elements) + const isFunctionComponent = + fiber.type instanceof Function + if (isFunctionComponent) { + updateFunctionComponent(fiber) + } else { + updateHostComponent(fiber) + }
if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } }
// 处理普通组件 + function updateHostComponent(fiber) { + if (!fiber.dom) { + fiber.dom = createDom(fiber) + } + reconcileChildren(fiber, fiber.props.children) + }
// 处理函数组件 + function updateFunctionComponent(fiber) { + // 执行函数组件,返回jsx + const children = [fiber.type(fiber.props)] + reconcileChildren(fiber, children) + }
|
处理函数组件没有 dom 的问题
因为函数组件会出现没有 dom 的情况,那 commitWork
方法的逻辑就要修正一下,通过递归
往上去找有 dom 的父元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| // 递归插入所有 dom function commitWork(fiber) { if (!fiber) return
- const domParent = fiber.parent.dom + let domParentFiber = fiber.parent + while (!domParentFiber.dom) { + domParentFiber = domParentFiber.parent + } + const domParent = domParentFiber.dom
if ( fiber.effectTag // 插入新 dom domParent.appendChild(fiber.dom) // ... } else if (fiber.effectTag // 删除 dom domParent.removeChild(fiber.dom) } commitWork(fiber.child) commitWork(fiber.sibling) } + // 函数组件没有 dom,需要一直往上递归找父 dom + function commitDeletion(fiber, domParent) { + if (fiber.dom) { + domParent.removeChild(fiber.dom) + } else { + commitDeletion(fiber.child, domParent) + } }
|
hooks
截止目前,我们还不支持 hooks
,我们替换一个有 hooks 的 demo 来支持一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - function App(props) { - return <h1>Hi {props.name}</h1> - } - const profile = <App name="foo" />
+ function Counter() { + const [state, setState] = Didact.useState(1) + return ( + <div> + <button onClick={() => setState(c => c + 1)}> + 点击 + 1 + </button> + <p>Count: {state}</p> + </div> + ) + } + const profile = <Counter />
const container = document.getElementById("root") Didact.render(profile, container)
|
fiber 新增 hooks 属性
保存当前被设置 hooks
的 fiber
,因为 useState
可以调用多次,所以需要维护一个 hooks
队列,用来存放多个hook
,修改 updateFunctionComponent
方法:
1 2 3 4 5 6 7 8 9 10 11
| + let wipFiber = null + let hookIndex = null
function updateFunctionComponent(fiber) { + wipFiber = fiber + hookIndex = 0 + wipFiber.hooks = [] // 执行函数组件,返回jsx const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children) }
|
实现 useState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| function useState(initial) { const oldFiber = wipFiber.alternate; const oldHook = oldFiber?.hooks && oldFiber.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 deletions = [] }
wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState] }
|
- 因为是通过当前
index
去找 老hooks
数组里对应的 hook
,新老hooks
数组里的hook
是一一对应的,所以在 react 中 hook 不能放在条件判断语句内,这样 hook 在数组里的位置就会有变化,新旧的 index 不能对应起来
useState
除了要返回最后计算的state
和对应的setState
方法,还要在这之前执行上一次hooks
队列里的任务
- 每调用一次
useState
,hook
队列就又入列一个任务
- 执行
setState
,会赋值nextUnitOfWork
,这样就启动了浏览器闲时处理的开关,下一次闲时就会更新diff
- 为了简单,这里的
setState
只支持传入一个函数,不能传入一个值,但要支持其实也很简单,判断一下是个值就转换成一个返回该值的函数,即可
到这里,我们就实现了自己的一个 react——Didact
整体源码