React学习笔记
1.前言
本文记录了React学习的全过程以及个人理解,由于作者对Vue比较熟悉,会在文章中将陌生的React知识点用Vue中的相应内容做比较,以加深理解。
2.环境搭建
全局安装react官方脚手架
npm install -g create-react-app // 安装失败提示没有权限则加上sudo命令
初始化一个用于学习的react项目
create-react-app react-learn
进入并启动项目
cd react-learn
npm start
3.主要概念
3.1 Hello World
学习任何一门语言都是从Hello World开始,React也不例外:
-将src目录下的所有文件删除,并新建一个index.js和一个index.css,
在index.js中写入如下代码
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
ReactDOM.render(
<h1>Hello, world</h1>,
document.getElementById('root')
)
来看一下上面例子中的代码,ReactDOM
对象通过render
方法,传入两个参数,第一个参数为一个JSX表达式,第二个对象是挂载的目标节点,这样就达到了Hello World的效果
打开http://localhost:3000/,就可以看到Hello World的字样啦
3.2 JSX
什么是JSX
JSX,是一种 JavaScript 的语法扩展,在React中,使用JSX来描述用户界面。
在Vue中,我们使用template
(模板)进行用户界面的实现,而JSX就是类似于template
的一种语法,但需要主要的是,JSX是由JavaSript实现的,这也就是它和Vue的重要区别。
那么一个赋值为JSX的变量究竟是怎样一个东西呢,我们修改代码为如下格式:
const element = <h1>Hello, world</h1>
console.log(element)
ReactDOM.render(
element,
document.getElementById('root')
)
在控制台得到这样的输出:
原来它是一个React封装好的对象,这里我们先不作深究。
在JSX中使用表达式
在Vue中,我们在template
中可以插入js表达式,在React中,我们也可以在JSX中插入js表达式:
function formatName(user) {
return user.firstName + ' ' + user.lastName
}
const user = {
firstName: 'Oliver',
lastName: 'Shen'
}
const element = <h1>Hello,{formatName(user)}</h1>
console.log(element)
ReactDOM.render(
element,
document.getElementById('root')
)
不同的是,在Vue中只能插入data和methods中声明的变量和方法,而在React中可以插入任何位置声明的变量和方法。
贯彻着你中有我,我中有你的思想,由于JSX编译后会被转化为js对象,你可以将它赋值给任意的变量,甚至在方法中作为返回值:
function formatName(user) {
return user.firstName + ' ' + user.lastName
}
const user = {
firstName: 'Oliver',
lastName: 'Shen'
}
function getJSX() {
return <h1>Hello,{formatName(user)}</h1>
}
ReactDOM.render(
getJSX(),
document.getElementById('root')
)
3.3 JSX属性
与Vue相同,你可以为JSX中的标签设置属性和值,值可以是字符串,也可以是一个变量。
const isEditable = true
const element = <div contentEditable="false"></div>
const element2 = <div contentEditable={isEditable}></div>
3.4 JSX嵌套
JSX自然也是可以嵌套使用的:
onst element = <div></div>
const element2 = <div>{element}</div>
ReactDOM.render(
element2,
document.getElementById('root')
)
3.5 另一种书写方式
由于Babel会把JSX转换成React.createElement()的方法调用,我们可以直接用这个方法书写React元素:
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
4.元素渲染
元素,也就是上面定义的一个个element,是构成React应用的最小单位。在主要概念中也讲到了RenderDOM.render()这个方法的作用,
这里要讲的是,如何更新元素渲染
在React中,所有元素都是不可变的,也就是在元素创建之后,无法改变其内容或属性,这里有一种通过setInterval()方法实现更新的方式:
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);
此时,我们打开浏览器检查元素,会发现,只有不断改变的这个变量,在渲染过程中进行了更新。
也就是说:即便我们每秒都创建了一个描述整个UI树的新元素,React DOM 也只会更新渲染文本节点中发生变化的内容。
5.组件&Props
5.1 定义组件
在Vue中,一个.vue文件就可以称为一个组件,而在React中,一个组件可以是一个JavaScript函数:
function Hello(props) {
return <h1>Hello, {props.name}</h1>
}
ReactDOM.render(
<Hello name="Oliver"></Hello>,
document.getElementById('root')
)
上述代码中,定义了一个Hello函数,其实就是定义了一个Hello组件,这个组件接受一个props参数(类似Vue中的data),会读取传入的所有属性,并在组件中使用。需要注意的是:组件的首字母必须是大写的,否则无法区分html的原生标签,无法通过编译。
我们常用这种方式定义和使用组件:
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
ReactDOM.render(
<Hello name="Oliver"></Hello>,
document.getElementById('root')
)
- 上述两种方法有何区别呢?
通过类实现的组件可以使用其私用的状态,也就是生命周期的钩子函数,而通过声明函数实现的组件缺不行,所以接下来的组件,都会以类的形式展现
5.2 提取组件
与Vue相同,在开发过程中,我们希望代码能够简介易读,同时能够提高代码的复用性,这也是组件的最初用意,我们试着在React中提取代码,封装成组件。
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
class Info extends React.Component {
render() {
return <ul><li>Name: {this.props.name}</li><li>Age: {this.props.age}</li></ul>
}
}
class Wrap extends React.Component {
render() {
return <section><Hello name={this.props.name}></Hello><Info name={this.props.name} age={this.props.age}></Info></section>
}
}
ReactDOM.render(
<Wrap name="Oliver" age="22"></Wrap>,
document.getElementById('root')
)
上述代码提取了一个Hello组件和一个Info组件,并在Wrap组件中进行调用。
5.3 Props的只读性
顾名思义,在React中,自身的props是不可修改的:
class Hello extends React.Component {
render() {
this.props.name = 'Tom' // error
return <h1>Hello, {this.props.name}</h1>
}
}
那我们如何修改组件的状态呢,且听下回分解。
6.State&生命周期
6.1 State
在React中,我们使用state(状态)作为组件内可修改的数据,但它只能在以类声明的组件中使用,如:
class Clock extends React.Component {
static defaultProps = {
name: 'sls',
age: 23
};
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
其中,在constructor
中声明该组件的state和props。注意,此处的props是通过super
方法继承父组件的—这就类似于vue中从父组件获取data,在子组件中用props接收,并且可通过defaultProps
关键字设置默认值。
那么问题来了,既然state是可修改的,那如何正确的使用state呢?
- 使用setState()
this.setState({comment: 'Hello'});
setState
会将参数中提供的对象浅合并到当前状态,这意味着该合并之后影响到state中你提供的属性,说白了也就是属性的修改不相互影响
- 状态更新可能是异步的
当setState修改到的属性依赖于其他属性时,使用以下方式:
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
看完了state的工作原理,这里就要引出react区别于vue,angular的一点了,那就是vue中数据的单向流动:
这通常被称为自顶向下或单向数据流。 任何状态始终由某些特定组件所有,并且从该状态导出的任何数据或 UI 只能影响树中下方的组件。
6.2 生命周期
本章参考了掘金文章图解ES6中的React生命周期
React的生命周期与Vue相似,但又有一定的差异,我们对照着来看:
- React
- vue
一、初始化阶段
1.设置组件的默认属性
static defaultProps = {
name: 'sls',
age: 23
};
// or
Counter.defaultProps = {
name: 'sls',
age: 23
};
defaultProps是react中使用类实现组件时保留的关键字,用于初始化组件的props,并且props作为组件的属性是不可修改的
2.设置组件的初始化状态
constructor() {
super();
this.state = {number: 0}
}
state也是 react使用类实现组件时保留的关键字,用于初始化组件的states,区别于props,states是可修改的,这也意味着,react中的states类似于vue中的data,常用于实际操作,如:父组件传值给子组件,父组件传递state给子组件,子组件用props接收。
上述两个过程类似于Vue中的create过程,初始化的组件的数据(data)
3.componentWillMount()
组件即将被渲染到页面之前触发,此时可以进行开启定时器、向服务器发送请求等操作,该钩子相当于vue中的beforeMount();
二、运行中阶段
1.componentWillReceiveProps()
组件接收到属性时触发,组件首次渲染时并不会触发
2.shouldComponentUpdate()
当组件接收到新属性,或者组件的状态发生改变时触发。组件首次渲染时并不会触发
shouldComponentUpdate(newProps, newState) {
if (newProps.number < 5) return true;
return false
}
//该钩子函数可以接收到两个参数,新的属性和状态,返回true/false来控制组件是否需要更新。
当一个父组件拥有多个子组件时,会出现以下这种影响性能的问题:父组件的state更新,导致所有子组件重新执行render方法,形成新的虚拟DOM,再通过比较新旧虚拟DOM判断组件是否需要重新渲染,这无疑造成了性能浪费。
因此我们可以在shouldComponentUpdate()
中加入条件判断,筛去不必要的更新,从而优化性能
3.componentWillUpdate()
组件即将被更新时触发,相当于vue中的beforeUpdate(),
4.componentDidUpdate()
组件被更新完成后触发。页面中产生了新的DOM的元素,可以进行DOM操作,相当于vue中的updated()
三、销毁阶段
1.componentWillUnmount()
组件被销毁时触发。这里我们可以进行一些清理操作,例如清理定时器,取消Redux的订阅事件等等。相当于vue中的beforeDestroy()
7.事件处理
在React中,所有的事件命名采用驼峰式写法,并且传入的参数为js表达式:
<button onClick={handleClick}>Click</button>
在实际应用常中使用类声明组件的方式,则相应的事件处理如下:
class Alert extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'Oliver'
}
// this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this.state)
alert('hahahahaha')
}
render() {
return (
// <button onClick={this.handleClick}>click</button> // 如果使用bind绑定this则不需要使用箭头函数
<button onClick={() => this.handleClick()}>click</button>
)
}
}
由于React中,类的方法不会自动绑定this到当前组件上,因此需要通过bind为这个方法绑定this;或者也可以使用箭头函数,保持this指向当前组件。
- 参数传递
React中会自动向事件的回调函数返回一个event参数,你可以通过以下方式接收:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
// or
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button> // 该方法的e已通过隐式传递
当然你也可以传入自定义的参数,需要注意的是e必须在你传入的自定参数之后
8.条件渲染
条件渲染,也就是在通过不同条件控制组件是否渲染,渲染内容, 这一章更多的是讲在实际项目中的运用了,也就是Vue中的v-if指令了
function LoginButton(props) {
return <button onClick={props.onClick}>Login</button>
}
function LogoutButton(props) {
return <button onClick={props.onClick}>Logout</button>
}
class LoginArea extends React.Component {
constructor(props) {
super(props)
this.state = {
status: false
}
}
login() {
this.setState({
status: true
})
}
logout() {
this.setState({
status: false
})
}
render() {
let button = null
if (this.state.status) {
button = <LoginButton onClick={() => this.logout()} />
} else {
button = <LogoutButton onClick={() => this.login()} />
}
return <div>{button}</div>
}
}
这里通过if判断status的状态,从而渲染不同的内容,这就类似于vue中的
v-if
指令。在render
方法中可以也可以写一些业务代码,vue中的watch
和computed
方法可以在这里有相似的实现
- 阻止组件渲染
有时候,你可能希望隐藏组件,即使它被其他组件渲染。让 render 方法返回 null 而不是它的渲染结果即可实现:
function Warning(props) {
if (!props.isShow) {
return null
}
return <div>warning!!!!!</div>
}
class WarnArea extends React.Component {
constructor(props) {
super(props)
this.state = {
isShow: false
}
}
showWarning() {
this.setState(prevState => ({
isShow: !prevState.isShow
}))
}
render() {
return (
<div>
<Warning isShow={this.state.isShow} />
<button onClick={() => this.showWarning()}>
{this.state.isShow ? "hidden" : "show"}
</button>
</div>
)
}
}
9.列表渲染
列表渲染也就是通过js循环的方式,生成一个列表,一次性渲染,这和Vue中的v-for指令是相似的:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
通常,我们使用map方法来实现列表渲染, 这种声明式的代码可读性更好
与Vue相同,列表循环的列表项必须指定key属性:
function ListView(props) {
return props.numbers.map(number => {
return <li key={number}>{number * 2}</li>
})
}
class List extends React.Component {
constructor(props) {
super(props)
this.state = {
numbers: [1, 2, 3, 4, 5]
}
}
render() {
return (
<div>
<ul>
<ListView numbers={this.state.numbers} />
</ul>
<ul>
<ListView numbers={this.state.numbers} />
</ul>
</div>
)
}
}
需要注意的是,这里和vue有一定的区别:在Vue中,key值是组件内的唯一的,而在React中,key值是在兄弟元素间唯一的
10.受控组件&非受控组件
受控组件&非受控组件是指表单的可输入/可选择标签:input
,textarea
,select
是否受用户控制。
受控组件
受控组件是指:用户的操作能够影响到组件的状态:
class MyForm extends React.Component {
constructor(props) {
super(props)
this.state = {
value: "haha"
}
}
handleChange(event) {
this.setState({
value: event.target.value
})
}
handleClick(event) {
alert(this.state.value)
event.preventDefault()
}
changeState(event) {
event.preventDefault()
this.setState({
value: "heihei"
})
}
render() {
return (
<form action="">
<input
type="text"
value={this.state.value}
onChange={e => this.handleChange(e)}
/>
<input
type="submit"
value="Submit"
onClick={e => this.handleClick(e)}
/>
<button onClick={e => this.changeState(e)}>change</button>
</form>
)
}
}
上述代码中能够,为input绑定了一个value,value的初始值即为state中的value属性。此时用户手动输入会造成value的变化,但这不会修改到state,因此,通过onChange方法,获取value并修改了state。这便是受控组件,并且state的value也和input实现了双向绑定。
非受控组件
相对的,非受控组件是指:用户的输入不会影响state中绑定的属性。
class MyForm extends React.Component {
constructor(props) {
super(props)
this.state = {
value: "haha"
}
}
handleClick(event) {
this.setState({
value: this.input.value
})
alert(this.state.value)
alert(this.input.value)
event.preventDefault()
}
render() {
return (
<form action="">
<input
type="text"
defaultValue={this.state.value}
ref={input => (this.input = input)}
/>
<input
type="submit"
value="Submit"
onClick={e => this.handleClick(e)}
/>
</form>
)
}
}
在非受控组件中,绑定属性需要使用
defaultValue
这个属性,当用户修改输入后,我们输出了state,发现其value属性并未发生变化,这也就实现了非受控组件。如果需要获取input变化后的值,可以通过ref中的回调参数来得到,回调返回的是一个input元素,通过其value属性可以获得相应的值
11.状态提升
由于React是单向数据流,因此无法像Vue那样做到父子组件通信。在React中,这种类似父子组件通信的场景是通过状态提升来实现的。
所谓状态提升,是指当遇到多个组件共用状态数据时,将状态数据提升到离它们最近的公共父组件当中,由父组件进行管理。
下面实现一个百分数与非百分数的相互转换组件:
- 转换函数
该函数根据传入的currenType判断为何种类型的数据,再将value进行相应转换
function change(value, currentType) {
if (isNaN(value)) {
return ""
}
if (currentType === 1) {
return value * 100
} else if (currentType === 2) {
return value / 100
}
}
- 定义子组件
定义子组件,并传入以下props:- type: 类型名称
- value:对应输入框的值
- onValueChange: 输入框值发生改变的回调
子组件的状态提升到了父组件,也就不需要再子组件中对数据进行操作了
class MyInput extends React.Component {
constructor(props) {
super(props)
}
handleChange(e) {
this.props.onValueChange(e.target.value)
}
render() {
return (
<fieldset>
<legend>{this.props.type}</legend>
<input
type="text"
value={this.props.value}
onChange={e => this.handleChange(e)}
/>
</fieldset>
)
}
}
- 定义父组件
父组件中的state由内部函数onValue1Change和onValue2Change控制,这两个函数是子组件的回调,当某个子组件修改数据时,执行相应回调
const TYPE_1 = "正常数据"
const TYPE_2 = "百分比"
// 父组件
class Wrap extends React.Component {
constructor(props) {
super(props)
this.state = {
currentType: 1,
value: 0
}
this.onValue1Change = this.onValue1Change.bind(this)
this.onValue2Change = this.onValue2Change.bind(this)
}
onValue1Change(value) {
this.setState({
currentType: 1,
value
})
}
onValue2Change(value) {
this.setState({
currentType: 2,
value
})
}
render() {
const currentType = this.state.currentType
const value = this.state.value
console.log(currentType, value)
const type_1Value = currentType === 1 ? value : change(value, currentType)
const type_2Value = currentType === 2 ? value : change(value, currentType)
return (
<div>
<MyInput
type={TYPE_1}
value={type_1Value}
onValueChange={this.onValue1Change}
/>
<MyInput
type={TYPE_2}
value={type_2Value}
onValueChange={this.onValue2Change}
/>
</div>
)
}
}
12.组合
组合这个概念在之前的学习中其实已经有涉及了,如:
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
在父组件中向子组件传入属性,以达到子组件的特殊实现,这就是组合的一种实现
接下来讲一讲组合的另一种实现:包含关系
包含关系类似于Vue中的插槽(slot),具体实现如下:
// 子组件
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{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>
);
}
父组件可以引用子组件的标签内些内容,其全部内容会通过props的
children
属性传递到子组件,并在子组件调用的位置展示
13.结束语
至此,React的基本概念已经学习完了,说实话与Vue相比异曲同工,但又略显复杂,接下来要做的便是在项目中实战了。之后可能会写一篇React的进阶篇,尽情期待吧~