React16源码: React中处理hydrate的核心流程源码实现

hydrate


1 )概述

  • hydrate 在react当中不算特别重要, 但是很多时候会用到的一个API
  • 这个 API 它主要作用就是在进入第一次渲染的时候,如果本身 dom 树上面已经有一个dom结构存在
  • 是否可以去利用这一部分已经存在的dom,然后去避免掉在第一次渲染的时候
  • 要去创建很多 dom 节点的一个过程,可以大大的提升第一次渲染的性能
  • 看下 react 什么时候需要去执行 hydrate 什么时候又不需要去执行它
  • 在 ReactDOM.js 中找到 react 里面的两个渲染 API
    • 一个是 hydrate 另外一个是 render
    • 它们接收的参数都是一样的
    • 它们都调用同一个函数 legacyRenderSubtreeIntoContainer
    • 唯一的区别是它们传入的第4个参数不一样

2 )源码

定位到 packages/react-dom/src/client/ReactDOM.js#L627

hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {// TODO: throw or warn if we couldn't hydrate?return legacyRenderSubtreeIntoContainer(null,element,container,true,callback,);
}render(element: React$Element<any>,container: DOMContainer,callback: ?Function,
) {return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,);
}

对于 legacyRenderSubtreeIntoContainer 第4个参数 forceHydrate 表示是否强制融合

function legacyRenderSubtreeIntoContainer(parentComponent: ?React$Component<any, any>,children: ReactNodeList,container: DOMContainer,forceHydrate: boolean,callback: ?Function,
) {// TODO: Ensure all entry points contain this checkinvariant(isValidContainer(container),'Target container is not a DOM element.',);if (__DEV__) {topLevelUpdateWarnings(container);}// TODO: Without `any` type, Flow says "Property cannot be accessed on any// member of intersection type." Whyyyyyy.let root: Root = (container._reactRootContainer: any);if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate,);if (typeof callback === 'function') {const originalCallback = callback;callback = function() {const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Initial mount should not be batched.DOMRenderer.unbatchedUpdates(() => {if (parentComponent != null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {root.render(children, callback);}});} else {if (typeof callback === 'function') {const originalCallback = callback;callback = function() {const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Updateif (parentComponent != null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {root.render(children, callback);}}return DOMRenderer.getPublicRootInstance(root._internalRoot);
}
  • 在调用 legacyCreateRootFromDOMContainer 这个方法的时候,会传入这个 forceHydrate 这个参数
    function legacyCreateRootFromDOMContainer(container: DOMContainer,forceHydrate: boolean,
    ): Root {// 在这个方法里面,首先它声明了一个参数叫 shouldHydrate,如果 forceHydrate 为 true, shouldHydrate 也一定为 true// 不是 true, 需要调用 `shouldHydrateDueToLegacyHeuristic` 来进行判断// 这也是目前在 react dom 环境中,即便使用了 render API,但仍然有可能会去执行 hydrate 的一个过程// 注意,只针对目前版本const shouldHydrate =forceHydrate || shouldHydrateDueToLegacyHeuristic(container);// First clear any existing content.if (!shouldHydrate) {let warned = false;let rootSibling;while ((rootSibling = container.lastChild)) {if (__DEV__) {if (!warned &&rootSibling.nodeType === ELEMENT_NODE &&(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)) {warned = true;warningWithoutStack(false,'render(): Target node has markup rendered by React, but there ' +'are unrelated nodes as well. This is most commonly caused by ' +'white-space inserted around server-rendered markup.',);}}container.removeChild(rootSibling);}}if (__DEV__) {if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {warnedAboutHydrateAPI = true;lowPriorityWarning(false,'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +'will stop working in React v17. Replace the ReactDOM.render() call ' +'with ReactDOM.hydrate() if you want React to attach to the server HTML.',);}}// Legacy roots are not async by default.const isConcurrent = false;// 后续在创建 ReactRoot 的时候,传入了 shouldHydratereturn new ReactRoot(container, isConcurrent, shouldHydrate);
    }
    
    • 进入 shouldHydrateDueToLegacyHeuristic
      function shouldHydrateDueToLegacyHeuristic(container) {// 这里面我们拿到了rootElement 之后,我们首先判断的条件// 就是它是否存在, 以及它是否是一个普通的节点// 以及它是否有 `ROOT_ATTRIBUTE_NAME` 这个attribute// ROOT_ATTRIBUTE_NAME 的 值是 data-reactroot// 这个是在调用服务端渲染的API的时候,生成的HTML上面它的container节点上面会增加这个attribute// 这也是react服务端渲染的API帮助我们去在客户端去识别是否需要融合节点的一个帮助的attribute// 如果符合这个条件,React它就会猜测, 即便你调用的不是 Hydrate API// 它仍然猜测你是需要执行 hydrate,它的 shouldHydrate 就等于 trueconst rootElement = getReactRootElementInContainer(container);return !!(rootElement &&rootElement.nodeType === ELEMENT_NODE &&rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME));
      }
      
    • 进入 getReactRootElementInContainer
      function getReactRootElementInContainer(container: any) {if (!container) {return null;}// DOCUMENT_NODE 表示 window.document, 返回 documentElement 就是 html节点if (container.nodeType === DOCUMENT_NODE) {return container.documentElement;} else {return container.firstChild;}
      }
      
      • 这也是强烈不推荐在调用 render API 的时候传入 document 的原因
      • 因为如果传入了document,那么它就会对整个HTML文档来进行一个调和的过程
      • 如果我不需要一个融合的过程,它可能会直接把HTML里面所有的内容都删掉,那么这就很可能会导致一些问题了
      • 所以在这里面,如果是你传入的是 document,它返回的是 documentElement,那就是HTML节点
      • 如果它传入的不是 document, 就是这个 container.firstChild 节点
        • 比如说传入的是root节点,root节点如果它没有子节点,它的firstChild 肯定就是一个 null这种情况
        • 这时候肯定不需要融合的一个过程
    • 进入 ReactRoot
      function ReactRoot(container: Container,isConcurrent: boolean,hydrate: boolean,
      ) {const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);this._internalRoot = root;
      }
      
      • 进入 createContainer
        export function createContainer(containerInfo: Container,isConcurrent: boolean,hydrate: boolean,
        ): OpaqueRoot {return createFiberRoot(containerInfo, isConcurrent, hydrate);
        }
        
        • 进入 createFiberRoot
          export function createFiberRoot(containerInfo: any,isConcurrent: boolean,hydrate: boolean,
          ): FiberRoot {// Cyclic construction. This cheats the type system right now because// stateNode is any.const uninitializedFiber = createHostRootFiber(isConcurrent);let root;if (enableSchedulerTracing) {root = ({current: uninitializedFiber,containerInfo: containerInfo,pendingChildren: null,earliestPendingTime: NoWork,latestPendingTime: NoWork,earliestSuspendedTime: NoWork,latestSuspendedTime: NoWork,latestPingedTime: NoWork,didError: false,pendingCommitExpirationTime: NoWork,finishedWork: null,timeoutHandle: noTimeout,context: null,pendingContext: null,hydrate, // 注意这里挂载了nextExpirationTimeToWorkOn: NoWork,expirationTime: NoWork,firstBatch: null,nextScheduledRoot: null,interactionThreadID: unstable_getThreadID(),memoizedInteractions: new Set(),pendingInteractionMap: new Map(),}: FiberRoot);} else {root = ({current: uninitializedFiber,containerInfo: containerInfo,pendingChildren: null,earliestPendingTime: NoWork,latestPendingTime: NoWork,earliestSuspendedTime: NoWork,latestSuspendedTime: NoWork,latestPingedTime: NoWork,didError: false,pendingCommitExpirationTime: NoWork,finishedWork: null,timeoutHandle: noTimeout,context: null,pendingContext: null,hydrate, // 注意这里挂载了nextExpirationTimeToWorkOn: NoWork,expirationTime: NoWork,firstBatch: null,nextScheduledRoot: null,}: BaseFiberRootProperties);}uninitializedFiber.stateNode = root;// The reason for the way the Flow types are structured in this file,// Is to avoid needing :any casts everywhere interaction tracing fields are used.// Unfortunately that requires an :any cast for non-interaction tracing capable builds.// $FlowFixMe Remove this :any cast and replace it with something better.return ((root: any): FiberRoot);
          }
          
          • 把 hydrate 挂载上去,也就是告诉后续的流程,这一次渲染是需要 hydrate 的
    • 设置了 root 上面的 hydrate 之后,在后续更新root的时候,比如在 updateHostRoot
    • 在它的更新流程当中会调用一个方法叫做 enterHydrationState 这个方法

