網站首頁 編程語言 正文
引言
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 lang="en"> <head> <meta charset="utf-8" /> <title>Mini React</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> <div id="root"><div id="root"><div id="container"><h1 id="A">1<div id="A2">A2</div></h1><p id="B"><span id="B1">B1</span></p><span id="C">C</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 id="container"> <div id="A"> {count} <div id="A2">A2</div> </div> <p id="B"> <span id="B1">B1</span> </p> </div> ); } } ReactDOM.hydrate(<Home />, document.getElementById("root"));
對比服務端和客戶端的內容可知,服務端h1#A
和客戶端的div#A
不同,同時服務端比客戶端多了一個span#C
在客戶端開始執行之前,即 ReactDOM.hydrate
開始執行前,由于服務端已經返回了 html 內容,瀏覽器會立馬顯示內容。對應的真實 DOM 樹如下:
注意,這不是 fiber 樹?。?/p>
ReactDOM.render
先來回顧一下 React 渲染更新過程,分為兩大階段,五小階段:
render 階段
- beginWork
- completeUnitOfWork
commit 階段。
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
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結構,這里為了直觀,我格式化了一下,按理服務端返回的內容,是不允許換行或者有空字符串的 <body> <div id="root"> <div id="container"> <h1 id="A"> 1 <div id="A2">A2</div> </h1> <p id="B"> <span id="B1">B1</span> </p> <span id="C">C</span> </div> </div> </body> // 客戶端生成的內容 <div id="container"> <div id="A"> 1 <div id="A2">A2</div> </div> <p id="B"> <span id="B1">B1</span> </p> </div>
render 階段,按以下順序:
div#container
滿足hydrate
的條件,因此關聯 dom,fiber.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#A
dom 實例,因此p#B
和h1#A
對比發現不能復用,React 嘗試和h1#A
的兄弟節點p#B
對比,發現 fiberp#B
和 domp#B
能混,因此將h1#A
標記為刪除,同時關聯 dom 實例:fiber.stateNode = p#B
,保存hydrationParentFiber = fiber
,nextHydratableInstance
指向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 id="A" onClick={this.handleClick}> button <div>
我們知道React在渲染時,會將fiber的props關聯到真實的dom的__reactProps$
屬性上,此時
div#A.__reactProps$ = { onClick: this.handleClick }
當我們點擊按鈕時,會觸發div#root
上的事件監聽器:
function onclick(e){ const target = e.target const fiberProps = target.__reactProps$ const clickhandle = fiberProps.onClick if(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 的條件時,這個變量為 false
- hydrationParentFiber。當前混合的 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 為 domA,tryToClaimNextHydratableInstance
會首先調用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 節點及其子節點都完成了工作,即都執行了completeWork
,isHydrating
才會被設置為 true,以便繼續比較 fiberA 的兄弟節點
這里還需要注意一點,如果兩個 dom 都無法滿足和 fiberA 混合,那么nextHydratableInstance
依然保存的是 domA,domA 會繼續和 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是可以混合的,此時需要刪除當前位置的dom deleteHydratableInstance(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 以及 props:dom.__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
標記為 false,hydrationParentFiber = fiberP#B
。p#B
的子孫節點都不再進行混合的過程。
div#B1
fiber 沒有子節點,因此它可以調用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#A
fiber 的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 = fiber updateFiberProps(domInstance, props); // domInstance.__reactProps$w63z5ormsqk = props // 比較dom.attributes和props的差異,如果dom.attributes的屬性比props多,說明服務端添加了額外的屬性,此時控制臺提示。 // 注意,在對比過程中,只有服務端和客戶端的children屬性(即文本內容)不同時,控制臺才會提示錯誤,同時在commit階段,客戶端會糾正這個錯誤,以客戶端的文本為主。 // 但是,如果是id不同,則客戶端并不會糾正。 return diffHydratedProperties(domInstance, type, props); }
這里重點講下diffHydratedProperties
,以下面的demo為例:
// 服務端對應的dom <div id="root"><div extra="server attr" id="server">客戶端的文本</div></div> // 客戶端 render() { const { count } = this.state; return <div id="client">客戶端的文本</div>; }
在diffHydratedProperties
的過程中發現,服務端返回的id和客戶端的id不同,控制臺提示id不匹配,但是客戶端并不會糾正這個,可以看到瀏覽器的id依然是server
。
同時,服務端多返回了一個extra
屬性,因此需要控制臺提示,但由于已經提示了id不同的錯誤,這個錯誤就不會提示。
最后,客戶端的文本和服務端的children不同,即文本內容不同,也需要提示錯誤,同時,客戶端會糾正這個文本,以客戶端的為主。
prepareToHydrateHostTextInstance
對于HostText
類型的fiber會調用這個方法,這個方法邏輯比較簡單,就不詳細介紹了 務端對應的dom
<div id="root"><div extra="server attr" id="server">客戶端的文本</div></div> // 客戶端 render() { const { count } = this.state; return <div id="client">客戶端的文本</div>; }
在diffHydratedProperties
的過程中發現,服務端返回的id和客戶端的id不同,控制臺提示id不匹配,但是客戶端并不會糾正這個,可以看到瀏覽器的id依然是server
。
同時,服務端多返回了一個extra
屬性,因此需要控制臺提示,但由于已經提示了id不同的錯誤,這個錯誤就不會提示。
最后,客戶端的文本和服務端的children不同,即文本內容不同,也需要提示錯誤,同時,客戶端會糾正這個文本,以客戶端的為主。
prepareToHydrateHostTextInstance
對于HostText
類型的fiber會調用這個方法,這個方法邏輯比較簡單,就不詳細介紹了
原文鏈接:https://juejin.cn/post/7184966395762278437
相關推薦
- 2022-01-26 阿里云服務器端口請求失?。ㄔ诳刂婆_把端口添加到服務器的安全組)
- 2023-04-20 el-table多選+搜索
- 2022-04-27 幾個關于python??Pdf?技巧的分享_python
- 2022-10-30 Django視圖層與模板層實例詳解_python
- 2021-12-10 C#實現簡易灰度圖和酷炫HeatMap熱力圖winform(附DEMO)_C#教程
- 2023-12-15 Linux系統中date命令、hwclock命令 語法詳解
- 2022-09-21 Shell自動化配置SSH免密登錄和取消SSH免密配置腳本_linux shell
- 2023-02-12 完美解決Redis在雙擊redis-server.exe出現閃退問題_Redis
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支