控制權——這個概念在編程中相當重要。好比,「輪子」封裝層與業務消費層對於控制權的「爭奪」,就是一個頗有意思的話題。這在 React 世界裏也不例外。表面上看,咱們固然但願「輪子」掌控的事情越多越好:由於抽象層處理的邏輯越多,業務調用時關心的事情就越少,使用就越方便。但是有些設計卻「不敢越雷池一步」。「輪子」與業務在控制權上的拉鋸,就很是有意思了。html
同時,控制能力與組件設計也息息相關:Atomic components 這樣的原子組件設計被受推崇;在原子組件這個概念之上,還有分子組件:Molecules components。不論是分子仍是原子,在解決業務問題上都有存在的理由。前端
這篇文章將以 React 框架爲背景,談談我在開發當中對於控制權的一些想法和總結。若是你並不使用 React,原則上仍不妨礙閱讀。java
在文章開始以前,我想先向你們介紹一本書。react
從去年起,我和知名技術大佬顏海鏡開始了合著之旅,今年咱們共同打磨的書籍**《React 狀態管理與同構實戰》**終於正式出版了!這本書以 React 技術棧爲核心,在介紹 React 用法的基礎上,從源碼層面分析了 Redux 思想,同時着重介紹了服務端渲染和同構應用的架構模式。書中包含許多項目實例,不只爲用戶打開了 React 技術棧的大門,更能提高讀者對前沿領域的總體認知。git
若是各位對圖書內容或接下來的內容感興趣,還望多多支持!文末有詳情,不要走開!github
初入 React 大門,關於控制權概念,咱們最早接觸到的就是受控組件與非受控組件。這兩個概念每每與表單關聯在一塊兒。在大部分狀況下,推薦使用受控組件來實現表單、輸入框等狀態控制。在受控組件中,表單等數據都由 React 組件本身處理。而非受控組件,是指表單的數據由 Dom 本身控制。下面就是一個典型的非受控組件:編程
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
複製代碼
對於 React 來講,非受控組件的狀態和用戶輸入都沒法直接掌控,只能依賴 form 標籤的原生能力進行交互。若是使上例非受控組件變爲一個受控組件,代碼也很簡單:redux
class NameForm extends React.Component {
state= {value: ''}
handleChange = event => {
this.setState({value: event.target.value});
}
handleSubmit = event => {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
)
}
}
複製代碼
這時候表單值和行爲都由 React 組件控制,使得開發更加便利。promise
這固然是很基礎的概念,藉此拋出控制權的話題,請讀者繼續閱讀。前端工程師
前文介紹的樣例,我稱之爲「狹義受控和非受控」組件。廣義來講,我認爲徹底的非受控組件是指:不含有內部 states,只接受 props 的函數式組件或無狀態組件。它的渲染行爲徹底由外部傳入的 props 控制,沒有自身的「自治權」。這樣的組件在很好地實現了複用性,且具備良好的測試性。
但在 UI 「輪子」設計當中,**「半自治」或者「不徹底受控」**組件,有時也會是一個更好的選擇。咱們將此稱之爲 「control props」 模式。簡單來講就是:組件具備自身 state,當沒有相關 porps 傳入時,使用自身狀態 statea 完成渲染和交互邏輯;當該組件被調用時,若是有相關 props 傳入,那麼將會交出控制權,由業務消費層面控制其行爲。
在研究大量社區 UI 「輪子」 以後,我發現由 Kent C. Dodds 編寫的,在 paypal 使用的組件庫 downshift 便普遍採用了這樣的模式。
簡單用一個 Toogle 組件舉例,這個組件由業務方調用時:
class Example extends React.Component {
state = {on: false, inputValue: 'off'}
handleToggle = on => {
this.setState({on, inputValue: on ? 'on' : 'off'})
}
handleChange = ({target: {value}}) => {
if (value === 'on') {
this.setState({on: true})
} else if (value === 'off') {
this.setState({on: false})
}
this.setState({inputValue: value})
}
render() {
const {on} = this.state
return (
<div>
<input
value={this.state.inputValue}
onChange={this.handleChange}
/>
<Toggle on={on} onToggle={this.handleToggle} />
</div>
)
}
}
複製代碼
效果如圖:
咱們能夠經過輸入框來控制 Toggle 組件狀態切換(輸入 「on「 激活狀態,輸入 」off「 狀態置灰),同時也能夠經過鼠標來點擊切換,此時輸入框內容也會相應變化。
請思考:對於 UI 組件 Toggle 來講,它的狀態能夠由業務調用方來控制其狀態,這就賦予了使用層面上的消費便利。在業務代碼中,不論是 Input 仍是其餘任何組件均可以控制其狀態,調用時咱們具備徹底的控制權掌控能力。
同時,若是在調用 Toggle 組件時,不去傳 props 值,該組件仍然能夠正常發揮。以下:
<Toggle>
{({on, getTogglerProps}) => (
<div>
<button {...getTogglerProps()}>Toggle me</button>
<div>{on ? 'Toggled On' : 'Toggled Off'}</div>
</div>
)}
</Toggle>
複製代碼
Toggle 組件在狀態切換時,本身維護內部狀態,實現切換效果,同時經過 render prop 模式,對外輸出本組件的狀態信息。
咱們看 Toggle 源碼(部分環節已刪減):
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
class Toggle extends Component {
static defaultProps = {
defaultOn: false,
onToggle: () => {},
}
state = {
on: this.getOn({on: this.props.defaultOn}),
}
getOn(state = this.state) {
return this.isOnControlled() ? this.props.on : state.on
}
isOnControlled() {
return this.props.on !== undefined
}
getTogglerStateAndHelpers() {
return {
on: this.getOn(),
setOn: this.setOn,
setOff: this.setOff,
toggle: this.toggle,
}
}
setOnState = (state = !this.getOn()) => {
if (this.isOnControlled()) {
this.props.onToggle(state, this.getTogglerStateAndHelpers())
} else {
this.setState({on: state}, () => {
this.props.onToggle(
this.getOn(),
this.getTogglerStateAndHelpers()
)
})
}
}
setOn = this.setOnState.bind(this, true)
setOff = this.setOnState.bind(this, false)
toggle = this.setOnState.bind(this, undefined)
render() {
const renderProp = unwrapArray(this.props.children)
return renderProp(this.getTogglerStateAndHelpers())
}
}
function unwrapArray(arg) {
return Array.isArray(arg) ? arg[0] : arg
}
export default Toggle
複製代碼
關鍵的地方在於組件內 isOnControlled 方法判斷是否有命名爲 on 的屬性傳入:若是有,則使用 this.props.on 做爲本組件狀態,反之用自身 this.state.on 來管理狀態。同時在 render 方法中,使用了 render prop 模式,關於這個模式本文再也不探討,感興趣的讀者能夠在社區中找到不少資料,同時也能夠在我新書中找到相關內容。
盤點一下,control props 模式反應了典型的控制權問題。這樣的**「半自治」**可以完美適應業務需求,在組件設計上也更加靈活有效。
提到控制權話題,怎能少得了 Redux 這樣的狀態管理工具。Redux 的設計在方方面面都體現出來良好的控制權處理,這裏咱們把注意力集中在異步狀態上,更多的內容還請讀者關注個人新書。
Redux 處理異步,最爲人熟知的就是 Redux-thunk 這樣的中間件,它由 Dan 親自編寫,並在 Redux 官方文檔上被安利。它與其餘全部中間件同樣,將 action 到 reducer 中間的過程進行掌控,使得業務使用時能夠直接 dispatch 一個函數類型的 action,實現代碼也很簡單:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
export default thunk;
複製代碼
可是很快就有人認爲,這樣的方案由於在中間件實現中的控制不足,致使了業務代碼不夠精簡。咱們仍是須要遵循傳統的 Redux 步驟:八股文似的編寫 action,action creactor,reducer......因而,控制粒度更大的中間件方案應運而生。
Redux-promise 中間件控制了 action type,它限制業務方在 dispatch 異步 action 時,action的 payload 屬性須要是一個 Promise 對象時,執行 resolve,該中間件觸發一個類型相同的 action,並將 payload 設置爲 promise 的 value,並設 action.status 屬性爲 "success"。
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}
複製代碼
這樣的設計與 Redux-thunk 徹底不一樣,它將 thunk 過程控制在中間件自身中,這樣一來,第三方輪子作的事情更多,所以在業務調用時更加簡練方便。咱們只須要正常編寫 action 便可:
dispatch({
type: GET_USER,
payload: http.getUser(userId) // payload 爲 promise 對象
})
複製代碼
咱們對比一下 Redux-thunk,相對於「輪子」控制權較弱,業務方控制權更多的 Redux-thunk,實現上述三行代碼,就得不得不須要:
dispatch(
function(dispatch, getState) {
dispatch({
type: GET_USERE,
payload: userId
})
http.getUser(id)
.then(response => {
dispatch({
type: GET_USER_SUCCESS,
payload: response
})
})
.catch(error => {
dispatch({
type: GET_DATA_FAILED,
payload: error
})
})
}
)
複製代碼
固然,Redux-promise 控制權越多,一方面帶來了簡練,可是另外一方面,業務控制權越弱,也喪失了必定的自主性。好比若是想實現樂觀更新(Optimistic updates),那就很難作了。具體詳見 Issue #7
爲了平衡這個矛盾,在 Redux-thunk 和 Redux-promise 這兩個極端控制權理念的中間件之間,因而便存在了中間狀態的中間件:Redux-promise-middleware,它與 Redux-thunk 相似,掌控粒度也相似,可是在 action 處理上更加溫和和漸進,它會在適當的時機 dispatch XXX_PENDING、XXX_FULFILLED 、XXX_REJECTED 三種類型的 action,也就是說這個中間件在掌控更多邏輯的基礎上,增長了和外界第三方的通訊程度,再也不是直接高冷地觸發 XXX_FULFILLED 、XXX_REJECTED,請讀者仔細體會其中不一樣。
瞭解了異步狀態中的控制權問題,咱們再從 Redux 全局角度進行分析。在內部分享時,我將基於 Redux 封裝的狀態管理類庫共同特性總結爲這一頁 slide:
以上四點都是相關類庫基於 Redux 所進行的簡化,其中很是有意思的就是後面三點,它們無一例外地與控制權相關。以 Rematch 爲表明,它再也不是處理 action 到 reducer 的中間件,而是徹底控制了 action creator,reducer 以及聯經過程。
具體來看:
業務方再也不須要顯示申明 action type,它由類庫直接函數名直接生成,若是 reducer 命名爲 increment,那麼 action.type 就是 increment;
同時控制 reducer 和 action creator 合二爲一,態管理從未變得如此簡單、高效。
我把這樣的實踐稱爲控制主義或者極簡主義,相比 Redux-actions 這樣的狀態管理類庫,這樣的作法更加完全、完善。具體思想可參考 Shawn McKay 的文章,介紹的比較充分,這裏我再也不贅述。
控制權說究竟是一種設計思想,是第三方類庫和業務消費的交鋒和碰撞。它與語言和框架無關,本文只是以 React 舉例,實際上在編程領域控制權的爭奪隨處可見;他與抽象類別無關,本文已經在 UI 抽象和狀態抽象中分別例舉分析;控制權與碼農息息相關,它直接決定了咱們的編程體驗和開發效率。
但是在編程的初期階段,優秀的控制權設計難以一蹴而就。只有投身到一線開發當中,真正瞭解自身業務需求,進而總結大量最佳實踐,同時參考社區精華,分析優秀開源做品,相信咱們都會獲得成長。
最後,前端學習永無止境,但願和每一位技術愛好者共同進步,你們能夠在知乎找到我!
Happy coding!
Happy coding!
《React 狀態管理與同構實戰》這本書由我和前端知名技術大佬顏海鏡協力打磨,凝結了咱們在學習、實踐 React 框架過程當中的積累和心得。**除了 React 框架使用介紹之外,着重剖析了狀態管理以及服務端渲染同構應用方面的內容。**同時吸收了社區大量優秀思想,進行概括比對。
本書受到百度公司副總裁沈抖、百度資深前端工程師董睿,以及知名 JavaScript 語言專家阮一峯、Node.js 佈道者狼叔、Flarum 中文社區創始人 justjavac、新浪移動前端技術專家小爝、百度資深前端工程師顧軼靈等前端圈衆多專家大咖的聯協力薦。
有興趣的讀者能夠點擊這裏,瞭解詳情。也能夠掃描下面的二維碼購買。再次感謝各位的支持與鼓勵!懇請各位批評指正!