對於常用的框架,如果僅限於會用,我覺得還是遠遠不夠,至少要理解它的思想,這樣才不會掉入各種坑裡面,這篇文章是基於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佇列為空就不會進行更新)。
參考: