本文將首先講述如何通過React nodes建立基礎的React元件,然後進一步剖析React元件內部的點滴,包括該如何理解React元件,獲取React元件例項的兩種辦法,React事件系統,對React生命週期函式的理解,獲取React元件的子元件和子節點的方法,字串
ref
和函式式ref
,以及觸發React元件重新渲染的四種方法。
本文是React啟蒙系列的第六章,依舊講的是React的基礎使用方法,但是如果你對上面提到的概念有不理解或不熟悉的地方,跳到對應地方觀看閱讀,你應該會能有所收穫。
理解React元件
在具體說明如何建立React元件的語法之前,對什麼是React元件,其存在的意思及其劃分依據等做一個論述是很有必要的。
我們設想現在有一個webApp,這個app可以用來實現很多功能,依據功能,我們可以把其劃分為多個功能碎片。要實現這麼一個功能碎片,可能需要更多更小的邏輯單元,甚至還可以繼續分。而我們程式設計其實就是在有一個總體輪廓的前提下,通過解決一個個小小的問題來解決一個小問題,解決一個個小問題來實現軟體的開發。React元件就是這樣,你可以就把它當做一個個可組合的功能單元。
以一個登陸框為例,登入框本身就是網站的一個元件,但是其內包含諸如文字輸入框,登陸按鈕等,當然如果你想要做的只是最基礎的功能,輸入框和按鈕等可以只是一個個React 節點,但是如果你想為輸入框加上輸入檢測,輸入框可能就有必要寫成一個單獨的元件了,這樣也有利於複用,之後需要做的可能只是簡單的通過props
傳入不同的引數就可以實現不同的檢測。假想我們現在的登入框元件,包含React <Button>
元素形成登入按鈕,也包含多個文字輸入檢測元件。那麼父元件的作用一方面在於聚合小元件形成更復雜的功能單元,另一方面在於為子元件資訊的溝通提供渠道(比如說在滿足一定的輸入條件後,登入按鈕的狀態從不可點選變為可點選)。
建立React元件
React元件通過呼叫React.createClass()
方法建立,該方法需要傳入一個物件形式的引數。在該物件中可以為所建立元件配置各種引數,其可用引數如下表:
方法(配置引數)名稱 | 描述 |
---|---|
render() | 必填,通常為一個返回React nodes或者其它元件的函式 |
getInitialState() | 一個用於設定最初的state的函式,返回一個物件 |
getDefaultProps() | 一個用於設定預設props 的函式,返回值為一個物件 |
propTypes | 一個用於驗證特定props 型別的物件 |
mixins | 元件間共享方法的途徑 |
statics | 一個由多個靜態方法組成的物件,靜態方法中不能直接呼叫props 和state (可通過引數) |
displayName | 是一個用於命名元件的字串,用於展示除錯資訊,使用JSX時將自動設定?? |
componentWillMount() | 在元件首次渲染前觸發,只會觸發一次 |
componentDidMount() | 在元件首次渲染後觸發,只會觸發一次 |
componentWillReceiveProps() | 在元件將接受新props 時觸發 |
shouldComponentUpdate() | 元件再次渲染前觸發,可用於判斷是否需要再次渲染 |
componentWillUpdate() | 元件再次渲染前立即觸發 |
componentDidUpdate() | 元件渲染後立即觸發 |
componentWillUnmount() | 元件解除安裝前立即觸發 |
在上述所以方法中,最重要且必不可少的是render()
,它的作用是返回React節點和元件,其它所有的方法是可選的。
實際寫一個例子總比空說要容易理解,以下是使用React的React.createClass()
建立的Timer元件
var Timer = React.createClass({
getInitialState: function() {
return {
secondsElapsed: Number(this.props.startTime) || 0
};
},
tick: function() { //自定義方法
this.setState({
secondsElapsed: this.state.secondsElapsed + 1
});
},
componentDidMount: function() {//生命週期函式
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {//生命週期函式
clearInterval(this.interval);
},
render: function() { //使用JSX返回節點
return (
<div>
Seconds Elapsed: {this.state.secondsElapsed}
</div>
);
}
});
ReactDOM.render(< Timer startTime = "60" / >, app); //pass startTime prop, used for state
現在如果對上述元件建立的程式碼有所疑惑也不要緊,本文接下來將一步步的介紹上述程式碼中設計都的各個概念,包括this
,生命週期函式,React返回值的格式,如何在React中自定義函式,以及React元件中事件的定義等等。
在此需要注意的是元件名是以大寫開頭的。
當一個元件被建立(掛載)以後,我們就可以使用元件的API了,一個元件包含以下四個API
this.setState()
,
this.setState({mykey: `my new value`});
this.setState(function(previousState, currentProps)
{ return {myInteger: previousState.myInteger + 1};
});
作用:
用以重新渲染元件或者子元件
replaceState()
this.replceState({mykey: `my new value`});
作用:
效果和`setState()`類似,不過並不會和老的狀態合併,而是直接刪除老的狀態,應用新的狀態。
forceUpdate()
this.forceUpdate(function(){//callback});
作用:
呼叫此方法將跳過元件的`shouldComponentUpdate()`事件,直接呼叫`render()`
isMounted()
this.isMounted()
作用
判斷元件是否被掛載在DOM中,元件被掛載返回`true`,否則返回`false`
最常用的元件API是setState()
,後文還會細講。
小結
-
componentWillUnmount
,componentDidUpdate
,componentWillUpdate
,shouldComponentUpdate
,componentWillReceiveProps
,componentDidMount
,componentWillMount
等方法被稱作React 元件的生命週期函式,它們會在元件生命過程的不同階段被觸發。 -
React.createClass()
是一個方便的建立元件例項的方法; -
render()
方法應該保持純潔;
render()
方法中不能更改元件狀態
React元件的返回值
上文已經提到每個React元件必須有的方法就是render()
,這個方法的返回值只能是一個react 節點或一個react元件,這個節點或元件中可以包含任意多的子節點或者子元素。在下面的例子中我們可以看到在<reactNode>
中包含了多個子節點。
var MyComponent = React.createClass({
render: function() {
return <reactNode> <span>test</span> <span>test</span> </reactNode>;
}
});
ReactDOM.render(<MyComponent />, app);
值得注意的地方在於,如果你想返回的react 節點超過一行,應該用括號把返回值包圍起來,如下所示
var MyComponent = React.createClass({
render: function() {
return (
<reactNode>
<span>test</span>
<span>test</span>
</reactNode>
);
}
});
ReactDOM.render(<MyComponent />, app);
另一個值得注意的地方是返回值最外層不能出現多個節點(元件),否者會報錯
var MyComponent = React.createClass({
render: function() {
return (
<span>test</span>
<span>test</span>
);
}
});
ReactDOM.render(<MyComponent />, app);
上述程式碼就會報錯,報錯資訊如下
babel.js:62789 Uncaught SyntaxError: embedded: Adjacent JSX elements must be wrapped in an enclosing tag (10:3)
8 | return (
9 | <span>test</span>
> 10 | <span>test</span>
| ^
11 | );
12 | }
13 | });
一般來說開發者會在最外層加上一個<div>
元素包裹其它節點以避免此類錯誤。
同樣,如果return()
中的最外層出現了多個元件,也會出錯。
獲取元件例項的兩種方法
當一個元件被render
後,一個元件便通過傳入的引數例項化了,我們有兩種辦法獲取這個例項及其內部屬性(this.props
和this.setState()
)。
第一種方法就是使用this
關鍵字,在元件內部的方法中使用this
我們發現,這個this
指向的就是該元件例項。
var Foo = React.createClass({
componentWillMount:function(){ console.log(this) },
componentDidMount:function(){ console.log(this) },
render: function() {
return <div>{console.log(this)}</div>;
}
});
ReactDOM.render(<Foo />, document.getElementById(`app`));
獲取某元件例項的另外一種方法是呼叫ReactDOM.render()
方法,這個方法的返回值是最外層的元件例項。
看如下程式碼可以更好的理解這句話
var Bar = React.createClass({
render: function() {
return <div></div>;
}
});
var foo; //store a reference to the instance outside of function
var Foo = React.createClass({
render: function() {
return <Bar>{foo = this}</Bar>;
}
});
var FooInstance = ReactDOM.render(<Foo />, document.getElementById(`app`));
console.log(FooInstance === foo); //true,說明返回值和指向一致
小結this
的最常見用法就是在一個元件內呼叫該元件的各個屬性和方法,如this.props.[NAME OF PROP]
,this.props.children
,this.state
,this.setState()
,this.replaceState()
等。
在元件上定義事件
第四章和第五章已經多次介紹過React的事件系統,事件可以被直接新增都React節點上,下面的程式碼示例中,我們新增了兩個React事件(onClick
&onMouseOver
)到React<div>
節點中
var MyComponent = React.createClass({
mouseOverHandler:function mouseOverHandler(e) {
console.log(`you moused over`);
console.log(e); //e is sysnthetic event instance
},
clickHandler:function clickhandler(e) {
console.log(`you clicked`);
console.log(e); //e is sysnthetic event instance
},
render:function(){
return (
<div onClick={this.clickHandler} onMouseOver={this.mouseOverHandler}>click or mouse over</div>
)
}
});
ReactDOM.render(<MyComponent />, document.getElementById(`app`));
事件可以被看做是特殊的props
,只是React對這些特殊的props
的處理方式和普通的props
有所不同。
這種不同表現在會自動為事件的回撥函式繫結上下文,在下面的示例中,回撥函式中的this
指向了元件例項本身。
var MyComponent = React.createClass({
mouseOverHandler:function mouseOverHandler(e) {
console.log(this); //this is component instance
console.log(e); //e is sysnthetic event instance
},
render:function(){
return (
<div onMouseOver={this.mouseOverHandler}>mouse over me</div>
)
}
});
ReactDOM.render(<MyComponent />, document.getElementById(`app`));
React所支援的所以事件可見此表
小結
-
React規範化了事件在不同瀏覽器中的表現,你可以放心的跨瀏覽器使用;
-
React事件預設在事件冒泡階段(bubbling)觸發,如果想在事件捕獲階段觸發需要在事件名後加上
Capture
(如onClick
變為onClickCapture
); -
如果你想獲知瀏覽器事件的詳情,你可以通過在回撥函式中檢視
SyntheticEvent
物件中的nativeEvent
值; -
React實際上並未直接為React nodes新增事件,它使用的是event delegation事件委託機制
-
想要阻止事件冒泡,需要手動呼叫
e.stopPropagation()
或e.preventDefault()
,不要直接使用returning false
, -
React其實並沒有支援所有的JS事件,不過它還提供額外的生命週期函式以供使用React lifecycle methods.
元件組合
React元件的render()
方法中可以包含對其它元件的引用,這使得元件之間可以巢狀,一般我們把被巢狀的元件稱為巢狀元件的子元件。
下例中元件BadgeList包含了BadgeBill和BadgeTom兩個元件。
var BadgeBill = React.createClass({
render: function() {return <div>Bill</div>;}
});
var BadgeTom = React.createClass({
render: function() {return <div>Tom</div>;}
});
var BadgeList = React.createClass({
render: function() {
return (<div>
<BadgeBill/>
<BadgeTom />
</div>);
}
});
ReactDOM.render(<BadgeList />, document.getElementById(`app`));
此處為展示巢狀關係,程式碼有所簡化。
小結
-
編寫可維護性UI的關鍵之一在於可組合元件,React元件天然適用這一原理;
-
在
render
方法中,元件和HTML
可以組合使用;
React元件的生命週期函式
每個元件都具有一系列的發生在其生命中不同階段的事件,這些事件被稱為生命週期函式。
生命週期函式可以理解為React為元件的不同階段提供了的鉤子函式,用以更好的操作元件,下例是一個定時器元件,其在不同生命週期函式中執行了不同的事件
var Timer = React.createClass({
getInitialState: function() {
console.log(`getInitialState lifecycle method ran!`);
return {secondsElapsed: Number(this.props.startTime) || 0};
},
tick: function() {
console.log(ReactDOM.findDOMNode(this));
if(this.state.secondsElapsed === 65){
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);
return;
}
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
console.log(`componentDidMount lifecycle method ran!`);
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
console.log(`componentWillUnmount lifecycle method ran!`);
clearInterval(this.interval);
},
render: function() {
return (<div>Seconds Elapsed: {this.state.secondsElapsed}</div>);
}
});
ReactDOM.render(< Timer startTime = "60" / >, app);
元件的生命週期可被分為掛載(Mounting),更新(Updating)和解除安裝(UnMounting)三個階段。
下面將對不同階段各函式的功能及用途進行描述,弄清這一點很重要
掛載階段
這是React元件生命週期的第一個階段,也可以稱為元件出生階段,這個階段元件被初始化,獲得初始的
props
並定義將會用到的state
,此階段結束時,元件及其子元素都會在UI中被渲染(DOM,UIview等),我們還可以對渲染後的元件進行進一步的加工。這個階段的所有方法在元件生命中只會被觸發一次。React-in-depth
對掛載階段的生命週期函式的描述
| 方法 | 描述 |
| ——— | —— |
|getInitialState()
| 在元件掛載前被觸發,富狀態元件應該呼叫此方法以獲得初始的狀態值 |
|componentWillMount()
|在元件掛載前被觸發,富狀態元件應該呼叫此方法以獲得初始的狀態值|
|componentDidMount()
| 元件被掛載後立即觸發,在此可以對DOM進行操作了 |
更新階段
這個階段的函式會在元件的整個生命週期中不斷被觸發,這是元件一生中最長的時期。這個階段的函式可以獲得新的
props
,可以更改state
,可以對使用者的互動進行反應。React-in-depth
對更新階段的生命週期函式的描述
方法 | 描述 |
---|---|
componentWillReceiveProps(object nextProps) |
在元件接受新的props 時被觸發,可以用來比較新老props ,並使用this.setState() 來改變元件狀態 |
shouldComponentUpdate(object nextProps, object nextState) |
此元件可以對比新老props 和state ,用以確認該元件是否需要重新渲染,如果返回值為false ,將跳過此次渲染,此方法常用於優化React效能 |
componentWillUpdate(object nextProps, object nextState) |
在元件重新渲染前被觸發,此時不能再呼叫this.setState() 對state 進行更改 |
componentDidUpdate(object prevProps, object prevState) |
在重新渲染後立即被觸發,此時可呼叫新的DOM了 |
解除安裝階段
這是元件生命的最後一個階段,也可以被稱為是元件的死亡階段,此階段對應元件從Native UI中解除安裝之時,具體說來可能是使用者切換了頁面,或者頁面改變去除了某個元件,解除安裝階段的函式只會被觸發一次,然後該元件就會被加入瀏覽器的垃圾回收機制。React-in-depth
對此階段的生命週期函式的描述
方法 | 描述 |
---|---|
componentWillUnmount() |
元件解除安裝前立即被觸發,此階段常用來執行一些清理工作(比如說清除setInterval ) |
小結
-
componentDidMount
和componentDidUpdate
常用來載入第三方的庫(此時真實DOM存在,可載入各種圖表庫)。 -
元件掛載階段的各事件執行順序如下
-
Initialize / Construction
-
獲取初始的props,ES5中使用
getDefaultProps()
(React.createClass),ES6中使用MyComponent.defaultProps
(ES6 class) -
初始元件的
state
值,ES5中使用getInitialState()
(React.createClass) ,ES6中使用this.state = ...
(ES6 constructor) -
componentWillMount()
-
render()
第一次渲染 -
Children initialization & life cycle kickoff,子元件重複上述(1~5步)過程;
-
componentDidMount()
-
通過上面的過程分析,我們可以知道,在父元素執行
componentDidMount()
時,子元素和子元件都已經存在於真實DOM中了,因此在此可以放心呼叫。
-
元件更新階段各函式執行順序如下
-
componentWillReceiveProps()
:比較新老props
,對state
進行改變; -
shouldComponentUpdate()
:判斷元件是否需要重新渲染 -
render()
:重新渲染 -
Children Life cycle methods
:子元素重複上述過程 -
componentWillUpdate()
:此階段可以呼叫新的DOM了
-
-
元件解除安裝階段各函式執行順序如下
-
componentWillUnmount()
-
Children Life cycle methods:觸發子元素的生命週期函式,也將被解除安裝
-
被瀏覽器從記憶體中清除;
-
獲取子元件和子節點的方法
如果一個元件包含子元件或React節點(如<Parent><Child /></Parent>
或 <Parent><span>test<span></Parent>
),這些子節點和子元件可以通過React的this.props.children
的方法來獲取。
下面的例子展示瞭如何使用this.props.children
var Parent2 = React.createClass({
componentDidMount: function() {
//將會獲得<span>child2text</span>,
console.log(this.props.children);
//將會獲得 child2text, 或得了子元素<span>的子元素
console.log(this.props.children.props.children);
},
render: function() {return <div />;}
});
var Parent = React.createClass({
componentDidMount: function() {
//獲得了一個陣列 <div>test</div> <div>test</div>
console.log(this.props.children);
//獲得了這個陣列中的對應子元素中的子元素 childtext,
console.log(this.props.children[1].props.children);
},
render: function() {return <Parent2><span>child2text</span></Parent2>;}
});
ReactDOM.render(
<Parent><div>child</div><div>childtext</div></Parent>,
document.getElementById(`app`)
);
觀察上述的程式碼可以看出以下幾點
-
Parent元件例項的
this.props.children
獲取到由直系子元素組成的陣列,可以對子元素套用此方法獲得子元素(元件)的子元素(元件)(this.props.children[1].props.children
); -
子元素指的是由該例項圍起來的元素,而非該例項內部元素;
為了更好的操作this.props.children
包含的是一組元素,React還提供了以下方法
方法 | 描述 |
---|---|
React.Children.map(this.props.children, function(){}) |
在每一個直接子級(包含在 children 引數中的)上呼叫 fn 函式,此函式中的 this 指向 上下文。如果 children 是一個內嵌的物件或者陣列,它將被遍歷,每個鍵值對都會新增到新的 Map。如果 children 引數是 null 或者 undefined,那麼返回 null 或者 undefined 而不是一個空物件。 |
React.Children.forEach(this.props.children, function(){}) |
類似於Children.map() 但是不會反回陣列 |
React.Children.count(this.props.children) |
返回元件子元素的總數量,其數目等於Children.map() 和Children.forEach() 的執行次數。 |
React.Children.only(this.props.children) |
返回唯一的子元素否則報錯 |
React.Children.toArray(this.props.children) |
返回一個由各子元素組成的陣列,如果你想在render事件中操作子元素的集合時,這個方法特別有用,尤其是在重新排序或分割子元素時 |
小結
-
當只有一個子元素時,
this.props.children
之間返回該子元素,不會用一個陣列包裹著該子元素; -
需要注意的是
children
並非某元件內部的節點,而是由該元件包裹的元件或節點‘
兩種ref
ref
屬性使得我們獲取了對某一個React節點或某一個子元件的引用,這個在你需要直接操作DOM時非常有用。
字串ref
的使用很簡單,可分為兩步:
-
一是給你想引用的的子元素或元件新增
ref
屬性, -
然後在本元件中通過
this.refs.value(你所設定的屬性名)
即可引用;
不過還存在一種函式式的ref,看下面的例子
var C2 = React.createClass({
render: function() {return <span ref={function(span) {console.log(span)}} />}
});
var C1 = React.createClass({
render: function() {return(
<div>
<C2 ref={function(c2) {console.log(c2)}}></C2>
<div ref={function(div) {console.log(div)}}></div>
</div>)}
});
ReactDOM.render(<C1 ref={function(ci) {console.log(ci)}} />,document.getElementById(`app`));
上述例子的console結果都是指向ref所在的元件或元素,通過console的結果我們也可以發現,列印結果說明其指向的是真實的HTML DOM而非Virtual DOM。
如果不想用字串ref
,通過下面的方法也可以引用到你想引用的節點
var MyComponent = React.createClass({
handleClick: function() {
// focus()對真實DOM元素有效
this.textInput.focus();
},
render: function() {
// ref中傳入了一個回撥函式,把該節點本身賦值給this.input
return (
<div>
<input type="text" ref={(thisInput) => {this.textInput = thisInput}} />
<input
type="button"
value="Focus the text input"
onClick={this.handleClick}
/>
</div>
);
}
});
ReactDOM.render(
<MyComponent />,
document.getElementById(`app`)
);
小結
-
對無狀態函式式元件不能使用
ref
,因為這種元件並不會返回一個例項; -
ref有兩種,字串ref和函式式ref,不過字串ref(通過refs呼叫這種)在未來可能被放棄,函式式ref是趨勢;
-
元件有ref,可以通過ref呼叫該元件內部的方法;
-
使用行內函式表示式使用ref意味著每次更新React都會將其視為一個不同的函式物件,ref中的函式會以null為引數被立即執行(和在例項中呼叫不衝突)。比如說,當ref所指向的物件被解除安裝時,或者ref改變時,老的的ref函式都會以null為引數被呼叫。
-
對應ref的使用,React官方有兩點建議:
-
ref允許你直接操作節點,這一點有些情況下是非常方便的,不過需要注意的是,如果可以通過更改
state
來達到你想要的效果,那就不要隨便使用ref啦; -
如果你剛剛接觸React,在你想用ref的時候,還是儘量多思考一下看能不能用
state
來解決,仔細思考你會發現,state
可以解決大部分操作問題的,比較直接操作DOM並未React的初衷。
-
重新渲染一個元件
我們已經接觸了ReactDOM.render()
方法,這個方法使得元件及其子元件被初始化渲染。在這次渲染之後,React為我們提供了兩種方法來重新渲染某個元件
-
在元件內呼叫
setState()
方法; -
在元件中呼叫
fouceUpdate()
方法;
每當一個元件被重新渲染時,其子元件也會被重新渲染(在Virtual DOM中發生,在真實DOM中表現出來)。不過需要注意的是Virtual DOM的改變並不是一定在真實DOM中就會有所表現。
在下面的例子中,ReactDOM.render(< App / >, app)
初始化渲染了<App/>
及其子元件<Timer/>
,接下來的<App/>
中的setInterval()
事件呼叫this.setState()
致使兩個元件被重新渲染。在5秒後,setInterval()
被清除,而在十秒後this.forceUpdate()
被觸發又使得頁面被重新渲染。
var Timer = React.createClass({
render: function() {
return (
<div>{this.props.now}</div>
)
}
});
var App = React.createClass({
getInitialState: function() {
return {now: Date.now()};
},
componentDidMount: function() {
var foo = setInterval(function() {
this.setState({now: Date.now()});
}.bind(this), 1000);
setTimeout(function(){ clearInterval(foo); }, 5000);
//DON`T DO THIS, JUST DEMONSTRATING .forceUpdate()
setTimeout(function(){ this.state.now = `foo`; this.forceUpdate() }.bind(this), 10000);
},
render: function() {
return (
<Timer now={this.state.now}></Timer>
)
}
});
ReactDOM.render(< App / >, app);
後文
從開始翻譯本書到現在已有一個多月,基礎的翻譯工作終於算是告一段落。
《React Enlightenment》的第七章和第八章講述的是React的props
和state
已由@linda102翻譯完成。
在大概一個多月前看到本書原文時,我已經用了快五個月React,但是看完本書還是挺有收穫。
翻譯本書的初衷有兩點,一是加強自己對React基礎的理解,二是回想起,我在初學React時曾購買過國內的一本關於React的基礎書籍,價格是四十多,但是其實看完並未有太多收穫,該書大多就是翻譯的官方文件,而且翻譯的也不全面,並不那麼容易理解,所以希望這篇譯文對初學者友好,讓初學者少走彎路。
由於翻譯時間和水平都有限,譯文內部不可避免存在一些不恰當的地方,如果您在閱讀的過程中有好的建議,請直接提出,我會盡快修改。謝謝