閑話不多說,開篇擼代碼,你可以會看到類似如下的結構: See the Pen react props by 糊一笑 (@rynxiao) on CodePen. 當一個組件嵌套了若幹層子組件時,而想要在特定的組件中取得父組件的屬性,就不得不將 一層一層地往下傳,我這裡只是簡單的列舉了3個子組件,而當 ...
閑話不多說,開篇擼代碼,你可以會看到類似如下的結構:
import React, { Component } from 'react';
// 父組件
class Parent extends Component {
constructor() {
super();
this.state = { color: 'red' };
}
render() {
return <Child1 { ...this.props } />
}
}
// 子組件1
const Child1 = props => {
return <Child2 { ...props } />
}
// 子組件2
const Child2 = props => {
return <Child3 { ...props } />
}
// 子組件3
const Child3 = props => {
return <div style={{ color: props.color }}>Red</div>
}
See the Pen react props by 糊一笑 (@rynxiao) on CodePen.
當一個組件嵌套了若幹層子組件時,而想要在特定的組件中取得父組件的屬性,就不得不將props
一層一層地往下傳,我這裡只是簡單的列舉了3個子組件,而當子組件嵌套過深的時候,props
的維護將成噩夢級增長。因為在每一個子組件上你可能還會對傳過來的props
進行加工,以至於你最後都不確信你最初的props
中將會有什麼東西。
那麼React
中是否還有其他的方式來傳遞屬性,從而改善這種層層傳遞式的屬性傳遞。答案肯定是有的,主要還有以下兩種形式:
Redux等系列數據倉庫
使用Redux
相當於在全局維護了整個應用數據的倉庫,當數據改變的時候,我們只需要去改變這個全局的數據倉庫就可以了。類似這樣的:
var state = {
a: 1
};
// index1.js
state.a = 2;
// index2.js
console.log(state.a); // 2
當然這隻是一種非常簡單的形式解析,Reudx
中的實現邏輯遠比這個要複雜得多,有興趣可以去深入瞭解,或者看我之前的文章:用react+redux編寫一個頁面小demo以及react腳手架改造,下麵大致列舉下代碼:
// actions.js
function getA() {
return {
type: GET_DATA_A
};
}
// reducer.js
const state = {
a: 1
};
function reducer(state, action) {
case GET_DATA_A:
state.a = 2;
return state;
default:
return state;
}
module.exports = reducer;
// Test.js
class Test extends React.Component {
constructor() {
super();
}
componentDidMount() {
this.props.getA();
}
}
export default connect(state => {
return { a: state.reducer.a }
}, dispatch => {
return { getA: dispatch => dispatch(getA()) }
})(Test);
這樣當在Test
中的componentDidMount
中調用了getA()
之後,就會發送一個action
去改變store
中的狀態,此時的a已經由原先的1變成了2。
這隻是一個任一組件的大致演示,這就意味著你可以在任何組件中來改變store
中的狀態。關於什麼時候引入redux
我覺得也要根據項目來,如果一個項目中大多數時候只是需要跟組件內部打交道,那麼引入redux
反而造成了一種資源浪費,更多地引來的是學習成本和維護成本,因此並不是說所有的項目我都一定要引入redux
。
context
關於context
的講解,React
文檔中將它放在了進階指引裡面。具體地址在這裡:https://reactjs.org/docs/context.html。主要的作用就是為瞭解決在本文開頭列舉出來的例子,為了不讓props
在每層的組件中都需要往下傳遞,而可以在任何一個子組件中拿到父組件中的屬性。
但是,好用的東西往往也有副作用,官方也給出了幾點不要使用context
的建議,如下:
- 如果你想你的應用處於穩定狀態,不要用
context
- 如果你不太熟悉
Redux
或者MobX
等狀態管理庫,不要用context
- 如果你不是一個資深的
React
開發者,不要用context
鑒於以上三種情況,官方更好的建議是老老實實使用props
和state
。
下麵主要大致講一下context
怎麼用,其實在官網中的例子已經十分清晰了,我們可以將最開始的例子改一下,使用context
之後是這樣的:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { color: 'red' };
}
getChildContext() {
return { color: this.state.color }
}
render() {
return <Child1 />
}
}
const Child1 = () => {
return <Child2 />
}
const Child2 = () => {
return <Child3 />
}
const Child3 = ({ children }, context) => {
console.log('context', context);
return <div style={{ color: context.color }}>Red</div>
}
Parent.childContextTypes = {
color: PropTypes.string
};
Child3.contextTypes = {
color: PropTypes.string
};
ReactDOM.render(<Parent />, document.getElementById('container'));
可以看到,在子組件中,所有的{ ...props }
都不需要再寫,只需要在Parent
中定義childContextTypes
的屬性類型,以及定義getChildContext
鉤子函數,然後再特定的子組件中使用contextTypes
接收即可。
See the Pen react context by 糊一笑 (@rynxiao) on CodePen.
這樣做貌似十分簡單,但是你可能會遇到這樣的問題:當改變了context
中的屬性,但是由於並沒有影響父組件中上一層的中間組件的變化,那麼上一層的中間組件並不會渲染,這樣即使改變了context
中的數據,你期望改變的子組件中並不一定能夠發生變化,例如我們在上面的例子中再來改變一下:
// Parent
render() {
return (
<div className="test">
<button onClick={ () => this.setState({ color: 'green' }) }>change color to green</button>
<Child1 />
</div>
)
}
增加一個按鈕來改變state
中的顏色
// Child2
class Child2 extends React.Component {
shouldComponentUpdate() {
return true;
}
render() {
return <Child3 />
}
}
增加shouldComponentUpdate
來決定這個組件是否渲染。當我在shouldComponentUpdate
中返回true
的時候,一切都是那麼地正常,但是當我返回false
的時候,顏色將不再發生變化。
See the Pen react context problem by 糊一笑 (@rynxiao) on CodePen.
既然發生了這樣的情況,那是否意味著我們不能再用context
,沒有絕對的事情,在這篇文章How to safely use React context中給出了一個解決方案,我們再將上面的例子改造一下:
// 重新定義一個發佈對象,每當顏色變化的時候就會發佈新的顏色信息
// 這樣在訂閱了顏色改變的子組件中就可以收到相關的顏色變化訊息了
class Theme {
constructor(color) {
this.color = color;
this.subscriptions = [];
}
setColor(color) {
this.color = color;
this.subscriptions.forEach(f => f());
}
subscribe(f) {
this.subscriptions.push(f)
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { theme: new Theme('red') };
this.changeColor = this.changeColor.bind(this)
}
getChildContext() {
return { theme: this.state.theme }
}
changeColor() {
this.state.theme.setColor('green');
}
render() {
return (
<div className="test">
<button onClick={ this.changeColor }>change color to green</button>
<Child1 />
</div>
)
}
}
const Child1 = () => {
return <Child2 />
}
class Child2 extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <Child3 />
}
}
// 子組件中訂閱顏色改變的信息
// 調用forceUpdate強制自己重新渲染
class Child3 extends React.Component {
componentDidMount() {
this.context.theme.subscribe(() => this.forceUpdate());
}
render() {
return <div style={{ color: this.context.theme.color }}>Red</div>
}
}
Parent.childContextTypes = {
theme: PropTypes.object
};
Child3.contextTypes = {
theme: PropTypes.object
};
ReactDOM.render(<Parent />, document.getElementById('container'));
看上面的例子,其實就是一個訂閱發佈者模式,一旦父組件顏色發生了改變,我就給子組件發送消息,強制調用子組件中的forceUpdate
進行渲染。
See the Pen react context problem resolve by 糊一笑 (@rynxiao) on CodePen.
但在開發中,一般是不會推薦使用forceUpdate
這個方法的,因為你改變的有時候並不是僅僅一個狀態,但狀態改變的數量只有一個,但是又會引起其他屬性的渲染,這樣會變得得不償失。
另外基於此原理實現的有一個庫: MobX,有興趣的可以自己去瞭解。
總體建議是:能別用context
就別用,一切需要在自己的掌控中才可以使用。
總結
這是自己在使用React
時的一些總結,本意是朝著偷懶的方向上去瞭解context
的,但是在使用的基礎上,必須知道它使用的場景,這樣才能夠防範於未然。