React Hooks在SD-WAN項目中實踐

前端 | React Hooks在SD-WAN項目的實踐.png

前言

React Hooks是React16新出的基于函數(shù)式組件的一組新的api肥印,其不同于之前class組件的內(nèi)層嵌套方式,利用hooks進行鉤子方式的對數(shù)據(jù)進行了組件間的流向組織锥债,sdwan項目中都是基于函數(shù)式組件的封裝曼振,本文為sdwan項目中的react hooks的應用實踐

目錄

  • 添加警告規(guī)則彈窗組件實踐
  • React Hooks源碼解讀
  • React Fiber數(shù)據(jù)結(jié)構分析

探索案例

添加警告規(guī)則彈窗組件實踐

addRule.gif

[組件目錄]

  • components

  • addRule.jsx

  • RuleList.jsx

  • index.jsx

  • index.less

[目錄描述] addRule是點擊彈窗后彈出的主體組件

[源碼分析] addRule是添加規(guī)則的彈窗,其中在告警規(guī)則一欄中仗嗦,需要對列表中的行進行加減操作,這里最先想到的就是利用useState進行數(shù)據(jù)的管理,但其實useState是useReducer的語法糖虱饿,后續(xù)源碼中會分析,我們看到使用了useState后可以將所有狀態(tài)抽離到頂部悼嫉,后續(xù)凡是需要使用trNum或setTrNum的便可以直接使用解孙,這樣就省去了在setState中的設置以及對相應this的綁定問題,使得數(shù)據(jù)的操作更加純粹而且明晰

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">const AddRule = (props) => { const { children, title } = props; ...... const [trNum, setTrNum] = useState(1); const trLoop = (n) => { let arr = []; for(let i=0; i< n; i++) { arr.push( <tr> <td> <Select placeholder='請選擇' defaultValue='0' style={{width:'120px'}} > {options.params.map((d) => ( <Select.Option value={d.status} key={d.status}> {d.text} </Select.Option> ))} </Select> </td> <td> <Select placeholder='請選擇' defaultValue='0' > {options.compare.map((d) => ( <Select.Option value={d.status} key={d.status}> {d.text} </Select.Option> ))} </Select> </td> <td> <Select placeholder='請選擇' defaultValue={currentType} onChange={val => setTypeValue(val)} > {options.type.map((d) => ( <Select.Option value={d.status} key={d.status}> {d.text} </Select.Option> ))} </Select> </td> <td> { typeValue == options.type[1].status ? <span style={{display: 'inline-flex', verticalAlign: 'middle', lineHeight: '32px', width: '120px'}}> <Input placeholder=""/>dBm </span> : <Select placeholder='請選擇' defaultValue='0' style={{width:'120px'}} > {options.params.map((d) => ( <Select.Option value={d.status} key={d.status}> {d.text} </Select.Option> ))} </Select> } </td> <td> <PlusOutlined style={{color: '#1890ff'}} onClick={()=>setTrNum(trNum + 1)}/> </td> <td> <CloseOutlined style={{color: '#ff4d4f'}} onClick={()=> trNum>1 && setTrNum(trNum - 1)}/> </td> </tr> ) }; return arr; }; ...... return ( <> <span onClick={showModelHandler}>{children}</span> <Modal title={title} visible={visible} onCancel={hideModelHandler} onOk={handleOk} maskClosable={false} destroyOnClose > <Form form={form} layout="vertical"> ...... <Form.Item name="告警規(guī)則" label="告警規(guī)則"> <div style={{width: '100%', backgroundColor: '#ececec', padding: '10px'}}> <span> 符合以下?<Select placeholder='請選擇' defaultValue='0' style={{width: '120px'}} > {options.rule.map((d) => ( <Select.Option value={d.status} key={d.status}> {d.text} </Select.Option> ))} </Select>?條件: </span> <div style={{ border: '1px solid #ccc', width: '100%', background: '#fff', marginTop: '10px', padding: '4px' }} > <table > <tbody > { trLoop(trNum) } </tbody> </table> </div> </div> </Form.Item> ...... </Form> </Modal> </> ); };</pre>

React Hooks源碼解讀

image

[組件目錄]

  • packages

  • react

  • src

  • ReactHooks.js

這里僅僅是做了一個名稱的導出包括:

  • useContext
  • useState
  • useReducer
  • useRef
  • useEffect
  • useLayoutEffect
  • useCallback
  • useMemo
  • useImperativeHandles
  • useDebugValue
  • useTransition
  • useDeferredValue
  • useOpaqueIdentifier
  • useMutableSource

這里真正的源碼是放在了packages/react-reconciler/src/ReactFiberHooks.js里双戳,可以看出其利用的仍然是React的核心數(shù)據(jù)結(jié)構Fiber的調(diào)度作用

