React | 2020-07-01 22:47:08 848次 0次
一、render
第一篇中创建了 React 节点,那么将节点渲染或者触发节点的更新可以通过 render、setState、forceUpdate 方法,本篇先分析 ReactDom.render 方法,它是首次渲染。
ReactDom 中会暴露出如下方法,其实这里面常用的很少:
export { createPortal, batchedUpdates as unstable_batchedUpdates, flushSync, ReactVersion as version, findDOMNode, hydrate, render, unmountComponentAtNode, createRoot, createBlockingRoot, discreteUpdates as unstable_discreteUpdates, flushDiscreteUpdates as unstable_flushDiscreteUpdates, flushControlled as unstable_flushControlled, scheduleHydration as unstable_scheduleHydration, renderSubtreeIntoContainer as unstable_renderSubtreeIntoContainer, };
先看下 render 方法,里面什么也没做,直接调用了 legacyRenderSubtreeIntoContainer,顺带提一下 invariant 这个方法已经没了,可能为了安全,经过编译后去除了这个调用,直接判断然后抛出错误。
export function render( element: React$Element<any>, container: Container, callback: ?Function, ) { ... return legacyRenderSubtreeIntoContainer( null, element, container, false, callback, ); }
function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) { ... let root: RootType = (container._reactRootContainer: any); let fiberRoot; if (!root) { // 初次创建 先创建一个 root 根 root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); fiberRoot = root._internalRoot; if (typeof callback === 'function') { ... } // 非批量更新,因为是初次渲染所以要快 unbatchedUpdates(() => { updateContainer(children, fiberRoot, parentComponent, callback); }); } else { //更新时候再来看这里 ... } //初始渲染返回的就是 container.current.child.stateNode; return getPublicRootInstance(fiberRoot); }
上面这个方法中接收了一个 forceHydrate 参数,和服务端渲染有关,浏览器端渲染默认为 false,首次渲染会创建一个 root 根节点,另外指向 container._reactRootContainer ,同时赋值 fiberRoot,此时 root、fiberRoot、container(传入的 DOM 节 点)形成了一个数据环。其中 fiberRoot 上的 containerInfo 指向 container,下面会提及。
二、createLegacyRoot
function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, ): RootType { // 这里又做了一次是否服务端渲染判断 目前是false const shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // First clear any existing content. if (!shouldHydrate) { let warned = false; let rootSibling; while ((rootSibling = container.lastChild)) { ... //只保留一个干净的根节点 container.removeChild(rootSibling); } } ... return createLegacyRoot( container, shouldHydrate ? { hydrate: true, } : undefined, ); }
export function createLegacyRoot( container: Container, options?: RootOptions, ): RootType { // LegacyRoot 默认为 0 return new ReactDOMBlockingRoot(container, LegacyRoot, options); }
接下来又一个函数调用,给 this(上面提到的 root) 挂载一个 _internalRoot 属性,所以创建 root 后可以直接取到这个值:
... this._internalRoot = createRootImpl(container, tag, options); ...
function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) { ...// 判断是否服务端相关处理 省略 const root = createContainer(container, tag, hydrate, hydrationCallbacks); //container[_reactContainere$+随机数] = root.current; markContainerAsRoot(root.current, container); if (hydrate && tag !== LegacyRoot) { ... } return root; }
层层调用之后到了这个方法中终于看到了返回 root 对象,但是又是调用了 createContainer 来生成:
... return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); ...
export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); // Suspense开启后的服务端渲染回调 if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } const uninitializedFiber = createHostRootFiber(tag); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; initializeUpdateQueue(uninitializedFiber); return root; }
到了这里终于闻到了一丝丝大名鼎鼎的 fiber 气息,在分析 Fiber 之前有必要先梳理一下目前一些变量的关系图,更方便后续的理解。
其中 FiberRoot 是整个应用的起点,记录整个应用更新过程中各种信息,只能有一个。
RootFiber 则是具体的 Fiber 节点的根,可以有多个,因为可以 ReactDom.render 多次,可以理解为每一个 DOM 节点都是一个 fiber 结构,记录节点状态,每个节点的连接通过 child 指针指向子节点,sibling 指向自己的兄弟节点,每个子节点有个 return 指针指向自己的父节点,这个 return 就是单纯的一个指针命名,这样整颗 fiber 树就通过链表的形式展现。
三、FiberRoot
这个东西就是单纯的一个对象,先简单的熟悉下里面一些信息说明,这个结构目前还在一直的变化之中:
function FiberRootNode(containerInfo, tag, hydrate) { this.tag = tag; //0 LegacyRoot // 当前应用对应的 Fiber 对象,RootFiber this.current = null; // 传入的 container this.containerInfo = containerInfo; // react-dom中无用 this.pendingChildren = null; // handleError 时用到 记录错误信息 this.pingCache = null; //当前更新对应的过期时间 this.finishedExpirationTime = NoWork; // 完成的任务,commit这个标识的数据 this.finishedWork = null; // 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时,清理还没触发的timeout this.timeoutHandle = noTimeout; //renderSubtreeIntoContainer调用,Portal代替了这个方法现在 this.context = null; //。。。 this.pendingContext = null; //服务端渲染 this.hydrate = hydrate; //这些应该和 Suspense 相关 this.callbackPriority = NoPriority; this.firstPendingTime = NoWork; this.firstSuspendedTime = NoWork; this.lastSuspendedTime = NoWork; this.nextKnownPendingLevel = NoWork; this.lastPingedTime = NoWork; this.lastExpiredTime = NoWork; if (enableSchedulerTracing) { this.interactionThreadID = unstable_getThreadID(); this.memoizedInteractions = new Set(); this.pendingInteractionMap = new Map(); } if (enableSuspenseCallback) { this.hydrationCallbacks = null; } }
四、Fiber
一直说的 fiber 架构,我的理解是并不只是单纯的有了 fiber 这种数据结构在里面,而是因为要去避免之前的递归方式,因为不可暂停,大量节点导致渲染时间过长,从而使一些页面的交互变得延迟影响用户体验,所以借助 fiber 数据结构形成一种链表的结构串联整棵树,从而使得调和阶段是可以中断的,然后利用浏览器的空闲时间再来执行我们的业务代码,浏览器中会优先执行一些事件输入等优先级高的任务,能够提升用户体验。这里不再详细介绍,可以参考之前的文章:浏览器渲染 和 React模拟实现
接着第二小节的代码然后执行了 createHostRootFiber 来创建节点:
export function createHostRootFiber(tag: RootTag): Fiber { let mode; if (tag === ConcurrentRoot) { mode = ConcurrentMode | BlockingMode | StrictMode; } else if (tag === BlockingRoot) { mode = BlockingMode | StrictMode; } else { mode = NoMode; } ... return createFiber(HostRoot, null, null, mode); } const createFiber = function( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ): Fiber { // tag : 3 return new FiberNode(tag, pendingProps, key, mode); };
目前 mode 还在处于实验阶段,如果后面时间充裕的话就跟着官方的脚步维护这个系列的文章。第一篇中介绍了三种不同的模式,最终这里进行了判断采用哪一种,然后执行创建 fiber 节点方法,返回一个 Fiber 节点对象:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // 初始3 标记不同的组件类型 this.tag = tag; //节点中的key this.key = key; //createElement中第一个参数 this.elementType = null; //异步组件resolved之后返回的内容,一般是`function`或者`class` this.type = null; //对应的组件实例 this.stateNode = null; // Fiber中的最重要的链条结构,对照之前文章中出现的那张图 this.return = null; //父节点 this.child = null; //孩子节点 this.sibling = null;//兄弟节点 this.index = 0; //索引 this.ref = null; //新的变动带来的新的props this.pendingProps = pendingProps; //上一次渲染完成之后的props this.memoizedProps = null; //该Fiber对应的组件产生的Update会存放在这个队列里面 this.updateQueue = null; //上一次渲染的时候的state this.memoizedState = null; this.dependencies = null; //legacy blocking concurrent this.mode = mode; // 它记录了节点的更新、修改或删除。这个也对照之前的react模拟实现的文章看下 this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; //代表任务在未来的哪个时间点应该被完成,不包括子树 this.expirationTime = NoWork; //子树中过期时间 this.childExpirationTime = NoWork; //为了优化,替身 this.alternate = null; if (enableProfilerTimer) { //以下是为了避免v8引擎的性能悬崖。和 Object.preventExtensions 这个有关系 this.actualDuration = Number.NaN; this.actualStartTime = Number.NaN; this.selfBaseDuration = Number.NaN; this.treeBaseDuration = Number.NaN; //没太明白,后面再研究研究,应该是更优化的方式 this.actualDuration = 0; this.actualStartTime = -1; this.selfBaseDuration = 0; this.treeBaseDuration = 0; } }
这个创建完之后,还有一步操作,初始化一个更新队列,这个数据结构较之前的小版本发生了改变;当更新数据时候,把更新的东西仍然放到更新队列中,调用完 render 方法后,取到最新的虚拟节点,再执行后续的对比渲染操作。
export function initializeUpdateQueue<State>(fiber: Fiber): void { const queue: UpdateQueue<State> = { baseState: fiber.memoizedState, //先前的状态,作为 payload 函数的 prevState 参数 baseQueue: null, //存储执行中的更新任务 Update 队列 shared: { pending: null, //存储待执行的更新任务 Update 队列 }, effects: null, //有影响的节点记录,commit阶段使用 }; fiber.updateQueue = queue; }
五、updateContainer
最终回到第一部分,创建好了这个 root 节点,并且这个过程中也创建了相互依赖的关系和基本的数据模型,接下来执行 unbatchedUpdates(后续再介绍)回调中执行 updateContainer,这里面做的事情就开始变得抽象起来了,先简要的看下这里面的主要逻辑:
export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTime { ... //RootFiber const current = container.current; ... //一个任务的过期时间,结合之前的文章浏览器调度那里的说明 const expirationTime = computeExpirationForFiber( currentTime, current, suspenseConfig, ); ... const update = createUpdate(expirationTime, suspenseConfig); ... //更新队列有两条,baseQueue 执行中的更新队列, //这里是创建一个 pendingQueue(即 shared.pending)待执行的更新队列 enqueueUpdate(current, update); //开始调度,这里面是一个核心部分,下下篇开始介绍 scheduleWork(current, expirationTime); return expirationTime; }
这里面进行任务队列的创建,最终会去触发 scheduleWork 调度过程,其中还有很多的小细节,比如渲染时间相关的一些计算,下一篇继续。
0人赞