擁抱react新生命週期–getDerivedStateFromProps

LucasTwilight發表於2019-03-04

preface

作為一個前端開發,從來不能夠說自己已經學不動了,新的事物每天都在出現,原本已經熟稔的框架也會時不時出來騷擾你一下。

學不動了

React 16是最近一年多React更新最大的版本。除了讓大家喜聞樂見的向下相容的Fiber,防止了客戶端react在進行渲染的時候阻塞頁面的其他互動行為。Fiber原始碼速覽

不過Fiber的更新是不可見的,我們只需要瞭解他實現Fiber的大概思路,並不會影響到我們日常的業務開發工作。

但是除了Fiber,整個React生命週期的變化對於所有開發者來說才是最可見的。目前的業務環境已經更新到了React 16.4,

react 16.4 change log

從change log中我們可以看到React DOM的更新,getDerivedStateFromProps的修改,無論如何,這個函式都會在re-rendering之進行呼叫。

componentWillReceiveProps這個歷史遺留即將在未來被標記為deprecated,那麼為了新的工程能夠保證在未來不需要進行程式碼的重構,所以現在就需要開始擁抱新的生命週期函式了。

新的生命週期過程

先來看看最新版本react的生命週期圖:

擁抱react新生命週期–getDerivedStateFromProps

對於已經比較瞭解react的童鞋,這個圖應該看起來非常熟悉了(雖然我畫的很醜。。)

變化在於+2 生命週期,-3 UNSAFE。

新增:getDerivedStateFromPropsgetSnapshotBeforeUpdate
UNSAFE:UNSAFE_componentWillMountUNSAFE_componentWillUpdateUNSAFE_componentWillReceiveProps

getDerivedStateFromProps

React生命週期的命名一直都是非常語義化的,這個生命週期的意思就是從props中獲取state,可以說是太簡單易懂了。

可以說,這個生命週期的功能實際上就是將傳入的props對映到state上面。

由於16.4的修改,這個函式會在每次re-rendering之前被呼叫,這意味著什麼呢?

是的,這意味著即使你的props沒有任何變化,而是state發生了變化,導致元件發生了re-render,這個生命週期函式依然會被呼叫。看似一個非常小的修改,卻可能會導致很多隱含的問題。

使用

這個生命週期函式是為了替代componentWillReceiveProps存在的,所以在你需要使用componentWillReceiveProps的時候,就可以考慮使用getDerivedStateFromProps來進行替代了。

兩者的引數是不相同的,而getDerivedStateFromProps是一個靜態函式,也就是這個函式不能通過this訪問到class的屬性,也並不推薦直接訪問屬性。而是應該通過引數提供的nextProps以及prevState來進行判斷,根據新傳入的props來對映到state

需要注意的是,如果props傳入的內容不需要影響到你的state,那麼就需要返回一個null,這個返回值是必須的,所以儘量將其寫到函式的末尾。

static getDerivedStateFromProps(nextProps, prevState) {
    const {type} = nextProps;
    // 當傳入的type發生變化的時候,更新state
    if (type !== prevState.type) {
        return {
            type,
        };
    }
    // 否則,對於state不進行任何操作
    return null;
}
複製程式碼

問題1 — 多來源的不同狀態

假設我們有一個列表,這個列表受到頁面主體,也就是根元件的驅動,也受到其本身資料載入的驅動。

因為這個頁面在開始渲染的時候,所有的資料請求可能是通過batch進行的,所以要在根元件進行統一處理,而其列表的分頁操作,則是由其本身控制。

這會出現什麼問題呢?該列表的狀態受到兩方面的控制,也就是re-render可能由props驅動,也可能由state驅動。這就導致了getDerivedStateFromProps會在兩種驅動狀態下被重新渲染。

當這個函式被多次呼叫的時候,就需要注意到,stateprops的變化將會怎樣影響到你的元件變化。

// 元件接收一個type引數
static propTypes = {
    type: PropTypes.number
}

// 元件還具有自己的狀態來渲染列表
class List extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            list: [],
            type: 0,
        }
    }
}
複製程式碼

如上面程式碼的例子所示,元件既受控,又控制自己。當type發生變化,會觸發一次getDerivedStateFromProps,在這裡更新元件的type狀態,然而,在進行非同步操作之後,元件又會更新list狀態,這時你的getDerivedStateFromProps函式就需要注意,不能夠僅僅判斷type是否變化來更新狀態,因為list的變化也會更新到元件的狀態。這時就必須返回一個null,否則會導致元件無法更新並且報錯。

問題2 — 組織好你的元件

考慮一下,如果你的元件內部既需要修改自己的type,有需要接收從外部修改的type

是不是非常混亂?getDerivedStateFromProps中你根本不知道該做什麼。

static getDerivedStateFromProps(nextProps, prevState) {
    const {type} = nextProps;
    // type可能由props驅動,也可能由state驅動,這樣判斷會導致state驅動的type被回滾
    if (type !== prevState.type) {
        return {
            type,
        };
    }
    // 否則,對於state不進行任何操作
    return null;
}
複製程式碼

