摘要:我們可以通過(guò)剛剛高階函數(shù)的思想來(lái)創(chuàng)建一個(gè)中間組件,也就是我們說(shuō)的高階組件。僅傳遞組件所需要的屬性。在受控組件中,表單數(shù)據(jù)由組件負(fù)責(zé)處理。作為頂層組件接收一個(gè)名為的,可以接收任意需要被放入中的字符串,數(shù)字,甚至是函數(shù)。
React組件設(shè)計(jì) 組件分類 展示組件和容器組件
展示組件 | 容器組件 | |
---|---|---|
關(guān)注事物的展示 | 關(guān)注事物如何工作 | |
可能包含展示和容器組件,并且一般會(huì)有DOM標(biāo)簽和css樣式 | 可能包含展示和容器組件,并且不會(huì)有DOM標(biāo)簽和css樣式 | |
常常允許通過(guò)this.props.children傳遞 | 提供數(shù)據(jù)和行為給容器組件或者展示組件 | |
對(duì)第三方?jīng)]有任何依賴,比如store 或者 flux action | 調(diào)用flux action 并且提供他們的回調(diào)給展示組件 | |
不要指定數(shù)據(jù)如何加載和變化 | 作為數(shù)據(jù)源,通常采用較高階的組件,而不是自己寫(xiě),比如React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create() | |
僅通過(guò)屬性獲取數(shù)據(jù)和回調(diào) | null | |
很少有自己的狀態(tài),即使有,也是自己的UI狀態(tài) | null | |
除非他們需要的自己的狀態(tài),生命周期,或性能優(yōu)化才會(huì)被寫(xiě)為功能組件 | null |
下面是一個(gè)可能會(huì)經(jīng)常寫(xiě)的組件,評(píng)論列表組件,數(shù)據(jù)交互和展示都放到了一個(gè)組件里面。
// CommentList.js class CommentList extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: "json", success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return
我們對(duì)上面的組件進(jìn)行拆分,把他拆分成容器組件 CommentListContainer.js 和展示組件 CommentList。
// CommentListContainer.js class CommentListContainer extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: "json", success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return; } } // CommentList.js class CommentList extends React.Component { constructor(props) { super(props); } render() { return
優(yōu)勢(shì):
展示和容器更好的分離,更好的理解應(yīng)用程序和UI
重用性高,展示組件可以用于多個(gè)不同的state數(shù)據(jù)源
展示組件就是你的調(diào)色板,可以把他們放到多帶帶的頁(yè)面,在不影響應(yīng)用程序的情況下,讓設(shè)計(jì)師調(diào)整UI
迫使你分離標(biāo)簽,達(dá)到更高的可用性
有狀態(tài)組件和無(wú)狀態(tài)組件下面是一個(gè)最簡(jiǎn)單的無(wú)狀態(tài)組件的例子:
function HelloComponent(props, /* context */) { returnHello {props.name}} ReactDOM.render(, mountNode)
可以看到,原本需要寫(xiě)“類”定義(React.createClass 或者 class YourComponent extends React.Component)來(lái)創(chuàng)建自己組件的定義(有狀態(tài)組件),現(xiàn)在被精簡(jiǎn)成了只寫(xiě)一個(gè) render 函數(shù)。更值得一提的是,由于僅僅是一個(gè)無(wú)狀態(tài)函數(shù),React 在渲染的時(shí)候也省掉了將“組件類” 實(shí)例化的過(guò)程。
結(jié)合 ES6 的解構(gòu)賦值,可以讓代碼更精簡(jiǎn)。例如下面這個(gè) Input 組件:
function Input({ label, name, value, ...props }, { defaultTheme }) { const { theme, autoFocus, ...rootProps } = props return (
無(wú)狀態(tài)組件不像上述兩種方法在調(diào)用時(shí)會(huì)創(chuàng)建新實(shí)例,它創(chuàng)建時(shí)始終保持了一個(gè)實(shí)例,避免了不必要的檢查和內(nèi)存分配,做到了內(nèi)部?jī)?yōu)化。
無(wú)狀態(tài)組件不支持 "ref"高階組件
高階組件通過(guò)函數(shù)和閉包,改變已有組件的行為,本質(zhì)上就是 Decorator 模式在 React 的一種實(shí)現(xiàn)。
當(dāng)寫(xiě)著寫(xiě)著無(wú)狀態(tài)組件的時(shí)候,有一天忽然發(fā)現(xiàn)需要狀態(tài)處理了,那么無(wú)需徹底返工:)
往往我們需要狀態(tài)的時(shí)候,這個(gè)需求是可以重用的。
高階組件加無(wú)狀態(tài)組件,則大大增強(qiáng)了整個(gè)代碼的可測(cè)試性和可維護(hù)性。同時(shí)不斷“誘使”我們寫(xiě)出組合性更好的代碼。
高階函數(shù)function welcome() { let username = localStorage.getItem("username"); console.log("welcome " + username); } function goodbey() { let username = localStorage.getItem("username"); console.log("goodbey " + username); } welcome(); goodbey();
我們發(fā)現(xiàn)兩個(gè)函數(shù)有一句代碼是一樣的,這叫冗余唉。(平時(shí)可能會(huì)有一大段代碼的冗余)。
下面我們要寫(xiě)一個(gè)中間函數(shù),讀取username,他來(lái)負(fù)責(zé)把username傳遞給兩個(gè)函數(shù)。
function welcome(username) { console.log("welcome " + username); } function goodbey(username) { console.log("goodbey " + username); } function wrapWithUsername(wrappedFunc) { let newFunc = () => { let username = localStorage.getItem("username"); wrappedFunc(username); }; return newFunc; } welcome = wrapWithUsername(welcome); goodbey = wrapWithUsername(goodbey); welcome(); goodbey();
好了,我們里面的 wrapWithUsername 函數(shù)就是一個(gè)“高階函數(shù)”。
他做了什么?他幫我們處理了 username,傳遞給目標(biāo)函數(shù)。我們調(diào)用最終的函數(shù) welcome的時(shí)候,根本不用關(guān)心 username是怎么來(lái)的。
下面是兩個(gè)冗余的組件。
import React, {Component} from "react" class Welcome extends Component { constructor(props) { super(props); this.state = { username: "" } } componentWillMount() { let username = localStorage.getItem("username"); this.setState({ username: username }) } render() { return (welcome {this.state.username}) } } export default Welcome;
import React, {Component} from "react" class Goodbye extends Component { constructor(props) { super(props); this.state = { username: "" } } componentWillMount() { let username = localStorage.getItem("username"); this.setState({ username: username }) } render() { return (goodbye {this.state.username}) } } export default Goodbye;
我們可以通過(guò)剛剛高階函數(shù)的思想來(lái)創(chuàng)建一個(gè)中間組件,也就是我們說(shuō)的高階組件。
import React, {Component} from "react" export default (WrappedComponent) => { class NewComponent extends Component { constructor() { super(); this.state = { username: "" } } componentWillMount() { let username = localStorage.getItem("username"); this.setState({ username: username }) } render() { return} } return NewComponent }
import React, {Component} from "react"; import wrapWithUsername from "wrapWithUsername"; class Welcome extends Component { render() { return (welcome {this.props.username}) } } Welcome = wrapWithUsername(Welcome); export default Welcome;
import React, {Component} from "react"; import wrapWithUsername from "wrapWithUsername"; class Goodbye extends Component { render() { return (goodbye {this.props.username}) } } Goodbye = wrapWithUsername(Goodbye); export default Goodbye;
看到?jīng)]有,高階組件就是把 username 通過(guò) props 傳遞給目標(biāo)組件了。目標(biāo)組件只管從 props里面拿來(lái)用就好了。
為了代碼的復(fù)用性,我們應(yīng)該盡量減少代碼的冗余。
提取共享的state,如果有兩個(gè)組件都需要加載同樣的數(shù)據(jù),那么他們會(huì)有相同的 componentDidMount 函數(shù)。
找出重復(fù)的代碼,每個(gè)組件中constructor 和 componentDidMount都干著同樣的事情,另外,在數(shù)據(jù)拉取時(shí)都會(huì)顯示Loading... 文案,那么我們應(yīng)該思考如何使用高階組件來(lái)提取這些方法。
遷移重復(fù)的代碼到高階組件
包裹組件,并且使用props替換state
盡可能地簡(jiǎn)化
組件開(kāi)發(fā)基本思想 單功能原則使用react時(shí),組件或容器的代碼在根本上必須只負(fù)責(zé)一塊UI功能。
讓組件保持簡(jiǎn)單如果組件根本不需要狀態(tài),那么就使用函數(shù)定義的無(wú)狀態(tài)組件。
從性能上來(lái)說(shuō),函數(shù)定義的無(wú)狀態(tài)組件 > ES6 class 定義的組件 > 通過(guò) React.createClass() 定義的組件。
僅傳遞組件所需要的屬性。只有當(dāng)屬性列表太長(zhǎng)時(shí),才使用{...this.props}進(jìn)行傳遞。
如果組件里面有太多的判斷邏輯(if-else語(yǔ)句)通常意味著這個(gè)組件需要被拆分成更細(xì)的組件或模塊。
使用明確的命名能夠讓開(kāi)發(fā)者明白它的功能,有助于組件復(fù)用。
基本準(zhǔn)則在shouldComponentUpdate中避免不必要的檢查.
盡量使用不可變數(shù)據(jù)類型(Immutable).
編寫(xiě)針對(duì)產(chǎn)品環(huán)境的打包配置(Production Build).
通過(guò)Chrome Timeline來(lái)記錄組件所耗費(fèi)的資源.
在componentWillMount或者componentDidMount里面通過(guò)setTimeOut或者requestAnimationFram來(lái)延遲執(zhí)行那些需要大量計(jì)算的任務(wù).
組件開(kāi)發(fā)技巧 form表單里的受控組件和不受控組件 受控組件在大多數(shù)情況下,我們推薦使用受控組件來(lái)實(shí)現(xiàn)表單。在受控組件中,表單數(shù)據(jù)由 React 組件負(fù)責(zé)處理。下面是一個(gè)典型的受控組建。
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ""}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert("A name was submitted: " + this.state.value); event.preventDefault(); } render() { return (); } }
設(shè)置表單元素的value屬性之后,其顯示值將由this.state.value決定,以滿足React狀態(tài)的同一數(shù)據(jù)理念。每次鍵盤(pán)敲擊之后會(huì)執(zhí)行handleChange方法以更新React狀態(tài),顯示值也將隨著用戶的輸入改變。
對(duì)于受控組件來(lái)說(shuō),每一次 state(狀態(tài))變化都會(huì)伴有相關(guān)聯(lián)的處理函數(shù)。這使得可以直接修改或驗(yàn)證用戶的輸入和提交表單。
不受控組件因?yàn)椴皇芸亟M件的數(shù)據(jù)來(lái)源是 DOM 元素,當(dāng)使用不受控組件時(shí)很容易實(shí)現(xiàn) React 代碼與非 React 代碼的集成。如果你希望的是快速開(kāi)發(fā)、不要求代碼質(zhì)量,不受控組件可以一定程度上減少代碼量。否則。你應(yīng)該使用受控組件。
一般情況下不受控組件我們使用ref來(lái)獲取DOM元素進(jìn)行操作。
class NameForm extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit(event) { alert("A name was submitted: " + this.input.value); event.preventDefault(); } render() { return (); } } 組件條件判斷 三元函數(shù)組件判斷渲染
const sampleComponent = () => { return isTrue ?使用&&表達(dá)式替換不必要的三元函數(shù)True!
:false!
};
const sampleComponent = () => { return isTrue ?True!
:};
const sampleComponent = () => { return isTrue &&True!
};
需要注意的是如果isTrue 為 0 ,其實(shí)會(huì)轉(zhuǎn)換成 false,但是在頁(yè)面中顯示的時(shí)候,&&還是會(huì)返回0顯示到頁(yè)面中。
多重嵌套判斷// 問(wèn)題代碼 const sampleComponent = () => { return ({flag && flag2 && !flag3 ? flag4 ?) };Blah
: flag5 ?Meh
:Herp
:Derp
}
解決方案:
最佳方案: 將邏輯移到子組件內(nèi)部
使用IIFE(Immediately-Invoked Function Expression 立即執(zhí)行函數(shù))
滿足條件的時(shí)候使用return強(qiáng)制跳出函數(shù)
const sampleComponent = () => { const basicCondition = flag && flag2 && !flag3; if (!basicCondition) returnsetState異步性Derp
; if (flag4) returnBlah
; if (flag5) returnMeh
; returnHerp
}
在某些情況下,React框架出于性能優(yōu)化考慮,可能會(huì)將多次state更新合并成一次更新。正因?yàn)槿绱耍?b>setState實(shí)際上是一個(gè)異步的函數(shù)。 如果在調(diào)用setState()函數(shù)之后嘗試去訪問(wèn)this.state,你得到的可能還是setState()函數(shù)執(zhí)行之前的結(jié)果。
但是,有一些行為也會(huì)阻止React框架本身對(duì)于多次state更新的合并,從而讓state的更新變得同步化。 比如: eventListeners, Ajax, setTimeout 等等。
React框架之所以在選擇在調(diào)用setState函數(shù)之后立即更新state而不是采用框架默認(rèn)的方式,即合并多次state更新為一次更新,是因?yàn)檫@些函數(shù)調(diào)用(fetch,setTimeout等瀏覽器層面的API調(diào)用)并不處于React框架的上下文中,React沒(méi)有辦法對(duì)其進(jìn)行控制。React在此時(shí)采用的策略就是及時(shí)更新,確保在這些函數(shù)執(zhí)行之后的其他代碼能拿到正確的數(shù)據(jù)(即更新過(guò)的state)。
解決setState函數(shù)異步的辦法?根據(jù)React官方文檔,setState函數(shù)實(shí)際上接收兩個(gè)參數(shù),其中第二個(gè)參數(shù)類型是一個(gè)函數(shù),作為setState函數(shù)執(zhí)行后的回調(diào)。通過(guò)傳入回調(diào)函數(shù)的方式,React可以保證傳入的回調(diào)函數(shù)一定是在setState成功更新this.state之后再執(zhí)行。
this.setState({count: 1}, () => { console.log(this.state.count); // 1 })React源碼中setState的實(shí)現(xiàn)
ReactComponent.prototype.setState = function(partialState, callback) { invariant( typeof partialState === "object" || typeof partialState === "function" || partialState == null, "setState(...): takes an object of state variables to update or a " + "function which returns an object of state variables." ); this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, "setState"); } };
updater的這兩個(gè)方法,和React底層的Virtual Dom(虛擬DOM樹(shù))的diff算法有緊密的關(guān)系,所以真正決定同步還是異步的其實(shí)是Virtual DOM的diff算法。
依賴注入在React中,想做依賴注入(Dependency Injection)其實(shí)相當(dāng)簡(jiǎn)單。可以通過(guò)props來(lái)進(jìn)行傳遞。但是,如果組件數(shù)量很多,并且組件嵌套層次很深的話,這種方式就不太合適。
高階組件// inject.jsx var title = "React Dependency Injection"; export default function inject(Component) { return class Injector extends React.Component { render() { return () } }; }
// Title.jsx export default function Title(props) { return{ props.title }
; }
// Header.jsx import inject from "./inject.jsx"; import Title from "./Title.jsx"; var EnhancedTitle = inject(Title); export default function Header() { return (context); }
React v16.3.0 之前的 Context:
var context = { title: "React in patterns" }; class App extends React.Component { getChildContext() { return context; } // ... } App.childContextTypes = { title: PropTypes.string };
class Inject extends React.Component { render() { var title = this.context.title; // ... } } Inject.contextTypes = { title: PropTypes.string };
之前的 Context 作為一個(gè)實(shí)驗(yàn)性質(zhì)的 API,直到 React v16.3.0 版本前都一直不被官方所提倡去使用,其主要原因就是因?yàn)樵谧咏M件中使用 Context 會(huì)破壞 React 應(yīng)用的分型架構(gòu)。
這里的分形架構(gòu)指的是從理想的 React 應(yīng)用的根組件樹(shù)中抽取的任意一部分都仍是一個(gè)可以直接運(yùn)行的子組件樹(shù)。在這個(gè)子組件樹(shù)之上再包一層,就可以將它無(wú)縫地移植到任意一個(gè)其他的根組件樹(shù)中。
但如果根組件樹(shù)中有任意一個(gè)組件使用了支持透?jìng)鞯?Context API,那么如果把包含了這個(gè)組件的子組件樹(shù)多帶帶拿出來(lái),因?yàn)槿鄙倭颂峁?Context 值的根組件樹(shù),這時(shí)的這個(gè)子組件樹(shù)是無(wú)法直接運(yùn)行的。
并且他有一個(gè)致命缺陷:任何一個(gè)中間傳遞的組件shouldComponentUpdate 函數(shù)返回false,組件都不會(huì)得到更新。
新的Context Api
新的Context Api 采用聲明式的寫(xiě)法,并且可以透過(guò)shouldComponentUpdate 函數(shù)返回false的組件繼續(xù)向下傳播,以保證目標(biāo)組件一定可以接收到頂層組件 Context 值的更新,一舉解決了現(xiàn)有 Context API 的兩大弊端,也終于成為了 React 中的第一級(jí)(first-class) API。
新的 Context API 分為三個(gè)組成部分:
React.createContext 用于初始化一個(gè) Context。
XXXContext.Provider作為頂層組件接收一個(gè)名為 value的 prop,可以接收任意需要被放入 Context 中的字符串,數(shù)字,甚至是函數(shù)。
XXXContext.Consumer作為目標(biāo)組件可以出現(xiàn)在組件樹(shù)的任意位置(在 Provider 之后),接收 children prop,這里的 children 必須是一個(gè)函數(shù)(context => ())用來(lái)接收從頂層傳來(lái)的 Context。
const ThemeContext = React.createContext("light"); class App extends React.Component { render() { return (事件處理中的this指向問(wèn)題); } } function Toolbar(props) { return ( ); } function ThemedButton(props) { return ({theme => } ); }
class Switcher extends React.Component { constructor(props) { super(props); this.state = { name: "React in patterns" }; } render() { return ( ); } _handleButtonClick() { console.log(`Button is clicked inside ${ this.state.name }`); // 將導(dǎo)致 // Uncaught TypeError: Cannot read property "state" of null } }
我們可以通過(guò)下面三種方式簡(jiǎn)單實(shí)現(xiàn)this指向的綁定:
在constructor 中事先綁定 this._buttonClick = this._handleButtonClick.bind(this);
調(diào)用時(shí)使用箭頭函數(shù)
ES7中的綁定操作符
給setState傳入回調(diào)函數(shù)setState() 不僅能接受一個(gè)對(duì)象,還能接受一個(gè)函數(shù)作為參數(shù)呢,該函數(shù)接受該組件前一刻的 state 以及當(dāng)前的 props 作為參數(shù),計(jì)算和返回下一刻的 state。
// assuming this.state.count === 0 this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); // this.state.count === 1, not 3 this.setState((prevState, props) => ({ count: prevState.count + props.increment }));
// Passing object this.setState({ expanded: !this.state.expanded }); // Passing function this.setState(prevState => ({ expanded: !prevState.expanded }));組件切換技巧
import HomePage from "./HomePage.jsx"; import AboutPage from "./AboutPage.jsx"; import UserPage from "./UserPage.jsx"; import FourOhFourPage from "./FourOhFourPage.jsx"; const PAGES = { home: HomePage, about: AboutPage, user: UserPage }; const Page = (props) => { const Handler = PAGES[props.page] || FourOhFourPage; returnReact style 組件分類};
基礎(chǔ)組件, 布局組件, 排版組件
給無(wú)狀態(tài)的純UI組件應(yīng)用樣式請(qǐng)保持樣式遠(yuǎn)離那些離不開(kāi)state的組件. 比如路由, 視圖, 容器, 表單, 布局等等不應(yīng)該有任何的樣式或者css class出現(xiàn)在組件上. 相反, 這些復(fù)雜的業(yè)務(wù)組件應(yīng)該有一些帶有基本功能的無(wú)狀態(tài)UI組件組成.
class SampleComponent extends Component { render() { return () } } // 表達(dá)組件(帶樣式) const Button = ({ ...props }) => { const sx = { fontFamily: "inherit", fontSize: "inherit", fontWeight: "bold", textDecoration: "none", display: "inline-block", margin: 0, paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16, border: 0, color: "white", backgroundColor: "blue", WebkitAppearance: "none", MozAppearance: "none" } return (
摘要:前端每周清單第期微服務(wù)實(shí)踐,與,組件技巧,攻防作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開(kāi)發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開(kāi)發(fā)教程工程實(shí)踐深度閱讀開(kāi)源項(xiàng)目巔峰人生等欄目。 前端每周清單第 26 期:Node.js 微服務(wù)實(shí)踐,Vue.js 與 GraphQL,Angular 組件技巧,HeadlessChrome 攻防 作者:王下邀月熊 編輯:徐川...
摘要:精讀原文介紹了學(xué)習(xí)源碼的兩個(gè)技巧,并利用實(shí)例說(shuō)明了源碼學(xué)習(xí)過(guò)程中可以學(xué)到許多周邊知識(shí),都讓我們受益匪淺。討論地址是精讀源碼學(xué)習(xí)如果你想?yún)⑴c討論,請(qǐng)點(diǎn)擊這里,每周都有新的主題,周末或周一發(fā)布。 1. 引言 javascript-knowledge-reading-source-code 這篇文章介紹了閱讀源碼的重要性,精讀系列也已有八期源碼系列文章,分別是: 精讀《Immer.js》源...
摘要:需要注意的是,同樣的行為也適用于。這意味著我們必須重新綁定每個(gè)事件。組件的由調(diào)用它的父組件提供,這意味著所有事件都應(yīng)該與父組件相關(guān)聯(lián)。 原文鏈接:Vue.js — Considerations and Tricks showImg(https://segmentfault.com/img/bVbqHOd?w=1600&h=1599); Vue.js 是一個(gè)很棒的框架。然而,當(dāng)你開(kāi)始構(gòu)建...
摘要:特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 本以為自己收藏的站點(diǎn)多,可以很快搞定,沒(méi)想到一入?yún)R總深似海。還有很多不足&遺漏的地方,歡迎補(bǔ)充。有錯(cuò)誤的地方,還請(qǐng)斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應(yīng)和斧正,會(huì)及時(shí)更新,平時(shí)業(yè)務(wù)工作時(shí)也會(huì)不定期更...
閱讀 2850·2021-08-20 09:37
閱讀 1607·2019-08-30 12:47
閱讀 1090·2019-08-29 13:27
閱讀 1685·2019-08-28 18:02
閱讀 749·2019-08-23 18:15
閱讀 3084·2019-08-23 16:51
閱讀 931·2019-08-23 14:13
閱讀 2125·2019-08-23 13:05