React專題:不可變屬性

馬蹄疾發表於2019-03-03

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出

來我的 GitHub repo 閱讀完整的專題文章

來我的 個人部落格 獲得無與倫比的閱讀體驗

React是用來解決狀態同步的,但它卻有一個與this.state並駕齊驅的概念。

這就是this.props

this.props是元件之間溝通的一個介面。

原則上來講,它只能從父元件流向子元件,但是開發者有各種hack技巧,基本上近親之間溝通是不成問題的。

this.props

this.props是一個極其簡單的介面。世界需要更多這樣的傻瓜介面

你只需要像寫HTML標籤的屬性一樣,把它寫上去,它就傳到了子元件的this.props裡面。

不過有幾個地方需要注意:

  • 有兩個特殊的屬性refkey,它們各有用途,並不會傳給子元件的this.props
  • 如果只給屬性不給值,React會預設解析成布林值true
  • 除了字串,其他值都要用花括號包裹。
  • 如果你把屬性給了標籤而不是子元件,React並不會解析。
import React, { Component, createRef } from `react`;
import Child from `./Child`;

class App extends Component {
    isPopular = false;
    refNode = createRef();
    
    render() {
        return [
            <Child key="react" ref={this.refNode} isPopular />,
            <Child key="vue" url="https://github.com/vuejs/vue" star={96500} />,
            <Child key="angular" owner="google" isPopular={this.isPopular} />,
        ];
    }
}

export default App;
複製程式碼

this.props是一個不可變物件

React具有濃重的函數語言程式設計的思想。

提到函數語言程式設計就要提一個概念:純函式。

純函式有幾個特點:

  • 給定相同的輸入,總是返回相同的輸出。
  • 過程沒有副作用。
  • 不依賴外部狀態。
function doSomething(a, b) {
    return a + b;
}
複製程式碼

這是一種程式設計思想。如果你對這個概念有點模糊,我可以舉個例子:

你的殺父仇人十年後突然現身,於是你決定僱傭一個冷麵殺手去解決他。

你會找一個什麼樣的殺手呢?

  • 給多少錢辦多少事,效果可預期,從不失手。
  • 不誤傷百姓,不引起動靜。
  • 沒有團伙,單獨作案,乾淨利落,便於封口。

如果你面對殺父仇人有這樣的覺悟,那麼純函式便是你的囊中之物了。

為什麼要提純函式?因為this.props就是汲取了純函式的思想。

它最大的特點就是不可變。

this.state不一樣的是,this.props來真的。雖然this.state也反對開發者直接改變它的屬性,但畢竟只是嘴上說說,還是要靠開發者自己的約束。然而this.props會直接讓你的程式崩潰。

加上React也沒有this.setProps方法,所以不需要開發者自我約束,this.props就是不可變的。

溝通基本靠吼

父元件給子元件傳值

這個無需贅言,最直觀的傳值方式。

import React from `react`;
import Child from `./Child`;

const App = () => {
    return (
        <Child star={1000} />
    );
}

export default App;
複製程式碼

子元件給父元件傳值

其實就是利用回撥函式的引數傳遞值。

父元件定義一個方法,將該方法通過props傳給子元件,子元件需要給父元件傳值時,便傳參執行該方法。由於方法定義在父元件裡,父元件可以接收到該值。

import React, { Component } from `react`;
import Child from `./Child`;

class App extends Component {
    state = { value: `` };
    
    render() {
        return (
            <Child handleSomething={this.handleSomething} />
        );
    }
    
    handleSomething = (e) => {
        this.setState({ value: e.target.value });
    }
}

export default App;
複製程式碼
import React from `react`;

const Child = (props) => {
    return (
        <input type="text" onChange={props.handleSomething} />
    );
}

export default Child;
複製程式碼

兄弟元件之間傳值

原理和回撥函式一樣,只不過這裡父元件只是一個橋樑。

父元件接收到回撥函式的值以後,通過this.setState儲存該值,並觸發另一個子元件重新渲染,重新渲染後另一個子元件便可以獲得該值。

import React, { Component, Fragment } from `react`;
import ChildA from `./ChildA`;
import ChildB from `./ChildB`;

class App extends Component {
    state = { value: `` };
    
    render() {
        return (
            <Fragment>
                <ChildA handleSomething={this.handleSomething} />
                <ChildA value={this.state.value} />
            </Fragment>
        );
    }
    
