React | 2020-07-17 23:56:17 643次 0次
上一篇磕磕碰碰的总算梳理了一下调度部分代码,还是需要回过头不停的看,目前理解只能说还停留在第二层,其实是在第一层,希望后续能到第五层,接下来开始 render 阶段的笔记。这里开始之前再回顾下之前模拟实现系列文章模拟实现react。
一、概述
render 阶段有两个任务:
① 根据虚拟 DOM 生成 fiber 树 ,diff 也是这个过程中进行
② 收集 effectlist ,它记录了节点的更新、修改或删除。
调度最后会执行到 performSyncWorkOnRoot 同步方法或者 performConcurrentWorkOnRoot 异步方法,这两个方法里面做的事情还比较多,但是目前先看主要的一件事,就是根据不同方式去执行 performUnitOfWork,在执行过程中创建 fiber 树并按照深度优先遍历的规则进行渲染,如果到达某个节点后此节点无子节点则证明这个节点渲染完成,也是第一个渲染完成的节点,以此类推。
function workLoopSync() { //无中止的执行,直到执行完 while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); } } function workLoopConcurrent() { // 调度的部分提到过,分片执行 while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } }
在第一次更新整个过程中每一个 fiber 节点都会被创建一个 alternate 指向旧的 fiber 节点,第二次更新时如果节点类型相同会复用上一次更新后的 fiber alternate,并将此时的节点 alternate 指向上一次更新后的节点,这样做的目的就是一直相互复用对象,大量节点的情况下避免了垃圾回收机制不断触发,节省一定的 cpu 开销。图片参考
这个过程是一个深度优先的遍历,其中完成工作是在最左侧的子节点(其没有子节点)开始依次完成到根节点。
function performUnitOfWork(unitOfWork: Fiber): Fiber | null { const current = unitOfWork.alternate; ... let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { //性能分析,运行时间计算 放在了 fiber.actualDuration 属性中 startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, renderExpirationTime); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { next = beginWork(current, unitOfWork, renderExpirationTime); } ... unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { //没有子节点了 开始执行完成工作 next = completeUnitOfWork(unitOfWork); } ... //返回节点 下次继续开始工作 return next; }
二、beginWork
开始进行工作处理,分为初始渲染和更新两种情况,更新时候可以选择性的跳过更新进行优化。
function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes): Fiber | null { //过期时间 const updateExpirationTime = workInProgress.expirationTime; // 更新 --> 初次创建为空 workInProgress.alternate if (current !== null) { //本次更新之前的 props const oldProps = current.memoizedProps; //新产生的 props const newProps = workInProgress.pendingProps; if ( //前后props不同 oldProps !== newProps || //兼容旧版写法 context hasLegacyContextChanged() || ... ) { didReceiveUpdate = true; } else if (updateExpirationTime < renderExpirationTime) { // 优先级低 didReceiveUpdate = false; ... // 可以跳过更新 或者 复用节点 return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } } else { //初始渲染 didReceiveUpdate = false; } // 根据tag标识,创建对应的子Fiber节点 switch (workInProgress.tag) { case IndeterminateComponent: // ... case FunctionComponent: // ... case ClassComponent: // ... case HostRoot: // ... case HostComponent: // ... .... }}
function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { ... const childExpirationTime = workInProgress.childExpirationTime; if (childExpirationTime < renderExpirationTime) { //子树不需要更新 return null; } else { //需要更新 clone节点 等待下一个 performUnitOfWork cloneChildFibers(current, workInProgress); return workInProgress.child; } }
三、组件类型
tag 类型里面有很多分支,大概分为五大类,参考来源。
1. 有状态组件:ClassComponent,ContextConsumer, ContextProvider
2. 无状态组件:FunctionComponent,IndeterminateComponent, ForwardRef, MemoComponent 与SimpleMemoComponent
IndeterminateComponent 中会区分类组件或者函数组件,初始不知道组件类型
3. 原生组件:HostRoot, HostPortal, HostComponent 与 HostText
HostRoot 默认为 3,初次要渲染这里 ReactDOM.render 的渲染起点;HostPortal 即为 Portal 组件,独立渲染;
HostComponent 就是元素节点; HostText为文本节点。
4. 虚拟组件:Fragment, Mode,Profiler
Fragment 空标签包裹,相当于数组包裹。
5. 懒加载组件:SuspenseComponent,LazyComponent
接下来进一步的看下每种不同类型的组件做的事情,不一一列举,只阅读部分组件,最终他们都会调用 reconcileChildren。
四、HostRoot
它对应着 updateHostRoot 方法:
function updateHostRoot(current, workInProgress, renderExpirationTime) { ... const updateQueue = workInProgress.updateQueue; ... //props state const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; const prevChildren = prevState !== null ? prevState.element : null; //拷贝前者给后者的 updateQueue cloneUpdateQueue(current, workInProgress); //更新 update 队列,并更新 state processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime); const nextState = workInProgress.memoizedState; // 初始 element挂载到了 update.payload = { element } 就是dom.render传入的第一个节点 const nextChildren = nextState.element; if (nextChildren === prevChildren) { //不需要更新则跳过 bailoutOnAlreadyFinishedWork方法上面有说 ... } const root: FiberRoot = workInProgress.stateNode; if (root.hydrate && enterHydrationState(workInProgress)) { ... } else { // 调和作用 这里面做最重要的事情,后面再分析 reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); ... } // 给到 beginWork 函数 next 下个时间片继续干活,记录位置 return workInProgress.child; }
processUpdateQueue 这个方法先在这里进行简单分析,后面使用还比较多,参考自卡颂代码注释:
// 通过遍历update链表,根据fiber.tag不同,通过不同的路径计算新的state export function processUpdateQueue(workInProgress, nextProps, instance, renderExpirationTime) { const queue = workInProgress.updateQueue; // base update 为 单向非环链表 let firstBaseUpdate = queue.firstBaseUpdate; let lastBaseUpdate = queue.lastBaseUpdate; // 如果有 pendingUpdate,需要将 pendingUpdate单向环状链表剪开并拼在baseUpdate单向链表后面 let pendingQueue = queue.shared.pending; if (pendingQueue) { queue.shared.pending = null; const lastPendingUpdate = pendingQueue; const firstPendingUpdate = pendingQueue.next; // 将环剪开 lastPendingUpdate.next = null; // 将pendingUpdate拼入baseUpdate if (!lastBaseUpdate) { firstBaseUpdate = firstPendingUpdate; } else { lastBaseUpdate.next = firstPendingUpdate; } lastBaseUpdate = lastPendingUpdate; const current = workInProgress.alternate; // 存在current 更新其updateQueue if (current) { const currentQueue = current.updateQueue; const currentLastBaseUpdate = currentQueue.lastBaseUpdate; if (lastBaseUpdate !== currentLastBaseUpdate) { if (!currentLastBaseUpdate) { currentQueue.firstBaseUpdate = firstPendingUpdate; } else { currentLastBaseUpdate.next = firstPendingUpdate; } current.lastBaseUpdate = lastPendingUpdate; } } } if (firstBaseUpdate) { // 存在update时遍历链表,计算出update后的值 let newState = queue.baseState; let newExpirationTime = NoWork; let newBaseState = null; let newFirstBaseState = null; let newLastBaseState = null; let update = firstBaseUpdate; do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // 该update优先级不够,跳过 // 新的state是基于baseUpdate计算得出 // 如果这是第一个跳过的update,则之前的update/state就是新的 base update/state const clone = { expirationTime: update.expirationTime, tag: update.tag, payload: update.payload, next: null } if (newLastBaseState === null) { newFirstBaseState = newLastBaseState = clone; newBaseState = newState; } else { newLastBaseState = newLastBaseState.next = clone; } if (updateExpirationTime > newExpirationTime) { newExpirationTime = updateExpirationTime; } } else { // 该update有足够的优先级,基于该update计算newState if (newLastBaseState) { // 同时将该update加入baseUpdate // 对于 !newLastBaseState 的情况在下面 newBaseState = newState; 处处理 const clone = { expirationTime: Sync, tag: update.tag, payload: update.payload, next: null }; newLastBaseState = newLastBaseState.next = clone; } // Object.assign({}, prevState, update.payload); 计算新的状态 newState = getStateFromUpdate(workInProgress, queue, update, newState, nextProps, instance); } update = update.next; if (!update) { pendingQueue = queue.shared.pending; if (!pendingQueue) { break; } else { // 在reducer内部又产生了新的update,则继续计算他 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next; lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; } } } while(true) if (!newLastBaseState) { newBaseState = newState; } //赋值最新的数据 queue.baseState = newBaseState; queue.firstBaseUpdate = newFirstBaseState; queue.lastBaseUpdate = newLastBaseState; workInProgress.expirationTime = newExpirationTime; workInProgress.memoizedState = newState; } }
这个方法很长,目前里面有些东西还不太确定对应场景,其中调用的 getStateFromUpdate 方法,计算新的 state,遵守 immutable 思想,返回一个全新的对象。
五、IndeterminateComponent
这个是一个默认值,createFiberFromTypeAndProps 方法会生成对应的各种组件类型,方法在后面再分析,先了解下即可,但是此方法没有指定 FunctionComponent,导致 tag 使用的就是默认值,所以如果组件内包含了一个函数组件,初始进入时候会进入这个分支,并不是直接进入 FunctionComponent,但是初始渲染之后会标记为函数组件,之后就不会再进入这里了。其中又有一个判断是否为类组件的情况,难道是要函数组件内支持类组件 api ?答案:是的!可以通过如下方式,可以正常渲染但是会给出警告信息:
//这是一种非常不好的做法,尽管可以渲染 export default function Com() { return { render(){ return ( <div> 1111111111111 </div> ) } } }
function mountIndeterminateComponent( _current, workInProgress, Component, renderExpirationTime, ) { ... if ( typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined ) { // 因为函数组件支持类组件的方法,通过return 一个对象,这部分逻辑和最下面的classCom差不多 } else { // 打标记 FunctionComponent workInProgress.tag = FunctionComponent; ... reconcileChildren(null, workInProgress, value, renderExpirationTime); ... return workInProgress.child; } }
六、FunctionComponent
函数组件类型:
function updateFunctionComponent( current, workInProgress, Component, nextProps: any, renderExpirationTime, ) { ... if (__DEV__) { ... } else { // hooks 相关 后面再看 nextChildren = renderWithHooks(...); } if (current !== null && !didReceiveUpdate) { //跳过 hooks 更新 bailoutHooks(current, workInProgress, renderExpirationTime); // 普通的跳过更新 return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; //调和过程 reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); return workInProgress.child; }
七、HostComponent
元素节点类型,对于只包含纯文本的节点 <div>111</div> 或者文本域、选择项做一种优化措施,回顾一下模拟实现2中的那张图,从 beginWork 到 completeWork 的过程中走到如上举例的 div 后如果内容为文本则立即完成,避免再走到 111 文本内容再完成。
function updateHostComponent(current, workInProgress, renderExpirationTime) { ... const isDirectTextChild = shouldSetTextContent(type, nextProps); if (isDirectTextChild) { //对于文本节点的一种优化 nextChildren = null; } ... reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime ) return workInProgress.child; }
// 只包含纯文本的节点 <div>111</div> 或者文本域、选择项 export function shouldSetTextContent(type: string, props: Props): boolean { return ( type === 'textarea' || type === 'option' || type === 'noscript' || typeof props.children === 'string' || typeof props.children === 'number' || (typeof props.dangerouslySetInnerHTML === 'object' && props.dangerouslySetInnerHTML !== null && props.dangerouslySetInnerHTML.__html != null) ); }
八、ClassComponent
类组件的一些逻辑处理,包括生命周期的触发,这部分源码是更加贴近我们的业务,所以看起来没有太抽象。
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) { ... const instance = workInProgress.stateNode; let shouldUpdate; if (instance === null) { /... // 构建类组件实例 constructClassInstance(workInProgress, Component, nextProps); //生命周期钩子调用 mountClassInstance(...); shouldUpdate = true; } else if (current === null) { // 渲染被中断过,所以会被反复触发 // 调用生命周期(componentWillMount,componentDidMount),返回 shouldUpdate shouldUpdate = resumeMountClassInstance(...); } else { //当已经创建实例并且不是第一次渲染的话 //调用更新的生命周期方法为componentWillReceiveProp componentWillUpdate,componentDidUpdate(), shouldUpdate = updateClassInstance(...); } //判断是否执行 render,并返回 render 下的第一个 child const nextUnitOfWork = finishClassComponent(...); ... return nextUnitOfWork; }
第一次渲染:如果还没创建实例,初始化生成实例instance,然后挂载实例更新 instance.state,并且执行一些生命周期
渲染被中断:复用实例并且调用首次渲染的生命周期,所以 react 需要抛弃旧的生命周期钩子
更新渲染:处理更新阶段的生命周期钩子
最终执行 finishClassComponent 调和子节点 reconcileChildren
在进入 reconcileChildren 之前,下一篇先对类组件详细分析。
0人赞