如何解決這個棘手的問題呢?

  1. 好好組織你的元件,在非必須的時候,摒棄這種寫法。type要麼由props驅動,要麼完全由state驅動。
  2. 如果實在沒有辦法解耦,那麼就需要一個hack來輔助:繫結propsstate上。
constructor(props) {
    super(props);
    this.state = {
        type: 0,
        props,
    }
}
static getDerivedStateFromProps(nextProps, prevState) {
    const {type} = nextProps;
    const {props} = prevState;
    // 這段程式碼可能看起來非常混亂,這個props可以被當做快取,僅用作判斷
    if (type !== props.type) {
        return {
            type,
            props: {
                type,
            },
        };
    }
    // 否則,對於state不進行任何操作
    return null;
}
複製程式碼

上面的程式碼可以保證在進行多資料來源驅動的時候,狀態能夠正確改變。當然,這樣的程式碼很多情況下是會影響到別人閱讀你的程式碼的,對於維護造成了非常大的困難。

從這個生命週期的更新來看,react更希望將受控的propsstate進行分離,就如同Redux作者Dan Abramov在redux文件當中寫的一樣Presentational and Container Components,將所有的元件分離稱為展示型元件和容器型元件,一個只負責接收props來改變自己的樣式,一個負責保持其整個模組的state。這樣可以讓程式碼更加清晰。但是在實際的業務邏輯中,我們有時很難做到這一點,而且這樣可能會導致容器型元件變得非常龐大以致難以管理,如何進行取捨還是需要根據實際場景決定的。

問題3 — 非同步

以前,我們可以在props發生改變的時候,在componentWillReceiveProps中進行非同步操作,將props的改變驅動到state的改變。

componentWillReceiveProps(nextProps) {
    if (props.type !== nextProps.type) {
        // 在這裡進行非同步操作或者更新狀態
        this.setState({
            type: props.type,
        });
        this._doAsyncOperation();
    }
}
複製程式碼

這樣的寫法已經使用了很久,並且並不會存在什麼功能上的問題,但是將componentWillReceiveProps標記為deprecated的原因也並不是因為功能問題,而是效能問題。

當外部多個屬性在很短的時間間隔之內多次變化,就會導致componentWillReceiveProps被多次呼叫。這個呼叫並不會被合併,如果這次內容都會觸發非同步請求,那麼可能會導致多個非同步請求阻塞。

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

這個生命週期函式會在每次呼叫render之前被觸發,而讀過一點react原始碼的童鞋都會了解,reactsetState操作是會通過transaction進行合併的,由此導致的更新過程是batch的,而react中大部分的更新過程的觸發源都是setState,所以render觸發的頻率並不會非常頻繁(感謝 @leeenx20 的提醒,這裡描述進行了修改)。

在使用getDerivedStateFromProps的時候,遇到了上面說的props在很短的時間內多次變化,也只會觸發一次render,也就是隻觸發一次getDerivedStateFromProps。這樣的優點不言而喻。

那麼如何使用getDerivedStateFromProps進行非同步的處理呢?

If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.

官方教你怎麼寫程式碼系列,但是其實也沒有其他可以進行非同步操作的地方了。為了響應props的變化,就需要在componentDidUpdate中根據新的propsstate來進行非同步操作,比如從服務端拉取資料。

// 在getDerivedStateFromProps中進行state的改變
static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.type !== prevState.type) {
        return {
            type: nextProps.type,
        };
    }
    return null;
}
// 在componentDidUpdate中進行非同步操作,驅動資料的變化
componentDidUpdate() {
    this._loadAsyncData({...this.state});
}
複製程式碼

以上

以上是本期開發過程中使用新的生命週期函式的時候遇到的一點小問題和一些相關思考。react為了防止部分開發者濫用生命週期,可謂非常盡心盡力了。既然你用不好,我就乾脆不讓你用。一個靜態的生命週期函式可以讓狀態的修改更加規範和合理。

至於為什麼全文沒有提到getSnapshotBeforeUpdate,因為自己並沒有用到#誠實臉。簡單看了一下,這個函式返回一個update之前的快照,並且傳入到componentDidUpdate中,元件更新前後的狀態都可以在componentDidUpdate中獲取了。一些需要在元件更新完成之後進行的操作所需要的資料,就可以不需要掛載到state或者是cache下來了。比如官方例子中所舉例的保留更新之前的頁面滾動距離,以便在元件update完成之後恢復其滾動位置。也是一個非常方便的周期函式。

最後

新週期新氣象~,react不斷更新並且往好的方向發展才是對前端社群最有利的事情,我們這些開發者們能夠穩定的使用一個前端框架在幾年前都是奢望,從backbonejQuery再到Angular,鬼知道我們這幾年都經歷了什麼,Angular 2的更新讓我放棄了這個使用了很久的框架。希望react和vue能夠持續更新下去吧~

擁抱react新生命週期–getDerivedStateFromProps

相關文章