MobX 的「間接引用值儘可能晚使用」產生的強耦合問題應該如何解決?
MobX 強調為了性能,要間接引用值儘可能晚使用,見於:
https://mobx.js.org/best/react.html的最後一段
If the Author component was invoked like: &
. Then Message would be the dereferencing component and react to changes to message.author.name. Nonetheless & would rerender as well, because it receives a new value. So performance wise it is best to dereference as late as possible. 以及https://mobx.js.org/best/pitfalls.html的中上部分「Dereference values as late as possible」小節。
但是,這使得父組件把自己的無關的邏輯暴露給了子組件,而且使得子組件與父組件產生強耦合以致無法運用於其他類似的場景。一般我們應該怎麼做來避免 MobX 的性能和內聚不能共存的衝突?
這是個很好的問題。
太長不看:我們可以通過額外的數據組件層來解決這個問題。
=====具體答案的分割線======
我先舉個例子來具象化這個問題。假設我們有這樣一個非常簡單的 &
const Author = ({ name }) =&> &{name}&
它需要在不同場景被複用。目前它的狀態很好,只需要關心自己要渲染的 name 屬性而已。但是當它在 mobx 中使用的時候,按照文檔的觀點,它不應該直接接收 name,而是應該接收 name 的父層對象 author,以便 observer 可以更精確地 re-render。這時候它和它的父級組件 &
const Message = observer(({ message }) =&>
&
{message.title}
&
&
&
const Author = observer(({ author }) =&>
&{author.name}&
)
聰明的題主立刻意識到了問題: &
那麼如何同時在解耦的同時保持 &
const RawAuthor = ({ name }) =&> &{name}&
const Author = observer(({ author }) =&>
&
)
const Message = observer(({ message }) =&>
&
{message.title}
&
&
&
這樣一來 &
P.S. 如果這個地方性能不敏感的話,其實不用操心這麼多,直接 &
P.P.S 這個例子僅適用於 &
問題提的很好。
先說結論:存在性能損失但不認為存在強耦合,對於一般的業務系統而言,性能跟代碼可讀性之間的衝突,不要猶豫,優先可讀性。至於解決方案,mobx 官方已經給出答案 Optimizing React components | MobX 跟 @陳蒙迪 給出的方案一致。
不確定所有人都有興趣閱讀問題的上下文從而明白提問者問的是什麼,我來(試圖)解釋一下。
======== 解釋開始 ===============
上下文的代碼是這樣的:
const Message = observer(({ message }) =&>
& &&
const Author = observer(({ author }) =&>
&{author.name}&)message 是一個聲明為 observable 的對象(mobx 默認配置的 observable 為 deep enhance),當message的屬性(包括子代屬性)發生變更時,被裝飾為 observer 的組件會自動 re-render。
所以基於以上代碼,當
message.author.name = abc
的動作產生時,Author 組件會 re-render 但 Message 不會。因為 Message 觀察的是 message.author 引用,而在整個過程中 message.author 的引用並沒有發生改變。
假如將 Author 組件改寫為:
const Author = observer(({name}) =&> &{name}&)
使用方式變為:
const Message = observer((message) =&> &&
&
此時若發生 message.author.name 變更,Author 跟 Message 均會 re-render。
所以文檔里建議,出於性能考慮,Author 組件的設計應該是這種
const Author = observer(({author}) =&> &{author.name}&)
=========== 解釋完畢 =================
樓主認為第二種方式是對上層數據結構的強耦合,我並不這麼認為,我覺得這只是兩種不同的組件設計方式。
// 屬性平鋪
const Person = ({name, age}) =&> &{name} {age}&
// 屬性集合const Person = ({info}) =&> &{info.name} {info.age}&
兩種方式各有利弊,個人更認可平鋪的方式,這樣寫jsx的時候,組件的數據結構描述的更清晰可讀性更強。而屬性集合的方式往往會因為組件編寫者的懈怠而放鬆了 props validation 的精確性(從而使編輯器的提示變弱)。比如集合方式 validation 理應這麼寫:
info: PropTypes.shape({
name: PropTypes.string, age: PropTypes.number})
但估計更多的人就一行
info: PropTypes.object
完事.... 這就非常坑了..
mobx 官方給出的最佳實踐中有一條是,盡量多的用 observer 來裝飾組件,這樣 re render 會更精準,給人一種 mobx-react 比 react-redux 更高效的錯覺。事實上如果你在 react-redux 體系下將所有組件用 connect 裝飾一下同時使用這種「盡量晚的解引用」的組件props設計方式,也能獲得像素級拷貝的性能提升。。但我相信沒人會這麼干。。。redux 官方指南中給出了純展示型組件的設計參考,即presentational component 不應與redux耦合,從而使其具備良好的可移植型。而這裡 Author 組件明顯屬於 presentational component, 使用 observer 裝飾則將其與 mobx 這個狀態管理庫捆綁在一起了,這顯然是不合理的。我認為正確的使用 mobx 的姿勢是 observer 只用來裝飾容器組件,對於純展示型組件,使用最標準的 react 組件方式實現。redux 同理。
最後重複一遍我的觀點,業務系統中,性能跟代碼可讀性,優先代碼可讀性。另外,用mobx時不要所有組件都用observer裝飾,明確區分容器組件跟純展示型組件,別聽官方文檔那一套...
這個問題確實十分神奇,在用vue的時候,我可根本沒有mobx作者考慮的那麼多,這種情況果斷重新render然後vdom就好了,有啥呢?畢竟這個組件並不是最上層的組件,因為它已經開始監聽數據的變化了。那麼,它就算重渲染也沒啥大不了的,畢竟無論是多寫幾步多幾個watcher和observer,還是像作者這樣直接強耦合,都是有開銷的,這個作者這麼討厭vdom的diff的話,為啥還要用react,為了兼容IE8嗎?
不想寫render里解構,那在上層加一個數據組件不就好了inject 4.x以上就可以了啊
父組件轉遞給子組件的僅僅 MobX 中的 store,狀態管理庫本來就允許狀態全局共享。在這種情況下,每個組件僅僅依賴於它的 props,表面上看是對父組件產生了依賴,其實只是借用了父組件來獲取 store。這樣的組件更加易於測試(因為傳遞 props 就可以了)。另外,如果你不喜歡這樣傳遞 props,可以在每個組件的代碼頂部 import 你的 store,這樣整個組件看起來就沒有 store,測試可以使用 webpack 的 inject-loader。至於這種做法和 props 層層傳遞相比哪個性能更好,我倒是沒有測試過。
mobx關注數據狀態和數據的修改,也就是關注的是業務處理,使用hoc組件把store中的數據分解為純展現組件的props
推薦閱讀: