代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0 ,并且经过简化,仅保留了关键的逻辑,以便于理解流程
前面我们一直在讲React渲染的流程,但是我们并不是从我们最初的调用开始讲的。回顾前面的十篇文章,我们是先讲了React渲染的流程,先跳过了触发渲染这一步。然后讲了一个setState或者说是dispatch的行为是怎么触发渲染的,然后又讲了Scheduler在其中起到了什么作用。
而现在,我们还没有说到最初,比如说浏览器刚拿到js资源的时候,他是怎么从零开始,把React应用渲染成页面的。那么今天,我们就来看一看。
createRoot React18开始,react-dom有了createRoot这个API。在React17及以前,我们通常是这样写:ReactDom.render(<App />, document.getElementById('root'))。而现在有了新的API,React更推荐这样写:createRoot(document.getElementById('root')).render(<APP />)
通过新写法来生成的React应用,才可以使用并发特性。新写法分成了两部,先是从一个dom节点,生成一个root,然后调用root.render方法,把React应用渲染出来。
我们一点一点看这其中都发生了什么。
这部分代码在packages\react-dom\src\client\ReactDOMRoot.js里
1 2 3 4 5 6 export function createRoot ( container : Element | Document | DocumentFragment , options ?: CreateRootOptions , ): RootType { }
对于createRoot方法,有两个参数,第一个参数就是真实的dom节点,我们会把React应用挂到这个dom下面。第二个参数我们通常不会传,不过它里面有些有用的字段比如onUncaughtError,onCaughtError,onRecoverableError之类的,可以用来捕获并上报异常(对于水合时候用的hydrateRoot更重要,可以捕获水合错误,不然你就猜是哪里水合匹配不上吧),详见React文档。
createRoot的核心逻辑如下
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 export function createRoot ( container : Element | Document | DocumentFragment , options ?: CreateRootOptions , ): RootType { const root = createContainer ( container, ConcurrentRoot , null , isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks, ); markContainerAsRoot (root.current , container); const rootContainerElement : Document | Element | DocumentFragment = !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ? (container.parentNode : any ) : container; listenToAllSupportedEvents (rootContainerElement); return new ReactDOMRoot (root); }function ReactDOMRoot (internalRoot : FiberRoot ) { this ._internalRoot = internalRoot; }
首先,通过createContainer方法,得到FiberRootNode, 就是上面的变量root
然后markContainerAsRoot方法,其实就是在dom节点上面,用一个字段,把FiberRootNode保存起来,便于直接从dom节点获取到FiberRootNode。
接下来,通过listenToAllSupportedEvents方法完成事件委托,将监听事件都绑定到传入的dom上,这一环节我们会在后续文章里专门探讨。
接下来,new一个ReactDOMRoot实例,将root传入,挂在_internalRoot字段上, 将ReactDOMRoot实例return回去。
我们先看一下,createContainer方法干了什么
createContainer与createFiberRoot 以下代码在packages\react-reconciler\src\ReactFiberReconciler.js与packages\react-reconciler\src\ReactFiberRoot.js里
createContainer可以认为直接调用了createFiberRoot,所以我们看createFiberRoot就好了
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 export function createFiberRoot ( containerInfo : Container , tag : RootTag , hydrate : boolean , initialChildren : ReactNodeList , hydrationCallbacks : null | SuspenseHydrationCallbacks , isStrictMode : boolean , identifierPrefix : string , onUncaughtError : ( error: mixed, errorInfo: {+componentStack?: ?string }, ) => void , onCaughtError : ( error: mixed, errorInfo: { +componentStack?: ?string , +errorBoundary?: ?React$Component<any , any >, }, ) => void , onRecoverableError : ( error: mixed, errorInfo: {+componentStack?: ?string }, ) => void , transitionCallbacks : null | TransitionTracingCallbacks , formState : ReactFormState <any , any > | null , ): FiberRoot { const root : FiberRoot = (new FiberRootNode ( containerInfo, tag, hydrate, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, formState, ): any ); const uninitializedFiber = createHostRootFiber (tag, isStrictMode); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; const initialState : RootState = { element : initialChildren, isDehydrated : hydrate, cache : initialCache, }; uninitializedFiber.memoizedState = initialState; initializeUpdateQueue (uninitializedFiber); return root; }
将root赋值为一个FiberRootNode的实例,这个FiberRootNode函数里面的逻辑就很简单了,只是单纯的给实例挂了很多很多字段,没有其他逻辑。
接下来,调用createHostRootFiber,得到一个HostRootFiber, 也就是变量uninitializedFiber。生成它的逻辑也不复杂,就是调用createFiber, 生成一个Fiber的实例,差不多也就是new一个对象,我们略过。
然后,将FiberRootNode和HostRootFiber互相绑定起来,FiberRootNode的current指向HostRootFiber,而HostRootFiber的stateNode指向FiberRootNode。当然最开始,我们讲到Fiber树的双缓冲实现,是在两棵树之间切换的,指的就是FiberRootNode的current指针,可能会指向另一颗Fiber树。
然后,将HostRootFiber的memoizedState字段,赋值为一个初始的State,然后执行initializeUpdateQueue方法,将queue也挂到HostRootFiber上面,如下面所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export function initializeUpdateQueue<State >(fiber : Fiber ): void { const queue : UpdateQueue <State > = { baseState : fiber.memoizedState , firstBaseUpdate : null , lastBaseUpdate : null , shared : { pending : null , lanes : NoLanes , hiddenCallbacks : null , }, callbacks : null , }; fiber.updateQueue = queue; }
这里我们注意到,之前我们介绍hooks的时候,也提到过memoizedState啊,queue之类的,而且他们很像。其实是这样的,只不过hooks是以双向链表的形式挂在函数组件Fiber的memoizedState上的,然后每个hooks对应的更新放在对应hooks的queue里,这是函数组件。
而HostRootFiber节点和类组件,则是将state和更新,直接挂在了Fiber.memoizedState和Fiber.queue上。
那我们回过头去看createFiberRoot方法,其实他已经得到了一个FiberRootNode,返回给了createRoot方法。
createRoot().render 接下来,我们就该看这个render方法了。
1 2 3 4 5 6 7 8 9 10 ReactDOMHydrationRoot .prototype .render = ReactDOMRoot .prototype .render = function (children : ReactNodeList ): void { const root = this ._internalRoot ; if (root === null ) { throw new Error ('Cannot update an unmounted root.' ); } updateContainer (children, root, null , null ); };
前面我们知道,createRoot方法,返回的是一个ReactDOMRoot的实例,真正的FiberRootNode是挂在这个实例的_internalRoot属性上的。
而我们调用的render方法,是实例的方法,所以定义在了构造函数的prototype上。
updateContainer 核心的逻辑在updateContainer里面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export function updateContainer ( element : ReactNodeList , container : OpaqueRoot , parentComponent : ?React $Component <any , any >, callback : ?Function , ): Lane { const current = container.current ; const lane = requestUpdateLane (current); updateContainerImpl ( current, lane, element, container, parentComponent, callback, ); return lane; }
我们可以从前文看到,render方法传过来的container是FiberRootNode,那么container.current就是HostRootFiber了。
然后获取lane。如果你没有用transition包裹createRoot方法,即通常情况下,优先级最终可以追溯到下面这个方法里。由于此时window.event是undefined,所以会进入到默认优先级DefaultLane。这是一个略低于SyncLane,以及其他交互相关的优先级,但是它比transition的优先级要高。(注意,优先级的绝对值没有什么意义,因为他们在不同版本可能会变化。它们之间的大小关系是更重要的)
1 2 3 4 5 6 7 8 9 10 11 export function resolveUpdatePriority ( ): EventPriority { const updatePriority = ReactDOMSharedInternals .p ; if (updatePriority !== NoEventPriority ) { return updatePriority; } const currentEvent = window .event ; if (currentEvent === undefined ) { return DefaultEventPriority ; } return getEventPriority (currentEvent.type ); }
接下来,调用包了一层的方法,updateContainerImpl
updateContainerImpl 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 updateContainerImpl ( rootFiber : Fiber , lane : Lane , element : ReactNodeList , container : OpaqueRoot , parentComponent : ?React $Component <any , any >, callback : ?Function , ): void { const context = getContextForSubtree (parentComponent); if (container.context === null ) { container.context = context; } else { container.pendingContext = context; } const update = createUpdate (lane); update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null ) { update.callback = callback; } const root = enqueueUpdate (rootFiber, update, lane); if (root !== null ) { scheduleUpdateOnFiber (root, rootFiber, lane); entangleTransitions (root, rootFiber, lane); } }
记得当前调用的时候,后两个参数都是null。
创建一个update字面量对象,将render方法的参数,也就是element赋值给update.payload。然后,执行enqueueUpdate方法,将这个update放到FiberRootNode的更新队列里。等到后面开始渲染,遍历Fiber树的时候,会在beginWork里面完成对FiberRootNode的更新.
接下来,调用scheduleUpdateOnFiber方法,让调度器安排一个渲染,这个方法我们前面已经介绍过了。
entangleTransitions方法,则只在当前是transition优先级的时候才有实际作用,我们前面提过了,没有用transition包裹的createRoot是DefaultLane。
以上,由于调用了scheduleUpdateOnFiber,React将会开始渲染整颗Fiber树,然后你的React应用就呈现在页面上了。
总结 首先,createRoot方法,生成了FiberRootNode和HostRootFiber两个变量,他们两个的关系如下:
1 2 FiberRootNode .current = HostRootFiber HostRootFiber .stateNode = FiberRootNode
其中,FiberRootNode可以理解为React应用的最高管理者,它的current指针指向HostRootFiber,并且可以切换到HostRootFiber.alternate上,以实现前后缓冲区的切换。
然后,执行render方法的时候,给HostRootFiber添加一个update,update.payload就是render方法传入的Fiber。然后调用scheduleUpdateOnFiber,触发一次React的渲染,就完成了初次渲染。