1. react基础
1.1 es6语法
ES 6是JavaScript语言的新一代标准。React的项目一般都是用ES 6语法来写的。
1.let const
let和const是ES 6中新增的两个关键字,用来声明变量。
const声明一个只读的常量,一旦声明,常量的值就不能改变。
2.箭头函数
ES 6允许使用“箭头”(=>)定义函数。
例如:
var f = (a, b) => a + b;
3.模板字符串
模板字符串是增强版的字符串,用反引号(`)标识字符串。
它可以用来定义多行字符串,以及在字符串中嵌入变量,功能很强大。
//多行字符串
'JS is wonderful !
React is wonderful! '
//字符串中嵌入变量
var name = "React";
'Hello, ${name} ! ';
4.解构赋值
ES 6允许按照一定模式从数组和对象中提取值,对变量进行赋值,
这被称为解构。
//数组解构
let [a,b,c] = [1, 2, 3];
//对象解构
let name = 'Lily';
let age = 4;
let person = {name, age};
person // Object {name: "Lily", age: 4}
//对象解构的另一种形式
let person = {name: 'Lily', age: 4};
let {name, age} = person;
name // "Lily"
age //4
//嵌套结构的对象解构
let {person: {name, age}, foo} = {person: {name: 'Lily',
age: 4}, foo: 'foo'};
name //"Lily"
age //4
foo //"foo"
5. rest参数
ES 6引入rest参数(形式为…变量名)用于获取函数的多余参数,以代替arguments对象的使用。rest参数是一个数组,数组中的元素是多余的参数。
function languages(lang, ...types){
console.log(types);
}
languages('JavaScript', 'Java', 'Python'); //["Java",
"Python"]
6. 扩展运算符
扩展运算符是三个点(…),它将一个数组转为用逗号分隔的参数序列,类似于rest参数的逆运算。
function sum(a, b, c){
return a + b + c;
}
let numbers = [1, 2, 3];
sum(...numbers); //6
扩展运算符还常用于合并数组以及与解构赋值结合使用。
//合并数组
let arr1 = ['a'];
let arr2 = ['b', 'c'];
let arr3 = ['d', 'e'];
[...arr1, ...arr2, ...arr3]; //['a', 'b', 'c', 'd', 'e'];
//与解构赋值结合
let [a, ...rest] = ['a', 'b', 'c'];
rest //['b', 'c']
扩展运算符还可以用于取出参数对象的所有可遍历属性,复制到当
前对象之中。
let bar = {a: 1, b: 2};
let foo = {...bar};
foo //Object {a: 1, b: 2};
foo === bar //false
7. class
ES 6引入了class(类)这个概念,新的class写法让对象原型的写法
更加清晰,也更像传统的面向对象编程语言的写法。
//定义一个类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName(){
return this.name;
}
getAge(){
return this.age;
}
}
//根据类创建对象
let person = new('Lily', 4);
class之间可以通过extends关键字实现继承
class Man extends Person {
constructor(name, age) {
super(name, age);
}
getGender() {
return 'male';
}
}
let man = new Man('Jack', 20);
8. import、export
ES 6实现了自己的模块化标准。
export用于规定模块对外暴露的接口,import用于引入其他模块提供的接口。
//a.js,导出默认接口和普通接口
const foo = () => 'foo';
const bar = () => 'bar';
export default foo; //导出默认接口
export {bar}; //导出普通接口
//b.js(与a.js在同一目录下),导入a.js中的接口
//注意默认接口和普通接口导入写法的区别
import foo, {bar} from './a';
foo(); //"foo"
bar(); //"bar"
1.2 JSX
JSX 是一种用于描述UI的JavaScript扩展语法,React使用这种语法
描述组件的UI。
React 认为,一个组件应该是具备UI描述和UI数据的完整体,不应该将它们分开处理,于是发明了JSX,作为UI描述和UI数据之间的桥梁。
1.2.1 JSX语法
1 基本语法
JSX的基本语法和XML语法相同,都是使用成对的标签构成一个树
状结构的数据
const element = (
<div>
<h1>Hello, world!</h1>
</div>
)
const element = <h1>Hello, world!</h1>;
2 标签类型
在JSX语法中,使用的标签类型有两种:DOM类型的标签(div、
span等)和React组件类型的标签。
当使用DOM类型的标签时,标签的首字母必须小写;当使用React组件类型的标签时,组件名称的首字母必须大写。React 正是通过首字母的大小写判断渲染的是一个DOM类型的标签还是一个React组件类型的标
签。
// DOM类型标签
const element = <h1>Hello, world!</h1>;
// React组件类型标签
const element = <HelloWorld />;
// 二者可以互相嵌套使用
const element = (
<div>
<HelloWorld />
</div>;
)
3 JavaScript表达式
在JSX中使用JavaScript表达式需要将表达式用大括号“{}”包起来。
表达式在JSX中的使用场景主要有两个:通过表达式给标签属性赋值和通过表达式定义子组件。
// 通过表达式给标签属性赋值
const element = <MyComponent foo={ 1 + 2 } />
// 通过表达式定义子组件(map虽然是函数,但它的返回值是JavaScript表达
式)
const todos = ['item1', 'item2', 'item3'];
const element = (
<ul>
{todos.map(message => <Item key={message} message=
{message} />)}
</ul>
);
注意,JSX中只能使用JavaScript表达式,而不能使用多行JavaScript语句。
// 错误
const element = <MyComponent foo={const val = 1 + 2; return val; } />
// 错误
let complete;
const element = (
<div>
{
if(complete){
return <CompletedList />;
}else{
return null;
}
}
</div>
)
不过,JSX中可以使用三目运算符或逻辑与(&&)运算符代替if语
句的作用。
// 正确
let complete;
const element = (
<div>
{
complete ? <CompletedList /> : null
}
</div>
)
//正确
let complete;
const element = (
<div>
{
complete && <CompletedList />
}
</div>
)
4 标签属性
当JSX 标签是DOM类型的标签时,对应DOM标签支持的属性JSX也支持,例如id、class、style、onclick等。但是,部分属性的名称会有所改变,主要的变化有:class 要写成className,事件属性名采用驼峰格式,例如onclick 要写成 onClick。原因是,class是JavaScript的关键字,所以改成className;React对DOM标签支持的事件重新做了封装,
封装时采用了更常用的驼峰命名法命名事件。
<div id='content' className='foo' onClick={() =>
{console.log('Hello, React')}} />
5 注释
JSX中的注释需要用大括号“{}”将/**/包裹起来。
const element = (
<div>
{/* 这里是一个注释 */}
<span>React</span>
</div>
)
6 注释
JSX语法对使用React来说并不是必需的,实际上,JSX语法只是
React.createElement (component, props, …children)的语法糖,所有的JSX
语法最终都会被转换成对这个方法的调用。
//JSX语法
const element = <div className='foo'>Hello, React</div>
//转换后
const element = React.createElement('div', {className:
'foo'}, 'Hello, React')
1.3 组件
1 组件定义
定义一个组件有两种方式,使用ES 6 class(类组件)和使用函数(函数组件)。
使用class定义组件需要满足两个条件:
(1)class继承自React.Component。
(2)class内部必须定义render方法,render方法返回代表该组件UI
的React元素。
假设定义了一个PostList组件。如何使组件显示到页面上呢?
需要使用ReactDOM.render() 完成这一个工作
// index.js
import React from "react";
import ReactDOM from "react-dom";
import PostList from "./PostList";
ReactDOM.render(<PostList />,
document.getElementById("root"));
注意,使用ReactDOM.render() 需要先导入react-dom库,这个库会完成组件所代表的虚拟DOM节点到浏览器的DOM节点的转换。
2 组件的props
组件的props用于把父组件中的数据或方法传递给子组件,供子组件使用。
props是一个简单结构的对象,它包含的属性正是由组件作为JSX标签使用时的属性组成。
<User name='React' age='4' address='America' >
此时User组件的props结构如下:
props = {
name: 'React',
age: '4',
address: 'America'
}
3 组件的state
组件的state是组件内部的状态,state的变化最终将反映到组件UI的变化上。
我们在组件的构造方法constructor中通过this.state定义组件的初始状态,并通过调用this.setState方法改变组件状态(也是改变组件状态的唯一方式),进而组件UI也会随之重新渲染。
// 点赞的例子
import React, { Component } from "react";
class PostItem extends Component {
constructor(props) {
super(props);
this.state = {
vote: 0
};
}
// 处理点赞逻辑
handleClick() {
let vote = this.state.vote;
vote++;
this.setState({
vote: vote
});
}
render() {
const { title, author, date } = this.props;
return (
<li>
<div>
{title}
</div>
<div>
创建人:<span>{author}</span>
</div>
<div>
创建时间:<span>{date}</span>
</div>
<div>
<button
onClick={() => {
this.handleClick();
}}
>
点赞
</button>
<span>
{this.state.vote}
</span>
</div>
</li>
);
}
}
export default PostItem;
注意
(1)在组件的构造方法constructor内,首先要调用super(props),这一步实际上是调用了React.Component这个class的constructor方法,用来完成React组件的初始化工作。
(2)在constructor中,通过this.state定义了组件的状态。
(3)在render方法中,我们为标签定义了处理点击事件的响应函数,在响应函数内部会调用this.setState更新组件的点赞数。
组件的props和state都会直接影响组件的UI。事实上,React组件可以看作一个函数,函数的输入是props和state,函数的输出是组件的UI。
UI = Component(props,state)
React组件正是由props和state两种类型的数据驱动渲染出组件UI。props是组件对外的接口,组件通过props接收外部传入的数据(包括方法);state是组件对内的接口,组件内部状态的变化通过state来反映。另外,props是只读的,你不能在组件内部修改props;state是可变的,组件状态的变化通过修改state来实现。
4 有状态组件和无状态组件
如果一个组件的内部状态是不变的,当然就用不到state,这样的组件称之为无状态组件。
一个组件的内部状态会发生变化,就需要使用state来保存变化,这样的组件称之为有状态组件。
定义无状态组件除了使用ES 6 class的方式外,还可以使用函数定义,也就是我们在本节开始时所说的函数组件。
一个函数组件接收props作为参数,返回代表这个组件UI的React元素结构。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
React应用组件设计的一般思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过props传递给其余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关注组件UI的渲染。
5 属性校验和默认属性
React提供了PropTypes这个对象,用于校验组件属性的类型。
class PostItem extends React.Component {
//......
}
PostItem.propTypes = {
post: PropTypes.object,
onVote: PropTypes.func
};
当使用PropTypes.object或PropTypes.array校验属性类型时,我们只知道这个属性是一个对象或一个数组,至于对象的结构或数组元素的类型是什么样的,依然无从得知。这种情况下,更好的做法是使用PropTypes.shape或PropTypes.arrayOf。
style: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
sequence: PropTypes.arrayOf(PropTypes.number)
表示style是一个对象,对象有color和fontSize两个属性,color是字符串类型,fontSize是数字类型;sequence是一个数组,数组的元素是数字。
如果属性是组件的必需属性,就需要在PropTypes的类型属性上调用isRequired。
PostItem.propTypes = {
post: PropTypes.shape({
id: PropTypes.number,
title: PropTypes.string,
author: PropTypes.string,
date: PropTypes.string,
vote: PropTypes.number
}).isRequired,
onVote: PropTypes.func.isRequired
}
提供了defaultProps,为组件属性指定默认值。
function Welcome(props) {
return <h1 className='foo'>Hello, {props.name}</h1>;
}
Welcome.defaultProps = {
name: 'Stranger'
};
6 组件样式
为组件添加样式的方法主要有两种:外部CSS样式表和内联样式。
1.外部CSS样式表
这种方式和我们平时开发Web应用时使用外部CSS文件相同,CSS样式表中根据HTML标签类型、ID、class等选择器定义元素的样式。唯一的区别是,React元素要使用className来代替class作为选择器。
// style.css
.foo {
width:100%;
height:50px;
background-color:blue;
font-size: 20px;
}
function Welcome(props) {
return <h1 className='foo'>Hello, {props.name}</h1>;
}
样式表的引入方式有两种,一种是在使用组件的HTML页面中通过
标签引入:
<link rel="stylesheet" type="text/css" href="style.css">
另一种是把样式表文件当作一个模块,在使用该样式表的组件中,
像导入其他组件一样导入样式表文件:
import './style.css'; //要保证相对路径设置正确
function Welcome(props) {
return <h1 className='foo'>Hello, {props.name}</h1>;
}
2.内联样式
将CSS样式写到JS文件中,用JS对象表示CSS样式,然后通过DOM类型节点的style属性引用相应样式对象。
function Welcome(props) {
return (
<h1
style={{
width: "100%",
height: "50px",
backgroundColor: "blue",
fontSize: "20px"
}}
>
Hello, {props.name}
</h1>
);
}
style使用了两个大括号,第一个大括号表示style的值是一个JavaScript表达式,第二个大括号表示这个JavaScript表达式是一个对象。
当使用内联样式时,样式的属性名必须使用驼峰格式的命名。所以,在Welcome组件中,background-color写成backgroundColor,font-size写成fontSize
1.4 组件的生命周期
组件从被创建到被销毁的过程称为组件的生命周期。通常,组件的生命周期可以被分为三个阶段:挂载阶段、更新阶段、卸载阶段。
1 挂载阶段
这个阶段组件被创建,执行初始化,并被挂载到DOM中,完成组件的第一次渲染。
依次调用的生命周期方法有:
(1)constructor
(2)componentWillMount
(3)render
(4)componentDidMount
1.constructor
这是ES 6 class的构造方法,组件被创建时,会首先调用组件的构造方法。这个构造方法接收一个props参数,props是从父组件中传入的属性对象,如果父组件中没有传入属性而组件自身定义了默认属性,那么
这个props指向的就是组件的默认属性。你必须在这个方法中首先调用
super(props)才能保证props被传入组件中。constructor通常用于初始化组
件的state以及绑定事件处理方法等工作。
2.componentWillMount
这个方法在组件被挂载到DOM前调用,且只会被调用一次。这个方法在实际项目中很少会用到,因为可以在该方法中执行的工作都可以提前到constructor中。在这个方法中调用this.setState不会引起组件的重新渲染。
3.render
这是定义组件时唯一必要的方法(组件的其他生命周期方法都可以省略)。在这个方法中,根据组件的props和state返回一个React元素,用于描述组件的UI。
render是一个纯函数,在这个方法中不能执行任何有副作用的操作,所以不能在render中调用this.setState,这会改变组件的状态。
4.componentDidMount
在组件被挂载到DOM后调用,且只会被调用一次。这时候已经可以获取到DOM结构,因此依赖DOM节点的操作可以放到这个方法中。这个方法通常还会用于向服务器端请求数据。在这个方法中调用this.setState会引起组件的重新渲染。
2 更新阶段
组件被挂载到DOM后,组件的props或state可以引起组件更新。props引起的组件更新,本质上是由渲染该组件的父组件引起的,也就是当父组件的render方法被调用时,组件会发生更新过程,这个时候,组件props的值可能发生改变,也可能没有改变,因为父组件可以使用相同的对象或值为组件的props赋值。但是,无论props是否改变,父组件render方法每一次调用,都会导致组件更新。
State引起的组件更新,是通过调用this.setState修改组件state来触发的。
组件更新阶段,依次调用的生命周期方法有:
(1)componentWillReceiveProps
(2)shouldComponentUpdate
(3)componentWillUpdate
(4)render
(5)componentDidUpdate
1.componentWillReceiveProps
这个方法只在props引起的组件更新过程中,才会被调用,此方法在子组件中定义。State引起的组件更新并不会触发该方法的执行。方法的参数nextProps是父组件传递给当前组件的新的props。
2.shouldComponentUpdate
这个方法决定组件是否继续执行更新过程,默认返回true。
一般通过比较nextProps、nextState和组件当前的props、state决定这个方法的返回结果。这个方法可以用来减少组件不必要的渲染,从而优化组件的性能。
3.componentWillUpdate
这个方法在组件render调用前执行,可以作为组件更新发生前执行某些工作的地方,一般也很少用到。
shouldComponentUpdate和componentWillUpdate中都不能调用setState,否则会引起循环调用问题,render永远无法被调用,组件也无法正常渲染。
4.render
5.componentDidUpdate
组件更新后被调用,可以作为操作更新后的DOM的地方。这个方
法的两个参数prevProps、prevState代表组件更新前的props和state。
3 卸载阶段
组件从DOM中被卸载的过程,只有一个生命周期方法:
componentWillUnmount
这个方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。
只有类组件才具有生命周期方法,函数组件是没有生命周期方法的,因此永远不要在函数组件中使用生命周期方法。
1.5 事件处理
在React元素中绑定事件有两点需要注意:
(1)在React中,事件的命名采用驼峰命名方式,而不是DOM元素中的小写字母命名方式。例如,onclick要写成onClick,onchange要写成onChange等。
(2)处理事件的响应函数要以对象的形式赋值给事件属性,而不
是DOM中的字符串形式。
在DOM中绑定一个点击事件这样写:
<button onclick="clickButton()">
Click
</button>
而在React元素中绑定一个点击事件变成这种形式:
<button onclick={clickButton}> //clickButton是一个函数
Click
</button>
ES 6中class并不会为方法自动绑定this到当前对象。
class MyComponent extends React.Component {
handleClick() {
console.log('Button clicked!');
console.log('this:', this); // 这里的this默认可能是undefined,需要通过一些方式绑定
}
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
React事件处理函数的写法主要有三种方式:
1 使用箭头函数
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {number: 0};
}
handleClick(event) {
const number = ++this.state.number;// 这个方法体的this
this.setState({
number: number
});
}
render() {
return (
<div>
<div>{this.state.number}</div>
<button onClick={(event)=>
{this.handleClick(event);}}>
Click
</button>
</div>
);
}
}
因为箭头函数中的this指向的是函数定义时的对象,所以可以保证this总是指向当前组件的实例对象。
2 使用组件方法
直接将组件的方法赋值给元素的事件属性,同时在类的构造函数中,将这个方法的this绑定到当前对象。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {number: 0};
// 将this绑定到this.handleClick方法
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
const number = ++this.state.number;
this.setState({
number: number
});
}
render() {
return (
<div>
<div>{this.state.number}</div>
<button onClick={this.handleClick}>
Click
</button>
</div>
);
}
}
1.6 表单
1 受控组件
表单元素的值是由React来管理的,那么它就是一个受控组件。
对于不同的表单元素,React的控制方式略有不同。
文本框
文本框包含类型为text的input元素和textarea元素。它们受控的主要原理是,通过表单元素的value属性设置表单元素的值,通过表单元素的onChange事件监听值的变化,并将变化同步到React组件的state中。
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {name: '', password: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// 监听用户名和密码两个input值的变化
handleChange(event) {
const target = event.target;
this.setState({[target.name]: target.value});
}
// 表单提交的响应函数
handleSubmit(event) {
console.log('login successfully');
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
用户名:
{/* 通过value 设置input显示内容,通过onChange 监听
value的变化 */}
<input type="text" name="name" value=
{this.state.name} onChange={this.handleChange} />
</label>
<label>
密码:
<input type="password" name="password" value=
{this.state.password} onChange={this.handleChange} />
</label>
<input type="submit" value="登录" />
</form>
);
}
}
列表
在React中,对select的处理方式有所不同,它通过在select上定义value属性来决定哪一个option元素处于选中状态。这样,对select的控制只需要在select这一个元素上修改即可,而不需要关注option元素。
class ReactStackForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'mobx'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// 监听下拉列表的变化
handleChange(event) {
this.setState({value: event.target.value});
}
// 表单提交的响应函数
handleSubmit(event) {
alert('You picked ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick one library:
{/* select的value属性定义当前哪个option元素处于选中状态
*/}
<select value={this.state.value} onChange=
{this.handleChange}>
<option value="react">React</option>
<option value="redux">Redux</option>
<option value="mobx">MobX</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
复选框和单选框
复选框是类型为checkbox的input元素,单选框是类型为radio的input元素,它们的受控方式不同于类型为text的input元素。通常,复选框和单选框的值是不变的,需要改变的是它们的checked状态,因此React控制的属性不再是value属性,而是checked属性。
class ReactStackForm extends React.Component {
constructor(props) {
super(props);
this.state = { react: false, redux: false, mobx: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// 监听复选框变化,设置复选框的checked状态
handleChange(event) {
this.setState({ [event.target.name]:
event.target.checked });
}
// 表单提交的响应函数
handleSubmit(event) {
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
{/* 设置3个复选框 */}
<label>React
<input
type="checkbox"
name="react"
value="react"
checked={this.state.react}
onChange={this.handleChange}
/>
</label>
<label>Redux
<input
type="checkbox"
name="redux"
value="redux"
checked={this.state.redux}
onChange={this.handleChange}
/>
</label>
<label>MobX
<input
type="checkbox"
name="mobx"
value="mobx"
checked={this.state.mobx}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
2 非受控组件
非受控组件不再通过React的state来管理组件的值,一般通过ref对象来直接操作DOM的状态。
import React, { useRef } from 'react';
function NameForm() {
// 使用 useRef 创建一个 ref 对象
const inputRef = useRef(null);
// 处理表单提交
const handleSubmit = (event) => {
event.preventDefault();
// 使用 ref 访问 input 元素的值
alert('A name was submitted: ' + inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
{/* 使用 ref 属性将 ref 对象附加到 input 元素 */}
<label>
名字:
<input type="text" ref={inputRef} />
</label>
<input type="submit" value="提交" />
</form>
);
}
export default NameForm;
1.7 React 16新特性
1 render新的返回类型
render方法支持两种新的返回类型:数组(由React元素组成)和字符串。
class ListComponent extends Component {
render() {
return [
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>
];
}
}
class StringComponent extends Component {
render() {
return "Just a strings";
}
}
渲染
export default class App extends Component {
render() {
return [
<ul>
<ListComponent />
</ul>,
<StringComponent />
];
}
}
2 Portals
Portals特性让我们可以把组件渲染到当前组件树以外的DOM节点上,这个特性典型的应用场景是渲染应用的全局弹框。
Portals的实现依赖ReactDOM的一个新的API:
ReactDOM.createPortal(child, container)
第一个参数child是可以被渲染的React节点,例如React元素、由React元素组成的数组、字符串等,container是一个DOM元素,child将被挂载到这个DOM节点。
3 错误处理
React 16之前,组件在运行期间如果执行出错,就会阻塞整个应用的渲染,这时候只能刷新页面才能恢复应用。React 16引入了新的错误处理机制,默认情况下,当组件中抛出错误时,这个组件会从组件树中卸载,从而避免整个应用的崩溃。
React 16还提供了一种更加友好的错误处理方式——错误边界(Error Boundaries)。错误边界是能够捕获子组件的错误并对其做优雅处理的组件。
定义了componentDidCatch(error, info)这个方法的组件将成为一个错误边界.
2. react进阶
2.1 组件state
1 state和props
state和props又有什么区别呢?state和props都直接和组件的UI渲染有关,它们的变化都会触发组件重新渲染,但props对于使用它的组件来说是只读的,是通过父组件传递过来的,要想修改props,只能在父组件中修改;而state是组件内部自己维护的状态,是可变的。
组件中用到的一个变量是不是应该作为state可以通过下面的4条依据进行判断:
(1)这个变量是否通过props从父组件中获取?如果是,那么它不是一个状态。
(2)这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
(3)这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不是一个状态。
(4)这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性。
2 正确修改state
state 的更新是异步的
调用setState时,组件的state并不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且出于性能原因,可能会将多次setState的状态修改合并成一次状态修改。所以不要依赖当前的state,计算下一个state。
另外,需要注意的是,同样不能依赖当前的props计算下一个状态,因为props的更新也是异步的。
举个例子,在购物车点击购买按钮,购买会增加1.
如果连续点击两次按钮,就会连续调用两次this.setState({quantity: this.state.quantity + 1}),在React合并多次修改为一次的情况下,相当于等价执行了如下代码:
Object.assign(
previousState,
{quantity: this.state.quantity + 1},
{quantity: this.state.quantity + 1}
)
于是,后面的操作覆盖前面的操作,最终购买的数量只增加1。
// 正确
this.setState((preState, props) => ({
counter: preState.quantity + 1;
}))
这个函数有两个参数,第一个是当前最新状态(本次组件状态
修改生效后的状态)的前一个状态preState(本次组件状态修改前的状
态),第二个参数是当前最新的属性props。
state与不可变对象
React官方建议把state当作不可变对象。
当state中的某个状态发生变化时,应该重新创建这个状态对象,而不是直接修改原来的状态。因为在一些场景下,组件会比较state是否有变化,无变化不会重新渲染组件。比较对象时比较的是对象地址,可能会造成对象属性变化了,但是react判定没有变化。
react推荐使用对象扩展语法,生成新对象。
// state是对象
this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'};
}))
// state 是数组
this.setState(preState => ({
books: [...preState.books, 'React Guide'];
}))
2.2 组件与服务器通信
1 组件挂载阶段通信
React官方推荐,在componentDidMount中与服务器进行通信。
这时候组件已经挂载,真实DOM也已经渲染完成,是调用服务器API最安全的地方。
class UserListContainer extends React.Component{
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
}
2 组件更新阶段通信
组件在更新阶段常常需要再次与服务器通信,获取服务器上的最新数据。
componentWillReceiveProps非常适合做这个工作。
class UserListContainer extends React.Component{
componentWillReceiveProps(nextProps) {
if(nextProps.category !== this.props.category) {
fetch('/path/to/user-api?category='+
nextProps.category). then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
}
}
3 组件通信
父子组件通信
(1)父组件向子组件通信
是通过父组件向子组件的props传递数据完成的。
// 子组件
class UserList extends React.Component{
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map(function(user) {
return (
<li key={user.id}>
<span>{user.name}</span>
</li>
);
})}
</ul>
</div>
)
}
}
// 父组件
import UserList from './UserList'
class UserListContainer extends React.Component{
constructor(props){
super(props);
this.state = {
users: []
}
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function(response) {
response.json().then(function(data) {
// 父组件调用setState,触发父组件的重新渲染,进而触发子组件的重新渲染
that.setState({users: data})
});
});
}
render() {
return (
{/* 通过props传递users */}
<UserList users={this.state.users} />
)
}
}
1. 组件通过 state 改变状态后,组件一定会重新渲染吗?
答案:是的,组件一定会重新渲染。
- 在 React 中,当组件的状态(state)通过setState发生改变时,React 会触发父组件的重新渲染。
- 这是因为 React 的核心机制是基于状态驱动的 UI 更新:当状态发生变化时,React 会重新调用父组件的 render 方法(类组件)或重新执行函数组件的主体代码,从而生成新的虚拟 DOM 并与之前的虚拟 DOM 进行比较。
// •每次点击按钮时,setCount 会更新父组件的状态,导致父组件重新渲染。
// •即使 Child 组件没有依赖于父组件的状态,它也会被重新渲染
function Parent() {
const [count, setCount] = useState(0);
console.log('Parent rendered');
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child />
</div>
);
}
function Child() {
console.log('Child rendered');
return <div>Child Component</div>;
}
export default Parent;
2. 父组件重新渲染,一定会触发子组件重新渲染吗?
答案:父组件重新渲染通常会导致子组件重新渲染,但可以通过一些优化手段避免不必要的子组件重新渲染。
- 如果子组件是一个普通的函数组件或类组件,那么即使它的 props 和 state 没有变化,父组件的重新渲染仍然会触发子组件的重新渲染。
- 这是因为 React 默认会对整个组件树进行递归渲染。
但是可以通过一下手段避免不必要的重新渲染。
- 类组件中的 shouldComponentUpdate
- 如果子组件是一个类组件,可以通过实现 shouldComponentUpdate 方法来控制是否需要重新渲染。
- shouldComponentUpdate 接收新的 props 和 state,返回 true 表示需要重新渲染,返回 false 表示不需要重新渲染。
示例:class Child extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate called');
return false; // 不重新渲染
}
render() {
console.log('Child rendered');
return <div>Child Component</div>;
}
}
- 使用 PureComponent 或 React.memo
- 如果子组件是一个类组件,可以继承 React.PureComponent,它会自动对 props 和 state 进行浅比较,避免不必要的重新渲染。
- 对于函数组件,可以使用 React.memo,效果类似。
(2)子组件向父组件通信
依然是props。
父组件可以通过子组件的props传递给子组件一个回调函数,子组件在需要改变父组件数据时,调用这个回调函数即可。
// 子组件
class UserList extends React.Component{
constructor(props){
super(props);
this.state = {
newUser : ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({newUser: e.target.value});
}
// 通过props调用父组件的方法新增用户
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0)
{
this.props.onAddUser(this.state.newUser);
}
}
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map(function(user) {
return (
<li key={user.id}>
<span>{user.name}</span>
</li>
);
})}
</ul>
<input onChange={this.handleChange} value=
{this.state.newUser} />
<button onClick={this.handleClick}>新增</button>
</div>
)
}
}
// 父组件
import UserList from './UserList'
class UserListContainer extends React.Component{
constructor(props){
super(props);
this.state = {
users: []
}
this.handleAddUser = this.handleAddUser.bind(this);
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
// 新增用户
handleAddUser(user) {
var that = this;
fetch('/path/to/save-user-api',{
method: 'POST',
body: JSON.stringify({'username':user})
}).then(function(response) {
response.json().then(function(newUser) {
// 将服务器端返回的新用户添加到state中
that.setState((preState) => ({users:
preState.users.concat ([newUser])}))
});
});
}
render() {
return (
{/* 通过props传递users和 handleAddUser方法 */}
<UserList users={this.state.users}
onAddUser={this.handleAddUser}
/>
)
}
}
子组件UserList通过调用props.onAddUser方法成功地将待新增的用户传递给父组件UserListContainer的handleAddUser方法执行保存操作,保存成功后,UserListContainer会更新状态users,从而又将最新的用户列表传递给UserList。这一过程既包含子组件到父组件的通信,又包含父组件到子组件的通信,而通信的桥梁就是通过props传递的数据和回调方法。
兄弟组件通信
当两个组件不是父子关系但有相同的父组件时,称为兄弟组件。兄弟组件在整个组件树上并不一定处于同一层级。
兄弟组件不能直接相互传送数据,需要通过state状态提升的方式实现兄弟组件的通信,即把组件之间需要共享的状态保存到距离它们最近的共同父组件内,任意一个兄弟组件都可以通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过props向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
Context
React提供了一个context上下文,让任意层级的子组件都可以获取父组件中的状态和方法。
创建context的方式是:在提供context的组件内新增一个getChildContext方
法,返回context对象,然后在组件的childContextTypes属性上定义context对象的属性的类型信息。
class UserListContainer extends React.Component{
/** 省略其余代码 **/
// 创建context对象,包含onAddUser方法
getChildContext() {
return {onAddUser: this.handleAddUser};
}
// 新增用户
handleAddUser(user) {
this.setState((preState) => ({users:
preState.users.concat([{'id':'c', 'name': 'cc' }])}))
}
render() {
const filterUsers = this.state.users.filter((user) =>
{user.id = this.state.currentUserId});
const currentUser = filterUsers.length > 0 ?
filterUsers[0] : null;
return (
<UserList users={this.state.users}
currentUserId = {this.state.currentUserId}
onSetCurrentUser = {this.handleSetCurrentUser}
/>
<UserDetail currentUser = {currentUser} />
)
}
}
// 声明context的属性的类型信息
UserListContainer.childContextTypes = {
onAddUser: PropTypes.func
};
class UserAdd extends React.Component{
/**省略其余代码**/
handleChange(e) {
this.setState({newUser: e.target.value});
}
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0)
{
this.context.onAddUser(this.state.newUser);
}
}
render() {
return (
<div>
<input onChange={this.handleChange} value=
{this.state.newUser} />
<button onClick={this.handleClick}>Add</button>
</div>
)
}
}
// 声明要使用的context对象的属性
UserAdd.contextTypes = {
onAddUser: PropTypes.func
};
增加contextTypes后,在UserAdd内部就可以通过this.context.onAddUser的方式访问context中的onAddUser方法。
这里的示例传递的是组件的方法,组件中的任意数据也可以通过context自动向下传递。另外,当context中包含数据时,如果要修改context中的数据,一定不能直接修改,而是要通过setState修改,组件state的变化会创建一个新的context,然后重新传递给子组件。
2.3 特殊的ref
ref不仅可以用来获取表单元素,还可以用来获取其他任意DOM元素,甚至可
以用来获取React组件实例。
但绝大多数场景下,应该避免使用ref,因为它破坏了React中以props为数据传递介质的典型数据流。
在DOM元素上使用ref
class AutoFocusTextInput extends React.Component {
componentDidMount() {
// 通过ref让input自动获取焦点
this.textInput.focus();
}
render() {
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
</div>
);
}
}
在组件上使用ref
React组件也可以定义ref,此时ref的回调函数接收的参数是当前组件的实例,这提供了一种在组件外部操作组件的方式。
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.blur = this.blur.bind(this);
}
componentDidMount() {
// 通过ref让input自动获取焦点
this.textInput.focus();
}
// 让input失去焦点
blur() {
this.textInput.blur();
}
render() {
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
</div>
);
}
}
class Container extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 通过ref调用AutoFocusTextInput组件的方法
this.inputInstance.blur();
}
render() {
return (
<div>
<AutoFocusTextInput ref={(input) =>
{this.inputInstance = input}}/>
<button onClick={this.handleClick}>失去焦点</button>
</div>
);
}
}
注意,只能为类组件定义ref属性,而不能为函数组件定义ref属性。
2.4 虚拟DOM和性能优化
1 虚拟DOM
在传统的前端开发中,对DOM进行增删改操作,每一次对DOM的修改都会引起浏览器对网页的重新布局和重新渲染,而这个过程是很耗时的。这也是为什么前端性能优化中有一条原则:尽量减少DOM操作。
DOM效率低下的这个问题通过增加一层抽象解决。虚拟DOM就是这层抽象。
虚拟DOM使用普通的JavaScript对象来描述DOM元素。例如,下面是一个DOM结构:
<div className="foo">
<h1>Hello React</h1>
</div>
可以用这样的一个JavaScript对象来表述:
{
type: 'div',
props: {
className: 'foo',
children: {
type: 'h1',
props:{
children: 'Hello React'
}
}
}
}
虚拟DOM是普通的JavaScript对象,访问JavaScript对象当然比访问真实DOM要快得多。到这里,大家可以发现,虚拟DOM并不是什么神奇的东西,它只是用来描述真实DOM的JavaScript对象而已。
2 Diff算法
React采用声明式的API描述UI结构,每次组件的状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象,用来表述新的UI结构。React会通过比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM上执行的操作,提高程序执行效率。这一过程就是React的调和过程(Reconciliation),其中的关键是比较两个树形结构的Diff算法。
尽量不要使用元素在列表中的索引值作为key,因为列表中的元素顺序一旦发
生改变,就可能导致大量的key失效,进而引起大量的修改操作。
2 性能优化
1.使用生产环境版本的库
2.通过shouldComponentUpdate避免不必要的组件渲染
3.对列表元素使用key,避免顺序错乱导致大量的重新渲染
2.5 高阶组件
高阶组件(简称HOC)接收React组件作为参数,并且返回一个新的React组件。高阶组件本质上也是一个函数。
例如:
class MyComponent extends Component {
render() {
return <div>{this.props.data}</div>
}
}
// 组件给所有的传入组件增加componentWillMount逻辑,不用每个组件有单独写逻辑
import React, { Component } from 'react'
function withPersistentData(WrappedComponent1) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
return <WrappedComponent1 data={this.state.data}
{...this.props} />
}
}
}
const MyComponentWithPersistentData = withPersistentData(MyComponent)
通过这个例子可以看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。
3. react实战
3.1 路由:用React Router开发单页面应用
通过不同的URL标识不同的页面,也就是存在页面间路由的需求,这时候就用React Router。页面路由的控制由服务器端负责,这种路由方式称为后端路由。这种方式,每次页面切换都需要向服务器发送一次请求,页面使用到的静态资源也需要重新请求加载,存在一定的浪费。
而单页面应用,上无论URL如何变化,对应的HTML文件都是同一个。页面内容发生变化,但不会向服务器发送新的请求。
所以“逻辑页面”的路由只能由前端负责,这种路由方式称为前端路由。
3.2 Router、Route
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import Login from "./components/Login";
class App extends Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/posts" component={Home} />
</Switch>
</Router>
);
}
}
export default App;
一个应用中只需要一个Router实例,所有的路由配置组件Route都定义为Router的子组件。
Route提供了3个属性,用于定义待渲染的组件。
- component
Route中,component的值是一个组件,当URL和Route匹配时,component属性定义的组件就会被渲染。 - render
render的值是一个函数,这个函数返回一个React元素。这种方式可以为待渲染的组件传递额外的属性
例如
render() {
const { match, location } = this.props;
const { userId, username } = this.state;
return (
<div>
<Header
username={username}
onLogout={this.handleLogout}
location={location}
/>
<Route
path={match.url}
exact
render={props => <PostList userId={userId} {...props} />}
/>
<Route
path={`${match.url}/:id`}
render={props => <Post userId={userId} {...props} />}
/>
</div>
);
}
3.3 Switch、exact
当URL和多个Route匹配时,这些Route都会执行渲染操作。如果只想让第一个匹配的Route渲染,那么可以把这些Route包到一个Switch组件中。如果想让URL和Route完全匹配时,Route才渲染,那么可以使用
Route的exact属性。
3.3 嵌套路由
嵌套路由是指在Route渲染的组件内部定义新的Route。
const Posts = ({ match }) => {
return (
<div>
{/* 这里match.url 等于 /posts */}
<Route path={`${match.url}/:id`} component=
{PostDetail} />
<Route exact path={match.url} component={PostList} />
</div>
);
}
3.4 Link
Link组件定义了当点击该Link时,页面应该如何路由。
<Link to={{ pathname: "/login", state: { from: location } }}>
登录
</Link>
1.基本用法
在传统的Web应用中,浏览器根据地址栏的URL向服务器发送一个HTTP请求,服务器根据URL返回一个HTML页面。
4. Redux
4.1
5. 问题
1.react在构造函数中调用bind(this)方法,是为什么
在 React 中,尤其是在类组件中,你可能会看到在构造函数中使用 bind(this) 方法。这种做法的目的是为了解决 JavaScript 中 this 的绑定问题。以下是详细的原因和解释:
1. JavaScript 中的 this 绑定规则在 JavaScript 中,this 的值取决于函数的调用方式,而不是函数定义的位置。这可能导致以下问题:
- 在类组件中,如果你直接将类的方法(如事件处理函数)传递给子组件或 DOM 元素,那么该方法在调用时的 this 可能会丢失。
- 如果没有正确绑定 this,函数内部的 this 将指向 undefined 或其他意外的对象。
示例:this 丢失的问题
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "Hello, World!" };
}
handleClick() {
console.log(this.state.message); // 这里的 `this` 是 undefined
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
在这个例子中,当你点击按钮时,控制台会报错,提示 Cannot read properties of undefined (reading ‘state’)。这是因为 handleClick 方法被作为回调函数传递给了 onClick,而它的 this 没有绑定到当前的类实例。
2. 使用 bind(this) 解决问题
通过在构造函数中显式地调用 bind(this),可以确保方法在任何地方调用时,this 都指向当前的类实例。
修改后的代码:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "Hello, World!" };
// 手动绑定 this
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.state.message); // 正确输出 "Hello, World!"
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
- 在构造函数中,this.handleClick = this.handleClick.bind(this); 将 handleClick 方法绑定到当前的类实例。
- 现在,无论 handleClick 被如何调用,this 都会指向 MyComponent 实例。
3. 为什么需要在构造函数中绑定?
虽然你可以在其他地方绑定 this(例如在 render 方法中),但在构造函数中绑定是最高效和推荐的方式,原因如下:
-
- 性能优化:
- 如果你在 render 方法中绑定 this(例如 ),每次组件重新渲染时都会创建一个新的函数实例。这会增加不必要的性能开销。
- 在构造函数中绑定只会在组件初始化时执行一次,避免了重复绑定的性能问题。
-
- 代码清晰性:
- 在构造函数中集中绑定 this,可以让代码更易于维护和理解。
4. 替代方案:使用箭头函数
从 ES6 开始,箭头函数提供了一种更简洁的方式来解决 this 绑定问题。箭头函数不会创建自己的 this,而是继承自外层作用域的 this。
示例:使用箭头函数
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "Hello, World!" };
}
// 使用箭头函数定义方法
handleClick = () => {
console.log(this.state.message); // 正确输出 "Hello, World!"
};
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
- 在这个例子中,handleClick 是一个箭头函数,因此它自动绑定了 this,无需手动调用 bind(this)。
- 这种方式更加简洁,但需要注意的是,箭头函数是类的实例属性,而不是原型方法。这意味着每个组件实例都会有自己的 handleClick 方法副本,可能稍微增加内存消耗。
5. 总结
- 为什么需要 bind(this)?
- 在 JavaScript 中,this 的值取决于函数的调用方式。如果类方法被作为回调函数传递,this 可能会丢失。
- 使用 bind(this) 确保方法中的 this 指向当前的类实例。
- 为什么在构造函数中绑定?
- 避免在每次渲染时重复绑定,提高性能。
- 代码更加清晰和集中。
- 替代方案:箭头函数
- 使用箭头函数可以自动绑定 this,无需手动调用 bind(this)。
- 更加简洁,但需要注意其对内存的影响。