    handleSomething = (e) => {
        this.setState({ value: e.target.value });
    }
}

export default App;
複製程式碼
import React from `react`;

const ChildA = (props) => {
    return (
        <input type="text" onChange={props.handleSomething} />
    );
}

export default ChildA;
複製程式碼
import React from `react`;

const ChildB = (props) => {
    return (
        <div>{props.value}</div>
    );
}

export default ChildB;
複製程式碼

createContext

?這是React v16.3.0釋出的API。

React為開發者提供了一扇傳送門,它就是Context物件。

嚴格來說,Context早就存在於React中了,不過一直以來都不是正式的API。

終於在v16.3.0轉正了。

為什麼說Context是一扇傳送門?因為它可以跨元件傳遞資料。不是父子之間的小打小鬧哦,而是可以跨任意層級。但是有一個限制,資料只能向下傳遞,原因就是後面要講到的單向資料流。

開發者通過createContext建立一個上下文物件(React特別喜歡create),然後找一個頂級元件作為Provider。接下來就可以在任意下級元件消費它提供的資料了。

  • 只要Provider的資料改變,就會觸發Consumer的更新。
  • 建立時可以提供一個預設值,另外掛載時可以通過value屬性傳遞資料。但是預設值只有在不提供Provider的情況下才起作用。
  • 開發者可以建立多個Context。
  • Consumer的children必須是一個函式。

舊的Context存在一個問題,如果接收元件的shouldComponentUpdate生命週期鉤子返回false,則它不會接收到Context中的資料,因為它是通過this.props一級一級往下傳的。

而新的Context採取的是訂閱釋出模式,所以不存在這個問題。

實際上react-redux庫的Provider元件內部就是使用了舊的Context API,不過redux做了一些優化。

import { createContext } from `react`;

const { Provider, Consumer } = createContext({ lang: `en` });

export { Provider, Consumer };
複製程式碼
import React, { Component } from `react`;
import { Provider } from `./context`;
import Todos from `./Todos`;

const App = () => {
    return (
        <Provider value={{ lang: `zh` }}>
            <Todos />
        </Provider>
    );
}

export default App;
複製程式碼
import React, { Fragment } from `react`;
import TodoItem from `./TodoItem`;

const Todos = () => {
    return (
        <Fragment>
            <TodoItem />
            <TodoItem />
            <TodoItem />
        </Fragment>
    );
}

export default Todos;
複製程式碼
import React from `react`;
import { Consumer } from `./context`;

const TodoItem = () => {
    return (
        <Consumer>
            {({ lang }) => <div>{lang === `en` ? `todo` : `要做`}</div>}
        </Consumer>
    );
}

export default TodoItem;
複製程式碼

單向資料流

水往低處流,這是自然規律。

React通過描述狀態來控制UI的表達,這就涉及到UI的更新機制。

狀態除了內部狀態之外,肯定有一些狀態是要元件之間共享的,所以,一旦一個元件的狀態更新了,可能會牽扯到很多元件的更新,框架的更新機制必將變的異常複雜。

但是迴歸到水的意象,如果狀態的流向是單向的,而且是自上往下流動,這就變的非常符合直覺,而且更新機制可以做到極簡:我更新,則我的所有下級也更新。

這就是this.props的思想源頭。

它雖然叫props,但它也是狀態,只不過是共享的狀態。

它只能自頂向下流動。

內部不能改變this.props

某個props的源頭更新了,則流經的所有元件都要更新,除非開發者手動禁止。

脈絡清晰,this.props才是賦予了React血液的東西。

關於React摒棄了表單雙向資料繫結的問題,它只是想把單向資料流做的更徹底一點。其實表單的狀態,歸根結底是元件內部的狀態,跟單向資料流無關。

什麼是雙向資料繫結?就是表單輸入,與之繫結的變數自動獲取到輸入的值,變數的值改變,與之繫結的表單的值隨即改變,兩種流向都自動繫結了。

但其實雙向資料繫結不就是value的單向繫結加onChange事件監聽麼!React也可以通過兩步做到。

總結:雙向資料繫結不影響單向資料流,React也可以實現雙向的同步。

React專題一覽

什麼是UI

JSX

可變狀態

不可變屬性

生命週期

元件

事件

操作DOM

抽象UI

相關文章