React入门学习笔记

React介绍

谷歌大法,一搜一大把

React环境安装

安装reactreact-dom模块:

1
cnpm install react react-dom --save

因为react中使用了JSX语法,所以需要babel进行转换:

1
cnpm install babel-preset-react babel-core --save-dev

如果项目使用ES6语法,还需要一个ES6转ES5的preset

react官方已极力推荐使用ES6语法编写应用,官方的demo都是基于ES6的。

1
cnpm install babel-preset-es2015 --save-dev

React团队提供了一个create-react-app的工具,只需在命令行输入create-react-app <app-name>,工具会帮你安装reactreact-domreact-script并生成项目的基本框架。

webpack配置

在安装完成后我们首先编写一段react语法的代码

main.js:

1
2
3
4
5
6
7
import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
<h1>Hello react!</h1>,
document.getElementById("app")
)

index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">hello vue!</div>
</body>
</html>

全局安装webapck:

cnpm install webpack -g

在项目根目录安装webpack:

cnpm install webpack --save-dev

然后在配置webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var webpack = require('webpack');

module.exports = {
entry: './index.jsx',
output: {
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
}
};

然后执行webpack打包就可以了

如果要热更新的话可以再安装:

cnpm install webpack-dev-server --save

然后执行:webpack-dev-server --progress --inline来启动热更新的服务器

当然也可以将这个命令写到package.jsonscripts里,然后通过npm run dev来启动

1
2
3
4
5
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --progress --inline",
"build": "webpack --env production"
}

或者一次性安装所有的:

cnpm install babel-core babel-loader babel-preset-es2015 babel-preset-react react react-dom webpack webpack-dev-server --save

React组件

组件化开发是react开发中最重要的功能,可以简单的认为react就是开发组件的.

组件系统react的重要概念,因为它是一种抽象,允许我们使用小型、自包含和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

组件树

定义react组件

编写一个Header组件:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react'

// 编写一个Header类继承自React.Component类
// 类名首字母要大写:JSX解析时会把小写开头的当做HTML标签解析
export default class Header extends React.Component{
render() {
// return后面的dom定义不能换行
return <header>
<h1>我是头部</h1>
</header>
}
}

render方法是React组件必要的方法。render方法中使用JSX语法定义DOM结构。

如果组件比较简单,还可以使用函数式声明的组件:

1
2
3
4
5
6
> function Header() {
> return <header>
> <h1>我是头部</h1>
> </header>
> }
>

>

具体使用详见官方文档:https://reactjs.org/docs/components-and-props.html#functional-and-class-components

main.js中引用上面定义的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'
import ReactDOM from 'react-dom'
// 引入组件
import Header from './components/header'

class App extends React.Component{
render() {
// 要换行得加括号
return (
// 只允许有一个顶层元素
// 顶层元素内部可以嵌套多个组件
<div>
<Header/>
<main>页面的主体内容</main>
</div>
)
}
}

// 定义app组件渲染的位置,相当于入口
ReactDOM.render(<App/>, document.getElementById("app"))

render方法中只允许有一个顶层元素返回,不过React提供了一个React.Fragment可以让你嵌套多个子元素

1
2
3
4
5
6
7
8
9
> render() {
> return (
> <React.Fragment>
> Some text.
> <h2>A heading</h2>
> </React.Fragment>
> );
> }
>

>

JSX

JSX=JS+XHTML,其中XHTML要求HTML格式符合XML标准,不能像写HTML一样随意:标签闭合,空标签<tag/>

JSX基本的解析规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。上面代码的运行结果如下。

其实JSX本质上会创建虚拟DOM(virtual-dom)对象。

比如上面的Header定义会被转换成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default class Header extends React.Component{
render() {
// return后面的dom定义不能换行
return <header className='header'>
<h1>我是头部</h1>
</header>
}
}
==>
var Header = React.createClass({
render: function() {
return React.createElement(
'header',
// js中通过className获取元素的class属性
{ className: 'header'},
React.createElement('h1',null,'我是头部')
);
}
});
==>
<header class='header'>
<h1>我是头部</h1>
</header>

React.createClass就是用来创建React组件的。

React.createClass是ES5创建组件的方式,新版本的React推荐使用继承React.Component的方式创建组件。如果不想使用ES6,需要添加create-react-class模块依赖,具体可以参考官方文档:React Without ES6

React.createElement就是用来创建虚拟DOM对象的。