定位到 packages/react-reconciler/src/ReactFiberBeginWork.js#L612


function updateHostRoot(current, workInProgress, renderExpirationTime) {// ... 跳过很多代码if ((current === null || current.child === null) &&root.hydrate &&enterHydrationState(workInProgress) // 注意这里) {// ... 跳过很多代码return workInProgress.child;
}

这个 enterHydrationState 方法标志着整个 react 应用的 hydrate 过程的一个开启

定位到 packages/react-reconciler/src/ReactFiberHydrationContext.js#L49

function enterHydrationState(fiber: Fiber): boolean {// supportsHydration 在 react dom 环境中是 写死的 trueif (!supportsHydration) {return false;}// 默认是 root, 也就是 ReactDOM.render 中的第2个参数const parentInstance = fiber.stateNode.containerInfo;// nextHydratableInstance 是一个全局变量nextHydratableInstance = getFirstHydratableChild(parentInstance);hydrationParentFiber = fiber;isHydrating = true;return true;
}
  • 进入 getFirstHydratableChild

    export function getFirstHydratableChild(parentInstance: Container | Instance,
    ): null | Instance | TextInstance {let next = parentInstance.firstChild;// Skip non-hydratable nodes.// 进入 while 循环while (next &&next.nodeType !== ELEMENT_NODE &&next.nodeType !== TEXT_NODE) {next = next.nextSibling; // 找到所有星弟节点}return (next: any);
    }
    
    • 找到 parentInstance 下面第一个 ELEMENT_NODE 或 TEXT_NODE
    • 这其实是一些全局变量的初始化
    • 因为调用这个方法,就代表着我们要正式开始执行 hydrate 的一个过程
    • 因为 FiberRoot 对应的是挂载的那个节点, 它不是我们要渲染的APP里面的对应的节点
    • 所以它并不是一个正式的流程里面的一部分,它只是标志着我们要开始了
  • 开始了之后要做的是什么呢?

    • 主要的集中在 HostComponent 以及 TextComponent
    • 定位到 packages/react-reconciler/src/ReactFiberBeginWork.js#L685
    • 找到 updateHostComponent
      function updateHostComponent(current, workInProgress, renderExpirationTime) {pushHostContext(workInProgress);if (current === null) {tryToClaimNextHydratableInstance(workInProgress);}const type = workInProgress.type;const nextProps = workInProgress.pendingProps;const prevProps = current !== null ? current.memoizedProps : null;let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);if (isDirectTextChild) {// We special case a direct text child of a host node. This is a common// case. We won't handle it as a reified child. We will instead handle// this in the host environment that also have access to this prop. That// avoids allocating another HostText fiber and traversing it.nextChildren = null;} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {// If we're switching from a direct text child to a normal child, or to// empty, we need to schedule the text content to be reset.workInProgress.effectTag |= ContentReset;}markRef(current, workInProgress);// Check the host config to see if the children are offscreen/hidden.if (renderExpirationTime !== Never &&workInProgress.mode & ConcurrentMode &&shouldDeprioritizeSubtree(type, nextProps)) {// Schedule this fiber to re-render at offscreen priority. Then bailout.workInProgress.expirationTime = Never;return null;}reconcileChildren(current,workInProgress,nextChildren,renderExpirationTime,);return workInProgress.child;
      }
      
      • 可以看到在 current 等于 null 的一个情况下,也就是初次渲染的情况下
      • 它会调用一个方法叫做 tryToClaimNextHydratableInstance 这么一个方法
        function tryToClaimNextHydratableInstance(fiber: Fiber): void {// 正在开始 hydrate 这里不会执行 跳过if (!isHydrating) {return;}// 这是刚在 enterHydrationState 找到的第一个合法的子节点let nextInstance = nextHydratableInstance;// 如果这个节点不存在, 说明我们当前正在更新的 HostComponent 是没有节点进行 hydrate 的流程的// 所以这个节点它是一个新增的节点,就是我们现在正在更新的这个 HostComponent,它是一个需要去新增的结点// 因为这个节点它已经找不到了,在 beginWork 这边更新完成之后,继续更新的是它的子节点,它本身已经不能找到跟它进行复用的一个节点了// 它的子节点就更不可能找到复用的子节点了, 所以到这边我们就可以停下来了// 对它的子树来说,就不需要再走 hydrate 的流程了,等到回过头来去更新它的兄弟节点的时候,又会重新开始这个 hydrate 的流程if (!nextInstance) {// Nothing to hydrate. Make it an insertion.insertNonHydratedInstance((hydrationParentFiber: any), fiber);isHydrating = false;hydrationParentFiber = fiber;return;}// 有可以 hydrate 节点的流程const firstAttemptedInstance = nextInstance;// 判断当前节点是否可以复用,这里是不可以复用if (!tryHydrate(fiber, nextInstance)) {// If we can't hydrate this instance let's try the next one.// We use this as a heuristic. It's based on intuition and not data so it// might be flawed or unnecessary.nextInstance = getNextHydratableSibling(firstAttemptedInstance);// 它又再去进行了一次 tryHydrate 为什么它这里要这么去做呢?// 它上面的注释向我们表明了它的一个用意// 如果发现这个节点它无法进行headict,我们尝试着去使用它的下一个兄弟节点// 这个尝试并不是基于数据分析得来的一个结果而纯粹是我作为一个开发者,直觉认为它可能会出现这样的一个情况// 所以这边的代码它并没有一个彻底的原理在里面,它只是出于开发者认为可能会出现这么一个情况// 而且加了这一段代码呢也没有太大的一个成本,所以就把它加进去了,可以防止一些很特意的情况出现了一个问题if (!nextInstance || !tryHydrate(fiber, nextInstance)) {// Nothing to hydrate. Make it an insertion.// 如果前后两次进行try的都不成功,说明我们当前的这个节点// 没有找到可以复用的那个原生的 dom 节点的,在没有找到的情况,我们就停止 hydrate 操作// 设置 isHydrating 为 false 代表着会继续向 hostComponent 的子节点去更新的过程中// 如果又遇到了新的 hostComponent,就不会走 hydrate 的一个流程insertNonHydratedInstance((hydrationParentFiber: any), fiber);isHydrating = false;hydrationParentFiber = fiber;return;}// We matched the next one, we'll now assume that the first one was// superfluous and we'll delete it. Since we can't eagerly delete it// we'll have to schedule a deletion. To do that, this node needs a dummy// fiber associated with it.deleteHydratableInstance((hydrationParentFiber: any),firstAttemptedInstance,);}// 可以复用,找下一个节点,child节点hydrationParentFiber = fiber;nextHydratableInstance = getFirstHydratableChild((nextInstance: any));
        }
        
        • insertNonHydratedInstance 方法非常简单
          • 给fiber对象加上了 Placement 这个 SideEffect
          • 因为它是一个需要去插入的节点
        • 进入 tryHydrate
          function tryHydrate(fiber, nextInstance) {switch (fiber.tag) {// 注意这里case HostComponent: {const type = fiber.type;const props = fiber.pendingProps;const instance = canHydrateInstance(nextInstance, type, props);if (instance !== null) {fiber.stateNode = (instance: Instance); // 注意,这里挂载到了 fiber.stateNode,在后续执行 hydrateInstance 时会直接使用return true;}return false;}// 注意这里case HostText: {const text = fiber.pendingProps;const textInstance = canHydrateTextInstance(nextInstance, text);if (textInstance !== null) {fiber.stateNode = (textInstance: TextInstance);return true;}return false;}default:return false;}
          }
          
          • 进入 canHydrateInstance
            // packages/react-dom/src/client/ReactDOMHostConfig.js#L462
            export function canHydrateInstance(instance: Instance | TextInstance,type: string,props: Props,
            ): null | Instance {if (instance.nodeType !== ELEMENT_NODE ||type.toLowerCase() !== instance.nodeName.toLowerCase() // 它们标签名不一样) {return null; // 这种情况,节点不能复用}// This has now been refined to an element node.return ((instance: any): Instance); // 可以复用并返回
            }
            
          • 进入 canHydrateTextInstance
            export function canHydrateTextInstance(instance: Instance | TextInstance,text: string,
            ): null | TextInstance {if (text === '' || instance.nodeType !== TEXT_NODE) {// Empty strings are not parsed by HTML so there won't be a correct match here.return null;}// This has now been refined to a text node.return ((instance: any): TextInstance);
            }
            
          • 主要是上述 HostComponentHostText 的处理过程
          • 这个方法主要是判断节点是否可以被复用
            • 比如在 HostComponent 中来帮助我们判断正在更新的这个 hostcomponent
            • 它跟随着 hydrate 的流程一步一步走下来,正处于的那一个原本 dom 树中存在的那个节点
            • 它是否可以被复用这么的一个判断
          • 如果 tryHydrate 是 true 的,就不符合这个条件,就继续向下去找下一个节点了
            • 就通过调用 getFirstHydratableChild
            • 因为按照正常的beginWork的逻辑,接下去要进入的节点是目前正在更新的这个 HostComponent 的child的节点
            • 在它的child节点上面一直往下去找, 进入这个方法
              export function getFirstHydratableChild(parentInstance: Container | Instance,
              ): null | Instance | TextInstance {let next = parentInstance.firstChild;// Skip non-hydratable nodes.while (next &&next.nodeType !== ELEMENT_NODE &&next.nodeType !== TEXT_NODE) {next = next.nextSibling;}return (next: any);
              }
              
          • 而如果 tryHydrate 是 false 的
            • 这里面并没有直接终止 hydrate 的流程,而是去找到了它的 nextHydratableSibling
            • 调用 getNextHydratableSibling 进入它
              export function getNextHydratableSibling(instance: Instance | TextInstance,
              ): null | Instance | TextInstance {// 获取当前节点的下一个兄弟节点let node = instance.nextSibling;// Skip non-hydratable nodes.// 去遍历兄弟节点, 找到一个符合需求的那么一个节点while (node &&node.nodeType !== ELEMENT_NODE &&node.nodeType !== TEXT_NODE) {node = node.nextSibling;}return (node: any);
              }
              
          • 可见 getNextHydratableSiblinggetFirstHydratableChild 基本上差不多
            • 只不过一个是遍历的它的兄弟节点,一个是遍历它的子节点当中的兄弟节点
    • 这边在beginWork的流程中,updateHostComponent,它继续执行的更新的是向下它的子节点
    • 如果在我们更新当前节点的时候都没有找到一个可以复用的 dom 节点
    • 它的子节点根本不可能找到一个可以复用的节点来进行对应, 这是很明显的一个情况
    • react在上述工作进行了跳过 hydrate 的一个流程,让后续的代码就不需要去进行一个判断之类的操作了
    • 也节省了一些性能的损耗
    • 这就是在 beginWork 当中去执行了 tryToClaimNextHydratableInstance 这个方法
      • 它的主要目的是判断我们当前的 hostcomponent 是否有可以复用的这个节点
      • 然后去继续往下找,直到找到 beginWork 的一侧子树被 hydrate 完之后
      • 后续就要进入 completeUnitOfWork 了
      • 这样对一侧的子树的更新已经完成了
    • 在后续执行 completeUnitOfWork 的时候,要去真正的创建dom节点了
      • 对于 hostcomponent,是需要去创建 instance
      • 对于 hydrate 的一个流程,就不需要去创建 instance,而是需要去复用原本 dom 就已经存在的这个 instance
  • 在 beginWork 的流程当中,去判断更新的一个 HostComponent

  • 是否可以从 hydrate 的 dom 节点当中去找到一个可以复用的节点

  • 如果不能找到,就对这节点以及它的子树停止一个 hydrate 的流程,然后设置一些全局变量

  • 接下去走的流程是在 completeUnitOfWork

定位到 packages/react-reconciler/src/ReactFiberCompleteWork.js#L540

进入 completeWork


function completeWork(current: Fiber | null,workInProgress: Fiber,renderExpirationTime: ExpirationTime,
): Fiber | null {const newProps = workInProgress.pendingProps;switch (workInProgress.tag) {case IndeterminateComponent:break;case LazyComponent:break;case SimpleMemoComponent:case FunctionComponent:break;case ClassComponent: {const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {popLegacyContext(workInProgress);}break;}case HostRoot: {popHostContainer(workInProgress);popTopLevelLegacyContextObject(workInProgress);const fiberRoot = (workInProgress.stateNode: FiberRoot);if (fiberRoot.pendingContext) {fiberRoot.context = fiberRoot.pendingContext;fiberRoot.pendingContext = null;}if (current === null || current.child === null) {// If we hydrated, pop so that we can delete any remaining children// that weren't hydrated.popHydrationState(workInProgress);// This resets the hacky state to fix isMounted before committing.// TODO: Delete this when we delete isMounted and findDOMNode.workInProgress.effectTag &= ~Placement;}updateHostContainer(workInProgress);break;}case HostComponent: {popHostContext(workInProgress);const rootContainerInstance = getRootHostContainer();const type = workInProgress.type;if (current !== null && workInProgress.stateNode != null) {updateHostComponent(current,workInProgress,type,newProps,rootContainerInstance,);if (current.ref !== workInProgress.ref) {markRef(workInProgress);}} else {if (!newProps) {invariant(workInProgress.stateNode !== null,'We must have new props for new mounts. This error is likely ' +'caused by a bug in React. Please file an issue.',);// This can happen when we abort work.break;}const currentHostContext = getHostContext();// TODO: Move createInstance to beginWork and keep it on a context// "stack" as the parent. Then append children as we go in beginWork// or completeWork depending on we want to add then top->down or// bottom->up. Top->down is faster in IE11.let wasHydrated = popHydrationState(workInProgress);if (wasHydrated) {// TODO: Move this and createInstance step into the beginPhase// to consolidate.if (prepareToHydrateHostInstance(workInProgress,rootContainerInstance,currentHostContext,)) {// If changes to the hydrated node needs to be applied at the// commit-phase we mark this as such.markUpdate(workInProgress);}} else {let instance = createInstance(type,newProps,rootContainerInstance,currentHostContext,workInProgress,);appendAllChildren(instance, workInProgress, false, false);// Certain renderers require commit-time effects for initial mount.// (eg DOM renderer supports auto-focus for certain elements).// Make sure such renderers get scheduled for later work.if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {markUpdate(workInProgress);}workInProgress.stateNode = instance;}if (workInProgress.ref !== null) {// If there is a ref on a host node we need to schedule a callbackmarkRef(workInProgress);}}break;}case HostText: {let newText = newProps;if (current && workInProgress.stateNode != null) {const oldText = current.memoizedProps;// If we have an alternate, that means this is an update and we need// to schedule a side-effect to do the updates.updateHostText(current, workInProgress, oldText, newText);} else {if (typeof newText !== 'string') {invariant(workInProgress.stateNode !== null,'We must have new props for new mounts. This error is likely ' +'caused by a bug in React. Please file an issue.',);// This can happen when we abort work.}const rootContainerInstance = getRootHostContainer();const currentHostContext = getHostContext();let wasHydrated = popHydrationState(workInProgress);if (wasHydrated) {if (prepareToHydrateHostTextInstance(workInProgress)) {markUpdate(workInProgress);}} else {workInProgress.stateNode = createTextInstance(newText,rootContainerInstance,currentHostContext,workInProgress,);}}break;}case ForwardRef:break;case SuspenseComponent: {const nextState = workInProgress.memoizedState;if ((workInProgress.effectTag & DidCapture) !== NoEffect) {// Something suspended. Re-render with the fallback children.workInProgress.expirationTime = renderExpirationTime;// Do not reset the effect list.return workInProgress;}const nextDidTimeout = nextState !== null;const prevDidTimeout = current !== null && current.memoizedState !== null;if (current !== null && !nextDidTimeout && prevDidTimeout) {// We just switched from the fallback to the normal children. Delete// the fallback.// TODO: Would it be better to store the fallback fragment on// the stateNode during the begin phase?const currentFallbackChild: Fiber | null = (current.child: any).sibling;if (currentFallbackChild !== null) {// Deletions go at the beginning of the return fiber's effect listconst first = workInProgress.firstEffect;if (first !== null) {workInProgress.firstEffect = currentFallbackChild;currentFallbackChild.nextEffect = first;} else {workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;currentFallbackChild.nextEffect = null;}currentFallbackChild.effectTag = Deletion;}}// The children either timed out after previously being visible, or// were restored after previously being hidden. Schedule an effect// to update their visiblity.if (//nextDidTimeout !== prevDidTimeout ||// Outside concurrent mode, the primary children commit in an// inconsistent state, even if they are hidden. So if they are hidden,// we need to schedule an effect to re-hide them, just in case.((workInProgress.effectTag & ConcurrentMode) === NoContext &&nextDidTimeout)) {workInProgress.effectTag |= Update;}break;}case Fragment:break;case Mode:break;case Profiler:break;case HostPortal:popHostContainer(workInProgress);updateHostContainer(workInProgress);break;case ContextProvider:// Pop provider fiberpopProvider(workInProgress);break;case ContextConsumer:break;case MemoComponent:break;case IncompleteClassComponent: {// Same as class component case. I put it down here so that the tags are// sequential to ensure this switch is compiled to a jump table.const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {popLegacyContext(workInProgress);}break;}default:invariant(false,'Unknown unit of work tag. This error is likely caused by a bug in ' +'React. Please file an issue.',);}return null;
}
  • 在 completeWork 中,对于 HostComponent 的一个更新
  • current,它等于 null 的情况在这里, 会先去执行一个方法叫做 popHydrationState
  • 这个方法就是我们开始要去准备复用这个节点的流程了
    function popHydrationState(fiber: Fiber): boolean {// 这里直接跳过,不会执行if (!supportsHydration) {return false;}if (fiber !== hydrationParentFiber) {// We're deeper than the current hydration context, inside an inserted// tree.return false;}if (!isHydrating) {// If we're not currently hydrating but we're in a hydration context, then// we were an insertion and now need to pop up reenter hydration of our// siblings.popToNextHostParent(fiber);isHydrating = true;return false;}const type = fiber.type;// If we have any remaining hydratable nodes, we need to delete them now.// We only do this deeper than head and body since they tend to have random// other nodes in them. We also ignore components with pure text content in// side of them.// TODO: Better heuristic.if (fiber.tag !== HostComponent ||(type !== 'head' &&type !== 'body' &&!shouldSetTextContent(type, fiber.memoizedProps))) {let nextInstance = nextHydratableInstance;while (nextInstance) {deleteHydratableInstance(fiber, nextInstance);nextInstance = getNextHydratableSibling(nextInstance);}}popToNextHostParent(fiber);nextHydratableInstance = hydrationParentFiber? getNextHydratableSibling(fiber.stateNode): null;return true;
    }
    
    • 关于 fiber !== hydrationParentFiber 如下图解释
  • 在这张图里面,其中有 div, input, span和 button 这几个是原生的dom节点
  • 在第一次渲染的时候,要去尝试执行 hydrate 的, 先回到 beginWork 的流程
  • 在 beginWork 当中,调用了 tryToClaimNextHydratableInstance 的时候,
  • enterHydrationState 的里面,设置了这些初始化的变量
    function enterHydrationState(fiber: Fiber): boolean {// supportsHydration 在 react dom 环境中是 写死的 trueif (!supportsHydration) {return false;}// 默认是 root, 也就是 ReactDOM.render 中的第2个参数const parentInstance = fiber.stateNode.containerInfo;// nextHydratableInstance 是一个全局变量nextHydratableInstance = getFirstHydratableChild(parentInstance);hydrationParentFiber = fiber;isHydrating = true;return true;
    }
    
  • 然后,它的 nextHydratableInstance 是等于 getFirstHydratableChild(parentInstance)
  • 对于这张图里面的情况,它找到的它第一个可以使用的节点,它肯定是 div 这个节点
  • 也就是说执行了这个 enterHydrationState 方法之后,这个变量它变成了 div
  • 然后这个 hydrationParentFiber 是目前传进来的 fiber,也就是这个 RootFiber
  • 等到在div这个节点更新的时候,我们会执行这个 tryToClaimNextHydratableInstance 方法
  • 执行这个方法的时候,if (!tryHydrate(fiber, nextInstance)) {}, 这个 tryHydrate 是成功的
  • 成功了之后会设置 hydrationParentFiber = fiber; 这里的fiber就是当前fiber, 也就是 div 所对应的fiber
  • nextHydratableInstance 是上图的 input,因为div要找到它一侧的子节点
  • 对于这个div节点来说,input span和button都是它的子节点,第一个符合条件的就是input
  • 找到这个input之后,我们对于 beginWork 中的 updateHostComponent,也就是这个input节点的时候,我们又会去执行这个方法
  • 继续执行这个方法之后,我们的 hydrationParentFiber 变成了 input,然后 nextHydratableInstance 变成null了,因为没有子节点了
    export function getFirstHydratableChild(parentInstance: Container | Instance,
    ): null | Instance | TextInstance {let next = parentInstance.firstChild; // 这时候是 null// Skip non-hydratable nodes.// 跳过 while 循环while (next &&next.nodeType !== ELEMENT_NODE &&next.nodeType !== TEXT_NODE) {next = next.nextSibling; // 找到所有星弟节点}return (next: any); // 最终 return null
    }
    
  • 到这里为止,我们的 beginWork 已经从一侧子树更新完了
  • 这个时候要对 input 执行 completeUnitOfWork
  • 那么 completeUnitOfWork 它首先执行的是 popHydrationState, 再次回到这个方法
      function popHydrationState(fiber: Fiber): boolean {// 这里直接跳过,不会执行if (!supportsHydration) {return false;}// 这个条件肯定是不符合的, 两者是相等的if (fiber !== hydrationParentFiber) {// We're deeper than the current hydration context, inside an inserted// tree.return false;}if (!isHydrating) {// If we're not currently hydrating but we're in a hydration context, then// we were an insertion and now need to pop up reenter hydration of our// siblings.popToNextHostParent(fiber);isHydrating = true;return false;}const type = fiber.type;// If we have any remaining hydratable nodes, we need to delete them now.// We only do this deeper than head and body since they tend to have random// other nodes in them. We also ignore components with pure text content in// side of them.// TODO: Better heuristic.if (fiber.tag !== HostComponent ||(type !== 'head' &&type !== 'body' &&!shouldSetTextContent(type, fiber.memoizedProps))) {let nextInstance = nextHydratableInstance;while (nextInstance) {deleteHydratableInstance(fiber, nextInstance);nextInstance = getNextHydratableSibling(nextInstance);}}popToNextHostParent(fiber);nextHydratableInstance = hydrationParentFiber? getNextHydratableSibling(fiber.stateNode): null;return true;}
    
  • if (fiber !== hydrationParentFiber) {} 这个条件不符合,目前两者相等
  • 那么这个条件什么时候会符合呢?
    • 就是之前在 tryToClaimNextHydratableInstance 的时候
    • 去执行 tryHydrate 比如说执行对于 div 的一个 tryHydrate,发现找不到对应的节点可以复用
    • 那么这个时候我们设置的 hydrationParentFiber 是 div, 然后停止了 hydrate
    • 这个时候更新到input节点的时候,是不会执行这个 tryToClaimNextHydratableInstance 的方法
    • 执行到头部的 if (!isHydrating) 之后,它就直接 return 了,后面的逻辑是完全不会执行到的
    • 所以这个时候 hydrationParentFiber 仍然是 div
    • 所以对 input 执行 completeUnitOfWork 的时候,if (fiber !== hydrationParentFiber) {} 它们这个就不会相等了
    • 就直接return false 就可以了,因为它不能被复用
  • 接下来 if (!isHydrating) {} 什么情况下它会是 false 呢?
    • 就是说在之前 tryToClaimNextHydratableInstance 的时候, if (!tryHydrate(fiber, nextInstance)) {}, 它是不成功的这么一个情况
    • 这个时候 isHydrating 就会变成false,但是要注意的是如果它能执行到这条代码说明,在这一个节点上发现它是不能复用的
    • 而所有不能复用的节点都是它或者它的子节点,所以它的父节点是可以复用的
    • 接下去要执行 completeUnitOfWork 的是 div,这时候就要设置 isHydrating 为 true 了
    • 同时,这边它就执行了 popToNextHostParent 这个方法
      function popToNextHostParent(fiber: Fiber): void {let parent = fiber.return;while (parent !== null &&parent.tag !== HostComponent &&parent.tag !== HostRoot) {parent = parent.return;}hydrationParentFiber = parent;
      }
      
    • 它就是往它的父链上面去找,第一个是 HostComponent 或者是 HostRoot 的节点就可以了
  • if (!isHydrating) {} 这边最后 return false
    • 因为它本身它这个节点是不能被 hydrate 的
  • 然后在以上3个 if 条件都不符合的情况下, 说明我们这个节点是可以复用的
  • 可以复用的情况,做了什么事情呢?
    • if (fiber.tag !== HostComponent || (type !== 'head' && type !== 'body' && !shouldSetTextContent(type, fiber.memoizedProps))) {}
    • 它判断了是否等于body,head,并且 shouldSetTextContent 这个方法
      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));
      }
      
      • 它其实就是判断了一下是否是input相关的标签
    • 符合上述if这个时候,就是一个 HostText
      • 既然它是一个 HostText,如果当前这边的正在要被 hydrate 的这个节点它存在的话就是不合理的
      • 因为要是把当前节点都替换成纯文字的, 所以在这里,它直接把所有的节点都给它删除了,就执行这个 deleteHydratableInstance
        function deleteHydratableInstance(returnFiber: Fiber,instance: HydratableInstance,
        ) {// 跳过if (__DEV__) {// ... 忽略}// 创建一个待删除的fiber节点const childToDelete = createFiberFromHostInstanceForDeletion();childToDelete.stateNode = instance;childToDelete.return = returnFiber;childToDelete.effectTag = Deletion; // 注意这里// This might seem like it belongs on progressedFirstDeletion. However,// these children are not part of the reconciliation list of children.// Even if we abort and rereconcile the children, that will try to hydrate// again and the nodes are still in the host tree so these will be// recreated.if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = childToDelete;returnFiber.lastEffect = childToDelete;} else {returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;}
        }
        
        • 其实这边做法是比较奇怪的一个做法
        • 单独去创建了一个fiber对象,然后给这个fiber对象指定了 effectTagDeletion
        • 就是我们要通过这个删除节点,也要放到 commitRoot 里面去做
        • 这个节点,在真正的react应用当中,它是没有对应的fiber节点的,它只是在dom当中存在
        • 为了要删除它,需要单独去为它创建一个fiber对象,然后把它挂到 returnFiber 的 effect的链上
        • 最终在 commitRoot 的时候能够执行到这个effect,然后对它执行一个删除的操作,最终把dom节点删除
        • 这样做的目的,是为了统一整个react更新的流程,所以单独这么去做的一个事情
      • 这是对 HostText 的一个处理,接下去就执行 popToNextHostParent
      • 因为接下去要执行 completeWork 的是它的 parent 上面的 host 节点
      • 然后 一个三目运算,成立,通过 getNextHydratableSibling 是找它的兄弟节点
      • 如果不成立,返回 null
      • 这边为什么要这么做呢?
        • 首先它返回 null 是可以理解的,因为它这个节点已经没有父节点了
        • 在父链上已经没有找到可以去被 hydrate 的节点
        • 那么说明这个节点它就是一个 FiberRoot,这个 null 肯定是可以的
        • 如果有的话,为什么要去找它的兄弟节点呢?
          • 因为如果有兄弟节点,在 completeUnitOfWork 的过程当中,说明它也应该存在当前节点是有兄弟节点的
          • 有兄弟节点,就在 completeUnitOfWork 中, 是会返回这个兄弟节点
          • 并对这个兄弟节点执行 beginWork 这个流程
          • 所以在这种情况下,肯定是要走这个流程的,它就去找它的兄弟节点
        • 如果这边是没有兄弟节点,它就变成null了,这也是没有任何问题的
          • 在唯一一个要使用到 nextHydratableInstance 的地方是删除它的节点
          • 因为它是 null 就不会进入到 while循环,它也没有任何的关系
  • 如果是 null 会不会对后面的流程有影响呢?
    • 接着在 completeWork 当中, wasHydrated 是 true 之后
    • 会调用一个方法叫做 prepareToHydrateHostInstance
      function prepareToHydrateHostInstance(fiber: Fiber,rootContainerInstance: Container,hostContext: HostContext,
      ): boolean {if (!supportsHydration) {invariant(false,'Expected prepareToHydrateHostInstance() to never be called. ' +'This error is likely caused by a bug in React. Please file an issue.',);}const instance: Instance = fiber.stateNode; // fiber.stateNode 一开始就已经被挂载了, 这时候是 nullconst updatePayload = hydrateInstance(instance, // nullfiber.type,fiber.memoizedProps,rootContainerInstance, // hostRoot 对应的 containerhostContext,fiber,);// TODO: Type this specific to this type of component.fiber.updateQueue = (updatePayload: any);// If the update payload indicates that there is a change or if there// is a new ref we mark this as an update.if (updatePayload !== null) {return true;}return false;
      }
      
      • 进入 hydrateInstance
        export function hydrateInstance(instance: Instance, type: string,props: Props,rootContainerInstance: Container,hostContext: HostContext,internalInstanceHandle: Object,
        ): null | Array<mixed> {precacheFiberNode(internalInstanceHandle, instance);// TODO: Possibly defer this until the commit phase where all the events// get attached.updateFiberProps(instance, props);let parentNamespace: string;if (__DEV__) {const hostContextDev = ((hostContext: any): HostContextDev);parentNamespace = hostContextDev.namespace;} else {parentNamespace = ((hostContext: any): HostContextProd);}return diffHydratedProperties(instance,type,props,parentNamespace,rootContainerInstance,);
        }
        
        • 这个 precacheFiberNode 在之前执行 finalizeInitialChildren 内部,也会执行这个方法
          • 就是在 node 上面挂载了 fiber 的一个引用
        • 之后执行 updateFiberProps 就是通过不同的节点来执行了一些属性
          export function updateFiberProps(node, props) {node[internalEventHandlersKey] = props;
          }
          
        • 接下去是重点,return diffHydratedProperties
          • 这个方法其实它就是对比了原生的dom节点上面的属性,以及在 react 渲染中拿到的 props 它们之间的一个情况
          • 然后,它也会生成一个 updatePayload
          • 在 prepareToHydrateHostInstance 里面, 去执行 hydrateInstance 的时候
          • 它返回的是一个 updatepayload,并且设置到了 fiber.updateQueen 上面
          • 最终它就直接return是true还是false了,以此来判断是否这个节点要更新
          • 这个就跟之前的更新流程是非常相似的,它通过 updateQueen 去记录了这个dom节点,它需要更新的属性
          • 最终在 commitRoot 的阶段,会把这些更新都应用到这个 dom 节点上面
          • 源码,参考: packages/react-dom/src/client/ReactDOMComponent.js#L834
          • 它跟 updateProperties 唯一的区别是
            • 这个node,它本身也是由react来创建的,已经存储了一些信息了
            • 而 hydrate 它的一个流程当中的这个dom节点不是react创建的
          • 所以它的对比过程可能会稍微更复杂一点,那其实也是差不多的
            • 比如,要去绑定事件,验证 props
            • 对应每一个props 去判断一下在原生的那个节点上面是否存在
            • 如果有存在,它就不加到 updatePayload
            • 如果不存在,那都加进去,所以其实就是这么一些流程
          • 这些代码非常的长是跟dom操作相关性比较大的一些内容
          • 这些是涉及到 dom 操作的内容,都是比较基础的内容,不涉及react核心
    • 在最初的 tryHydrate 中,已经执行 fiber.stateNode = (instance: Instance)
    • 这里,fiber.stateNode 已经被赋值了其上 canHydrateInstance 得到的 instance, 也即是从dom节点上找到的instance
    • 在后续 hydrateInstance 中的 instance 参数就是 fiber.stateNode,不需要使用在 popHydrationState 中的 nextHydratableInstance 属性了
    • 所以, 在上边 popHydrationState 的时候,nextHydratableInstance 最终拿到是 null 也没有任何关系
      • 因为这个节点是可以 hydrate 的
      • 它的节点已经对应拿到了
      • 这个 nextHydratableInstance 只是一个标识的量
  • 以上,在 beginWork 复用了这个节点之后
  • 并且已经进行了 diffHydratedProperties
  • 说明我们这个节点已经复用成功了
  • 之后就是走后面的 commitRoot 流程等内容了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/666077.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[晓理紫]CCF系列会议截稿时间订阅

关注{晓理紫|小李子}&#xff0c;每日更新CCF系列会议信息&#xff0c;如感兴趣&#xff0c;请转发给有需要的同学&#xff0c;谢谢支持&#xff01;&#xff01; 如果你感觉对你有所帮助&#xff0c;请关注我&#xff0c;每日准时为你推送最新会议信息。 SAC (CCF C) Select…

物流平台架构设计与实践

随着电商行业的迅猛发展&#xff0c;物流行业也得到了极大的发展。从最初的传统物流到现在的智慧物流&#xff0c;物流技术和模式也在不断的更新与升级。物流平台作为连接电商和物流的重要媒介&#xff0c;其架构设计和实践显得尤为重要。 一、物流平台架构设计 1. 前端架构设…

京东广告算法架构体系建设--高性能计算方案最佳实践 | 京东零售广告技术团队

1、前言 推荐领域算法模型的在线推理是一个对高并发、高实时有较强要求的场景。算法最初是基于Wide & Deep相对简单的网络结构进行建模&#xff0c;容易满足高实时、高并发的推理性能要求。但随着广告模型效果优化进入深水区&#xff0c;基于Transformer用户行为序列和Att…

OpenSSL:configure: error: OpenSSL library not found解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

项目02《游戏-05-开发》Unity3D

基于 项目02《游戏-04-开发》Unity3D &#xff0c; 【任务】UI背包系统&#xff0c; 首先将Game窗口设置成1920 * 1080&#xff0c; 设置Canvas的缩放模式&#xff0c;&#xff1a;这样设置能让窗口在任意分辨率下都以一个正确的方式显示&#xff0c; 设置数值&…

Mac安装Homebrew+MySQL+Redis+Nginx+Tomcat等

Mac安装HomebrewMySQLRedisNginxTomcat等 文章目录 Mac安装HomebrewMySQLRedisNginxTomcat等一、Mac安装Mysql 8①&#xff1a;下载②&#xff1a;安装③&#xff1a;配置环境变量④&#xff1a;外部连接测试 二、Mac安装Redis和可视化工具①&#xff1a;安装Redis01&#xff1…

【Linux系统 01】Vim工具

目录 一、Vim概述 1. 文件打开方式 2. 模式切换 二、命令模式 1. 移动与跳转 2. 复制与粘贴 3. 剪切与撤销 三、编辑模式 1. 插入 2. 替换 四、末行模式 1. 保存与退出 2. 查找与替换 3. 分屏显示 4. 命令执行 一、Vim概述 1. 文件打开方式 vim 文件路径&#…

美国纳斯达克大屏怎么投放:投放完成需要多长时间-大舍传媒Dashe Media

陕西大舍广告传媒有限公司&#xff08;Shaanxi Dashe Advertising Media Co., Ltd&#xff09;&#xff0c;简称大舍传媒&#xff08;Dashe Media&#xff09;&#xff0c;是纳斯达克在中国区的总代理&#xff08;China General Agent&#xff09;。与纳斯达克合作已经有八年的…

ChatGPT实战100例 - (15) 还不会写 Stable Diffusion (SD) 绘画提示词?没关系,ChatGPT帮你搞定

文章目录 ChatGPT实战100例 - (15) 还不会写 Stable Diffusion (SD) 绘画提示词&#xff1f;没关系&#xff0c;ChatGPT帮你搞定一、把场景描述转为镜头语言二、把镜头语言转换为Prompt三、把Prompt转换为图片 ChatGPT实战100例 - (15) 还不会写 Stable Diffusion (SD) 绘画提示…

24.云原生ArgoCD高级之钩子

云原生专栏大纲 文章目录 Argo CD钩子如何定义钩子钩子删除策略 Argo CD钩子 Argo CD 是一个用于部署和管理 Kubernetes 应用程序的工具&#xff0c;它提供了一种声明式的方式来定义和自动化应用程序的部署过程。Argo CD 钩子&#xff08;Hooks&#xff09;是一种机制&#x…

历年地震数据,shp格式,含时间、位置、类型、震级等信息

基本信息. 数据名称: 历年地震数据 数据格式: Shp 数据时间: 2023年 数据几何类型: 点 数据坐标系: WGS84坐标系 数据来源&#xff1a;网络公开数据 数据字段&#xff1a; 序号字段名称字段说明1dzlx地震类型2zj震级3zysd震源深度&#xff08;米&#xff09;4jtwz…

HDMI2.1之eARC简介-Dolby Atmos和DTS:X

文章目录 eARC目的更大的带宽更高质量音频支持对象型音频与CEC&#xff08;Consumer Electronics Control&#xff09;的兼容性&#xff1a; 适应流媒体发展Dolby AtmosDTS:X高分辨率音频更高的音频位深度和采样率低延迟音频 对象型音频格式独立对象三维定位动态音场适应性和灵…

RabbitMQ(一):最新版rabbitmq安装

目录 1 简介1.1特性及好处 2 安装2.1 Ubuntu22.04 apt安装最新rabbitmq1、一键部署2、验证3、RabbitMQWeb管理界面及授权操作4、添加远程用户5、一些常用命令 2.2 Docker安装RabbitMQ - Ubuntu22.041、安装docker2、启动rabbitmq 1 简介 RabbitMQ是一个开源的遵循AMQP协议实现…

挑战杯 opencv 图像识别 指纹识别 - python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器视觉的指纹识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 该项目较为新颖&#xff0c;适…

MagicVideo-V2:多阶段高保真视频生成框架

本项工作介绍了MagicVideo-V2&#xff0c;将文本到图像模型、视频运动生成器、参考图像embedding模块和帧内插模块集成到端到端的视频生成流程中。由于这些架构设计的好处&#xff0c;MagicVideo-V2能够生成具有极高保真度和流畅度的美观高分辨率视频。通过大规模用户评估&…

Zoho Projects与Jira:中国市场的理想替代品之争?

在软件开发生命周期中&#xff0c;项目管理一直是一个非常重要的环节。为了更好地协作、追踪项目的进程和管理任务&#xff0c;许多公司选择了Jira这款著名的项目管理工具&#xff0c;它是个非常强大的工具&#xff0c;但是作为一款纯国外产品&#xff0c;他可能不适合中国市场…

python统计分析——t分布

参考资料&#xff1a;用python动手学统计学 1、t统计量 t统计量的计算公式为&#xff1a; 其中&#xff0c;为样本均值&#xff0c;μ为总体均值&#xff0c;为实际样本的无偏标准差&#xff0c;N为样本容量。 t统计量的公式与标准化公式类似。t统计量可以理解为对样本均值…

css3动画的三种实现方式

目录 一、是什么二、实现方式transition 实现渐变动画transform 转变动画animation 实现自定义动画 三、总结参考文献 一、是什么 CSS动画&#xff08;CSS Animations&#xff09;是为层叠样式表建议的允许可扩展标记语言&#xff08;XML&#xff09;元素使用CSS的动画的模块 …

分布式session 笔记

概念 解决方案‘ 复制 session同步&#xff0c;让集群下的服务器进行session同步&#xff0c;一种传统的服务器集群session管理机制&#xff0c;常用于服务器不多的集群环境。<br /> 集群下&#xff0c;进行session同步的服务器的session数据是相同的&#xff0c;…