# 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](https://nyx-blog.readthedocs.io/zh/latest/note/react_code_note.html)),我们会先创建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 (

setState(c => c + 1)}> Count: {state}

) } const element = ``` 具体是通过`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`已更新成最新值。 > 1. useState是如何实现state的数据更新的? > 2. 数据更新时是如何触发重新渲染的 > > 答: > 1. useState通过执行action来更新state值。当调用setState时,会触发重新渲染,再次进入函数时,会遍历所有actions,并赋值给state。 > 2. setState方法会将wipRoot赋值为currentRoot,并且设置nextUnitOfWork为wipRoot,这样在workLoop中,会重新进入render阶段。