3. JSX
JSX是對JavaScript語言的一個擴展語法, 用于生產React“元素”,建議在描述UI的時候使用。
- 可以在JSX中插入任何表達式(注意只能是表達式,for循環之類的代碼段就無法插入),只要用花括號括起來,如:
const element = (
` <h1>
Hello, {formatName(user)}! // formatName是一個函數
</h1>`
);
- 在屬性中,插入string的時候,可以用引號,插入js表達式的時候,用花括號,不要同時使用引號和花括號,否則屬性可能會被當做字符串常量解析:如
const element = <div tabIndex="0"></div>;
或者
const element = <img src={user.avatarUrl}></img>;
- 在JSX中聲明孩子節點
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX可以防止注入攻擊
在rendering JSX之前,React DOM會忽略注入的值,所以可以保證不會注入任何在應用中沒有明確聲明的東西,在rendering之前,所有的東西都被轉變為string,這有助于防止 XSS (cross-site-scripting) 攻擊。在React.createElement()調用時,才會編譯JSX,下面兩段代碼是對等的
<pre>
const element = ( <h1 className="greeting"> Hello, world! </h1> );
</pre>
與
<pre>
const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' );
</pre>
注意:React DOM使用駝峰式命名HTML變量,如class變成className。
4. react 渲染元素
React的elements(元素)就是普通對象,components(組件)由elements組成,通過const element = <h1>Hello, world</h1>;
就可以創建一個element
render用法如下
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
- 使用ReactDOM.render()渲染一個React元素
- React elements是不可變( immutable)的,React DOM會將新創建的element以及它的children和前一個element相比,只改變DOM中需要改變的部分。上栗中,雖然每隔一秒都會調用ReactDOM.render(),實際只會調用一次
ReactDOM.render()
,通過stateful components可以了解上面例子是如何封裝的 - 改變UI的唯一方法就是創建一個新的element,然后把它傳遞給
ReactDOM.render()
5 react components和props
components將UI分割成獨立的、可復用的塊。從概念上來說,components像Js的函數,它接受任意輸入,然后返回在顯示在屏幕上的element
1. 函數和類表示的components
函數式component
function Welcome(props) {
return<h1>Hello, {props.name}</h1>;
}
es6對象定義的component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
2. element也可以是用戶定義的component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;`
ReactDOM.render(
element,
document.getElementById('root')
);
注意:component的首個字母大寫
3. Components可以引用多個其他的components
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
注意:components必須返回一個根元素,這就是為什么上面栗子中,添加一個<div>元素來包含<Welcome />
4. 有時候需要把一個component分割成多個更小的components,如:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
上述代碼包含多層嵌套,需要修改時就會非常微妙,這種情況下,上述代碼就適合分割成多塊。
如果一個代碼可能會多次復用,或者本身太復雜,就適合分割成多個可服用的components
5. props應該是只讀的
function sum(a, b) { return a + b;}
以上代碼是一個“prue”函數,因為它沒有修改輸入值,所有的React component都應該是“prue”函數,不要修改props
6. react 狀態和生命周期(State and Lifecycle)
State和props類似,但是它是私有的,并且完全由component控制,對比一下分別用state和props實現的一個實時鬧鐘
a:props
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
b:state
class Clock extends React.Component { // 對象繼承至React.component
constructor(props) {
super(props);
this.state = {date: new Date()}; // 構造函數提供初始值,state是一個對象
}
componentDidMount() { // 生命周期鉤子,在component在DOM中渲染完成之后執行
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() { // 生命周期鉤子,在component即將銷毀時執行
clearInterval(this.timerID);
}
tick() {
this.setState({ // 修改component的state
date: new Date()
});
}
render() { // class返回一個render
return ( // render返回一個“(element)”
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> // Js表達式中使用this.state
</div>
);
}
}
ReactDOM.render(
<Clock />, // clock中移除date
document.getElementById('root')
);`
正確使用state
- 使用setState修改state
- setState的callback函數可以傳入兩個參數:prevState, props(前一個狀態、參數),正確使用
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
state的修改會被合并(merge),執行淺拷貝的merge??梢岳斫鉃閳绦幸粋€Object.assign(oldState, newState)操作
可以將state作為props傳遞給child component(孩子組件),state只會向下傳遞
<FormattedDate date={this.state.date} />
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
7. react 事件處理器
React元素時間處理與DOM元素的不同
- 命名事件方式不同,react->駝峰式,DOM->小寫
- react傳遞函數給事件處理器,DOM傳遞string
- DOM調用return false來阻止默認行為,在react中,必須顯示調用preventDefault
對比
<button onclick="console.log('The link was clicked.'); return false">
Click me
</button>
function ActionLink() {
function handleClick(e) {
e.preventDefault(); // 顯式調用preventDefault
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}> // 駝峰式handleClick,{}傳遞function
Click me
</a>
);
}
注意:上述代碼e是SyntheticEvent
在react中,通常不使用addEventListener,替代方法是在elements第一次渲染時提供一個listener
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this); // 注意此處不綁定this,在真正使用的時候,this會變成undefined
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
8. 根據條件渲染組件
1 if
可以使用js的if 或者條件運算符 創建符合當前狀態的元素
如:通過判斷用戶是否登陸來創建一個Greeting
組件
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
創建一個登陸登出組件
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
);
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
);
}
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
2. &&
&&操作符編寫內聯的if邏輯,在JSX中插入表達式
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
3. 內聯的條件表達式
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
);
}
9 react lists和keys
1 rendering多個components
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
key
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);`
代碼中,<li key={number.toString()}>
如果沒有key換成<li>
,就會有一個warning信息。在創建elements列表時,key是一個特殊的屬性。
- 一個array創建的elements列表中,item的key應該是獨一無二的。
- 注意:react根據keys判斷列表的items(項)是否改增刪改
- element的key應該是穩定的,當你array的item沒有一個穩定的標識,你或許可以使用item的index作為element的key。
- 對于需要重新排序的array,不建議使用index作為key,這樣渲染會比較慢
- keys應該在array包裹的環境內使用,例如
function ListItem(props) {
// Wrong! There is no need to specify the key here:
//return <li key={value.toString()}>
// Correct! There is no need to specify the key here:
return (<li> {props.value} </li>);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// Wrong! The key should have been specified here:
// <ListItem value={number} />
// Correct! Key should be specified inside the array.
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
- jsx允許在{}中注入任何形式的表達式,上面代碼NumberList可以改寫成:
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
)}
</ul>
);
}
10. react 表單
controlled component
一個input表單element(元素),如果它的value(值)是由React控制的,我們就叫它controlled component。
在react中,一個可變狀態通常都保存在React的state中,state只能通過setState修改
栗子:input
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
通過表單可以很方便的驗證和修改用戶的輸入,如
handleChange(event) { this.setState({value: event.target.value.toUpperCase()}); }
官網地址:https://facebook.github.io/react/docs/forms.html
11. react state 提升
有時候,多個不同的component之間可能需要共享一個state,但是,在react中,state是私有的,這種情況下,可以通過提升state來實現共享,既將state提升至它們共同的最近的component祖先中(從上至下的數據流)
1:state提升需要寫一個共同的模板,可能需要寫更多的代碼
2:如果有些數據,既可以放在props又可以放在state中,那么建議不要放在state中
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value, this.props.scale);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCommonChange = this.handleCommonChange.bind(this)
this.state = {temperature: '', scale: 'c'};
}
handleCommonChange(temperature, scale) {
this.setState({scale, temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCommonChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleCommonChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
相關鏈接:https://facebook.github.io/react/docs/lifting-state-up.html
12. react 組合與集成
react有強大的組合模型,在react建議使用組合來代替繼承(避免不同組件之間共用部分代碼),本章節介紹一下新手在開發過程中經常遇到的,用組合代替繼承的案例
- props的參數也可以是組件,如
left={<Contacts />}
。可以實現插槽功能
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
SplitPane組件有兩個插槽left和right。<Contacts /> 和 <Chat />都是對象,可以像其他數據一樣傳遞給props,實現插槽的功能
- 通過props.children獲取孩子節點??梢酝ㄟ^props.children實現“特例”,例如SignUpDialog是Dialog的特例
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
官網:https://facebook.github.io/react/docs/composition-vs-inheritance.html
進階篇 PropTypes
JavaScript的擴展語言: Flow 和 TypeScript可以檢測語法,在react中,可以使用PropTypes對props做類型檢查,propTypes只在開發模式下做檢查,如果提供的props是一個無效值,會console出一個warning
-
React.PropTypes.element.isRequired
,可以為組件指定必須項
MyComponent.propTypes = {
children: React.PropTypes.element.isRequired
};
2:defaultProps:為props指定一個默認值
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
// Specifies the default values for props:
Greeting.defaultProps = {
name: 'Stranger'
};
- React.PropTypes:不同檢測器的文檔例子
MyComponent.propTypes = {
// You can declare that a prop is a specific JS primitive. By default, these
// are all optional.
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,
optionalSymbol: React.PropTypes.symbol,
// Anything that can be rendered: numbers, strings, elements or an array
// (or fragment) containing these types.
optionalNode: React.PropTypes.node,
// A React element.
optionalElement: React.PropTypes.element,
// You can also declare that a prop is an instance of a class. This uses
// JS's instanceof operator.
optionalMessage: React.PropTypes.instanceOf(Message),
// You can ensure that your prop is limited to specific values by treating
// it as an enum.
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
// An object that could be one of many types
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),
// An array of a certain type
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
// An object with property values of a certain type
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
// An object taking on a particular shape
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),
// You can chain any of the above with `isRequired` to make sure a warning
// is shown if the prop isn't provided.
requiredFunc: React.PropTypes.func.isRequired,
// A value of any data type
requiredAny: React.PropTypes.any.isRequired,
// You can also specify a custom validator. It should return an Error
// object if the validation fails. Don't `console.warn` or throw, as this
// won't work inside `oneOfType`.
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},
// You can also supply a custom validator to `arrayOf` and `objectOf`.
// It should return an Error object if the validation fails. The validator
// will be called for each key in the array or object. The first two
// arguments of the validator are the array or object itself, and the
// current item's key.
customArrayProp: React.PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};
14 進階篇 深入JSX
JSX是React.createElement(component, props, ...children)
的語法糖
指定React元素類型
JSX標簽是一個React元素類型,大寫的類型表示JSX標簽是一個React組件
- 因為JSX編譯時需要調用
React.createElement
,所以React必須在JSX代碼的作用域內 - 可以使用點記法標識一個JSX類型(適用于一個模塊導出多個component組件)
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
- 用戶自定義的組件必須大寫
- React元素不能是普通表達式,如果想使用普通表達式,可以先賦值給一個大寫的變量
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Wrong! JSX type can't be an expression.
// return <components[props.storyType] story={props.story} />;
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
JSX中的props
- js表達式:prop可以是任何{}包裹的Js表達式,如:
<MyComponent foo={1 + 2 + 3 + 4} />
- 可以使用string字面量,此時HTML是非轉義的
// JSX表達式等效
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
- props默認值是"true",
// 這兩種JSX表達式等效
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
4:擴展屬性,props是一個對象,想要傳遞給JSX,可以使用……
作為擴展操作來傳遞全部props對象
// 這兩種JSX表達式等效
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
JSX中的孩子
在組件開口和閉合標簽之間的內容是一種特殊的props:props.children
,有幾種特殊的props.children
- 字符串字面量
此時,props.children是字符串Hello world!
<MyComponent>Hello world!</MyComponent>
- JSX孩子
可以將JSX元素作為孩子,可以混用不同類型的JSX元素或者String作為孩子,這和HTML是類似的,在展現嵌套的組件時非常有用
<MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
- JSX表達式
可以傳遞任何表達式,只要使用{}包裹就可以
function Item(props) {
return <li>{props.message}</li>;
}
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
- 函數作為孩子
// Calls the children callback numTimes to produce a repeated component
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
- false, null, undefined, and true都是有效的children(孩子),不過他們不會被渲染,這在根據條件渲染React元素時非常有用
// 有些假值,例如0仍然會被渲染
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
反之,如果你想要渲染false, true, null, or undefined 之類的假值,需要先轉化為string
<div>
My JavaScript variable is {String(myVariable)}.
</div>
6:JSX的孩子是一種特殊的props:props.children,可以是字符串字面量、JSX、表達式、函數
7:false, null, undefined, and true都是有效孩子,不會被渲染,需要渲染時,使用{String(myVariable)}轉化為string
15. 進階篇 Refs and the DOM
典型的React數據流中,父子組件交互的唯一方法就是props。有時候需要在典型數據流以外的地方修改子元素,React提供了Refs(引用),用ref來獲取組件或者HTML元素的引用
Refs使用場景
1:處理焦點、文本選擇、媒體播放
2:觸發強制性動畫
3:集成第三方DOM庫
給DOM添加Refs
可以在任何component上設置一個ref屬性,ref有一個回調函數,會在組件mounted(安裝完成)和unmounted(卸載完成)的時候立即執行。當ref用在HTML元素上時,ref回調函數接收DOM元素作為入參,如
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.focus = this.focus.bind(this);
}
focus() {
// Explicitly focus the text input using the raw DOM API
this.textInput.focus();
}
render() {
// Use the `ref` callback to store a reference to the text input DOM
// element in an instance field (for example, this.textInput).
return (
<div>
<input
type="text"
ref={input => this.textInput = input} />
<input
type="button"
value="Focus the text input"
onClick={this.focus}
/>
</div>
);
}
}
如上例,使用ref回調函數在類上面設置一個屬性來獲取DOM元素是一種常用的形式
給類組件( Class Component)添加Refs
模擬CustomerTextInput安裝完成后被點擊的效果
class AutoFocusTextInput extends React.Component {
componentDidMount() {
this.textInput.focus();
}
render() {
return (
<CustomTextInput
ref={input => { this.textInput = input; }} />
);
}
}
引用和方法組件(Functional Components)
方法組件沒有實例,所以不適合使用ref屬性,可以將方法組件轉化為類組件以使用ref,正如使用生命周期方法和state
當引用一個DOM元素或者類組件時,可以在方法組件中使用ref,如
function CustomTextInput(props) {
// textInput must be declared here so the ref callback can refer to it
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
a:不要過度使用Refs
b:String refs有點爭議,以后版本可能會遺棄,建議使用回調函數
c:callback如果是內聯函數,會調用兩次,一次null一次是DOM元素,因為每次渲染都會產生一個新的實例,可以通過在類上綁定一個方法作為ref的回調函數來避免
16 進階篇 Uncontrolled Components
通常,建議使用controlled Components來實現form,在controlled Components中,表單數據都是React組件處理的,相反,Uncontrolled Components中表單數據都是DOM自己處理的
寫一個uncontrolled組件,與其為每一個state變化寫一個事件處理器不如使用ref獲取DOM表單的值,如:例子中獲取一個uncontrolled組件的名字
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
了解更多在一個特殊的情況下應該使用uncontrolled還是controlled組件,查看this article on controlled versus uncontrolled inputs可能會有幫助
在React的渲染周期,form元素的value
屬性可能會修改DOM的值,在一個uncontrolled組件中,會希望React指定初始值,但是不希望之后值被不受控制的修改,這種情況下可以使用defaultValue
代替value
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
同理,在checkbox
和radio
中,使用defaultChecked,select
使用defaultValue
17. 進階篇 優化
優化React應用的一些方法
使用構建工具
1:對單應用,.min.js
版本工具
2:對Brunch,build
命令使用-p
3:對Browserify,啟動在NODE_ENV=production
下
4:創建React App,執行npm run build
,然后跟著提示走
5:對Rollup,在 commonjs插件之前使用replace 插件,那么只在開發模式下需要的模塊就不會被導入,完整設置例子see this gist.
6:對webpack,在生成環境需要添加插件
<pre>new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), new webpack.optimize.UglifyJsPlugin()
</pre>
使用chrome的Timeline剖析組件
1:在app路徑query字符串后添加?react_perf
(如http://localhost:3000/?react_perf
)
2:打開Chrome DevTools Timeline然后點擊Record
3:執行你想要剖析的操作,不要記錄超過20分鐘,否則chrome可能會停止
4:停止記錄
5:在用戶 Timing標簽下,React事件會被分類
在生成環境中渲染會相對快點,目前只有 Chrome, Edge, 和IE 瀏覽器支持這個特性
Avoid Reconciliation(避免重新渲染)
對于已經渲染過的UI,React構建和保存一份內部表現,它包含了組件返回的React元素,這個表現使React可以避免創建DOM節點,并且在有必要的時候還可以獲取已經存在的DOM節點,由于操作JavaScript對象會比操作DOM更加快,有時候把這個內部表現叫做“virtual DOM”(虛擬DOM),在React Native中,virtual DOM也以同樣的方式工作
當一個組件的state或者props變化的時候,React通過將新返回的元素與前一個被渲染的元素進行比較來決定是否需要修改真實的DOM,當他們不相等時,React會修改DOM
有時候通過重寫生命周期函數shouldComponentUpdate
可以加速組件的渲染,shouldComponentUpdate
在重新渲染操作即將開始前執行,默認的函數實現是返回true
,React就會重新渲染組件
<pre>shouldComponentUpdate(nextProps, nextState) { return true; }
</pre>
如果你知道在什么情況下,你的組件不需要修改,你可以在shouldComponentUpdate
中返回一個false,那么對組件的重新渲染就不會執行
shouldComponentUpdate操作
下圖是組件的子樹,對每一個節點,SCU表示shouldComponentUpdate
的返回值,vDOMEq
表示需要渲染React元素與前一個已經被渲染的元素是否相等
由于C2節點的shouldComponentUpdate
返回false,React不會去渲染C2,因此C4和C5的shouldComponentUpdate
都不會觸發
對于C1和C3,shouldComponentUpdate
都返回true,因此React走到葉子節點并且檢查他們,C6的shouldComponentUpdate
返回true,并且與已經渲染的元素不相等,React必須修改DOM
最后一個是C8,shouldComponentUpdate
返回true,但是由于C8返回的React元素與前一個已經渲染的元素相等,所以React還是不會修改DOM
總結:只有遍歷C6節點時,React會修改DOM,對于C8,由于被比較的兩個React元素相等不會修改,對于C2的子樹和C7,由于 shouldComponentUpdate
沒有返回true,React甚至不會比較元素,render
方法也不會被調用
例子
下例中,只有props.color
和state.count
變化時component才會重新渲染,可以修改shouldComponentUpdate
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
可以使用類似的模式來對所有的props和state做一個“淺比較”來決定組件是否需要修改
大多數時候可以使用React.PureComponent
來代替shouldComponentUpdate
, React.PureComponent
會做一個“淺比較”,所以對于props或者state修改前后,淺比較值依舊相等的情況,不要使用React.PureComponent
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This section is bad style and causes a bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
PureComponent會對this.props.words的新舊值做一個簡單的比較,由于代碼中對words的修改是一個數組的修改,修改前后的值做淺比較依舊相等,ListOfWords不會被重新渲染。
上述代碼可以修改如下
1:由于concat返回一個新的數組,可以使用concat對words重新賦值。
words: prevState.words.concat(['marklar'])
2:es6的擴展語法,對words重新賦值
words: [...prevState.words, 'marklar']
對于對象的修改,可以使用object.assign
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
或者給對象添加擴展屬性
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
Using Immutable Data Structures
Immutable.js是另一個解決方案,它通過結構化共享提供一個不變化、持久化的集合
不可變:一個集合一旦創建后,它的數據就不會被改變
持久化:通過set做了修改,會根據原先的集合創建一個新的集合
結構化共享:新的集合會盡可能多的共享原先集合相同的部分
Immutable對改變的追蹤更加廉價,
const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true
類似的Immutable代碼
const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
x === y; // false
20. 進階篇 Reconciliation
React提供了API的說明,因此在每次修改的時候,你不需要知道做了什么,本文解釋了React的Diffing Algorithm(差分算法)
動機
創建一顆React元素的樹時,在state或props變化的下一個瞬間,你知道render()
方法會返回一顆不同的React元素的樹,React需要考慮如何有效的修改UI來最大程度的匹配現在的樹
針對這個算法問題,有一些解決方案可以使用最少的操作來將一顆樹轉為另一顆,但是,這個state of the art algorithms 有 O(n3) 的復雜度,n是樹種元素的個數
如果展示1000個元素,就要進行10億次比較,消耗太大?;谝韵聝牲c假想,React實現一個啟發式的O(n) 的算法
1:兩個不同的元素會生成兩個不同的樹
2:開發者可以通過key
暗示在不同的渲染中,哪個孩子元素是穩定不變的
實踐表明,這些假想基本上在所有的實際例子中都是正確的
Diffing算法
當分裂兩棵樹時,React首先比較兩個根元素。不同類型的根元素會產生不同的行為
不同類型的元素
一旦根元素不同,React就會銷毀一個舊的樹并且從頭構建一顆新樹,如從<a>
到<img>
, 從 <Article>
到<Comment>
, 或者從 <Button>
到<div>
,其中任何一個根元素的變化都會重新構建一顆全新的樹
銷毀一顆樹,舊的DOM節點就會銷毀,Component實例會調用componentWillUnmount()
事件。當構建一棵新樹,新的DOM節點會插入DOM,Component會依次執行componentWillMount()
、componentDidMount()
,所有與舊的樹有關聯的state都會消失
根下面的任何組件也將被卸載,它們的state也會被銷毀,例如
<div>
<Counter />
</div>
<span>
<Counter />
</span>
舊的Counter
以及它的state被銷毀,React重新安裝一個新的Counter
相同類型的DOM元素
當比較兩個相同類型的DOM元素,React查看兩者的屬性,保留相同的底層DOM節點,并且只更新已經更改的屬性,例如<div className="before" title="stuff" /> <div className="after" title="stuff" />
通過比較,React知道只更改底層DOM節點的className
處理DOM節點后,React對孩子節點進行同樣的遞歸操作
相同類型的Component元素
當一個Component改變了,改變前后Component的對象實例是同一個,從而在渲染過程中維持一個state,React更新底層組件的props來匹配新的元素,然后調用底層實例的componentWillReceiveProps()
和 componentWillUpdate()
然后render()
方法被調用,使用差分算法(diff algorithm)對先前的結果和新的結果進行遞歸
孩子的遞歸
默認的,當遞歸一個DOM孩子節點時,React會同時遞歸新舊孩子列表,一旦有不同的地方,React就會生成一個改變,
例如,在尾部添加一個節點
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React會比較前兩個<li>
樹,然后插入第三個<li>
樹
如果在開始插入節點,如
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React會修改每一個節點,而不會意識到保存<li>Duke</li>
和<li>Villanova</li>
這個完整的子樹,這個低效率是一個問題
keys
為了解決這個問題,React支持一個key
屬性,如果孩子節點有key,React使用key來比較修改前后樹中的孩子節點,例如,在上栗中添加key
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
現在React知道“2014”這個元素是一顆新的樹,而“2015”和“2016”這兩個key對應的元素只是移動了。
可以添加一個ID屬性或者對部分內容做哈希算法生成一個key,這個key只需要在與它的兄弟節點的key不同,而不需要全局獨一無二
可以將item在數組中的index作為key,不過如果這個數組會重新排序,重新排序可能會很慢
Tradeoffs
記住,reconciliation算法只是一個實現細節。React可以在每次操作后重新渲染整個App,結果是一樣的,我們經常啟發式的改進以使通常的用戶用例更快
因為React依賴啟發式,如果背后的假想不成立,性能會受損
1:這個算法不會試著匹配不同組件類型的子樹
2:key應該是穩定的,可預測的,和獨一無二的,不穩定的key(例如通過Math.random()
生成)會導致很多組件實例和DOM節點不必要的重建,會導致孩子組件的性能降低和state丟失。
生命周期事件
componentWillUnmount()
:在舊的DOM節點即將銷毀時調用
componentWillMount()
:即將安裝新的DOM時調用
componentDidMount()
:新的DOM安裝完成是調用
componentWillReceiveProps()
:組件即將更新props時調用
componentWillUpdate()
:組件接收到新的props或者state但還沒有render()
時被執行
在React中,根據React的組件,很容易就能夠跟蹤數據流。有時候,你需要通過Component組件來傳遞數據,但是又不想手動的沿著每一層級傳遞,你可以通過“context”API直接獲取
為什么不使用Context
大型的應用不需要使用context,context會破壞應用的穩定性。context是一個突破性的API,在將來的React版本中可能會突破
如果你不熟悉Redux 或者 MobX,不要使用context。在許多實際應用中,將這些庫與React綁定來管理組件的狀態是一個不錯的選擇,也就是說Redux比context更適合解決這個問題
如果你不是有經驗的React開發人員,請不要使用context,通常使用props和state來實現功能會更好
如果你不顧這些警告,堅持使用上下文,嘗試將context的使用隔離在一小片區域,并且盡可能的不使用context,以便API改變時,應用更容易升級
context的使用
const PropTypes = require('prop-types');
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
// 不定義contextTypes,context將為空
Button.contextTypes = {
color: PropTypes.string
};
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}
class MessageList extends React.Component {
// MessageList定義getChildContext
getChildContext() {
return {color: "purple"};
}
render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return <div>{children}</div>;
}
}
// MessageList定義childContextTypes
MessageList.childContextTypes = {
color: PropTypes.string
};
1:提供context的組件需要定義childContextTypes
和getChildContext
2:context提供者的所有子組件,通過定義contextTypes
都可以獲取context(未定義contextTypes,context為一個空對象)
React父子組件耦合
通過context可以創建父子組件通信的API
class Menu extends React.Component {
getChildTypes() {
return {
onItemClick: this.onItemClick
}
}
onItemClick(val) {
console.log(this)
alert(val)
}
render() {
return (
<ul>{this.props.children}</ul>
)
}
}
Menu.childContextTypes = {
onItemClick: React.PropTypes.func
}
class MenuItem extends React.Component {
render() {
return (
<li onClick={() => this.context.onItemClick(this.props.children)}>
{this.props.children}
</li>
)
}
}
MenuItem.contextTypes = {
onItemClick: React.PropTypes.func
}
class App extends React.Component {
render() {
return (
<Menu>
<MenuItem>aubergine</MenuItem>
<MenuItem>butternut squash</MenuItem>
<MenuItem>clementine</MenuItem>
</Menu>
)
}
}
Menu創建了一個API:onItemClick,并且傳遞給子組件。
生命周期函數中的context
如果在組件中定義了contextTypes
,那么它的生命周期將會收到一個額外的參數:context
<li>constructor(props, context)
<li>componentWillReceiveProps(nextProps, nextContext)
<li>shouldComponentUpdate(nextProps, nextState, nextContext)
<li>componentWillUpdate(nextProps, nextState, nextContext)
<li>componentDidUpdate(prevProps, prevState, prevContext)
沒有state的函數組件引用Context
在沒有state的函數組件中,也可以使用context
const PropTypes = require('prop-types');
const Button = ({children}, context) =>
<button style={{background: context.color}}>
{children}
</button>;
Button.contextTypes = {color: PropTypes.string};
Context的修改
React有修改context的API,但是不要使用
當state或者props變化時,就會調用getChildContext
方法,然后調用this.setState
修改context中的數據,子組件會獲取到一個新生成的context
問題是,如果context的值改變了,如果父組件的shouldComponentUpdate
返回false
,則使用該值的后代不會更新,修改context回事組件完全失控,This blog post解釋了為什么避開這個問題
22 Web Components
React和Web Components是為了解決不同的問題而創建,Web組件提供了高度封裝的、可復用的組件,而React提供了一個保持數據和DOM同步的聲明庫,它們相互互補,你可以在React中使用Web組件,也可以在Web組件中使用React
- 在React中使用Web Component,如
class HelloMessage extends React.Component {
render() {
return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
}
}
- 在Web組件中使用React
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
});
document.registerElement('x-search', {prototype: proto});
轉載地址:https://facebook.github.io/react/docs/web-components.html