React中封裝組件的一些方法

來源 | https://www.shymean.com/tags
1、extends 正向繼承
class LogPage extends React.Component {trackLog() {console.log("trackLog");}}class Page1 extends LogPage {onBtnClick = () => {console.log('click')this.trackLog();};render() {return <button onClick={this.onBtnClick}>click</button>;}}
借助OOP的思想,可以通過封裝、繼承和多態(tài)來實現(xiàn)數(shù)據(jù)的隔離和功能的復用。
2、HOC
高階組件其實就是參數(shù)為組件,返回值為新組件的函數(shù)
高階組件是React中比較常見的一種做法,主要用于增強某些組件的功能,封裝一些公共操作,如處理埋點日志、執(zhí)行公共邏輯、渲染公共UI等。
2.1. 劫持props
高階組件會返回一個新的組件,這個組件會攔截傳遞過來的props,這樣就可以做一些特殊的處理,或者僅僅是添加一些通用的props
function HOC(Comp) {const commonProps = { x: 1, commonMethod1, commonMethod2 };return props => <Comp {...commonProps} {...props} />;}
看起來像組件注入一些通用的props就更輕松了。
2.2. 反向繼承
高階組件的核心思想是返回一個新的組件,如果是類組件,甚至可以通過繼承的方式劫持組件原本的生命周期函數(shù),擴展新的功能
function HOC(Comp){return class SubComp extends Comp {componentDidMount(){// 處理新的生命周期方法,可以按需要決定是否調(diào)用supder.componentDidMount}render(){// 使用原始的renderreturn super.render();}}}
2.3. 控制渲染
比如我們需要判斷某個頁面是否需要登錄,一種做法是直接在頁面組件邏輯中編寫判斷,如果存在多個這種的頁面,就變得重復了
利用HOC可以方便地處理這些邏輯。
export default (Comp) => (props) => {const isLogin = checkLogin()if (isLogin) {return (<Comp {...props}/>)}return (<Redirect to={{ pathname: 'login' }}/>)}
對于被包裹的組件而言,HOC更像是一個裝飾器添加額外的功能;而對于需要處理多個組件的開發(fā)者而言,HOC是一種封裝公共邏輯的方案
2.4. HOC的缺點
劫持Props是HOC最常用的功能之一,但這也是它的缺點:層級的嵌套和狀態(tài)的透傳。
對于HOC本身而言,傳遞給他的props是不需要關心的,他只是負責將props透傳下去。這就要求對于一些特殊的prop如ref等,需要額外使用forwardRef才能夠滿足需求。
此外,我認為這也導致組件的props來源變得不清晰。最后組件經(jīng)過多個HOC的裝飾之后,我們就很難區(qū)分某個props注入的數(shù)據(jù)到底是哪里來的了
3、 Render Props
在某些場景下,對于組件調(diào)用方而言,希望組件能夠提供一些自定義的渲染功能,與vue的slot類似
3.1. prop傳遞ReactElement
React組件默認的prop: children可以實現(xiàn)default slot的功能
const Foo = ({ children }) => {return (<div><h1>Foo</h1>{children}</div>);};<Foo>hello from parent</Foo>
在jsX解析的時候,會將組件內(nèi)的內(nèi)容解析為React Element,然后作為children屬性傳遞給組件。基于“prop可以傳遞ReactElement”這個思路,可以實現(xiàn)很多騷操作。
const Bar = ({ head, body }) => {return (<div><h1>{head}</h1><p>{body}</p></div>);};<Barhead={<span>title</span>}body={<span>body</span>}></Bar>
類似于vue的具名插槽,使用起來卻更加直觀,這就是jsX靈活而強大的表現(xiàn)。
3.2. prop傳遞函數(shù)
但這種直接傳遞ReactElement也存在一些問題,那就是這些節(jié)點都是在父元素定義的。
如果能夠根據(jù)組件內(nèi)部的一些數(shù)據(jù)來動態(tài)渲染要展示的元素,這樣就會更加靈活了。換言之,我們需要實現(xiàn)在組件內(nèi)部動態(tài)構(gòu)建渲染元素。
最簡單的解決辦法就是傳遞一個函數(shù),由組件內(nèi)部通過傳參的形式通過函數(shù)動態(tài)生成需要渲染的元素。
const Baz = ({ renderHead }) => {const count = 123;return <div>{renderHead(count)}</div>;};<BazrenderHead={(count) => <span>count is {count}</span>}></Baz>
通過函數(shù)的方式,可以在不改動組件內(nèi)部實現(xiàn)的前提下,利用組件的數(shù)據(jù)實現(xiàn)UI分發(fā)和邏輯復用,類似于Vue的插槽作用域,也跟JavaScript中常見的回調(diào)函數(shù)作用一致。
React官方把這種技術稱作Render Props:
Render Props是指一種在 React 組件之間使用一個值為函數(shù)的 prop 共享代碼的簡單技術
Render Props有下面幾個特點
也是一個prop,用于父子組件之間傳遞數(shù)據(jù)
他的值是一個函數(shù),其參數(shù)由子組件在合適的時候傳入
通常用來render(渲染)某個元素或組件
再舉一個更常用的例子,渲染列表組件,
const DealItem = ({ item }) => {return (<li><p>{item.name}</p></li>);};
實現(xiàn)了列表單個元素組件之后,就可以Array.prototype.map一把梭渲染列表。
const DealListDemo = () => {const list = [{ name: "1" }, { name: "2" }];return (<ul>{list.map((item, index) => {return <DealItem key={index} item={item}></DealItem>;})}</ul>);};
如果需要渲染多個類似的列表,如DealItem2、DealItem3之類的,這個時候就可以把重復的list.map解耦出來,實現(xiàn)一個純粹的List組件。
// 這里假設List組件會執(zhí)行一些渲染列表的公共邏輯,如滾動加載、窗口列表啥的const List = ({ list, children }) => {return (<ul>{list.map((item, index) => {return children(item, index);})}</ul>);};
然后通過Render Props就可以動態(tài)渲染不同的元素組件列表了。
const DealListDemo = () => {const list = [{ name: "1" }, { name: "2" }];return (<List list={list}>{(item, index) => <DealItem key={index} item={item}></DealItem>}</List>);};// 渲染不同的組件元素,只需要提供新的元素組件即可const DealListDemo2 = () => {const list = [{ name: "1" }, { name: "2" }];return (<List list={list}>{(item, index) => <DealItem2 key={index} item={item}></DealItem>}</List>);};
3.3. prop傳遞組件
上面提到Render props是值為函數(shù)的prop,這個函數(shù)返回的是ReactElement。那不就是一個函數(shù)組件嗎?既然如此,是不是也可以直接傳遞組件呢?答案是肯定的。
比如現(xiàn)在需要實現(xiàn)一個toolTip組件,可以在某些場景下切換彈窗。
const Modal = ({ Trigger }) => {const [visible, setVisible] = useState(false);return (<div><Triggertoggle={() => {setVisible(!visible);}}/><dialog open={visible}><p>tooltip</p></dialog></div>);};
現(xiàn)在就可以很輕易的實現(xiàn)一些可以觸發(fā)tool的組件,但實現(xiàn)了和Modal的完全分離。
const ModalButton = ({ toggle }) => {return <button onClick={toggle}>click</button>;};// 當點擊該按鈕時會切換彈窗<Modal Trigger={ModalButton}></Modal>const ModalTitle = ({ toggle }) => {return <h1 onClick={toggle}>click</h1>;};// 點擊標題時會切換彈窗<Modal Trigger={ModalTitle}></Modal>
上面這種并不是使用Render props的常規(guī)方式,但也展示了利用prop實現(xiàn)UI擴展的一些特殊用法
3.4. Render Props存在的問題
Render Props可以有效地以松散耦合的方式設計組件,但由于其本質(zhì)是一個函數(shù),也會存在回調(diào)嵌套過深的問題:當返回的節(jié)點也需要傳入render props時,就會發(fā)生多層嵌套。
<Demo1>{(props1)=>{return (<Demo2>{(props2)=>{return (<span>{props1}, {props2}</span>)}}</Demo2>)}}</Demo1>
一種解決辦法是使用react-adopt,它提供了組合多個render props返回結(jié)果的功能。
4、Hooks
強烈建議閱讀官方文檔,比我自己寫的好得多。
4.1. Hooks解決的問題
React中組件分為了函數(shù)組件和Class組件,函數(shù)組件是無狀態(tài)的,在Hooks之前,只能通過props控制函數(shù)組件的數(shù)據(jù),如果希望實現(xiàn)一個帶狀態(tài)的組件,則需要通過Class組件的instace來維護。
Class組件主要有幾個問題
邏輯分散,相互關連的邏輯分散在各個生命周期函數(shù);每個生命周期函數(shù)又塞滿了各不相同的邏輯
邏輯復用需要通過高階組件HOC或者Render Props來處理,
不論是HOC還是Render Props,都需要重新組織組件結(jié)構(gòu),很容易形成組件嵌套,代碼閱讀性和可維護性都會變差。
因此需要一種扁平化的邏輯復用的方式,因此Hooks出現(xiàn)了。其優(yōu)點有
扁平化的邏輯復用,在無需修改組件結(jié)構(gòu)的情況下復用狀態(tài)邏輯
將相互關聯(lián)的部分放在一起,互不相關的地方相互隔離
函數(shù)式編程
本章節(jié)將介紹Hook常見的使用方式及注意事項
4.2. useState 初始值
useState傳入的初始化值只有首次會生效,這在使用props傳入值作為初始化值可能會帶來一些誤導
下面實現(xiàn)一個復選框組件。
const Checkbox = ({ initChecked }) => {const [checked, setChecked] = useState(initChecked);const toggleChecked = () => {setChecked(!checked);};return (<label><input type="checkbox" checked={checked} onChange={toggleChecked} />{" "}{checked ? "checked" : "unchecked"}</label>);};
假設傳入的props發(fā)生了變化。
const HookDemo = () => {const [checked, setChecked] = useState(false);useEffect(()=>{// 假設這里請求了接口獲取返回值,用來初始化默認的checkedsetTimeout(()=>{setChecked(true)},1000)}, [])return (<div><Checkbox initChecked={checked}></Checkbox></div>);};
此時修改了props initChecked的值,但Checkbox組件本身卻不會更新。如果希望當props更新時繼續(xù)修改Checkbox的選中狀態(tài),可以借助useEffect。
const Checkbox = ({ initChecked }) => {// ...useEffect(() => {setChecked(initChecked);}, [initChecked]);// ...};
這個寫法類型于Vue的自定義model寫法
export default {props: {initChecked: {type: Boolean,}},data(){return {checked: this.initChecked}},watch:{initChecked(newVal){this.checked = newValue},checked(newVal){this.$emit('input', newVal)}}}
4.3. 閉包陷阱
初使用Hooks時,比較常見的一個錯誤就是閉包。
下面實現(xiàn)了一個定時器組件,在掛載時開啟定時器,每秒更新數(shù)值。
const IntervalDemo = () => {const [count, setCount] = useState(0);useEffect(() => {let timer = setInterval(() => {setCount(count + 1);}, 1000);return ()=>{clearInterval(timer)}}, []);return <div>{count}</div>;};
事實上每次更新之后count的值都不會變化,其原因跟
for (var i = 0; i < 10; ++i) {setTimeout(function () {console.log(i);}, 1000);}
最后會打印出10個5的原因一樣,
定時器在首次渲染的時候注冊,后續(xù)的更新不會再修改定時器,因此其回調(diào)函數(shù)的作用域內(nèi)的自由變量count,始終都是首次渲染時的值,不會發(fā)生改變。
一種解決辦法是使用函數(shù)式的setCount,可以獲取到最新的count值。
const IntervalDemo2 = () => {const [count, setCount] = useState(0);useEffect(() => {let timer = setInterval(() => {setCount((c) => c + 1); // 可以拿到上一輪的值}, 1000);return () => {clearInterval(timer);};}, []);return <div>{count}</div>;};
但如果是想在定時器回調(diào)函數(shù)中先根據(jù)上一輪的值進行一些處理,這種方法就無能為力了
歸根到底是想在多次渲染之間保存一個值,最簡單的做法是使用外部自由變量來保存。
let globalCount = 0const IntervalDemo2 = () => {const [count, setCount] = useState(0);useEffect(() => {let timer = setInterval(() => {globalCount++console.log(globalCount)setCount(globalCount);}, 1000);return () => {clearInterval(timer);};}, []);return <div>{count}</div>;};
當然這種方式肯定是存在問題的,在組件被重復或繼續(xù)使用時,外部的globalCount會被污染。
要想在多次渲染期間共享同一個變量,官方的做法是使用useRef。
const IntervalDemo3 = () => {const [count, setCount] = useState(0);const countRef = useRef(0);useEffect(() => {let timer = setInterval(() => {countRef.current += 1;setCount(countRef.current);}, 1000);return () => {clearInterval(timer);};}, []);return <div>{count}</div>;};
4.4. render Hook
在Class組件的使用中,在某些場景下可能期望獲取組件的實例,方便調(diào)用組件上面的一些方法,最經(jīng)典的場景是調(diào)用Form.validate()表單組件的字段校驗。
class Form extends React.Component {validate = () => {console.log("validate form");};render() {return <div>form</div>;}}
可以通過ref獲取組件實例然后調(diào)用組件方法。
const Parent = () => {const ref = useRef(null)useEffect(()=>{const instance = ref.currentinstance.validate()},[])return (<Form ref={ref}></Form>);};
在函數(shù)組件中,并不存在組件instance這一說法,也無法直接設置ref屬性,直接在函數(shù)組件上使用ref會出現(xiàn)警告
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
為了實現(xiàn)與類組件的功能,需要使用借助forwardRef和useImperativeHandle
const Form2 = forwardRef((props, ref)=>{// 實現(xiàn)ref獲取到實例相關的接口useImperativeHandle(ref, ()=>{return {validate(){console.log('validate')}}})return (<div>form</div>)})
上面這種通過ref調(diào)用接口的操作,其思路都是先拿到組件實例,然后再進行操作。
但是現(xiàn)在有了Hook,我們可以將組件和操作組件的方法通過hook暴露出來,無需再通過ref了。
const useForm = () => {const validate = () => {console.log("validate form");};const render = () => {return <div>form</div>;};return {render,validate,};};const FormDemo = ()=>{const {render, validate} = useForm()useEffect(() => {validate()}, []);return render()}
相較于ref獲取類組件實例,這種實現(xiàn)看起來更加簡單清晰,一切皆是函數(shù)。
借助這種包含渲染render功能的hook和JSX的強大表現(xiàn)力,可以實現(xiàn)很多有趣的組件,如彈窗。
一般的全局彈窗組件是通過ReactDOM.render將彈窗組件渲染到body節(jié)點上,然后使用Modal.info等全局接口展示。
這種寫法的好處是靈活,缺點也很明顯,無法與當前應用共享同一個context,參考antd Modal FAQ。
借助render hook的思路,可以通過一種取巧的方式實現(xiàn)。
const Modal = ({ visible, children }) => {return <dialog open={visible}>{children}</dialog>;};const useModal = (content) => {const [visible, setVisible] = useState(false);const modal = <Modal visible={visible}>{content}</Modal>;const toggleModal = () => {setVisible(!visible);};return {modal,toggleModal,};};
使用起來很方便。
const ModalDemo = () => {const { modal, toggleModal } = useModal(<h1>hi model</h1>);return (<div>{modal}<button onClick={toggleModal}>toggle</button></div>);};
由于返回的ReactElement也是渲染在當前組件樹中,因此就不存在context丟失的問題。
5、小結(jié)
本文主要總結(jié)了幾種封裝React組件的方式,包括正向繼承、HOC、Render Props、 Hooks等方式,每種方式都有各自的優(yōu)缺點。恰好最近參與了新的React項目,可以多嘗試一下這些方法。
學習更多技能
請點擊下方公眾號
![]()

