react源码学习2
本文主要讲react是如何支持Function Components和Hooks的。
Function Components
以如下的App function component为例:
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
函数组件相较于直接调用createElement方法去渲染,有两点不同:
函数组件的fiber结构没有dom节点
函数组件的children来自于运行该函数,而非从props中获取
performUnitOfWork
在之前介绍的performUnitOfWork方法中(参考react源码学习1),我们会先创建dom节点,然后获取要渲染的子节点:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
...
}
为了支持函数组件,我们需要一些适配。
首先,根据fiber类型是否为function,做不同操作:
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
...
}
updateHostCompoent和先前保持一致,在updateFunctionComponent中:
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
通过直接调用函数,获取children节点。上例中fiber.type(fiber.props) = App(props),即h1。
有了children以后,reconcileChildren跟之前保持一致,无需修改。
commitWork
同时需要在commitWork方法中做一些修改。
因为函数组件没有dom节点,所以需要在寻找parent fiber时,不断回溯,直到parent fiber有dom节点:
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
...
}
这样在后续更新时,才能进行dom操作:
...
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
...
删除节点时,也需要通过递归向下寻找删除fiber节点,确保被删除的fiber节点有dom节点:
...
else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
...
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Hooks
以经典的计数器为例,通过点击实现计数的Counter组件:
function Counter() {
const [state, setState] = React.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
具体是通过React.useState获取并更新state值:
function useState(initial) {
// TODO
}
useState是如何实现state的数据更新的?数据更新时是如何触发重新渲染的?下文将一一解答。
updateFunctionComponent
在updateFunctionComponent方法中,新增hooks数组,用于支持该函数组件多次调用useState:
wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
同时定义hoookIndex索引,用于useState方法。
useState
当函数组件调用useState时,首先检查是否有oldHook,如果有则使用oldHook的值,否则则使用初始化的值;然后将这个hook添加到fiber中,并返回这个state。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
useState也需要返回一个函数用来更新state的值,因此我们定义一个setState函数用来接收一个action。对上述的useState方法进一步修改:
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
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]
}
这里将nextUnitOfWork赋值为wipRoot,这样做是为了重新进入render阶段。即每次更新状态时,都会重新触发渲染,这跟useState的逻辑一致。
那么何时调用action呢?继续做如下修改:
function useState(initial) {
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
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
当调用setState时,由前文可知,会重新触发渲染,此时的oldHook.queue=[action]。
在新的render阶段,则会直接调用所有actions,并一个一个赋值给新的state。当函数返回时,hook.state已更新成最新值。
useState是如何实现state的数据更新的?
数据更新时是如何触发重新渲染的
答:
useState通过执行action来更新state值。当调用setState时,会触发重新渲染,再次进入函数时,会遍历所有actions,并赋值给state。
setState方法会将wipRoot赋值为currentRoot,并且设置nextUnitOfWork为wipRoot,这样在workLoop中,会重新进入render阶段。