Mixin 已死,Composition 萬歲

jopen 10年前發布 | 14K 次閱讀 JavaScript

原文: Mixins Are Dead. Long Live Composition

當 React 0.13 推出的時候,大家都震驚了。

它的開篇表達得很明確,mixin 正在逐步退出歷史舞臺:

不好意思,React ES6 將不再支持mixin,否則有悖 JavaScript 語義化的初衷。

在 JavaScript 中我們找不到通用的標準來定義 mixin,事實上,ES6 也摒棄了不少支持 mixin 的特性。語義混亂的類庫已經很多了。盡管我們認為應該有一個統一的方法來定義 mixin,便于對 JavaScript 各種“類”的操作,但 React 并不打算這么做。

mixin 始終會來的,某些人可能會有這樣的理解。但實際上, Sebastian Markb?ge偉大的API終結者 ,也不太看好 mixin:

為什么要用 mixin?mixin 解決了什么問題?我們是否可以換一種無繼(Tong)承(ku)的方式去解決這些問題?

通用函數

這個例子舉得稍微有點腦殘。與其用 mixin 的方式去共享通用功能,直接將其提取出來并模塊化,需要的時候直接引用不是更好么?

生命周期和狀態選擇

這是 mixin 的主要用例。如果你對 React 的 mixin 系統還不是特別熟悉,可以這么理解,它“合并”了生命周期鉤子并且更加智能。假如同時使用了組合以及一些 mixin 去定義componentDidMountlifecycle鉤子,React 會自動合并它們以保證每個方法都能被調用。類似地,使用一些mixin也能作用于getInitialState方法。

在實踐中,這是唯一體現 mixin 用處的地方。mixin 可以向 Flux Store 訂閱組件的狀態或者作用于更新后的組件 DOM 節點。任何一個組件擴展機制均能獲得組件的生命周期,這一點是絕對有必要的。

然而mixin還是有不少弱點:

一個組件和它的 mixin 之間的關聯是隱式的。mixin 通常依賴于定義在組件中的特定方法,但是又沒有辦法可以從組件的定義中查看到。

當在單一組件中使用多個 mixin 時會產生沖突。例如當使用了一個 StoreMixin,然后又添加了另一個的 StoreMixin,React 會拋出異常,因為你的組件此時擁有兩版相同命名的的方法。不同的 mixin 定義了相同的狀態字段時也一樣會產生沖突。

mixin傾向于添加更多的狀態到你的組件中,但其實我們希望能努力讓狀態精簡一點。關于這一點,推薦大家讀一讀 Andrew Clark 寫的 Why Flux Component is better than Flux Mixin .

mixin讓性能優化復雜化了。如果你在組件中(手動地或者通過viaPureRenderMixin方式)定義shouldComponentUpdate方法,很可能會產生這樣的問題:某些 mixin 是否需要在自己的shouldComponentUpdate執行中被考慮到?雖然這可以通過 使用更多的合并魔法 來解決,但這真的是正確的發展方向咩?

加入高階組件

我第一次知道這個方法是源自 Sebastian Markb?ge 的談話要點 。這個要點略微有些難以理解,尤其是在還沒完全適應 ES6 語法的情況下,所以我打算用 Flux Store mixin 來解釋。

注意這只是用組合替代 mixin 的 方法之一 。要了解更多方法,可以關注文章結尾。

假設有一個 mixin,訂閱了特定的 Flux Stores,并且可觸發組件狀態改變。它可能長這樣:

function StoreMixin(...stores) {
    var Mixin = {
        getInitialState() {
            return this.getStateFromStores(this.props);
        },
        componentDidMount() {
            stores.forEach(store =>
                store.addChangeListener(this.handleStoresChanged)
            );
            this.setState(this.getStateFromStores(this.props));
        },
        componentWillUnmount() {
            stores.forEach(store =>
                store.removeChangeListener(this.handleStoresChanged)
            );
        },
        handleStoresChanged() {
            if (this.isMounted()) {
                this.setState(this.getStateFromStores(this.props));
            }
        }
    };
    return Mixin;
}

為了使用它,組件將StoreMixin添加到 mixin 列表并且定義了getStateFromStores(props)方法:

var UserProfilePage = React.createClass({
    mixins: [StoreMixin(UserStore)],
    propTypes: {
        userId: PropTypes.number.isRequired
    },
    getStateFromStores(props) {
        return {
            user: UserStore.get(props.userId);
        }
    }
    render() {
        var { user } = this.state;
        return <div>{user ? user.name : 'Loading'}</div>;
    }

那么在不使用任何 mixin 的前提下如何解決這個問題呢?

高階組件實際上只是一個方法,這個方法利用一個現有組件去返回另一個包裝它的組件。看一下這個connectToStores的執行:

function connectToStores(Component, stores, getStateFromStores) {
    const StoreConnection = React.createClass({
        getInitialState() {
            return getStateFromStores(this.props);
        },
        componentDidMount() {
            stores.forEach(store =>
                store.addChangeListener(this.handleStoresChanged)
            );
        },
        componentWillUnmount() {
            stores.forEach(store =>
                store.removeChangeListener(this.handleStoresChanged)
            );
        },
        handleStoresChanged() {
            if (this.isMounted()) {
                this.setState(getStateFromStores(this.props));
            }
        },
        render() {
            return <Component {...this.props} {...this.state} />;
        }
    });
    return StoreConnection;
};

這看起來和 mixin 非常類似,但是它包裝了組件并且傳遞狀態給這個被包裝的組件,用這種辦法替代管理組件的內在狀態。 通過簡單的組件嵌套,包裝組件的生命周期鉤子無需任何特殊的合并行為就可以發揮作用 。

接下來是這樣用的:

var ProfilePage = React.createClass({
    propTypes: {
        userId: PropTypes.number.isRequired,
        user: PropTypes.object // note that user is now a prop
    },
    render() {
        var { user } = this.props; // get user from props
        return <div>{user ? user.name : 'Loading'}</div>;
    }
});
// Now wrap ProfilePage using a higher-order component:
ProfilePage = connectToStores(ProfilePage, [UserStore], props => ({
    user: UserStore.get(props.userId)
});

這樣就 OK 啦!

最后被遺漏的部分是關于componentWillReceiveProps的處理,你可以在已更新的 Flux React Router ExampleconnectToStores 源碼中找到。

下一步

我打算在下一個版本的 React DnD中使用高階組件。

這暫時不能解決全部關于 mixin 的使用場景,不過快了。別忘了包裝組件可以傳遞任意屬性給被包裝的組件,包括回調函數在內。高階組件也可能存在被濫用的情況,但是不同于 mixin 的是,它們只依賴于簡單的組件組合而不是一大堆奇技淫巧和特殊的方式。

也有一些情況是不適合使用高階組件的。比如,在高階組件中PureRenderMixin無法執行,因為外層組件無法查詢自己的狀態以及定義自身的shouldComponentUpdate. 不過恰巧有這樣一個案例,在 React 0.13 里,你可能會想到用一個不同的基礎類,比如從Component繼承PureComponent然后實現shouldComponentUpdate。這是繼承的正確使用場景。

此外,在 DOM 節點上操作也會有些詭異,因為組件容器沒有辦法知道被包含組件什么時候更新狀態。不過我們可以通過將被組合的組件的一個屬性設計為回調函數來解決這一問題。然后再用ref = {this.props.someRef}來通知高階組件是否附上或者分離某個特定的DOM節點,高階組件接下來就能通過使用React.findDOMNode來找到這個節點。

Mixin 已死,Composition 萬歲

其他方法

除了上述方法,我們還有其他非常有效的途徑來組合,例如 Flummox 使用的在render()當中進行組合的方式 。這種方式同樣是基于嵌套,但是沒有高階組件那么繁冗。在 React0.14 轉變為基于上一級的環境 之后使用這種方式將會更加簡單。

愿意的話,你可以選擇編寫屬于你自己的 mixin 系統,而非受限于更高層級的組合。寫這篇文章是為了提供一個可參考的方法。接下來幾個月我們可以驗證一下哪種方式是最好的。我堅信,最后勝出的方法一定是通過組合的方式,而不是多重的繼承(承認吧,mixin 就是多重繼承)。

另外,React 通過一個新的基于屬性監控的 API 可以阻止sideways data loading 。[譯者注:關于sideways data loading,是指將數據直接推送給某些具體的組件,而非從父級層層傳遞,數據加載后基本上無需從底層刷新app,而是刷新若干組件中某個具體的部分。]

鑒于原文作者的高顏值,附贈頭像一張,你們自己決定要不要去推ter關注他。

Dan Abramov

Mixin 已死,Composition 萬歲

來自: http://efe.baidu.com/blog/mixins-are-dead-long-live-the-composition/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!