換個姿勢看 hooks !

語言: CN / TW / HK

一 前言

懂得 JSX 本質的同學都知道它只不過是一種語法糖,會被 babel 處理成 createElement 的形式,最後再變成常規的 js 物件。所以,我們就可以在 js 邏輯層面對  element 物件做處理,自定義 hooks 作為 element  邏輯處理層,也就變得理所當然了。

本文我們就來研究一下,自定義 hooks 的一些其他的用途,以及怎麼樣處理檢視層,還有一些新玩法。

4.jpeg

二 用 hooks 處理 element 物件

1 場景一

hooks 處理 element 的案例已經屢見不鮮了。比如我們相對一些 UI 層的內容做快取處理,像如下場景。

function Test(){
console.log('test rerender')
return <div>
hello, react !
</div>

}

function Index({ value }){
const [ number , setNumber ] = React.useState(0)
const element = React.useMemo(()=> <Test /> ,[ value ])
return <div>
{element}
<button onClick={() => setNumber(number +1 )} > 點選 {number} </button>
</div>

}

如上用 useMemo 快取處理 Test 元件對應的 element 物件,之後當 Index 中的 value 改變的時候,才會再次執行 useMemo 得到新的 element 物件。

當點選按鈕的時候,會觸發 setNumber 改變 state,會觸發 Index 的更新,但是 useMemo 會直接讀取快取的值,這樣效能上的體驗就是 Test 不會再更新。

這是一種基於 hooks 的實現的優化策略,本質上是對 element 的快取。這種方案處理後 Index 不再需要類似於 HOC 的 memo 元件包裹。可以根據條件和方向,做渲染上定製方向上的優化,這是一種父 -> 子的優化方案。

還有一些更為複雜的場景,就是多個 hooks 組合起來,來達到目的。

function Index(){
const [ number , setNumber ] = React.useState(0)
const { value } = React.useContext(valueContext)
const element = React.useMemo(()=> <Test /> ,[ value ])
return <div>
{element}
<button onClick={() => setNumber(number +1 )} > 點選 {number} </button>
</div>

}

通過 useContext 讀取 valueContext 中的 value 屬性, Test 元件訂閱 value 的變化,當 context 裡面的 value 改變的時候,重新生成 element 物件,也就是重新渲染 Test 元件。

場景二

react router v6 出來之後,有一個全新的 hooks —— useRoutes。它可以接受路由的配置的 js 路由樹,返回一個檢視層的 element tree。我們看一下具體使用。

const routeConfig = [
{
path:'/home',
element:<Home />
},
{
path:'/list/:id',
element:<List />
},
{
path:'/children',
element:<Layout />,
children:[
{ path:'/children/child1' , element: <Child1/> },
{ path:'/children/child2' , element: <Child2/> }
]
}
]

const Index = () => {
const element = useRoutes(routeConfig)
return <div className="page" >
<div className="content" >
<Menus />
{element}
</div>
</div>

}
const App = ()=> <BrowserRouter><Index /></BrowserRouter>

useRoutes 為自定義 hooks ,返回規範化的路由結構。hooks 不再像我們平時那樣只負責邏輯的處理,此場景下,hooks 完全充當了一個檢視容器。

2.jpeg

這個模式下,對自定義 hooks 理解打破了傳統觀念,可能這種由邏輯層到檢視層的轉化,會讓一部分同學不適應,不過這些不重要,我們要有一個思維上的轉變,這才顯得重要。

三 設計模式

下面設想一個場景,自定義 hooks 可不可以實現一種設計場景,可以類似於組合模式和 hoc 模式的結合,可以實現邏輯和檢視的分離呢?

1 傳統的組合模式缺點

首先看一下組合模式,傳統的組合模式如下所示:

function Index(){
return <GrandFather>
<Father>
<Son>{null}</Son>
</Father>
</GrandFather>

}

上面通過 GrandFather , Father, Son 三個元件進行組合模式。這種模式下,組合的內外層元件需要建立關聯和通訊的話,需要通過 cloneElement 混入一些通訊的方法。

以上面為例子,如果想要實現 Father <——> Son 雙向通訊,我們需要這麼處理:

/* 父元件 */
function Father({ children }){
const [ fatherSay , setFatherSay ] = React.useState('')
const toFather= ()=> console.log('son to father')
const element = React.cloneElement(children,{ fatherSay ,toFather })
return <div>
<p> Father </p>
<button onClick={() => setFatherSay('father to son')} >to Son</button>
{element}
</div>

}

/* 子元件 */
function Son({ children, fatherSay, toFather }){
console.log(fatherSay)
return <div>
<p> son </p>
<button onClick={toFather} >to Father</button>
{children || null}
</div>

}

如上

  • Father 元件通過 cloneElement 向 props 中混入 toFather 方法。Son 元件可以直接通過 props 拿到此方法向父元件通訊,實現 Son -> Father
  • Father 可以通過 useState 改變 fatherSay 並且傳遞給 Son,實現 Father -> Son

有一個顯而易見的弊端就是:

toFathercloneElement 等邏輯需要開發者去單獨處理,也就是邏輯層和 ui 層是強關聯的。這就需要開發者,在組合模式的上下層元件中分別處理邏輯。

如果再加上 GrandFather 元件,那麼就需要像下圖一樣處理:

3.jpeg

2 hoc 巢狀提供 idea

hoc 本身就是一個函式,接收原始元件,返回新的元件,多個 hoc 可以巢狀。

function Index(){
/* .... */
}
export default HOC1(styles)(HOC2( HOC3(Index) ))

HOC1 -> HOC2 -> HOC3 -> Index

hoc1.jpg

那麼可不可以用 hoc 這個思想,來實現組合模式呢,並且解決邏輯冗餘呢。

3 用自定義 hooks 實現

結合最開始講到的,可以通過自定義 hooks 來處理 ui 邏輯,那麼就能通過類似 hoc 的 多層巢狀 hooks ,解決組合模式的上述缺陷。

那麼自定義的 hooks 的設計如下:

useComposeHooks( component, Layout , mergeProps )
  • component 為需要通過組合模式處理的元件。

  • 需要組合的容器元件。

  • mergeProps 需要合併的新的 props 。

  • useComposeHooks 可以多個巢狀使用。比如如下:

function Index(){
const element = useComposeHooks( useComposeHooks( useComposeHooks(...) , Layout2,mergeProps ) ,Layout1,mergeProps)
return element
}

等價於:

<Layout1>
<Layout2>
{ ... }
</Layout2>

</Layout1>

接下來我們去實現這個功能。

四 程式碼實現及效果驗證

1 編寫 useComposeHooks

接下來我們編寫一下 useComposeHooks:

function useComposeHooks(component, layout, mergeProps) {
const sonToFather = useRef({})
const fatherToSon = useRef({})
/* 子對父元件通訊 */
const sonSay = React.useCallback((type, payload) => {
const cb = sonToFather.current[type]
typeof cb === 'function' && cb(payload)
}, [component])
/* 父監聽子元件 */
const listenSonSay = React.useCallback((type, fn) => {
sonToFather.current[type] = fn
}, [layout])
/* 父對子元件通訊*/
const fatherSay = React.useCallback((type,payload)=>{
const cb = fatherToSon.current[type]
typeof cb === 'function' && cb(payload)
},[layout])
/* 子監聽父元件 */
const listenFather = React.useCallback((type,fn)=>{
fatherToSon.current[type] = fn
},[ component ])
const renderChildren = React.useMemo(() => {
return component ? React.cloneElement(component, { listenFather, sonSay }) : null
}, [component])
return layout ? React.createElement(layout, { fatherSay,listenSonSay, ...mergeProps, children: renderChildren }) : renderChildren
}

  • 通過 useRef 儲存通訊方法。
  • 編寫 sonSay (子對父元件通訊),listenSonSay (父監聽子元件),fatherSay(父對子元件通訊),listenFather(子監聽父元件)方法。

  • 通過 cloneElement 克隆內層元件。

  • 通過 createElement 建立外層元件。

2 測試  demo

function GrandFather({ name, children }) {
return <div>
<p> {name} </p>
{children}
</div>

}

function Father({ children, listenSonSay, name ,fatherSay}) {
listenSonSay('sonSay', (message) => console.log(message))
return <div>
<p> {name} </p>
<button onClick={() => fatherSay('fatherSay','hello,son!')} >to Son</button>
{children}
</div>

}

function Son({ children, sonSay,listenFather ,name }) {
listenFather('fatherSay',(message) => console.log(message) )
return <div>
<p> {name} </p>
<button onClick={() => sonSay('sonSay', 'hello,father!')} >to Father</button>
{children || null}
</div>

}
export default function Index() {
return (
useComposeHooks(
useComposeHooks(
useComposeHooks(null,
Son, { name: 'Son' })
, Father, { name: 'Father' })
, GrandFather, { name: 'GrandFather' })
)
}
  • 如上,我們不再需要向業務層做其他的處理。只需要呼叫 props 裡面的相關方法就可以了。

接下來看一下效果(非動圖):

5.jpeg

如上,完美實現了。通過這個案例,主要向大家展示自定義 hooks 實現了組合模式。不要太關注程式碼的細節。

五 總結

今天通過一個創意想法講述了自定義 hooks 的一些其他玩法,當然本文中的 demo 只是一個案例,並不能使用在真實的業務場景下,通過本文希望大家對 hooks 有一個全新的理解。

-   E N D   -

3 6 0 W 3 C E C M A T C 3 9 L e a d e r 注和加