React發展歷程中找到問題
為什麼要寫這個主題,是因為首先就是因為週會立了一個flag要說分享,但是我又不想去分享一些什麼類庫了,因為一些別的類庫對於我們業務來說,並沒有什麼特別好的幫助,其次之前跟同事聊天裡面,他問了我一個問題“你們是怎麼做到對新庫,新API保持這種持續學習的熱情的”,我是這麼回答他的“因為業務”,哈哈,大家會覺得我在扯淡,但其實裡面是有道理的,一些新的api,新的庫其實就是為了解決一些歷史問題,當你在當前環境下無法解決一些複雜問題而頭疼,用一些繞來繞去的方式解決的時候,發現來了一個新的模式去解決,你就會保持興奮和激動。那這次就帶著思考的角度去講React的編年史,也希望各位能夠從中複習或者有些小夥伴從沒接觸過一些 react歷史問題,也同樣看看為什麼新的React會產生這些新的API來幫助開發者們。
Earlier than 0.14.x (2015年)
早於0.14x 的時候,ES6還沒有普及,所以大家建立一個React的類的時候,都是以函式呼叫的形式建立的,傳入對應的鍵值對函式執行相應的lifeCycle, state, 和 props。
一個普通最基本,帶有props 以及 state的元件是以這樣的形式組織的
var Counter = React.createClass({ getInitialState: function() { return { count: 0 } }, getDefaultProps: function() { return { name: 'Mary' }; }, componentDidMount: function() { this.setState({ count: this.state.count + 1 }) }, handleClick: function() { this.setState(function(preState) { return { count: preState.count + 1 } }) }, render: function() { return ( <div onClick={this.handleClick} > {this.state.count} </div> ) } }) 複製程式碼
JSX
Jsx和ES6版本是基本上沒有差異性的
State
state的初始值是以initialState的函式呼叫返回值來建立的
handler Method
不用ES6: 方法呼叫和ES6版本有一個最大的區別,對於ES6來說,我們都知道它必須手動bind this。但是對於React.CreateClass來說,是不需要的,原因其實是在舊版本的reactClass這個物件裡面,在初始化的時候,會遍歷所有的傳入到createClass的key value,預設的內建lifCycle都會走預設的行為,但是那些所有不是lifeCycle的,都統統會經過一個判斷迴圈,原始碼不貼了,大家可以自己翻, [ofollow,noindex">github.com/facebook/re… ]
if (this.__reactAutoBindMap) { bindAutoBindMethods(this); } function bindAutoBindMethods(component) { for (var autoBindKey in component.__reactAutoBindMap) { if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) { var method = component.__reactAutoBindMap[autoBindKey]; component[autoBindKey] = bindAutoBindMethod( component, method ); } } } 複製程式碼
上面的this,就是整個ReactClass的上下文環境,至此,對於handler的autoBind就是這樣實現的。
Es6: JS丟失this問題
var obj = { a: 123, test: function() { console.log(this.a) } } function render(func) { func() } render(obj.test) 複製程式碼
階段一總結:僅僅是比較新舊語法建立的React例項,就已經可以探討到2個小知識了
- ES5React.createClass 的方法體為什麼不需要bind this
- ES6Class extends React.Component this 指標的問題
Class Extends
對於class OOP來說,最基本的就是符合里氏替換原則,里氏替換原則中說,任何基類可以出現的地方,子類一定可以出現。也就是,打個比方,有一個BaseHeader,其中一個子類集成了BaseHeader,叫做HeaderWithAvatar,那麼所有BaseHeader出現的地方都能夠替換成HeaderWithAvatar,這就稱為同一型別的元件。
首先可以肯定的是,用繼承來實現邏輯複用沒有問題,但是有侷限性
先看一下程式碼
export class BaseHeaderInh extends React.Component { state = { classname: 'base' } componentDidMount() { console.log('shit') } render() { return ( <div className={this.state.classname}>HeaderInh base</div> ) } } export class HeaderInh extends BaseHeaderInh { componentDidMount() { console.log('bull shit') } componentWillReceiveProps(nextProps) { // code for base components/**/ console.log('base componentWillReceiveProps') } state = { classname: `${this.state.classname} red` } } export class HeaderReadAvatar extends HeaderInh { componentWillReceiveProps(nextProps) { super.componentWillReceiveProps() console.log('i want change somethin in domain props') } render() { return ( <div> {super.render()} icon </div> ) } } 複製程式碼
- lifeCycle或者method的override會導致元件不符合預期執行
- 嚴重耦合子類和父類的關係
- 如果不仔細閱讀基類,完全沒法放心地實現子類特有的應用場景。
- 必須小心翼翼地寫程式碼
階段二總結:基於React組織方式的繼承邏輯複用有些什麼問題
對於React的哲學思想來說,希望開發者對於每個元件都承擔著對應自己的單一職責,進行解耦,比方說:HOC,render props 模式。
繼承耦合度高,在React元件模式下更難用好,這是一個顯而易見的問題。
組合各個元件是分離的,所以組合更加符合單一責任原則,並且組合的情況能夠更好地利用好children, state, props。
說到底,繼承是一種多型工具,而不是一種程式碼複用工具,當使用組合來實現程式碼複用的時候,是不會產生繼承關係的。過度使用繼承的話,如果修改了父類,會損壞所有的子類。這是因為子類和父類的緊耦合關係是在編譯期產生的。
Mixins
var SetIntervalMixin = { componentWillMount: function() { this.intervals = []; }, setInterval: function() { this.intervals.push(setInterval.apply(null, arguments)); }, componentWillUnmount: function() { this.intervals.forEach(clearInterval); } }; var TickTock = createReactClass({ mixins: [SetIntervalMixin], // Use the mixin getInitialState: function() { return {seconds: 0}; }, componentDidMount: function() { this.setInterval(this.tick, 1000); // Call a method on the mixin }, tick: function() { this.setState({seconds: this.state.seconds + 1}); }, render: function() { return ( <p> React has been running for {this.state.seconds} seconds. </p> ); } }); 複製程式碼
- Mixins核心原始碼基本上和autoBind的機制差不多,不過多贅述了。
- Mixins的問題其實跟class extends有些相似的問題
-
mixins造成了隱式的依賴 假設,你有一個元件有一個狀態count = 1 ,然後有一個同事建立了一個mixins,是一個通用的mixins,去讀取了本地狀態裡面的count 處理一些複用邏輯,過了幾個月之後,你希望做一些狀態共享的業務,把count也傳遞給別人,那麼做了狀態提升,把count拉高到父元件,這時候這個mixins就會爆炸了。並且mixins之間可以相互依賴,移除其中一個,有可能會造成另外一個的爆炸。在這種情況下,很難準確的描述mixins之間的依賴關係
-
mixins會有命名衝突的問題
-
類似於class 繼承的問題
所以綜合以上的種種問題,在React裡面,甚至是facebook這麼多優秀工程師的團隊,都會出現以上的問題,並且無法很好的組織,重構,維護諸如此類的複雜問題。那明顯,mixins是一個bad design 。
所以,對於邏輯複用,元件複用,隨著時間的推移,經驗的推進,就出現了我們最熟悉的higher order component
了。
Higher Order Component
完美的解決了以上的種種問題,對於邏輯複用,元件複用能夠很好的處理,正是因為HOC是以組合的形式出現的。hoc就不需要過多的介紹了。但是hoc依然出現了一些問題:
- 冗餘的巢狀高階元件,比方說Reach Router裡面大量使用了hoc。一個最基本的路由顯示,就出現了7層的hoc。
- 多個hoc組合之後,相同的props命名無法區分到底是來自哪個hoc 所以就出現了render props
Render Props
Render props 和 hoc都是解決同樣的事情的,就是邏輯複用,所有的hoc都能夠通過render props 重寫,render props 剔除了所有上面hoc的問題,寫法會相對優雅一點,但是這是需要分場景的,我個人覺得並沒有說所有的邏輯複用的hoc都用render props去重寫。有一些場景,的確沒必要去用render props,比方說一些許可權問題,套一個render props是真的麻煩。因為render props終究是一個jsx,不能從外部解決問題,而是在render函式內解決問題。
function isAuth(Component) { return class Auth extends React.Component { state = { auth: false } componentDidMount() { setTimeout(() => { this.setState({ auth: true}) }, 300) } render() { return( <div> {this.state.auth ? <Component {...props} /> : '無權檢視'} </div> ) } } } @isAuth class Demo extends React.Component { } // render props class Auth extends React.Component { state = { auth: false } componentDidMount() { setTimeout(() => { this.setState({ auth: true}) }, 300) } renderError = () => { return( <div>Error component</div> ) } render() { return (this.props.children({ auth: this.state.auth, renderError: this.render })) } } class RenderProps extends React.Component{ render() { return( <div> <Auth> {({auth, renderError}) => { return <div> {auth ? Component : renderError} </div> }} </Auth> </div> ) } } 複製程式碼
對於Hoc來說基本沒有對JSX的入侵性,只需要套一個decorator或者套一個函式返回元件 但是對於render props就必然對JSX 有一個入侵性,也就是說,無論如何,你都需要有一個合理的JSX結構來組織。
但其實,Render props依然還有一些問題,就是call backhell了,比方說,最底下的一個div需要用到多個createContext的props,那就需要寫成這樣。
class Demo extends React.Component { render() { return( <Auth> {({ auth, renderError }) => ( <Game> {(props) => ( <Dude> {(props) => ( <Shit> {props => ( <div>213</div> )} </Shit> )} </Dude> )} </Game> )} </Auth> ) } } 複製程式碼
Hooks api 除了基本上能夠解決現階段所有的問題,還解決了一些額外的問題
- 編譯後代碼量
- 看起來更加FP一點(僅僅是看起來)
- 降低智障程式碼出錯率
- 讓更多的元件易於測試
階段總結一下:
-
Mixins(0.14x<): 邏輯複用初代目,雖然解決了邏輯複用,但是本質和Class inheritance有類似問題(工程協作會造成困擾)
-
Class Inheritance:是一種多型工具,而不是一種程式碼複用工具,(需要非常完整的OOP能力,但也沒法解決耦合問題)
-
Higher Order Component(0.14x-15.6):邏輯程式碼複用以組合的形式出現,顆粒度適中,具備完整的state , props形態,符合React核心的單一職責原則(但是會造成冗餘巢狀元件的問題)
-
Render Props(16.x):優雅地處理hoc剩餘問題,但依然可能會出現(Call BackHell)
-
Hooks (16.7 alpha):基本是現階段邏輯複用的解決方案
0.14.x – 16.6(2016年-2017、8年) 這個裡面出現了很多的api更新,Reconciler的架構不斷地改進,以及拆包,例如像ReactDom , React.CSSTransitionGroup,React.CreateClass,React.PropTypes等,包括開始加入fiber架構在之後的程式碼增進,ComponentDidCatch,getDerivedStateFromProps,getSnapShotBeforeUpdate等等等等
16.x RoadMap
除了一些我們所已經接觸到的還有以下兩個(這兩個細節其實都可以從官網和iceland dan的演講視訊中找到)
- 16.8: concurrent mode
- 16.9: suspense for Data fetching
最後總結一下
對於這麼多日新月異的框架和API,我自己個人來說,是以解決業務,解決問題的想法去保持熱情從而學習他們的。
- 在歷史中發掘真理,
- 在過程中迭代方案,
- 在業務中嘗試它們