React
一、核心概念
1、JSX: 在JSX中可以在{}
中放置任何有效的JS表达式(变量 + 函数 + …)
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>
const h2 = <div>{1 + 1}</div>
编译后,JSX表达式会被转化为普通的JS函数调用;
JSX里的class变成了className;
React DOM 在渲染所有输入内容之前,默认会进行转义;确保在应用中,永远不会注入非自身明确编写的内容;内容在渲染之前被转换成字符串,可以防止XSS(cross-site-scripting 跨站脚本)攻击;
Babel会把JSX转译成一个名为React.createElement()
函数调用;
下面两种代码是等效的:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement()会预先执行一些检查,实际上它创建了一个这样的对象
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
2、元素渲染
React元素是不可变对象
;一旦被创建,无法更改子元素或者属性,它代表某个特定时刻的UI;
更新UI的唯一方式是创建一个全新的元素,并将其传入ReactDOM.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);
React 只更新它需要更新的部分,React DOM 会将元素和它的子元素与它们之前的状态进行比较,并且只会进行必要的更新来使DOM达到预期的状态;
问题?需要将element中所有的元素都挨个比较吗?
使用了定时器,尽管每一秒都要新建一个描述整个UI树的元素,React DOM只会更新实际改变的内容。
3、组件&Props(组件名字必须以大写字母开头)
React会将以小写字母开头的组件视为原声DOM标签。
- 函数式组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
- 类式组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
- React自定义组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
页面上可以渲染 Hello,Sara
注意⚠️:所有React组件必须像纯函数一样保护props不被更改;
function sum(a, b) {
return a + b;
}
这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
function withdraw(account, amount) {
account.total -= amount;
}
这样的函数则不是“纯函数”,因为它更改了自己的入参;
4、state & 生命周期
state与props类似,但是state是私有的,完全受控于当前组件。
使用类式组件:
- 当Clock组件第一次被渲染到DOM中的时候 (“挂载 mount”),就为其设置一个计时器;
- 当DOM中Clock组件被删除的时候 (“卸载 unmount”),应该清除计时器;
class Clock extends React.Component {
constructor(props) {
super(props); // 继承父类的变量,将props传递到父类的构造函数中
this.state = {date: new Date()}; // 使用this.state赋初值
}
// 组件已经被渲染到DOM后运行
componentDidmount() {
this.timerID = setInterval(
() => this.tick(), 1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
})
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
// 父组件
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
-
Clock 组件的输出被插入到DOM中,React就会调用
ComponentDidMount
生命周期方法。此方法中,Clock组件向浏览器请求设置一个计时器每秒调用一次组件的tick()
方法。 -
tick()
方法中,Clock组件通过调用setState()
来计划进行一次UI更新。React知道state已经发生变化,就会重新调用render()
方法来确定页面上该显示什么。
一定要注意:State的更新可能是异步的;
出于性能考虑,React可能会把多个setState()
调用合并成一个调用;
this.props
和this.state
可能会异步更新,所以不要依赖它们的值来更新下一个状态。比如,下面这段代码可能会无法更新计数器。
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
解决办法:
让setState()
接收一个函数而不是一个对象,这个函数用上一个state作为第一个参数,将此次更新被应用时的props作为第二个参数;(使用箭头函数和普通函数都可以)
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
一定要注意:State的更新会被合并;
调用setState()
的时候,React会将你提供的对象合并到当前的state;
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
这里的合并是浅合并,所以this.setState({comments})
完整保留了this.state.posts
,但是完全替换了this.state.comments
。
5、事件处理
React元素的事件处理和DOM元素的很相似,但在语法上有些不同:
- React事件的命名采用小驼峰式(camelCase),而不是纯小写;
- 使用JSX语法时,你需要传入一个函数作为事件处理函数,而不是一个字符串;
传统的HTML:
<button onclick="activateLasers()">
Activate Lasers
</button>
React:
<button onClick={activateLasers}> // onClick中的Click首字母大写
Activate Lasers
</button>
- React不能通过返回false的方式阻止默认行为;必须显式的使用preventDefault;
HTML:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
React:
function ActionLink() {
function handleClick(e) {
e.preventDefault(); // 阻止默认行为
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
- 当使用React时,你一般不需要使用
addEventListener
为已创建的DOM元素添加监听器;使用ES6 class语法定义组件时,通常做法:将事件处理函数声明为class中的方法。
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
在JS中,class的方法默认不会绑定this,如果忘记绑定this.handleClick
,并把它传入了onClick
,当调用这个函数的时候,this
的值为undefined
,这样的话,this.setState()
方法不可用。
如果觉得bind
很麻烦,有两种方法可以解决;
方法一: 使用实验性的public class fields
语法,可以使用class fields正确的绑定回调函数。
class LoggingButton extends React.Component {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
// 注意: 这是 *实验性* 语法。
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
方法二: 如果没有使用class fields语法,可以在回调中使用箭头函数:(该回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染,通常建议在构造器中给绑定或使用class fields语法来避免这类性能问题)
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
向事件处理程序传递参数
在循环中,通常会为事件处理函数传递额外的参数,例如:若id是要删除那一行的ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
这两种情况下,React的事件对象e会被作为第二个参数传递;如果通过箭头函数的方式,事件对象必须显示的进行传递;通过bind的方式,事件对象以及更多的参数将会被隐式的进行传递。
6、条件渲染
- 可以使用逻辑与(&&):
在JS中,true && expression
总是会返回expression
, 而false && expression
总是会返回false
。 - 可以使用三目运算符:
condition ? true : false
阻止组件渲染:
让render方法直接返回null,那么组件不会渲染;但是不会影响组件的生命周期。
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
7、列表 & Key
注意:使用list时,一定要在每一项元素中添加一个key;
key帮助React识别哪些元素改变了。比如:被添加或者被删除。
一个元素的key最好是这个元素在列表中拥有的独一无二的字符串;
一般来说,使用数据中的id来作为元素的key,当元素没有确定id的时候,万不得已,可以使用元素索引index来作为key;(如果列表项目顺序可能会发生变化,不建议使用索引来当key值)。
经验: 在map()方法中的元素上设置key属性。key只是在兄弟节点之间必须唯一;但不需要全局唯一;生成两个不同的数组时,可以使用相同的key值。
注意: key
会传递信息给React,但不会传递给组件;如果组件中需要使用key属性的值,应当使用其他属性名显式传递这个值。
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
上面例子中,Post 组件可以读出 props.id
,但是不能读出 props.key
。
8、表单
- 受控组件
在 HTML 中,表单元素(如、 和 )之类的表单元素通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
渲染表单的React组件还控制着用户输入过程中表单发生的操作;被React以这种方式控制取值的表单输入元素就叫做 “受控组件”。
在表单元素上设置
value
属性,因此显示的值始终为this.state.value
,这使得React的state称为唯一数据源。
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('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
select标签
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
文件input标签
在 HTML 中, 允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API 进行控制。
<input type="file" />
文件标签的 value 只读,所以它是 React 中的一个非受控组件。
处理多个输入时,可以给每个元素添加name属性;
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.name === 'isGoing' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value // 注意:是[name],不是name
});
// 等同于
// var partialState = {};
// partialState[name] = value;
// this.setState(partialState);
}
render() {
return (
<form>
<label>
参与:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
来宾人数:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
受控组件指定value的prop会阻止用户更改输入,如果指定了value,仍然可以编辑,很有可能是将value
设置为undefined
或者null
。
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
9、状态提升(props是只读的)
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) { // 判断参数是否严格等于NaN
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
// 保留三位小数并四舍五入,round 四舍五入
return rounded.toString();
}
在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
10、组合 VS 继承
- 组合
有些组件无法提前知道它们子组件的具体内容;在Sidebar(侧边栏)
和Dialog(对话框)
等展现通用容器(box)的组件中特别容易遇到这种情况。建议使用特殊的children prop
来将它们的子组件传递到渲染结果中:(和Vue
中的slot
有异曲同工之妙)
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
<FancyBorder>
JSX 标签中的所有内容都会作为一个 children prop 传递给 FancyBorder 组件。因为 FancyBorder 将 {props.children}
渲染在一个 <div>
中,被传递的这些子组件最终都会出现在输出结果中。
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
少数情况下,可能需要在一个组件中预留多个“洞”;这种情况下,可以不是用children
,而是自行约定:将所需内容传入props,并使用相应的prop。
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left} // 使用left
</div>
<div className="SplitPane-right">
{props.right} // 使用right
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
- 继承
暂时并没有发现需要使用继承来构建组件层次的情况。