hooks02.png

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any { renderLanes = nextRenderLanes; currentlyRenderingFiber = workInProgress; if (DEV) { hookTypesDev = current !== null ? ((current._debugHookTypes: any): Array<HookType>) : null; hookTypesUpdateIndexDev = -1; // Used for hot reloading: ignorePreviousDependencies = current !== null && current.type !== workInProgress.type; } workInProgress.memoizedState = null; workInProgress.updateQueue = null; workInProgress.lanes = NoLanes; // The following should have already been reset // currentHook = null; // workInProgressHook = null; // didScheduleRenderPhaseUpdate = false; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy) // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. // Non-stateful hooks (e.g. context) don't get added to memoizedState, // so memoizedState would be null during updates and mounts. if (DEV) { if (current !== null && current.memoizedState !== null) { ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; } else if (hookTypesDev !== null) { // This dispatcher handles an edge case where a component is updating, // but no stateful hooks have been used. // We want to match the production code behavior (which will use HooksDispatcherOnMount), // but with the extra DEV validation to ensure hooks ordering hasn't changed. // This dispatcher does that. ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; } else { ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; } } else { ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; } let children = Component(props, secondArg); // Check if there was a render phase update if (didScheduleRenderPhaseUpdateDuringThisPass) { // Keep rendering in a loop for as long as render phase updates continue to // be scheduled. Use a counter to prevent infinite loops. let numberOfReRenders: number = 0; do { didScheduleRenderPhaseUpdateDuringThisPass = false; invariant( numberOfReRenders < RE_RENDER_LIMIT, 'Too many re-renders. React limits the number of renders to prevent ' + 'an infinite loop.', ); numberOfReRenders += 1; if (DEV) { // Even when hot reloading, allow dependencies to stabilize // after first render to prevent infinite render phase updates. ignorePreviousDependencies = false; } // Start over from the beginning of the list currentHook = null; workInProgressHook = null; workInProgress.updateQueue = null; if (DEV) { // Also validate hook order for cascading updates. hookTypesUpdateIndexDev = -1; } ReactCurrentDispatcher.current = DEV ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender; children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); } // We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrancy. ReactCurrentDispatcher.current = ContextOnlyDispatcher; if (DEV) { workInProgress._debugHookTypes = hookTypesDev; } // This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; renderLanes = NoLanes; currentlyRenderingFiber = (null: any); currentHook = null; workInProgressHook = null; if (DEV) { currentHookNameInDev = null; hookTypesDev = null; hookTypesUpdateIndexDev = -1; } didScheduleRenderPhaseUpdate = false; invariant( !didRenderTooFewHooks, 'Rendered fewer hooks than expected. This may be caused by an accidental ' + 'early return statement.', ); return children; }</pre>

從中抽離出核心的hooks渲染虹蒋,其他的具體的use方法可以在其上進行擴展,可以看出其實質(zhì)是是基于Fiber的workInProgress的全局變量的更改與調(diào)度飒货,其中包含記錄當前hook狀態(tài)的memoizedState以及需要更新的隊列updateQueue魄衅,hooks的隊列通過memoizedState及next構成了一個鏈表,整個hook的核心是基于Dispatcher的切換hook的調(diào)用塘辅,這里就涉及到Fiber的整個數(shù)據(jù)結(jié)構晃虫,在下一節(jié)中進行描述

React Fiber數(shù)據(jù)結(jié)構分析

hooks03.jpg
image

[組件目錄]

  • packages

  • react-reconciler

  • src

  • ReactFiber.js

簡單來說React的Fiber數(shù)據(jù)結(jié)構是維護了一個如下的數(shù)據(jù)格式:

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">Fiber = { // 標識 fiber 類型的標簽,詳情參看下述 WorkTag tag: WorkTag, // 指向父節(jié)點 return: Fiber | null, // 指向子節(jié)點 child: Fiber | null, // 指向兄弟節(jié)點 sibling: Fiber | null, // 在開始執(zhí)行時設置 props 值 pendingProps: any, // 在結(jié)束時設置的 props 值 memoizedProps: any, // 當前 state memoizedState: any, // Effect 類型扣墩,詳情查看以下 effectTag effectTag: SideEffectTag, // effect 節(jié)點指針哲银,指向下一個 effect nextEffect: Fiber | null, // effect list 是單向鏈表扛吞,第一個 effect firstEffect: Fiber | null, // effect list 是單向鏈表,最后一個 effect lastEffect: Fiber | null, // work 的過期時間荆责,可用于標識一個 work 優(yōu)先級順序 expirationTime: ExpirationTime, };</pre>

lifecycle.jpg

該數(shù)據(jù)結(jié)構是一個通過鏈表實現(xiàn)的樹的結(jié)構滥比,整個React的階段可分為Render Phase、Pre-Commit Phase以及Commit Phase做院,F(xiàn)iber的設計初衷是利用瀏覽器渲染過程中剩余的時間碎片來進行render盲泛,而要達到這個目的需要能夠?qū)︿秩具^程的工作進行暫停、終止以及復用键耕,F(xiàn)iber便是利用數(shù)據(jù)結(jié)構實現(xiàn)了這樣一個虛擬堆棧幀查乒。

hooks05.jpg

這里不再對協(xié)調(diào)(Reconciliation)和調(diào)度(Scheduling)的具體過程,如expirationTime的權重設計郁竟、Effect lists的DFS算法設計等進行講述玛迄,有興趣的同學可以參看這篇文章(React Fiber 源碼解析)

基于React Hooks涉及到的workInProgress,我們重點看一下這里的設計

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// This is used to create an alternate fiber to do work on. export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { // We use a double buffering pooling technique because we know that we'll // only ever need at most two versions of a tree. We pool the "other" unused // node that we're free to reuse. This is lazily created to avoid allocating // extra objects for things that are never updated. It also allow us to // reclaim the extra memory if needed. workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; if (DEV) { // DEV-only fields workInProgress._debugID = current._debugID; workInProgress._debugSource = current._debugSource; workInProgress._debugOwner = current._debugOwner; workInProgress._debugHookTypes = current._debugHookTypes; } workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type. workInProgress.type = current.type; // We already have an alternate. workInProgress.subtreeTag = NoSubtreeEffect; workInProgress.deletions = null; // The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null; if (enableProfilerTimer) { // We intentionally reset, rather than copy, actualDuration & actualStartTime. // This prevents time from endlessly accumulating in new commits. // This has the downside of resetting values for different priority renders, // But works for yielding (the common case) and should support resuming. workInProgress.actualDuration = 0; workInProgress.actualStartTime = -1; } } // Reset all effects except static ones. // Static effects are not specific to a render. workInProgress.effectTag = current.effectTag & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, }; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; } if (DEV) { workInProgress._debugNeedsRemount = current._debugNeedsRemount; switch (workInProgress.tag) { case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); break; case ClassComponent: workInProgress.type = resolveClassForHotReloading(current.type); break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; default: break; } } return workInProgress; }</pre>

這里涉及到的workInProgress和current兩個樹通過alternate這個指針的互相指引操作來實現(xiàn)首次渲染和非首次渲染的對比更新棚亩,保證兩個隊列都更新而不會丟失蓖议,并且確保更新始終是workInProgress的一部分,這里還做了一個內(nèi)存緩沖讥蟆,奇次更新和偶次更新的循環(huán)復用

總結(jié)

通過學習React16關于Fiber源碼及React Hooks的源碼勒虾,我們發(fā)現(xiàn)整個React16的底層核心是基于Fiber的優(yōu)化與擴展,包括dom-diff的擴展等瘸彤,相較于Vue3對于Vue2的更新修然,可以看出React的優(yōu)化迭代思路更加充滿對計算機原理底層的思考與發(fā)現(xiàn),當然這兩個框架從出發(fā)點設計上也是有所不同质况,Vue是基于組件級的優(yōu)化愕宋,因而并不需要這樣一個Fiber的數(shù)據(jù)結(jié)構去構建,但從真正的設計來看Fiber的架構設計思維方式確實更加符合國外程序員的方法與韻味结榄。(ps: 想要了解Andrew Clark介紹Fiber的同學中贝,可以參看這篇文章react-fiber-architecure)

參考

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市臼朗,隨后出現(xiàn)的幾起案子邻寿,更是在濱河造成了極大的恐慌,老刑警劉巖视哑,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绣否,死亡現(xiàn)場離奇詭異,居然都是意外死亡挡毅,警方通過查閱死者的電腦和手機蒜撮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慷嗜,“玉大人淀弹,你說我怎么就攤上這事丹壕。” “怎么了薇溃?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵菌赖,是天一觀的道長。 經(jīng)常有香客問我沐序,道長琉用,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任策幼,我火速辦了婚禮邑时,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘特姐。我一直安慰自己晶丘,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布唐含。 她就那樣靜靜地躺著浅浮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捷枯。 梳的紋絲不亂的頭發(fā)上滚秩,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音淮捆,去河邊找鬼郁油。 笑死,一個胖子當著我的面吹牛攀痊,可吹牛的內(nèi)容都是我干的桐腌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蚕苇,長吁一口氣:“原來是場噩夢啊……” “哼哩掺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涩笤,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盒件,沒想到半個月后蹬碧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡炒刁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年恩沽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翔始。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡罗心,死狀恐怖里伯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渤闷,我是刑警寧澤疾瓮,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站飒箭,受9級特大地震影響狼电,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弦蹂,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一肩碟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凸椿,春花似錦削祈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窿撬,卻和暖如春启昧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劈伴。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工密末, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跛璧。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓严里,卻偏偏與公主長得像,于是被迫代替她去往敵國和親追城。 傳聞我的和親對象是個殘疾皇子刹碾,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345