updateDom的代码写的比较简单,事件只处理了简单的on开头的,兼容性也有问题,prevProps和nextProps可能会遍历到相同的属性,有重复赋值,但是总体原理还是没错的。要想把这个处理写全,代码量还是不少的。
函数组件函数组件是React里面很常见的一种组件,我们前面的React架构其实已经写好了,我们这里来支持下函数组件。我们之前的fiber节点上的type都是DOM节点的类型,比如h1什么的,但是函数组件的节点type其实就是一个函数了,我们需要对这种节点进行单独处理。
首先需要在更新的时候检测当前节点是不是函数组件,如果是,children的处理逻辑会稍微不一样:
// performUnitOfWork里面 // 检测函数组件 function performUnitOfWork(fiber) { const isFunctionComponent = fiber.type instanceof Function; if(isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); } // ...下面省略n行代码... } function updateFunctionComponent(fiber) { // 函数组件的type就是个函数,直接拿来执行可以获得DOM元素 const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } // updateHostComponent就是之前的操作,只是单独抽取了一个方法 function updateHostComponent(fiber) { if(!fiber.dom) { fiber.dom = createDom(fiber); // 创建一个DOM挂载上去 } // 将我们前面的vDom结构转换为fiber结构 const elements = fiber.props.children; // 调和子元素 reconcileChildren(fiber, elements); }然后在我们提交DOM操作的时候因为函数组件没有DOM元素,所以需要注意两点:
获取父级DOM元素的时候需要递归网上找真正的DOM
删除节点的时候需要递归往下找真正的节点
我们来修改下commitRootImpl:
function commitRootImpl() { // const parentDom = fiber.return.dom; // 向上查找真正的DOM let parentFiber = fiber.return; while(!parentFiber.dom) { parentFiber = parentFiber.return; } const parentDom = parentFiber.dom; // ...这里省略n行代码... if{fiber.effectTag === 'DELETION'} { commitDeletion(fiber, parentDom); } } function commitDeletion(fiber, domParent) { if(fiber.dom) { // dom存在,是普通节点 domParent.removeChild(fiber.dom); } else { // dom不存在,是函数组件,向下递归查找真实DOM commitDeletion(fiber.child, domParent); } }现在我们可以传入函数组件了:
import React from './myReact'; const ReactDOM = React; function App(props) { return ( <div> <h1>{props.title}</h1> <a href="http://www.likecs.com/xxx">Jump</a> <section> <p> Article </p> </section> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') ); 实现useStateuseState是React Hooks里面的一个API,相当于之前Class Component里面的state,用来管理组件内部状态,现在我们已经有一个简化版的React了,我们也可以尝试下来实现这个API。
简单版我们还是从用法入手来实现最简单的功能,我们一般使用useState是这样的:
function App(props) { const [count, setCount] = React.useState(1); const onClickHandler = () => { setCount(count + 1); } return ( <div> <h1>Count: {count}</h1> <button onClick={onClickHandler}>Count+1</button> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') );上述代码可以看出,我们的useState接收一个初始值,返回一个数组,里面有这个state的当前值和改变state的方法,需要注意的是App作为一个函数组件,每次render的时候都会运行,也就是说里面的局部变量每次render的时候都会重置,那我们的state就不能作为一个局部变量,而是应该作为一个全部变量存储:
let state = null; function useState(init) { state = state === null ? init : state; // 修改state的方法 const setState = value => { state = value; // 只要修改了state,我们就需要重新处理节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [state, setState] }这样其实我们就可以使用了:
支持多个state上面的代码只有一个state变量,如果我们有多个useState怎么办呢?为了能支持多个useState,我们的state就不能是一个简单的值了,我们可以考虑把他改成一个数组,多个useState按照调用顺序放进这个数组里面,访问的时候通过下标来访问:
let state = []; let hookIndex = 0; function useState(init) { const currentIndex = hookIndex; state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex]; // 修改state的方法 const setState = value => { state[currentIndex] = value; // 只要修改了state,我们就需要重新处理这个节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } hookIndex++; return [state[currentIndex], setState] }来看看多个useState的效果:
支持多个组件