<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React中封裝組件的一些方法

          共 11869字,需瀏覽 24分鐘

           ·

          2021-07-27 20:15

          來源 | https://www.shymean.com/tags


          最近參與了一個基于Raact技術棧的項目,距離我上一次在工作中react已經(jīng)過去了挺長一段時間,因此打算整理在react中封裝組件的一些方法。

          1、extends 正向繼承

          對于類組件而言,可以通過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(){ // 使用原始的render return 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>    );};
          <Bar head={<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>;};
          <Baz renderHead={(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> <Trigger toggle={() => { 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(()=>{        // 假設這里請求了接口獲取返回值,用來初始化默認的checked        setTimeout(()=>{            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.current        instance.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項目,可以多嘗試一下這些方法。


          學習更多技能

          請點擊下方公眾號


          瀏覽 93
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  最新三级视频在线观看 | 操逼操逼操逼操逼操逼操逼视频 | 台湾中文字幕 | 女人高潮视频免费观看网站 | av手机天堂 |