陕西住房和城乡建设厅网站,怎么开始啊,深圳 网站设计,如何写网站开发需求React 渲染过程#xff0c;即ReactDOM.render执行过程分为两个大的阶段#xff1a;render 阶段以及 commit 阶段。React.hydrate渲染过程和ReactDOM.render差不多#xff0c;两者之间最大的区别就是#xff0c;ReactDOM.hydrate 在 render 阶段#xff0c;会尝试复用(hydr… React 渲染过程即ReactDOM.render执行过程分为两个大的阶段render 阶段以及 commit 阶段。React.hydrate渲染过程和ReactDOM.render差不多两者之间最大的区别就是ReactDOM.hydrate 在 render 阶段会尝试复用(hydrate)浏览器现有的 dom 节点并相互关联 dom 实例和 fiber以及找出 dom 属性和 fiber 属性之间的差异。 Demo
这里我们在 index.html 中直接返回一段 html以模拟服务端渲染生成的 html
!DOCTYPE html
html langenheadmeta charsetutf-8 /titleMini React/titlemeta nameviewport contentwidthdevice-width, initial-scale1 //headbodydiv idrootdiv idrootdiv idcontainerh1 idA1div idA2A2/div/h1p idBspan idB1B1/span/pspan idCC/span/div/div/div/body
/html注意root 里面的内容不能换行不然客户端hydrate的时候会提示服务端和客户端的模版不一致。
新建 index.jsx
import React from react;
import ReactDOM from react-dom;
class Home extends React.Component {constructor(props) {super(props);this.state {count: 1,};}render() {const { count } this.state;return (div idcontainerdiv idA{count} div idA2A2/div/divp idBspan idB1B1/span/p/div);}
}ReactDOM.hydrate(Home /, document.getElementById(root));对比服务端和客户端的内容可知服务端h1#A和客户端的div#A不同同时服务端比客户端多了一个span#C
在客户端开始执行之前即 ReactDOM.hydrate 开始执行前由于服务端已经返回了 html 内容浏览器会立马显示内容。对应的真实 DOM 树如下 注意这不是 fiber 树
ReactDOM.render
先来回顾一下 React 渲染更新过程分为两大阶段五小阶段
render 阶段 beginWorkcompleteUnitOfWork commit 阶段。 commitBeforeMutationEffectscommitMutationEffectscommitLayoutEffects
React 在 render 阶段会根据新的 element tree 构建 workInProgress 树收集具有副作用的 fiber 节点构建副作用链表。
特别是当我们调用ReactDOM.render函数在客户端进行第一次渲染时render阶段的completeUnitOfWork函数针对HostComponent以及HostText类型的 fiber 执行以下 dom 相关的操作 调用document.createElement为HostComponent类型的 fiber 节点创建真实的 DOM 实例。或者调用document.createTextNode为HostText类型的 fiber 节点创建真实的 DOM 实例 将 fiber 节点关联到真实 dom 的__reactFiber$rsdw3t27flk(后面是随机数)属性上。 将 fiber 节点的pendingProps 属性关联到真实 dom 的__reactProps$rsdw3t27flk(后面是随机数)属性上 将真实的 dom 实例关联到fiber.stateNode属性上fiber.stateNode dom。 遍历 pendingProps给真实的dom设置属性比如设置 id、textContent 等
React 渲染更新完成后React 会为每个真实的 dom 实例挂载两个私有的属性__reactFiber$和__reactProps$以div#container为例 ReactDOM.hydrate
hydrate中文意思是水合物这样理解有点抽象。根据源码我更乐意将hydrate的过程描述为React 在 render 阶段构造 workInProgress 树时同时按相同的顺序遍历真实的 DOM 树判断当前的 workInProgress fiber 节点和同一位置的 dom 实例是否满足hydrate的条件如果满足则直接复用当前位置的 DOM 实例并相互关联 workInProgress fiber 节点和真实的 dom 实例比如
fiber.stateNode dom;
dom.__reactProps$ fiber.pendingProps;
dom.__reactFiber$ fiber;如果 fiber 和 dom 满足hydrate的条件则还需要找出dom.attributes和fiber.pendingProps之间的属性差异。
遍历真实 DOM 树的顺序和构建 workInProgress 树的顺序是一致的。都是深度优先遍历先遍历当前节点的子节点子节点都遍历完了以后再遍历当前节点的兄弟节点。因为只有按相同的顺序fiber 树同一位置的 fiber 节点和 dom 树同一位置的 dom 节点才能保持一致
只有类型为HostComponent或者HostText类型的 fiber 节点才能hydrate。这一点也很好理解React 在 commit 阶段也就只有这两个类型的 fiber 节点才需要执行 dom 操作。
fiber 节点和 dom 实例是否满足hydrate的条件 对于类型为HostComponent的 fiber 节点如果当前位置对应的 DOM 实例nodeType为ELEMENT_NODE并且fiber.type dom.nodeName那么当前的 fiber 可以混合(hydrate) 对于类型为HostText的 fiber 节点如果当前位置对应的 DOM 实例nodeType为TEXT_NODE同时fiber.pendingProps不为空那么当前的 fiber 可以混合(hydrate)
hydrate的终极目标就是在构造 workInProgress 树的过程中尽可能的复用当前浏览器已经存在的 DOM 实例以及 DOM 上的属性这样就无需再为 fiber 节点创建 DOM 实例同时对比现有的 DOM 的attribute以及 fiber 的pendingProps找出差异的属性。然后将 dom 实例和 fiber 节点相互关联(通过 dom 实例的__reactFiber$以及__reactProps$fiber 的 stateNode 相互关联)
hydrate 过程
React 在 render 阶段构造HostComponent或者HostText类型的 fiber 节点时会首先调用 tryToClaimNextHydratableInstance(workInProgress) 方法尝试给当前 fiber 混合(hydrate)DOM 实例。如果当前 fiber 不能被混合那当前节点的所有子节点在后续的 render 过程中都不再进行hydrate而是直接创建 dom 实例。等到当前节点所有子节点都调用completeUnitOfWork完成工作后又会从当前节点的兄弟节点开始尝试混合。
以下面的 demo 为例
// 服务端返回的DOM结构这里为了直观我格式化了一下按理服务端返回的内容是不允许换行或者有空字符串的
bodydiv idrootdiv idcontainerh1 idA1 div idA2A2/div/h1p idBspan idB1B1/span/pspan idCC/span/div/div
/body
// 客户端生成的内容
div idcontainerdiv idA1 div idA2A2/div/divp idBspan idB1B1/span/p
/divrender 阶段按以下顺序 div#container 满足hydrate的条件因此关联 domfiber.stateNode div#container。然后使用hydrationParentFiber记录当前混合的 fiber 节点hydrationParentFiber fiber。获取下一个 DOM 实例这里是h1#A保存在变量nextHydratableInstance中nextHydratableInstance h1#A。
这里hydrationParentFiber 和 nextHydratableInstance 都是全局变量。 div#A 和 h1#A 不能混合这时并不会立即结束混合的过程React 继续对比h1#A的兄弟节点即p#B发现div#A还是不能和p#B混合经过最多两次对比React 认为 dom 树中已经没有 dom 实例满足和div#A这个 fiber 混合的条件于是div#A节点及其所有子孙节点都不再进行混合的过程此时将isHydrating设置为 false 表明div#A这棵子树都不再走混合的过程直接走创建 dom 实例。同时控制台提示Expected server HTML to contain a matching.. 之类的错误。 beginWork 执行到文本节点 1 时发现 isHydrating false因此直接跳过混合的过程在completeUnitOfWork阶段直接调用document.createTextNode直接为其创建文本节点 同样的beginWork 执行到节点div#A2时发现isHydrating false因此直接跳过混合的过程在completeUnitOfWork阶段直接调用document.createElement直接为其创建真实 dom 实例并设置属性 由于div#A的子节点都已经completeUnitWork了轮到div#A调用completeUnitWork完成工作将hydrationParentFiber指向其父节点即div#container这个 dom 实例。设置isHydrating true表明可以为当前节点的兄弟节点继续混合的过程了。div#A没有混合的 dom 实例因此调用document.createElement为其创建真实的 dom 实例。 为p#B执行 beginWork。由于nextHydratableInstance保存的还是h1#Adom 实例因此p#B和h1#A对比发现不能复用React 尝试和h1#A的兄弟节点p#B对比发现 fiberp#B和 domp#B能混因此将h1#A标记为删除同时关联 dom 实例fiber.stateNode p#B保存hydrationParentFiber fibernextHydratableInstance指向p#B的第一个子节点即span#B1
…省略了后续的过程。
从上面的执行过程可以看出hydrate 的过程如下
调用 tryToClaimNextHydratableInstance 开始混合判断当前 fiber 节点和同一位置的 dom 实例是否满足混合的条件。如果当前位置的 dom 实例不满足混合条件则继续比较当前 dom 的兄弟元素如果兄弟元素和当前的 fiber 也不能混合则当前 fiber 及其所有子孙节点都不能混合后续 render 过程将会跳过混合。直到当前 fiber 节点的兄弟节点 render才会继续混合的过程。
相关参考视频讲解进入学习
事件绑定
React在初次渲染时不论是ReactDOM.render还是ReactDOM.hydrate会调用createRootImpl函数创建fiber的容器在这个函数中调用listenToAllSupportedEvents注册所有原生的事件。
function createRootImpl(container, tag, options) {// ...var root createContainer(container, tag, hydrate);// ...listenToAllSupportedEvents(container);// ...return root;
}这里container就是div#root节点。listenToAllSupportedEvents会给div#root节点注册浏览器支持的所有原生事件比如onclick等。React合成事件一文介绍过React采用的是事件委托的机制将所有事件代理到div#root节点上。以下面的为例
div idA onClick{this.handleClick}
button
div我们知道React在渲染时会将fiber的props关联到真实的dom的__reactProps$属性上此时
div#A.__reactProps$ {onClick: this.handleClick
}当我们点击按钮时会触发div#root上的事件监听器
function onclick(e){const target e.targetconst fiberProps target.__reactProps$const clickhandle fiberProps.onClickif(clickhandle){clickhandle(e)}
}这样我们就可以实现事件的委托。这其中最重要的就是将fiber的props挂载到真实的dom实例的__reactProps$属性上。因此只要我们在hydrate阶段能够成功关联dom和fiber就自然也实现了事件的“绑定”
hydrate 源码剖析
hydrate 的过程发生在 render 阶段commit 阶段几乎没有和 hydrate 相关的逻辑。render 阶段又分为两个小阶段beginWork 和 completeUnitOfWork。只有HostRoot、HostComponent、HostText三种类型的 fiber 节点才需要 hydrate因此源码只针对这三种类型的 fiber 节点剖析
beginWork
beginWork 阶段判断 fiber 和 dom 实例是否满足混合的条件如果满足则为 fiber 关联 dom 实例fiber.stateNode dom
function beginWork(current, workInProgress, renderLanes) {switch (workInProgress.tag) {case HostRoot:return updateHostRoot(current, workInProgress, renderLanes);case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);case HostText:return updateHostText(current, workInProgress);}
}HostRoot Fiber
HostRoot fiber 是容器root的 fiber 节点。
这里主要是判断当前 render 是ReactDOM.render还是ReactDOM.hydrate我们调用ReactDOM.hydrate渲染时root.hydrate为 true。
如果是调用的ReactDOM.hydrate则调用enterHydrationState函数进入hydrate的过程。这个函数主要是初始化几个全局变量
isHydrating。表示当前正处于 hydrate 的过程。如果当前节点及其所有子孙节点都不满足 hydrate 的条件时这个变量为 falsehydrationParentFiber。当前混合的 fiber。正常情况下该变量和HostComponent或者HostText类型的 workInProgress 一致。nextHydratableInstance。下一个可以混合的 dom 实例。当前 dom 实例的第一个子元素或者兄弟元素。
注意getNextHydratable会判断 dom 实例是否是ELEMENT_NODE类型(对应的 fiber 类型是HostComponent)或者TEXT_NODE类型(对应的 fiber 类型是HostText)。只有ELEMENT_NODE或者HostText类型的 dom 实例才是可以 hydrate 的
function updateHostRoot(current, workInProgress, renderLanes) {if (root.hydrate enterHydrationState(workInProgress)) {var child mountChildFibers(workInProgress, null, nextChildren);}return workInProgress.child;
}
function getNextHydratable(node) {// 跳过 non-hydratable 节点.for (; node ! null; node node.nextSibling) {var nodeType node.nodeType;if (nodeType ELEMENT_NODE || nodeType TEXT_NODE) {break;}}return node;
}function enterHydrationState() {var parentInstance fiber.stateNode.containerInfo;nextHydratableInstance getNextHydratable(parentInstance.firstChild);hydrationParentFiber fiber;isHydrating true;
}HostComponent
function updateHostComponent(current, workInProgress, renderLanes) {if (current null) {tryToClaimNextHydratableInstance(workInProgress);}reconcileChildren(current, workInProgress, nextChildren, renderLanes);return workInProgress.child;
}HostText Fiber
function updateHostText(current, workInProgress) {if (current null) {tryToClaimNextHydratableInstance(workInProgress);}return null;
}tryToClaimNextHydratableInstance
假设当前 fiberA 对应位置的 dom 为 domAtryToClaimNextHydratableInstance 会首先调用tryHydrate判断 fiberA 和 domA 是否满足混合的条件
如果 fiberA 和 domA 满足混合的条件则将hydrationParentFiber fiberA;。并且获取 domA 的第一个子元素赋值给nextHydratableInstance如果 fiberA 和 domA 不满足混合的条件则获取 domA 的兄弟节点即 domB调用tryHydrate判断 fiberA 和 domB 是否满足混合条件 如果 domB 满足和 fiberA 混合的条件则将 domA 标记为删除并获取 domB 的第一个子元素赋值给nextHydratableInstance如果 domB 不满足和 fiberA 混合的条件则调用insertNonHydratedInstance提示错误“Warning: Expected server HTML to contain a matching”同时将isHydrating标记为 false 退出。
这里可以看出tryToClaimNextHydratableInstance最多比较两个 dom 节点如果两个 dom 节点都无法满足和 fiberA 混合的条件则说明当前 fiberA 及其所有的子孙节点都无需再进行混合的过程因此将isHydrating标记为 false。等到当前 fiberA 节点及其子节点都完成了工作即都执行了completeWorkisHydrating才会被设置为 true以便继续比较 fiberA 的兄弟节点
这里还需要注意一点如果两个 dom 都无法满足和 fiberA 混合那么nextHydratableInstance依然保存的是 domAdomA 会继续和 fiberA 的兄弟节点比对。
function tryToClaimNextHydratableInstance(fiber) {if (!isHydrating) {return;}var nextInstance nextHydratableInstance;var firstAttemptedInstance nextInstance;if (!tryHydrate(fiber, nextInstance)) {// 如果第一次调用tryHydrate发现当前fiber和dom不满足hydrate的条件则获取dom的兄弟节点// 然后调用 tryHydrate 继续对比fiber和兄弟节点是否满足混合nextInstance getNextHydratableSibling(firstAttemptedInstance);if (!nextInstance || !tryHydrate(fiber, nextInstance)) {// 对比了两个dom发现都无法和fiber混合因此调用insertNonHydratedInstance控制台提示错误insertNonHydratedInstance(hydrationParentFiber, fiber);isHydrating false;hydrationParentFiber fiber;return;}// 如果第一次tryHydrate不满足第二次tryHydrate满足则说明兄弟节点和当前fiber是可以混合的此时需要删除当前位置的domdeleteHydratableInstance(hydrationParentFiber, firstAttemptedInstance);}hydrationParentFiber fiber;nextHydratableInstance getFirstHydratableChild(nextInstance);
}// 将dom实例保存在 fiber.stateNode上
function tryHydrate(fiber, nextInstance) {switch (fiber.tag) {case HostComponent: {if (nextInstance.nodeType ELEMENT_NODE fiber.type.toLowerCase() nextInstance.nodeName.toLowerCase()) {fiber.stateNode nextInstance;return true;}return false;}case HostText: {var text fiber.pendingProps;if (text ! nextInstance.nodeType TEXT_NODE) {fiber.stateNode nextInstance;return true;}return false;}default:return false;}
}completeUnitOfWork
completeUnitOfWork 阶段主要是给 dom 关联 fiber 以及 propsdom.__reactProps$ fiber.pendingProps;dom.__reactFiber$ fiber;同时对比fiber.pendingProps和dom.attributes的差异
function completeUnitOfWork(unitOfWork) {var completedWork unitOfWork;do {var current completedWork.alternate;var returnFiber completedWork.return;next completeWork(current, completedWork, subtreeRenderLanes);var siblingFiber completedWork.sibling;if (siblingFiber ! null) {workInProgress siblingFiber;return;}completedWork returnFiber;workInProgress completedWork;} while (completedWork ! null);
}
function completeWork(current, workInProgress, renderLanes) {switch (workInProgress.tag) {case HostRoot: {if (current null) {var wasHydrated popHydrationState(workInProgress);if (wasHydrated) {markUpdate(workInProgress);}}return null;}case HostComponent:// 第一次渲染if (current null) {var _wasHydrated popHydrationState(workInProgress);if (_wasHydrated) {// 如果存在差异的属性则将fiber副作用标记为更新if (prepareToHydrateHostInstance(workInProgress)) {markUpdate(workInProgress);}} else {}}case HostText: {var newText newProps;if (current null) {var _wasHydrated2 popHydrationState(workInProgress);if (_wasHydrated2) {if (prepareToHydrateHostTextInstance(workInProgress)) {markUpdate(workInProgress);}}}return null;}}
}popHydrationState
function popHydrationState(fiber) {if (fiber ! hydrationParentFiber) {return false;}if (!isHydrating) {popToNextHostParent(fiber);isHydrating true;return false;}var type fiber.type;if (fiber.tag ! HostComponent ||!shouldSetTextContent(type, fiber.memoizedProps)) {var nextInstance nextHydratableInstance;while (nextInstance) {deleteHydratableInstance(fiber, nextInstance);nextInstance getNextHydratableSibling(nextInstance);}}popToNextHostParent(fiber);nextHydratableInstance hydrationParentFiber? getNextHydratableSibling(fiber.stateNode): null;return true;
}以下图为例 在 beginWork 阶段对 p#B fiber 工作时发现 dom 树中同一位置的h1#B不满足混合的条件于是继续对比h1#B的兄弟节点即div#C仍然无法混合经过最多两轮对比后发现p#B这个 fiber 没有可以混合的 dom 节点于是将 isHydrating 标记为 falsehydrationParentFiber fiberP#B。p#B的子孙节点都不再进行混合的过程。
div#B1fiber 没有子节点因此它可以调用completeUnitOfWork完成工作completeUnitOfWork 阶段调用 popHydrationState 方法在popHydrationState方法内部首先判断 fiber ! hydrationParentFiber由于此时的hydrationParentFiber等于p#B因此条件成立不用往下执行。
由于p#B fiber 的子节点都已经完成了工作因此它也可以调用completeUnitOfWork完成工作。同样的在popHydrationState函数内部第一个判断fiber ! hydrationParentFiber不成立两者是相等的。第二个条件!isHydrating成立进入条件语句首先调用popToNextHostParent将hydrationParentFiber设置为p#B的第一个类型为HostComponent的祖先元素这里是div#A fiber然后将isHydrating设置为 true指示可以为p#B的兄弟节点进行混合。
如果服务端返回的 DOM 有多余的情况则调用deleteHydratableInstance将其删除比如下图中div#D节点将会在div#Afiber 的completeUnitOfWork阶段删除 prepareToHydrateHostInstance
对于HostComponent类型的fiber会调用这个方法这里只要是关联 dom 和 fiber
设置domInstance.__reactFiber$w63z5ormsqk fiber设置domInstance.__reactProps$w63z5ormsqk props对比服务端和客户端的属性
function prepareToHydrateHostInstance(fiber) {var domInstance fiber.stateNode;var updatePayload hydrateInstance(domInstance,fiber.type,fiber.memoizedProps,fiber);fiber.updateQueue updatePayload;if (updatePayload ! null) {return true;}return false;
}
function hydrateInstance(domInstance, type, props, fiber) {precacheFiberNode(fiber, domInstance); // domInstance.__reactFiber$w63z5ormsqk fiberupdateFiberProps(domInstance, props); // domInstance.__reactProps$w63z5ormsqk props// 比较dom.attributes和props的差异如果dom.attributes的属性比props多说明服务端添加了额外的属性此时控制台提示。// 注意在对比过程中只有服务端和客户端的children属性(即文本内容)不同时控制台才会提示错误同时在commit阶段客户端会纠正这个错误以客户端的文本为主。// 但是如果是id不同则客户端并不会纠正。return diffHydratedProperties(domInstance, type, props);
}这里重点讲下diffHydratedProperties以下面的demo为例
// 服务端对应的dom
div idrootdiv extraserver attr idserver客户端的文本/div/div
// 客户端
render() {const { count } this.state;return div idclient客户端的文本/div;
}在diffHydratedProperties的过程中发现服务端返回的id和客户端的id不同控制台提示id不匹配但是客户端并不会纠正这个可以看到浏览器的id依然是server。
同时服务端多返回了一个extra属性因此需要控制台提示但由于已经提示了id不同的错误这个错误就不会提示。
最后客户端的文本和服务端的children不同即文本内容不同也需要提示错误同时客户端会纠正这个文本以客户端的为主。 prepareToHydrateHostTextInstance
对于HostText类型的fiber会调用这个方法这个方法逻辑比较简单就不详细介绍了 务端对应的dom
div idrootdiv extraserver attr idserver客户端的文本/div/div
// 客户端
render() {const { count } this.state;return div idclient客户端的文本/div;
}在diffHydratedProperties的过程中发现服务端返回的id和客户端的id不同控制台提示id不匹配但是客户端并不会纠正这个可以看到浏览器的id依然是server。
同时服务端多返回了一个extra属性因此需要控制台提示但由于已经提示了id不同的错误这个错误就不会提示。
最后客户端的文本和服务端的children不同即文本内容不同也需要提示错误同时客户端会纠正这个文本以客户端的为主。 prepareToHydrateHostTextInstance
对于HostText类型的fiber会调用这个方法这个方法逻辑比较简单就不详细介绍了