React函數式進階

  • 2019 年 12 月 4 日
  • 筆記

本文作者:IMWeb nixzheng 原文出處:IMWeb社區 未經同意,禁止轉載

React讓很多人讓追捧的一個特性是它的所有的組件都是完全由JavaScript組成的。組件的定義是JavaScript,組件的模板也可以是JavaScript,組件的樣式也可以是JavaScript(參考styled-component)。React並沒有創造太多概念,唯一的創造品——JSX,其內部的statement也是一段段純JavaScript程式碼,並且在Babel編譯後依然轉變成了JavaScript。

而JavaScript又是一個把函數當作一等公民的語言。函數不僅可以被聲明和調用,也可以像值一樣做賦值、傳參、返回的操作。

這樣運行在一個有著first-class functions特性的語言之上的純JavaScript組件庫,自然可以腦洞大開的有很多玩法。

Stateless Component

使用React的同學自然對這個概念一點都陌生。雖然大多數情況下我們都會使用 class extends React.Component 來聲明一個Stateful Component,雖然Stateless Component沒有完整的生命周期,雖然Stateless Component的性能相比Stateful Component並沒有提升,但是它在很多場合下仍然是有意義的。下面是一個最簡單的Stateless Component的聲明:

function Welcome({name}) {    return <div>{name}</div>  }

由於這是一個純函數,我們可以基於它創建一個簡單的High Order Component。

const withNameX = WrappedComponent => props => WrappedComponent(Object.assign({}, props, {name: 『x』}))  const WelcomeX = withNameX(Welcome)

我們也可以對 Welcome 做簡單的 compose。

const toUpperCase = props => Object.keys(props).reduce((target, next) => {    target[next] = props[next].toUpperCase()    return target  })  const UpperWelcome = compose(Welcome, toUpperCase )

使用Stateless Component好處很多,包括

  • Pure。單元測試很方便。
  • 強制你從更簡單的角度思考組件的組織。單個函數的程式碼量更小,功能更單一。「The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.」——《Clean Code》
  • 可以使用compose、curry等特性構件可復用組件。

Stateless Component最大的不足是它沒有能力在最佳實踐的前提下處理需要回傳屬性的事件回調,我們只能寫成這樣:

const Trigger = ({onClick, id}) => <Button onClick={() => onClick(id)}>test</Button>

由於每次調用都會生成一個新的匿名函數,子組件是無法利用PureComponent做優化的。這是我在實際工作場景下使用Stateless Component最大的障礙。

recompose

上面的障礙當然也是有解的,recompose是一個為Stateless React愛好者提供的一個工具庫。我們可以使用它提供的 withHandlers 方法。

const Trigger = withHandlers({    onClick: props => () => props.onClick(props.id)  })(({onClick}) => <Button onClick={onClick}>test</Button>)

是不是也很優雅。

當然為了能處理這種類型的回調,withHandlers 內部也是使用了Stateful Component的,感興趣的同學可以看看recompose的源碼。

recompose還有 withState, pure, onlyUpdateForKeys, withContext 等很多實用的工具函數,幫助我們至少從程式碼編寫角度實現全面使用Stateless Component替代Stateful Component。

Function as child Components

這也是React社區一種常見的組件構建方式。它也能解決HOC中丟失上下文、丟失ref的問題。它也能有效的提升程式碼復用率,而且某些情況下比HOC要更加優雅。

一個最簡單的Function as Child Component如下:

class MyComponent extends React.Component {    render() {      return (        <div>          {this.props.children('world')}        </div>      );    }  }    <MyComponent>    {(name) => (      <div>Hello {name}!</div>    )}  </MyComponent>

PayPal開源的downshift就是使用Function as Child Component模型來構建他們的autocomplete,dropdown, select等組件的。

一般我們寫一個autocomplete組件,是基於Popover -> Menu + InputTrigger -> AutoComplete這樣逐步組合、增強基礎組件的方式。這麼寫會有幾個問題:高級組件或者完全無法獲取底層組件的引用,或者需要通過很奇怪的方式把引用回調一層層傳下去;為了適配很多情況和需求,為了能控制各組合組件的行為,高級組件的參數會多的可怕:ant.design的AutoComplete組件有14個參數,material-ui則有27個參數。

Downshift則完全不處理組件的展示和組合,這部分邏輯交給開發者自己,通過Function as Child Components的方式自由設計他們希望的樣式和行為。Downshift只處理這一類組件的交互邏輯,維護組件狀態,並暴露少數幾個必須設置的子組件屬性的介面。這樣的程式碼組織,輸入輸出都很明確,組件間的耦合也很小,不僅解決了參數爆炸的問題,也提升了可維護性。

對比High Order Component與Function as Child Components

HOC

FaCC

使用者無關,HOC幫你完成了一切組件行為

使用者完全大部分組件展示和行為,更可控

HOC在運行時無法獲取組件相關的state和props

可以在運行時獲取組件的 state & props

HOC可以通過shouldComponentUpdate做優化

FaCC由於每次render都會改變,無法使用shouldComponentUpdate做優化

總結

本文提到了兩種組件設計的思路——利用recompose拆分組件為Stateless Component,使用Function as Child Components來剝離行為和展現——以此來提升程式碼的可讀性和可維護性。實際項目中可以按需使用。

參考資料