React入门笔记(一):简介和JSX
React入门笔记(二):组件和AJAX
React入门笔记(三):表单、事件、Ref属性
React中的Diff算法——Christopher Chedeau(原文翻译)
一、React组件
React允许将代码封装成组件,然后就像插入普通HTML标签一样,在网页中插入这个组件。
在上一章节介绍JSX时,对React组件进行了简单的介绍,包含React组件的三种创建方式:React.createClass()
,ES6 class
和无状态函数。
使用React.createClass()
方法生成一个组件类举例如下:
<div id="example"></div>
<script type="text/babel">
var MyComponent = React.createClass({
render: function(){
return <h1>Hello {this.props.name}</h1>;
}
});
React.render(
<MyComponent name="React" />
document.getElementById('example')
);
</script>
上面简单的栗子中,变量MyComponent
就是一个组件类。模板插入<MyComponent />
时,会自动生成MyComponent
的一个实例,所有的组件类都必须有自己的render
方法,用于输出组件。注意组件类的第一个字母必须大写,用于与普通HTML标签区别,否则会报错。另外,组件类只能包含一个顶层标签(这是XML的特点,即有且仅有一个根标签),否则也会报错。
组件的用法与原生的HTML标签一致,可以任意加入属性。组件的属性可以在组件类的this.props
对象上获取,比如name
属性可以通过this.props.name
读取,添加组件属性中一个特殊点需要注意:class
属性需要写成className
,for
属性需要写成htmlFor
,这是因为class
和for
是JavaScript的保留字。
我们可以通过创建多个组件来合成一个组件,这样把组件的不同功能点进行分离,以下栗子实现了输出网站名称和网址的组件:
class WebSite extends React.Component{
render: function(){
return (
<div>
<Name name={this.props.name} />
<Link site={this.props.site} />
</div>
);
}
}
var Name = React.createClass({
render: function(){
return (
<h1>{this.props.name}</h1>
);
}
});
var Link = React.createClass({
render: function(){
return (
<a href={this.props.site}>
{this.props.site}
</a>
);
}
});
ReactDOM.render(
<WebSite name="百度" site="www.baidu.com" />,
document.getElementById('example')
);
二、React状态
React把组件看成一个状态机(State Machine),组件免不了要与用户互动,React的一大创新就是将组件看成是一个状态机,一开始有初始状态,然后用户互动,导致状态变化,从而触发重新渲染UI。通过与用户的交互实现不同状态,然后渲染UI,让用户界面和数据保持一致。React李只需要更新组件的state,然后根据新的state重新渲染用户界面(不要操作DOM)。
在微博等社交平台常见的like按钮,可以通过如下的一个栗子实现。创建了一个LikeButton
组件,getInitialState
方法用于定义初始状态,也就是一个对象,这个对象可以通过this.state
属性读取。当用户点击组件,导致状态变化,this.setState
方法就修改状态值,每次修改以后,自动调用this.render
方法,再次渲染组件。
var LikeButton = React.createClass({
// 定义初始状态,也就是一个对象,这个对象可以通过`this.state`属性读取
getInitialState: function(){
return {liked: false};
},
handleClick: function(event) {
// 当用户点击组件导致状态变化,`this.setState`方法就修改状态值
// 每次修改后,自动调用`this.render`方法,再次渲染组件
this.setState({liked: !this.state.liked});
},
render: function() {
var text = this.state.liked? '喜欢':'不喜欢';
return (
// onClick等事件与原生HTML不同,
// on之后的第一个字母是大写,
// onclick是无效的
<p onClick={this.handleClick}>
<b>{text}</b>:点击切换状态!
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
三、React Props
Props
的使用
state
与props
主要区别是props
是不可变的,而state
可以根据与用户交互来改变。 有些容器组件需要定义state
来更新和修改数据,而子组件通常通过props
来传递数据。简单的栗子如下:var HelloMessage = React.createClass({ // 为props设置默认值 getDefaultProps: function() { return { name: 'Sussi' }; }, render: function() { return <h1>Hello {this.props.name}</h1>; } }); ReactDOM.render( <HelloMessage />, document.getElementById('example') );
State和Props
state
和props
常常在应用中组合使用,可以在父组件中设置state
,并通过在子组件上使用props
将其传递到子组件上。在render
函数中,我们设置name
和site
来获取父组件传递过来的数据。var WebSite = React.createClass({ getInitialState: function(){ return { name: '百度', site: 'http://baidu.com' }; }, render: function() { return ( <div> <Name name={this.state.name} /> <Link site={this.state.site} /> </div> ); } }); var Name = React.createClass({ render: function() { return ( <h1>{this.props.name}</h1> ); } }); var Link = React.createClass({ render: function() { return ( <a href={this.props.site}> {this.props.site} </a> ); } }); ReactDOM.render( <WebSite />, document.getElementById('example') );
Props验证
Props验证使用propTypes
,它可以保证我们的应用组件被正确使用,React.PropTypes
提供很多验证器(validator)来验证传入数据是否有效。当向props
传入无效数据时,JavaScript控制台会发出警告。一个简单的栗子如下,验证title属性必须为字符串:var title1 = "测试"; // var title1 = 123; var MyTitle = React.createClass({ propTypes: { title: React.PropTypes.string.isRequired, }, render: function(){ return <h1>{this.props.title}</h1> } }); ReactDOM.render( <MyTitle title={title1} />, document.getElementById('example') );
更多常见的验证器说明如下:
React.createClass({ propTypes: { // 可以声明prop为指定的JS基本数据类型,默认情况,这些数据是可选的 optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, // 可以被渲染的对象numbers,strings,elements或array optionalNode: React.PropTypes.node, // React 元素 optionalElement: React.PropTypes.element, // 用JS的instanceof操作符声明prop为类的实例 optionalMessage: React.PropTypes.instanceOf(Message), // 用enum来限制prop只接受指定的值 optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), // 可以是多个对象类型中的一个 optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), // 指定类型组成的数组 optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), // 指定类型的属性构成的对象 optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), // 特定shape参数的对象 optionalObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string, fontSize: React.PropTypes.number }), // 任意类型加上`isRequired`来使prop不可空 requiredFunc: React.PropTypes.func.isRequired, // 不可空的任意类型 requiredAny: React.PropTypes.any.isRequired, // 自定义验证器。若验证失败需要返回一个Error对象,不要直接使用`console.warn`或抛异常,因为这样`oneOfType`会失效。 customProp: function(props, propName, componentName) { if(!/matchme/.test(props[propName])){ return new Error('Validation failed!'); } } }, /* ... */ });
四、React组件API
React组件有以下7个方法:
- 设置状态:setState
- 替换状态:replaceState
- 设置属性:setProps
- 替换属性:replaceProps
- 强制更新:forceUpdate
- 获取DOM节点:findDOMNode
- 判断组件挂载状态:isMounted
==================================================
设置状态:setState
setState(object nextState[, function callback])
参数说明:
- nextState,将要设置的新状态,该状态会和当前的state合并;
- callback,可选参数,回调函数。该函数会在setState设置成功,且组件重新渲染后调用。
合并nextState和当前state,并重新渲染组件。setState是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
关于setState:- 不能在组件内部通过
this.state
修改状态,因为该状态会在调用setState()
后被替换。 setState()
并不会立即改变this.state
,而是创建一个即将处理的state。setState()
并不一定是同步的,为了提升性能React会批量执行state和DOM渲染。setState()
总是会触发一次组件重绘,除非在shouldComponentUpdate()
中实现了一些条件渲染逻辑。
以下给出一个使用的栗子:
var Counter = React.createClass({ getInitialState: function(){ return {clickCount: 0}; }, handleClick: function() { this.setState(function(state) { return {clickCount: state.clickCount+1}; }); }, render: function() { return ( <h3 onClick={this.handleClick}> 点我!点击次数为:{this.state.clickCount} </h3> ); } }); ReactDOM.render( <Counter />, document.getElementById('example') );
替换状态:replaceState
replaceState(object nextState[, function callback])
其中的参数含义与
setState()
相同。replaceState()
方法与setState()
类似,但是方法只会保留nextState
中状态,原state不在nextState中的状态都会被删除。设置属性:setProps
setProps(object nextProps[, function callback])
参数说明:
- nextProps,将要设置的新属性,该状态会和当前的props合并;
- callback,可选参数,回调函数。该函数会在
setProps
设置成功,且组件重新渲染后调用。
设置组件属性,并重新渲染组件。
props相当于组件的数据流,它总是从父组件向下传递至所有的子组件中。当和一个外部的JavaScript应用集成时,我们可能会需要向组件传递数据或通知React.render()
组件重新渲染,可以使用setProps()
。
更新组件,可以在节点上再次调用React.render()
,也可以通过setProps()
方法改变组件属性,触发组件重新渲染。替换属性:replaceProps
replaceProps(object nextProps[, function callback])
replaceProps()
方法与setProps
类似,但是会删除原有的props。强制更新:forceUpdate
forceUpdate([function callback])
参数说明:
- callback,可选参数,回调函数。该函数会在组件
render()
方法调用后调用。
forceUpdate()
方法会使组件调用自身的render()
方法重新渲染组件,组件的子组件也会调用自身的render()
。但是组件重新渲染时,依然会读取this.props
和this.state
,若状态没有改变,则React只会更新DOM。
forceUpdate()
方法适用于this.props
和this.state
之外的组件重绘(如修改了this.state
后),通过该方法通知React需要调用render()
。
应当尽量避免使用forceUpdate()
,而仅从this.props
和this.state
中读取状态并有React触发render()
调用。- callback,可选参数,回调函数。该函数会在组件
获取DOM节点:findDOMNode
DOMElement findDOMNode()
- 返回值:DOM元素DOMElement
如果组件已经挂载到DOM中,该方法返回对应的本地浏览器DOM元素。当render返回null或false时,
this.findDOMNode()
也会返回null。从DOM中读取值的时候,该方法会很有用,如:获取表单字段的值和做一些DOM操作。判断组件挂载状态:isMounted
bool isMounted()
- 返回值:true或false,表示组件是否已挂载到DOM中
isMounted()
方法用于判断组件是否已挂载到DOM中。可以使用该方法保证setState()
和forceUpdate()
在异步场景下的调用不会出错。
五、React组件生命周期
组件的生命周期可分为三个状态:
- Mounting:已插入真实的DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实DOM
React为每个状态都提供了will
和did
两种处理函数,will
函数在进入状态之前调用,did
函数在进入状态之后调用,三种状态共计5种处理函数(UNmount不需要did
状态),如下:
- componentWillMount:在渲染前调用,在客户顿也在服务端。
- componentDidMount:在第一次渲染之后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过
this.getDOMNode()
来进行访问。如果想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout
,setInterval
或者发送AJAX请求等操作(防止异步操作阻塞UI)。 - componentWillUpdate:在组件接收到新的props或者state但是还没有render时被调用。在初始化时不会调用。
- componentDidUpdate:在组件完成更新后立即调用。在初始化时不会被调用。
- componentWillUnmount:在组件从DOM中移除的时候立刻被调用。
此外还有两种特殊状态的处理函数:
- componentWillReceiveProps:在组件接收到一个新的prop时被调用。这个方法在初始化render时不会被调用。
- shouldComponentUpdate:返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
可以在你确认不需要更新组件时使用。
这些方法的详细说明,可参考官方文档。以下给出一个简单的栗子:
// 每隔100ms重新设置组件的透明度并重新渲染
var Hello = React.createClass({
getInitialState: function() {
return {
opacity: 1.0
};
},
componentDidMount: function() {
this.timer = setInterval(function(){
var opacity = this.state.opacity;
opacity -= .05;
if(opacity < 0.1) {
opacity = 1.0;
}
this.setState({
opacity: opacity
});
}.bind(this), 100);
},
render: function() {
return (
<div style={{opacity: this.state.opacity}}>
Hello {this.props.name}
</div>
);
}
});
ReactDOM.render(
<Hello name="world"/>,
document.getElementById('example');
);
注意style={{opacity: this.state.opacity}}
,React组件样式是一个对象,所以第一重括号表示是JavaScript语法,第二重括号表示样式对象。
以下栗子初始化state,setNewNumber用于更新state。所有生命周期在Content组件中。
var Button = React.createClass({
getInitialState: function(){
return {
data: 0
};
},
setNewNumber: function(){
this.setState({data: this.state.data+1})
},
render: function() {
return (
<div>
<button onClick={this.setNewNumber}>INCREMENT</button>
<Content myNumber={this.state.data}></Content>
</div>
);
}
});
var Content = React.createClass({
componentWillMount: function() {
console.log('Component will mount!');
},
componentDidMount: function() {
console.log('Component did mount!');
},
componentWillReceiveProps: function(newProps) {
console.log('Component will receive props!');
},
shouldComponentUpdate: function(newProps, nextState) {
return true;
},
componentWillUpdate: function(nextProps, nextState) {
console.log('Component will update!');
},
componentDidUpdate: function(prevProps, prevState) {
console.log('Component did update!');
},
componentWillUnmount: function() {
console.log('Component will unmount!');
},
render: function() {
return (
<div>
<h3>{this.props.myNumber}</h3>
</div>
);
}
});
五、React AJAX
React组件的数据可以通过componentDidMount
方法中的AJAX来获取,当从服务端获取数据库可以将数据存储在state中,再用this.setState
方法重新渲染UI。
当使用异步加载数据时,在组件卸载前使用componentWillUnmount
来取消未完成的请求。
以下栗子演示了获取Github用户最新gist共享描述:
var UserGist = React.createClass({
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
this.serverRequest = $.get(this.props.source, function(result) {
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}.bind(this));
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function() {
return (
<div>
{this.state.username} 用户最新的Gist共享地址:
<a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
</div>
);
}
});
ReactDOM.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
document.getElementById('example')
);
上述代码使用jQuery来完成AJAX请求,这是为了便于说明。React本身没有任何依赖,完全可以不用jQuery,而使用其他库,我们也可以通过把一个Promise对象传入组件,如下所示:
var RepoList = React.createClass({
getInitialState: function() {
return {
loading: true,
error: null,
data: null
};
},
componentDidMount() {
this.props.promise.then(
value => this.setState({loading: false, data: value}),
error => this.setState({loading: false, error: error}));
},
render: function() {
if(this.state.loading) {
return <span>Loading...</span>;
}
else if(this.state.error !== null) {
return <span>Error: {this.state.error.message}</span>;
}
else {
var repos = this.state.data.items;
var repoList = repos.map(function(repo, index){
return (
<li key={index}>
<a href={repo.html_url}>{repo.name}</a>
({repo.stargazers_count} stars)
<br/> {repo.description}
</li>
);
});
return (
<main>
<h1>Most Popular JavaScript Projects in Github</h1>
<ol>{repoList}</ol>
</main>
);
}
}
});
ReactDOM.render(
<RepoList promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')} />,
document.getElementById('example')
);
上述代码从github的API抓取数据,然后Promise对象作为属性,传给RepoList
组件。如果Promise对象正在抓取数据(pending状态),组件显示“正在加载”;如果Promise对象报错(rejected状态),组件显示报错信息;如果Promise对象抓取数据成功(fullfill状态),组件显示获取的数据。