1. 程式人生 > >深入Preact原始碼分析(四)setState發生了什麼

深入Preact原始碼分析(四)setState發生了什麼

setState發生了什麼

setState(state, callback) {
    let s = this.state;
    if (!this.prevState) this.prevState = extend({}, s);
    extend(s, typeof state==='function' ? state(s, this.props) : state);// 語句3
    if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
    enqueueRender(this
); },

setState的定義如上,程式碼邏輯很容易看出

1、prevState若不存在,將要更新的state合併到prevState上

2、可以看出Preact中setState引數也是可以接收函式作為引數的。將要更新的state合併到當前的state

3、如果提供了回撥函式,則將回調函式放進_renderCallbacks佇列

4、呼叫enqueueRender進行元件更新

why?我剛看到setState的第2、3行程式碼的時候也是一臉矇蔽。為什麼它要這樣又搞一個this.prevState又搞一個this.state,又有個state呢?WTF。
通過理清Preact的setState的執行原理。

應該是用於處理一個元件在一次流程中呼叫了兩次setState的情況。

// 例如這裡的handleClick是繫結click事件

handleClick = () =>{
    // 注意,preact中setState後state的值是會馬上更新的
    this.setState({a:this.state.a+1});
    console.log(this.state.a);
    this.setState({a:this.state.a+1});
    console.log(this.state.a);
} 

基本上每一個學react的人,都知道上述程式碼函式在react中執行之後a的值只會加一,but!!!!在Preact中是加2的!!!!通過分析Preact的setState可以解釋這個原因。
在上面的語句3,extend函式呼叫後,當前的state值已經改變了。但是即使state的值改變了,但是多次setState仍然是會只進行一次元件的更新(通過setTimeout把更新操作放在當前事件迴圈的最後),以最新的state為準。所以,這裡的prevState應該是用於記錄當前setState之前的上一次state的值,用於後面的diff計算。在enqueueRender執行diff時比較prevState和當前state的值

關於enqueueRender的相關定義

let items = [];

export function enqueueRender(component) {
    // dirty 為true表明這個元件重新渲染
    if (!component._dirty && (component._dirty = true) && items.push(component) == 1) {//語句1
        // 只會執行一遍
        (options.debounceRendering || defer)(rerender); // 相當於setTimeout render 語句2
    }
}

export function rerender() {
    let p, list = items;
    items = [];
    while ((p = list.pop())) {
        if (p._dirty) renderComponent(p);
    }
}

enqueueRender的邏輯主要是

1、語句1: 將呼叫了setState的元件的_dirty屬性設定為false。通過這段程式碼我們還可以發現,
如果在一次流程中,呼叫了多次setState,rerender函式實際上還是隻執行了一遍(通過判斷component._dirty的值來保證一個元件內的多次setState只執行一遍rerender和判斷items.push(component) == 1確保如果存在父元件呼叫setState,然後它的子元件也呼叫了setState,還是隻會執行一次rerender)。items佇列是用來存放當前所有dirty元件。

2、語句2。可以看作是setTimeout,將rerender函式放在本次事件迴圈結束後執行。rerender函式對所有的dirty元件執
renderComponent進行元件更新。

在renderComponent中將會執行的程式碼。只列出和初次渲染時有區別的主要部分

export function renderComponent(component, opts=undefined, mountAll=undefined, isChild=undefined) {
    ....
    if (isUpdate) {
        component.props = previousProps;
        component.state = previousState;
        component.context = previousContext;
        if (opts !== FORCE_RENDER && // FORCE_RENDER是在呼叫元件的forceUpdate時設定的狀態位
            component.shouldComponentUpdate &&
            component.shouldComponentUpdate(props, state, context) === false) {
            skip = true;// 如果shouldComponentUpdate返回了false,設定skip標誌為為true,後面的渲染部分將會被跳過
        } else if (component.componentWillUpdate) {
            component.componentWillUpdate(props, state, context);//執行componentWillUpdate生命週期函式
        }

        // 更新元件的props state context。因為componentWillUpdate裡面有可能再次去修改它們的值
        component.props = props;
        component.state = state;
        component.context = context;
    }
    ....
    component._dirty = false;
    ....
    // 省略了diff渲染和dom更新部分程式碼
    ...
    if (!skip) {
        if (component.componentDidUpdate) {
            //componentDidUpdate生命週期函式
            component.componentDidUpdate(previousProps, previousState, previousContext);
        }
    }

    if (component._renderCallbacks != null) {
        // 執行setState的回撥
        while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
    }
}

邏輯看程式碼註釋就很清晰了。先shouldComponentUpdate生命週期,根據返回值決定是都否更新(通過skip標誌位)。然後將元件的_dirty設定為true表明已經更新了該元件。然後diff元件更新,執行componentDidUpdate生命週期,最後執行setState傳進的callback。

流程圖如下:

這裡寫圖片描述

下一步,就是研究setState元件進行更新時的diff演算法幹了啥