深入理解React

尹光耀發表於2018-12-08

對於常用的框架,如果僅限於會用,我覺得還是遠遠不夠,至少要理解它的思想,這樣才不會掉入各種坑裡面,這篇文章是基於react-lite原始碼來寫的。

createElement和component

在react裡面,經過babel的解析後,jsx會變成createElement執行後的結果。

const Test = (props) => <h1>hello, {props.name}</h1>;
<Test name="world" />
複製程式碼

<Test name="world" />經過babel解析後會變為createElement(Test, {name: "world}),這裡的Test就是上面的Test方法,name就是Test方法裡面接受的props中的name。 實際上當我們從開始載入到渲染的時候做了下面幾步:

// 1. babel解析jsx
<Test name="world"> -> createElement(Test, {name: "world"})
// 2. 對函式元件和class元件進行處理
// 如果是類元件,不做處理,如果是函式元件,增加render方法
const props = {name: world};
const newTest = new Component(props);
newTest.render = function() {
    return Test(props);
}
// 3. 執行render方法
newTest.render();
複製程式碼

這樣也很容易理解,const Test = <div>hello, world</div>和const Test = () => <div>hello, world</div>的區別了。

key

react中的diff會根據子元件的key來對比前後兩次virtual dom(即使前後兩次子元件順序打亂),所以這裡的key最好使用不會變化的值,比如id之類的,最好別用index,如果有兩個子元件互換了位置,那麼index改變就會導致diff失效。

短路操作符判斷

為什麼布林型別和null型別的值可以這麼寫,而數字型別卻不行?

    showLoading && <Loading />
複製程式碼

如果showLoading是個數字0,那麼最後渲染出來的居然是個0,但是showLoading是個false或者null,最後就什麼都不渲染,這個是為什麼? 首先上述程式碼會被babel編譯為如下格式:

showLoading && React.createElement(Loading, null)
複製程式碼

而如果showLoading是false或者0的時候,就會短路掉後面的元件,最後渲染出來的應該是個showLoading。 但是react-lite在渲染子元件的時候(遞迴渲染虛擬dom),會判斷當前是否為布林型別和null,如果是布林型別或者null,則會被直接過濾掉。

function collectChild(child, children) {
    if (child != null && typeof child !== 'boolean') {
        if (!child.vtype) {
            // convert immutablejs data
            if (child.toJS) {
                child = child.toJS()
                if (_.isArr(child)) {
                    _.flatEach(child, collectChild, children)
                } else {
                    collectChild(child, children)
                }
                return
            }
            child = '' + child
        }
        children[children.length] = child
    }
}
複製程式碼

cloneElement

原來對cloneElement的理解就是類似cloneElement(App, {})這種寫法,現在看了實現之後才理解。原來第一個引數應該是一個reactElement,而不是一個reactComponent,應該是<App />,而不是App,這個也確實是我沒有好好看文件。

shouldComponentUpdate

當shouldComponentUpdate返回false的時候,元件沒有重新渲染,但是更新後的state和props已經掛載到了元件上面,這個時候如果列印state和props,會發現拿到的已經是更新後的了。

setState

react裡面setState後不會立即更新,但在某些場景下也會立即更新,下面這幾種情況列印的值你都能回答的上來嗎?

class App extends React.Component {
    state = {
        count: 0;
    }
    test() {
        this.setState({
            count: this.state.count + 1
        }); 
        console.log(this.state.count); // 此時為0
        this.setState({
            count: this.state.count + 1
        });
        console.log(this.state.count); // 此時為0
    }
    test2() {
        setTimeout(() => {
            this.setState({
                count: this.state.count + 1
            });
            console.log(this.state.count); // 此時為1
            this.setState({
                count: this.state.count + 1
            });
            console.log(this.state.count); // 此時為2
        })
    }
    test3() {
        Promise.resolve().then(() => {
            this.setState({
                count: this.state.count + 1
            });
            console.log(this.state.count); // 此時為1
            this.setState({
                count: this.state.count + 1
            });
            console.log(this.state.count); // 此時為2
        })
    }
    test4() {
        this.setState(prevState => {
            console.log(prevState.count); // 0
        return {
            count: prevState.count + 1
        };
        });
        this.setState(prevState => {
            console.log(prevState.count); // 1
            return {
                count: prevState.count + 1
            };
        });
    }
    async test4() {
        await 0;
        this.setState({
            count: this.state.count + 1
        });
        console.log(this.state.count); // 此時為1
        this.setState({
            count: this.state.count + 1
        });
        console.log(this.state.count); // 此時為2
    }
}
複製程式碼

在react中為了防止多次setState導致多次渲染帶來不必要的效能開銷,會將待更新的state放到佇列中,等到合適的時機(生命週期鉤子和事件)後進行batchUpdate,所以在setState後無法立即拿到更新後的state。所以很多人說setState是非同步的,setState表現確實是非同步,但是裡面沒有用非同步程式碼實現。而且不是等主執行緒程式碼執行結束後才執行的,而是需要手動觸發。 如果是給setState傳入一個函式,這個函式是執行前一個setState後才被呼叫的,所以函式返回的引數可以拿到更新後的state。 但是如果將setState在非同步方法中(setTimeout、Promise等等)呼叫,由於這些方法是非同步的,會導致生命週期鉤子或者事件方法先執行,執行完這些後會將更新佇列的pending狀態置為false,這個時候在執行setState後會導致元件立即更新。從這裡也能說明setState本質並不是非同步的,只是模擬了非同步的表現。

ref

ref用到原生的標籤上,可以直接在元件內部用this.refs.xxx的方法獲取到真實DOM。 ref用到元件上,需要用ReactDOM.findDOMNode(this.refs.xxx)的方式來獲取到這個元件對應的DOM節點,this.refs.xxx獲取到的是虛擬DOM。

合成事件

react裡面將可以冒泡的事件委託到了document上,通過向上遍歷父節點模擬了冒泡的機制。 比如當觸發onClick事件時,會先執行target元素的onClick事件回撥函式,如果回撥函式裡面阻止了冒泡,就不會繼續向上查詢父元素。否則,就會繼續向上查詢父元素,並執行其onClick的回撥函式。 當跳出迴圈的時候,就會開始進行元件的批量更新(如果沒有收到新的props或者state佇列為空就不會進行更新)。

參考:

  1. react-lite
  2. 從零寫一個react

相關文章