咱們都知道在業務開發的過程當中,若是徹底不一樣的組件有類似的功能,這就會產生橫切關注點(cross-cutting concerns)
問題。javascript
在React中,存在一些最佳實踐去處理橫切關注點的問題,能夠幫助咱們更好地進行代碼的邏輯複用。html
針對這個問題,在使用createReactClass
建立 React 組件的時候,引入 mixins 功能會是一個很好的解決方案。java
爲了在初始階段更加容易地適應和學習React,官方在 React 中包含了一些急救方案。mixin 系統是其中之一。react
因此咱們能夠將通用共享的方法包裝成Mixins方法,而後注入各個組件進行邏輯複用的實現。編程
const mixin = function(obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for (let prop in mixins) {
if (mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop];
}
}
return newObj;
}
複製代碼
上述代碼就實現了一個簡單的mixin函數,其實質就是將mixins中的方法遍歷賦值給newObj.prototype
,從而實現mixin返回的函數建立的對象都有mixins中的方法,也就是把額外的功能都混入進去。設計模式
在咱們大體明白了mixin做用後,讓咱們來看看如何在React使用mixin。數組
var RowMixin = {
renderHeader: function() {
return (
<div className='row-header'> <h1> {this.getHeaderText()} </h1> </div>
);
}
};
var UserRow = React.createClass({
mixins: [RowMixin], // 混入renderHeader方法
getHeaderText: function() {
return this.props.user.fullName;
},
render: function() {
return (
<div> {this.renderHeader()} <h2>{this.props.user.biography}</h2> </div>
)
}
});
複製代碼
使用React.createClass
,官方提供了mixins的接入口。須要複用的代碼邏輯從這裏混入就能夠。app
這是ES5的寫法,實際上React16版本後就已經廢棄了。ide
ES6 自己是不包含任何 mixin 支持。所以,當你在 React 中使用 ES6 class 時,將不支持 mixins 。函數式編程
官方也發現了不少使用 mixins 而後出現了問題的代碼庫。而且不建議在新代碼中使用它們。
Mixins 引入了隱式的依賴關係(Mixins introduce implicit dependencies)
Mixins 引發名稱衝突(Mixins cause name clashes)
Mixins 致使滾雪球式的複雜性(Mixins cause snowballing complexity)
引自官方博客: reactjs.org/blog/2016/0…
官方博客裏面有一篇文章詳細描述了棄用的緣由。裏面列舉了三條罪狀,如上所述。
在實際開發的過程當中,咱們沒法預知別人往代碼裏mixin了什麼屬性和狀態。若是想要mixin本身的功能,可能會發生衝突,甚至須要去解耦以前的代碼。
這樣的方式同時也破壞了組件的封裝性,代碼之間的依賴是不可見的,給重構代碼也帶來了必定的難度。若是對組件進行修改,極可能會致使mixin方法錯誤或者失效。
在日後的開發維護過程當中,就致使了滾雪球式的複雜性。
組件中含有多個mixin——
不一樣的mixin中含有相同名字的非生命週期函數,React會拋出異常(不是後面的函數覆蓋前面的函>數)。
不一樣的mixin中含有相同名字的生命週期函數,不會拋出異常,mixin中的相同的生命週期函數(除render方法)會按照createClass中傳入的mixins數組順序依次調用,所有調用結束後再調用組件內部的相同的聲明周期函數。
不一樣的mixin中默認props或初始state中存在相同的key值時,React會拋出異常。
mixin裏面對不一樣狀況名稱衝突的處理,只有當相同名稱的生命週期函數,纔會按照聲明的順序調用,最後調用組件內部的同名函數。其餘狀況下都會拋出異常。
mixin這種混入模式,會給組件不斷增長新的方法和屬性,組件自己不只能夠感知,甚至須要作相關的處理(例如命名衝突、狀態維護),一旦混入的模塊變多時,整個組件就變的難以維護,也就是爲何如此多的React庫都採用高階組件的方式進行開發。
在mixin廢棄後,不少開源組件庫都是使用的高階組件寫法。
高階組件屬於函數式編程(functional programming)思想。
對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具備功能加強的效果。
說到高階組件,先要說一下高階函數的定義。
在數學和計算機科學中,高階函數是至少知足下列一個條件的函數:
接受一個或多個函數做爲輸入
輸出一個函數
簡單地來講,高階函數就是接受函數做爲輸入或者輸出的函數。
const add = (x,y,f) => f(x)+f(y);
add(-5, 6, Math.abs);
複製代碼
A higher-order component is a function that takes a component and returns a new component.
高階組件是一個接受組件而且返回新組件的函數,注意雖然名字叫高階組件但它自身是一個函數,它能夠加強它所包裹的組件功能,或者說賦予了它所包裹的組件一個新的功能。
它不是React API的一部分,源自於React生態,是官方推崇的複用組合的一種方式。它對應着設計模式中的裝飾者模式。
高階組件,主要有兩種方式處理包裹組件的方式,分別是屬性代理和反向繼承。
實質上是經過包裹原來的組件來操做props
操做props
得到refs引用
抽象state
用其餘元素包裹組件
export default function withHeader(WrappedComponent) {
return class HOC extends Component {
render() {
const newProps = {
test:'hoc'
}
// 透傳props,而且傳遞新的newProps
return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } 複製代碼
屬性代理,其實是經過包裹原來的組件,來注入一些額外的props或者state。
爲了加強可維護性,有一些固有的約定,好比命名高階組件的時候須要使用withSomething
的格式。
對於傳入的props最好直接透傳,不要破壞組件自己的屬性和狀態。
渲染劫持
操做props和state
export default function (WrappedComponent) {
return class Inheritance extends WrappedComponent {
componentDidMount() {
// 能夠方便地獲得state,作一些更深刻的修改。
console.log(this.state);
}
render() {
return super.render();
}
}
}
複製代碼
反向繼承能夠經過super
關鍵字獲取到父類原型對象上的全部方法(父類實例上的屬性或方法則沒法獲取)。在這種方式中,它們的關係看上去被反轉(inverse)了。
反向繼承能夠劫持渲染,能夠進行延遲渲染/條件渲染等操做。
約定:將不相關的 props 傳遞給被包裹的組件
約定:包裝顯示名稱以便輕鬆調試
約定:最大化可組合性
// 而不是這樣...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... 你能夠編寫組合工具函數
// compose(f, g, h) 等同於 (...args) => f(g(h(...args)))
const enhance = compose(
// 這些都是單參數的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複製代碼
compose能夠幫助咱們組合任意個(包括0個)高階函數,例如compose(a,b,c)返回一個新的函數d,函數d依然接受一個函數做爲入參,只不過在內部會依次調用c,b,a,從表現層對使用者保持透明。 基於這個特性,咱們即可以很是便捷地爲某個組件加強或減弱其特徵,只須要去變動compose函數裏的參數個數便可。
模塊複用
頁面鑑權
日誌及性能打點
…
export const withTimer = (interval) => (wrappedComponent) => {
return class extends wrappedComponent {
constructor(props) {
super(props);
}
// 傳入endTime 計算剩餘時間戳
endTimeStamp = DateUtils.parseDate(this.props.endTime).getTime();
componentWillMount() {
// 未過時則手動調用計時器 開始倒計時
if (Date.now() < this.endTimeStamp) {
this.onTimeChange();
this.setState({expired: false});
this.__timer = setInterval(this.onTimeChange, interval);
}
}
componentWillUnmount() {
// 清理計時器
clearInterval(this.__timer);
}
onTimeChange = () => {
const now = Date.now();
// 根據剩餘時間戳計算出 時、分、秒注入到目標組件
const ret = Helper.calc(now, this.endTimeStamp);
if (ret) {
this.setState(ret);
} else {
clearInterval(this.__timer);
this.setState({expired: true});
}
}
render() {
// 反向繼承
return super.render();
}
};
};
複製代碼
@withTimer()
export class Card extends React.PureComponent {
render() {
const {data, endTime} = this.props;
// 直接取用hoc注入的狀態
const {expired, minute, second} = this.state;
// 略去render邏輯
return (...);
}
}
複製代碼
需求是須要進行定時器倒計時,不少組件都須要注入倒計時功能。那麼咱們把它提取爲一個高階組件。
這是一個反向繼承的方式,能夠拿到組件自己的屬性和狀態,而後把時分秒等狀態注入到了組件中。
原組件使用了ES7的裝飾器語法,就能夠增強它的功能。
組件自己只須要有一個endTime
的屬性,而後高階組件就能夠計算出時分秒而且進行倒計時。
也就是說,高階組件賦予了原組件倒計時的功能。
在使用高階組件寫法時,也有一些注意事項。
render() {
// 每次調用 render 函數都會建立一個新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!
return <EnhancedComponent />; } 複製代碼
若是在render函數中建立,每次都會從新渲染一個新的組件。這不只僅是性能問題,每次重置該組件的狀態,也可能會引發代碼邏輯錯誤。
// 定義靜態函數
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 加強組件沒有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼
當你將 HOC 應用於組件時,原始組件將使用容器組件進行包裝。這意味着新組件沒有原始組件的任何靜態方法。
你可使用hoist-non-react-statics
自動拷貝全部非 React 靜態方法:
通常來講,高階組件能夠傳遞全部的props屬性給包裹的組件,可是不能傳遞 refs 引用。由於並非像 key 同樣,refs 是一個僞屬性,React 對它進行了特殊處理。
若是你向一個由高級組件建立的組件的元素添加 ref 應用,那麼 ref 指向的是最外層容器組件實例的,而不是包裹組件。
在不編寫class的狀況下使用state以及其餘的React特性。
Hook是一些可讓你在函數組件hook react state及生命週期等特性的函數。它不能在class組件中使用。
在組件之間複用狀態邏輯
render props
任何被用於告知組件須要渲染什麼內容的函數props在技術上均可以被成爲稱爲render prop
若是在render方法裏建立匿名函數,那麼使用render prop會抵消使用React.PureComponent帶來的優點。 須要把render方法建立爲實例函數,或者做爲全局變量傳入。
hoc
providers
consumers
這些抽象層組成的組件會造成嵌套地獄,所以React須要爲共享狀態邏輯提供更好的原生途徑。
加強代碼可維護性
class難以理解
React社區接受了React hooks的提案,這將減小編寫 React 應用時須要考慮的概念數量。
Hooks 可使得你始終使用函數,而沒必要在函數、類、高階組件和 reader props之間不斷切換。
基礎 Hook
useState
useEffect
啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。
useContext
額外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
自定義Hook useSomething
自定義Hook是一種重用狀態邏輯的機制,全部的state和反作用都是徹底隔離的。
官方已經棄用了一些生命週期,useEffect
至關於componentDidMount
,componentDidUpdate
和 componentWillUnmount
。
除了官方提供的Hook API之外,你可使用自定義Hook。
自定義 Hook 不須要具備特殊的標識。咱們能夠自由的決定它的參數是什麼,以及它應該返回什麼(若是須要的話)。
換句話說,它就像一個正常的函數。可是它的名字應該始終以 use
開頭,這樣能夠一眼看出其符合 Hook 的規則。
動畫、訂閱聲明、計時器是自定義Hook的一些經常使用操做。
接下來,咱們來用React Hook改寫一下以前的高階組件demo。
export function useTimer(endTime, interval, callback) {
interval = interval || 1000;
// 使用useState Hook get/set狀態
const [expired, setExpired] = useState(true);
const endTimeStamp = DateUtils.parseDate(endTime).getTime();
function _onTimeChange () {
const now = Date.now();
// 計算時分秒
const ret = Helper.calc(now, endTimeStamp);
if (ret) {
// 回調傳出所需的狀態
callback({...ret, expired});
} else {
clearInterval(this.__timer);
setExpired(true);
callback({expired});
}
}
// 使用useEffect代替生命週期的調用
useEffect(() => {
if (Date.now() < endTimeStamp) {
_onTimeChange();
setExpired(false);
this.__timer = setInterval(_onTimeChange, interval);
}
return () => {
// 清除計時器
clearInterval(this.__timer);
}
})
}
複製代碼
export function Card (props) {
const {data, endTime} = props;
const [expired, setExpired] = useState(true);
const [minute, setMinute] = useState(0);
const [second, setSecond] = useState(0);
useTimer(endTime, 1000, ({expired, minute, second}) => {
setExpired(expired);
setMinute(minute);
setSecond(second);
});
return (...);
複製代碼
自定義Hook除了命名須要遵循規則,參數傳入和返回結果均可以根據具體狀況來定。
這裏,我在定時器每秒返回後傳出了一個callback,把時分秒等參數傳出。
除此以外能夠看到沒有class的生命週期,使用useEffect
來完成反作用的操做。
使用一個eslint-plugin-react-hooks
ESLint插件來強制執行這些規則
不要在循環
,條件
或嵌套函數
中調用 Hook, 確保老是在React 函數的最頂層調用他們。
由於React是根據你聲明的順序去調用hooks的,若是不在最頂層調用,那麼不能保證每次渲染的順序都是相同的。
遵照規則,React 纔可以在屢次的 useState
和 useEffect
調用之間保持 hook 狀態的正確。
只在 React 函數中調用 Hook
在 React 的函數組件中調用 Hook
在自定義 Hook 中調用其餘 Hook