㈠ 詳解React中傳入組件的props改變時更新組件的幾種實現方法
我們使用react的時候常常需要在一個組件傳入的props更新時重新渲染該組件,常用的方法是在componentWillReceiveProps中將新的props更新到組件的state中(這種state被成為派生狀態(Derived State)),從而實現重新渲染。React 16.3中還引入了一個新的鉤子函數getDerivedStateFromProps來專門實現這一需求。但無論是用componentWillReceiveProps還是getDerivedStateFromProps都不是那麼優雅,而且容易出錯。所以今天來探討一下這類實現會產生的問題和更好的實現方案。
何時使用派生狀態
咱們先來看一個比較常見的需求,一個用戶列表,可以新增和編輯用戶,當用戶點擊『新建'
按鈕用戶可以在輸入框中輸入新的用戶名;當點擊『編輯'按鈕的時候,輸入框中顯示被編輯的用戶名,用戶可以修改;當用戶點擊『確定'按鈕的時候用戶列表更新。
class UserInput extends React.Component {
state = {
user: this.props.user
}
handleChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { onConfirm } = this.props;
const { user } = this.state;
return (
<div>
<input value={user.name || ''} onChange={this.handleChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
);
}
}
class App extends React.Component {
state = {
users: [
{ id: 0, name: 'bruce' },
{ id: 1, name: 'frank' },
{ id: 2, name: 'tony' }
],
targetUser: {}
}
onConfirm = (user) => {
const { users } = this.state;
const target = users.find(u => u.id === user.id);
if (target) {
this.setState({
users: [
...users.slice(0, users.indexOf(target)),
user,
...users.slice(users.indexOf(target) + 1)
]
});
} else {
const id = Math.max(...(users.map(u => u.id))) + 1;
this.setState({
users: [
...users,
{
...user,
id
}
]
});
}
}
render() {
const { users, targetUser } = this.state;
return (
<div>
<UserInput user={targetUser} onConfirm={this.onConfirm} />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => { this.setState({ targetUser: u }) }}>編輯</button>
</li>
))
}
</ul>
<button onClick={() => { this.setState({ targetUser: {} }) }}>新建</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
運行後,效果如圖:
現在點擊『編輯'和『新建'按鈕,輸入框中的文字並不會切換,因為點擊『編輯'和『更新'時,雖然UserInput的props改變了但是並沒有觸發state的更新。所以需要實現props改變引發state更新,在UserInput中增加代碼:
componentWillReceiveProps(nextProps) {
this.setState({
user: nextProps.user
});
}
或者
static getDerivedStateFromProps(props, state) {
return {
user: props.user
};
}
這樣就實現了UserInput每次接收新的props的時候自動更新state。但是這種實現方式是有問題的。
派生狀態導致的問題
首先來明確組件的兩個概念:受控數據(controlled data lives)和不受控數據(uncontrollered data lives)。受控數據指的是組件中通過props傳入的數據,受到父組件的影響;不受控數據指的是完全由組件自己管理的狀態,即內部狀態(internal state)。而派生狀態揉合了兩種數據源,當兩種數據源產生沖突時,問題隨之產生。
問題一
當在修改一個用戶的時候,點擊『確定'按鈕,輸入框里的文字又變成了修改之前的文字。比如我將『bruce'修改為『bruce lee',確定後,輸入框中又變成了『bruce',這是我們不願意看到的。
出現這個問題的原因是,點擊確定,App會re-render,App又將之前的user作為props傳遞給了UserInput。我們當然可以在每次點擊確定之後將targetUser重置為一個空對象,但是一旦狀態多了之後,這樣管理起來非常吃力。
問題二
假設頁面載入完成後,會非同步請求一些數據然後更新頁面,如果用戶在請求完成頁面刷新之前已經在輸入框中輸入了一些文字,隨著頁面的刷新輸入框中的文字會被清除。
我們可以在App中加入如下代碼模擬一個非同步請求:
componentDidMount() {
setTimeout(() => {
this.setState({
text: 'fake request'
})
}, 5000);
}
導致這個問題的原因在於,當非同步請求完成,setState後App會re-render,而組件的componentWillReceiveProps會在父組件每次render的時候執行,而此時傳入的user是一個空對象,所以UserInput的內容被清空了。而getDerivedStateFromProps調用的更頻繁,會在組件每次render的時候調用,所以也會產生該問題。
為了解決這個問題我們可以在componentWillReceiveProps中判斷新傳入的user和當前的user是否一樣,如果不一樣才設置state:
componentWillReceiveProps(nextProps) {
if (nextProps.user.id !== this.props.user.id) {
this.setState({
user: nextProps.user
});
}
}
更好的解決方案
派生狀態的數據源的不確定性會導致各種問題,那如果每份數據有且只被一個component管理應該就能避免這些問題了。這種思路有兩種實現,一種是數據完全由父組件管理,一種是數據完全由組件自己管理。下面分別討論:
完全受控組件(fully controlled component)
組件的數據完全來自於父組件,組件自己將不需要管理state。我們新建一個完全受控版的UserInput:
class FullyControlledUserInput extends React.Component {
render() {
const { user, onConfirm, onChange } = this.props;
return (
<div>
<input value={user.name || ''} onChange={onChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
)
}
}
App中調用FullyControlledUserInput的方法如下:
...
<FullyControlledUserInput
user={targetUser}
onChange={(e) => {
this.setState({
targetUser: {
id: targetUser.id,
name: e.target.value
}
});
}}
onConfirm={this.onConfirm}
/>
...
現在FullyControlledUserInput中的所有的數據都來源於父組件,由此解決數據沖突和被篡改的問題。
完全不受控組件(fully uncontrolled component)
組件的數據完全由自己管理,因此componentWillReceiveProps中的代碼都可以移除,但保留傳入props來設置state初始值:
class FullyUncontrolledUserInput extends React.Component {
state = {
user: this.props.user
}
onChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { user } = this.state;
const { onConfirm } = this.props;
return (
<div>
<input value={user.name || ''} onChange={this.onChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
)
}
}
當傳入的props發生改變時,我們可以通過傳入一個不一樣的key來重新創建一個component的實例來實現頁面的更新。App中調用FullyUncontrolledUserInput的方法如下::
<FullyUncontrolledUserInput
user={targetUser}
onConfirm={this.onConfirm}
key={targetUser.id}
/>
大部分情況下,這是更好的解決方案。或許有人會覺得這樣性能會受影響,其實性能並不會變慢多少,而且如果組件的更新邏輯過於復雜的話,還不如重新創建一個新的組件來的快。
在父組件中調用子組件的方法設置state
如果某些情況下沒有合適的屬性作為key,那麼可以傳入一個隨機數或者自增的數字作為key,或者我們可以在組件中定義一個設置state的方法並通過ref暴露給父組件使用,比如我們可以在UserInput中添加:
setNewUserState = (newUser) => {
this.setState({
user: newUser
});
}
在App中通過ref調用這個方法:
...
<UserInput user={targetUser} onConfirm={this.onConfirm} ref='userInput' />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => {
this.setState({ targetUser: u });
this.refs.userInput.setNewUserState(u);
}}>
編輯
</button>
</li>
))
}
</ul>
<button onClick={() => {
this.setState({ targetUser: {} });
this.refs.userInput.setNewUserState({});
}}>
新建
</button>
...
這個方法不推薦使用,除非實在沒法了。
本文源碼請參考:ways-to-update-component-on-props-change
㈡ 深入理解React16之:(一).Fiber架構
React16雖然出了一陣子了。剛出來的時候,粗略看了一遍更新文檔。以為沒什麼大的改動,也聽說項目從react15-16的升級過度可以很平滑,再加上項目改版上線一直比較頻繁,所以一直還用的15.6的版本。
偶然在知乎看到@程墨Morgan大神的live,便抱著好奇心和學習的心態報名了,受益良多。
我理解的Fiber架構:
在我之前的一篇文章有簡單介紹, 閱讀react源碼--記錄:1.1 問題記錄
下面從一個具體實例理解一下,再加上我畫了圖,應該很好理解啦~(圖畫的有點渣)
假如有A,B,C,D組件,層級結構為:
我們知道組件的生命周期為:
掛載階段:
那麼在掛載階段,A,B,C,D的生命周期渲染順序是如何的呢?
以render()函數為分界線。從頂層組件開始,一直往下,直至最底層子組件。然後再往上。
組件update階段同理。
————————
前面是react16以前的組建渲染方式。這就存在一個問題,
好似一個潛水員,當它一頭扎進水裡,就要往最底層一直游,直到找到最底層的組件,然後他再上岸。在這期間, 岸上發生的任何事,都不能對他進行干擾,如果有更重要的事情需要他去做(如用戶操作),也必須得等他上岸
看一下fiber架構 組建的渲染順序
潛水員會每隔一段時間就上岸,看是否有更重要的事情要做。
加入fiber的react將組件更新分為兩個時期
這兩個時期以render為分界,
phase1的生命周期是可以被打斷的,每隔一段時間它會跳出當前渲染進程,去確定是否有其他更重要的任務。此過程,React 在 workingProgressTree (並不是真實的virtualDomTree)上復用 current 上的 Fiber 數據結構來一步地(通過requestIdleCallback)來構建新的 tree,標記處需要更新的節點,放入隊列中。
phase2的生命周期是不可被打斷的,React 將其所有的變更一次性更新到DOM上。
這里最重要的是phase1這是時期所做的事。因此我們需要具體了解phase1的機制。
這樣的話,就和react16版本之前有很大區別了,因為可能會被執行多次,那麼我們最好就得保證phase1的生命周期 每一次執行的結果都是一樣的 ,否則就會有問題,因此, 最好都是純函數。
對了,程墨大神還提到一個問題,飢餓問題,即如果高優先順序的任務一直存在,那麼低優先順序的任務則永遠無法進行,組件永遠無法繼續渲染。這個問題facebook目前好像還沒解決,但以後會解決~
所以,facebook在react16增加fiber結構,其實並不是為了減少組件的渲染時間,事實上也並不會減少,最重要的是現在可以使得一些更高優先順序的任務,如用戶的操作能夠優先執行,提高用戶的體驗,至少用戶不會感覺到卡頓~
㈢ 如何用reactjs構建一個完整的前端頁面
用reactjs構建一個完整的前端頁面的步驟:
准備:React 的安裝包,建議去官網下載安裝
1、使用 React 的網頁源碼,結構大致如下:
<!DOCTYPE html><html><head> <script src="../build/react.js"></script> <script src="../build/react-dom.js"></script> <script src="../build/browser.min.js"></script></head><body> <div id="example"></div> <script type="text/babel"> // **用戶代碼 ** </script></body></html>
上面代碼有兩個地方需要注意。
首先,最後一個<script>標簽的type屬性為text/babel。這是因為 React 獨有的 JSX 語法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上type="text/babel"。
其次,上面代碼一共用了三個庫:react.js、react-dom.js和Browser.js,它們必須首先載入。其中,react.js是 React 的核心庫,react-dom.js是提供與 DOM 相關的功能,Browser.js的作用是將 JSX 語法轉為 JavaScript 語法,這一步很消耗時間,實際上線的時候,應該將它放到伺服器完成。
2、將src子目錄的js文件進行語法轉換,轉碼後的文件全部放在build子目錄。
$ babel src --out-dir build
3、渲染轉換成html節點,以方便操作dom:
ReactDOM.render 是 React 的最基本方法,用於將模板轉為 HTML 語言,並插入指定的 DOM 節點。
這里以插入hello world為例來說明
ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('example'));
4、運行結果如下:
㈣ create-react-app build 打包隱藏源碼
在使用 create-react-app 時,打包生產環境 npm run build ,瀏覽器打開後仍然是可以看到源碼的。
在這里以新建一個默認項目為例:
項目根目錄新建 .env.proction 文件,內容如下:
然後重新打包,瀏覽器打開後就看不到源碼啦。
為了探究原理,執行 eject 後,可以看到webpack配置中有這么一段
這里的 process.env.GENERATE_SOURCEMAP 控制著是否捎帶源碼。所以我們可以配置環境變數 GENERATE_SOURCEMAP=false 即可。
當執行 build 時,將按順序優先尋找 .env.proction.local , .env.proction , .env.local , .env 文件來配置環境變數,所以就有了上面的操作。
更多關於環境變數的信息可查看 Adding Custom Environment Variables 。