注意:上面的header元素不通过class而使用className来引用CSS类,不仅仅因为class与ES6的关键字冲突;最主要的原因React将标签中的所有属性最终会作为一个对象传给React.createElement方法,JS中通过className(字符形式)或classList(数组形式)属性来操作class,显然用className更合适。

虚拟DOM

参考React官网的文档:https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom

Web App性能问题主要出在 DOM 对象的操作上,比如读写,创建,插入等等。之所以原生 DOM 性能低,是因为 DOM 的规范迫使浏览器在实现的时候为每一个 DOM 元素添加了非常多的属性,然而这其中很多我们都用不到。

具体到 React 主要是做了两点

其一是 VirtualDOM,一个很简化的虚拟文档对象模型系统,你可以操作类似 DOM 的对象,但是非常轻量化(这就是为啥 JSX 看起来是在 JS 代码里写 XML 的缘故,这是一层语法糖,方便开发者编写模板,但实际上还是 JS 对象,而不是真实的 DOM 对象);

其二则是当数据变化的时候,不直接去修改 DOM(因为变化的只是个别属性,但是修改 DOM 往往却要替换一整个 DOM 对象),而是先用dom diff算法(简单的理解成git diff这样的命令)比较前后的差异,最后只把变化的部分一次性应用到真实的 DOM 树上去。

可以联想到游戏开发中经常提到的“双缓冲”机制,两者有异曲同工之妙

使用组件对象的props访问标签属性

1
2
3
4
5
6
7
8
9
class Header extends React.Component{
render() {
return <header className='header'>
<h1>{this.props.text}</h1>
</header>
}
}

ReactDOM.render(<Header text='我是头部'/>, document.getElementById("app"));

this.props.children访问标签子元素

props中还有一个特殊的属性children用于访问标签中的子元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class List extends React.Component{
render() {
return <ol>{
React.Children.map(this.props.children, function(child){
return <li>{child}</li>;
})
}
</ol>
}
}

ReactDOM.render(<List>
<span>item1</span>
<span>item2</span>
</List>,
document.getElementById("app"));

最终会被渲染成:

1
2
3
4
<ol>
<li><span>item1</span></li>
<li><span>item2</span></li>
</ol>

因为标签子元素个数不确定:没有子元素,children=undefined;一个子元素,值为object类型;多个子元素,就是array类型。

为了方便操作React在React.Children中提供了一套工具方法,方便操作子元素。

使用defaultProps定义默认的props

如果标签中没有指定属性值,this.props会取出undefined值,可以在组件的defaultProps中定义默认的属性值。

1
2
3
4
5
6
7
8
9
10
11
12
class Header extends React.Component{
render() {
return <header className='header'>
<h1>{this.props.text}</h1>
</header>
}
}
Header.defaultProps = {
text: '默认标题文本'
}

ReactDOM.render(<Header/>, document.getElementById("app"));

如果不使用ES6,在调用createReactClass时,需要在对象中定义一个getDefaultProps方法。详见:https://reactjs.org/docs/react-without-es6.html#declaring-default-props

对props中的属性进行类型检查

参考:https://reactjs.org/docs/typechecking-with-proptypes.html

为了限制组件中属性的类型,需要React.PropTypes进行类型检查。

从React v15.5开始,React.PropTypes被移进了prop-types库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import PropTypes from 'prop-types';

class Header extends React.Component{
render() {
return <header className='header'>
<h1>{this.props.text}</h1>
</header>
}
}
Header.propTypes = {
// 告诉React:title必须是string类型的,而且是必填属性
text: PropTypes.string.isRequired
}

ReactDOM.render(<Header text='文本'/>, document.getElementById("app"));

获取真实的DOM节点

参考:https://reactjs.org/docs/refs-and-the-dom.html

前面说到React的render方法创建的是虚拟DOM对象,它并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,当虚拟DOM更新到浏览器的DOM后,我们要怎么操作真实的DOM节点呢?这时就要用到 ref 属性。

String refs

先看老版本API中的ref怎么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyComponent extends React.Component{
handleClick() {
// 使用refs属性访问元素
this.refs.textInput.focus();
},
render() {
return (
<div>
<input type="text" ref="textInput" />
<input type="button" value="点击按钮,让文本框获取焦点" onClick={this.handleClick} />
</div>
);
}
});

ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);

传统的方式要将在元素中定义ref属性,相当于给元素定义了一个id,然后组件中可以在this.refs属性根据ref指定的id获取标签元素。

ref callback

新版本的不再使用字符引用的方式,这里有关于两者的讨论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyComponent extends React.Component{
handleClick() {
// 下面的ref回调方法中将this.textInput指向了input元素
this.textInput.focus();
},
render() {
return (
<div>
<input type="text" ref={ (input)=>{this.textInput=input} } />
<input type="button" value="点击按钮,让文本框获取焦点" onClick={this.handleClick} />
</div>
);
}
});

ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);

setState方法改变组件状态this.state

UI组件避免不了与用户的交互,有交互就避免不了对组件的修改。React将组件看成一个状态机,一开始有一个初始状态,一旦有状态的改变,就会触发重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LikeButton extends React.Component {
constructor(props) {
// 调用父类的构造方法,父类可能也有自己的组件状态
super(props);
// 初始化组件状态
this.state = {liked: false};
}
handleClick(event) {
// 点击时间触发时,修改控件状态,控件重新渲染
this.setState({liked: !this.state.liked});
},
render() {
var text = this.state.liked ? 'like' : 'haven\'t liked';
return (
<p onClick={this.handleClick}>
You {text} this. Click to toggle.
</p>
);
}
}

注意

  • 不要通过this.state.comment = 'Hello';这种方式直接修改控件状态,因为这样不会触发控件重新渲染。而应该使用setState修改状态。
  • this.state = XXX这种赋值操作只能在构造函数中初始化时使用。

在ES5中使用React时,需要在createReactClass方法中的提供getInitialState方法返回初始状态。

详见官方文档:https://reactjs.org/docs/react-without-es6.html#setting-the-initial-state

根据前一个状态计算后续状态

因为setState是异步更新状态,我们不应该直接依赖当前的state值计算下一个状态:

1
2
3
4
5
6
7
8
9
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));

组件生命周期

参考:https://reactjs.org/docs/state-and-lifecycle.html

https://reactjs.org/docs/react-component.html#the-component-lifecycle

每个组件都有几个生命周期方法,可以重写这几个方法,React在特定的时候会回调这些方法。

这些生命周期方法有些特征:

前缀为will的方法表示某些事件即将发生;前缀为did的方法表示某些事件已经发生

组件生命周期

当组件实例被创建,再到组件被插入到DOM树中,这个过程会触发组件的以下方法:

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

propsstate被修改导致组件更新,组件渲染的时候会回调下面这些方法:

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

当组件从DOM树中移除的时候,会触发componentWillUnmount方法:

  • componentWillUnmount()

在组件渲染、生命周期回调或者构造子组件的过程中出现异常,会触发组件的componentDidCatch方法:

  • componentDidCatch()

方法的具体介绍可以点击相应链接看官方文档的介绍

一个简单的时钟组件例子

component/TimerClock.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React from 'react'

export default class TimerClock extends React.Component{
constructor(props) {
super(props);
this.state = {
date: new Date(),
running: true,
title: '点击暂停'
};
}
// 在组件挂载到DOM树时,开启定时器
componentDidMount() {
this.start();
}
// 在组件从DOM树卸载时,关闭定时器
componentWillUnmount() {
this.stop();
}
toggle() {
if(this.state.running) {
this.stop();
} else {
this.start();
}
}
start() {
this.timerID = setInterval(() => this.tick(), 1000);
this.setState({
running: true,
title: '点击暂停'
});
}
stop() {
clearInterval(this.timerID);
this.setState({
running: false,
title: '点击继续'
});
}
// 每秒重新设置组件状态
tick() {
this.setState({
date: new Date()
});
}
render() {
return <div onClick={() => this.toggle()} title={this.state.title}>
<span>当前时间为:</span>
<span>{this.state.date.toLocaleTimeString()}</span>
</div>
}
}

index.jsx

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom'
import TimerClock from './component/TimerClock.jsx'

ReactDOM.render(<TimerClock />, document.getElementById('react-container'));

index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>时钟组件</title>
</head>
<body>
<div id="react-container"></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var webpack = require('webpack');

module.exports = {
entry: './index.jsx',
output: {
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
}
};

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "my-component",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": ""
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"webpack": "^4.1.1"
}
}

参考:

https://reactjs.org/docs/react-api.html

http://www.ruanyifeng.com/blog/2015/03/react.html