react 入门与进阶教程
前端学习对于我们来说越来越不友好,特别是随着这几年的发展,入门门槛越来越高,连进阶道路都变成了一场马拉松。在学习过程中,我们面临很多选择,vue与react便是一个两难的选择。
两者都是非常优秀的框架,而且我不能非常主观的说谁好谁不好。但是从我们初学者的角度来说,其实我们没有必要去考虑谁的性能更好,谁的实现更优雅,谁更适合什么场景等各种因素,唯一一个需要考虑的标准就是,学习谁,能够让我们更快的掌握它。因为无论他们两个你掌握了谁,都能够让你在找工作时更有底气。这就足够了。
因此,我这篇文章的目的,则是希望从与官方文档不同的角度,来试图让react学习变得更加容易,如果你想要学习react,不妨花点时间读下去。
为什么对于新人来说,官方文档不能帮助你掌握得更好
对于vue的学习,很多朋友有一个大的误解,认为vue官方出了中文文档,所以掌握起来会更加容易。然而事实上并非如此。
官方文档可能告诉了你vue/react的基础知识有哪些,可是这些知识怎么用,官方文档并没有告诉我们。而且vue官方文档为了降低学习门槛(绕开了vue-cli),在讲述知识的时候,不少地方其实与实际开发是有差距的,这个差距会导致你看完了官方文档,仍然不知道如何使用vue做一些事情。
当然,这样的问题,react官方文档也存在。虽然对于经验丰富的大神来说,这并不是问题,但是对于新人来说,这样的差距往往会使得大家有一种似懂非懂的感觉。
这也是我为什么要从和官方文档不一样的角度来入手的原因。
学前准备
在准备学习本文的react知识之前,希望你已经拥有了ES6的知识与知道了create-react-app
的安装与使用,我们的学习将会建立在这基础之上,如果你暂时还没有接触过他们,不用担心,可以回过头去阅读我的前两篇文章。不用花太多时间就可以初步掌握。
ES6常用知识合集
详解create-react-app 与 ES6 modules
你可以暂时不用对react有什么基础的了解,我们可以从0开始,当然,如果你看过官方文档或者从其他地方学习过相关知识就更好了。
开始啦,万能的Hello World程序
首先,假设你已经在电脑上安装好了create-react-app
并知道如何使用,那么我就开始在你电脑上存放开发项目的目录(本文中假设为develop)里开始创建一个名为first-react
的react项目。操作顺序如下:
1
2
3
4
5
6
7
8
9
10
11
|
// 在develop目录创建first-react项目
> create-react-app first-react
// 进入新创建的项目
> cd first-react
// 安装项目依赖包
> npm install
// 安装完毕之后启动项目
> npm start
|
启动之后,效果分别如下图所示:
自动生成的项目是一个简单的react demo。这个时候项目中会有三个文件夹,我们来分别了解一下这三个文件夹的作用。
-
node_modules
项目依赖包存放位置。当我们运行npm install安装package.json中的依赖包时,该文件夹会自动创建,所有的依赖包会安装到该文件夹里。 -
public
主要的作用是html入口文件的存放。当然我们也可以存放其他公用的静态资源,如图片,css等。其中的index.html就是我们项目的入口html文件。 -
src
组件的存放目录。在create-react-app创建的项目中,每一个单独的文件都可以被看成一个单独的模块,单独的image,单独的css,单独js等,而所有的组件都存放于src目录中,其中index.js则是js的入口文件。虽然我们并没有在index.html中使用script标签引入他,但是他的作用就和此一样。
我们在最初学习开发一个页面的时候,就已经知道一个页面会有一个html文件,比如index.html,然后分别在html文件中,通过script与link标签引入js与css。但是在构建工具中,我们只需要按照一定的规则来组织文件即可,整合的工作构建工具会自动帮助我们完成,这也是构建工具给前端开发带来的便利之处,也因为如此,前端的模块化开发才成为了可能。
我们还是和上一篇文章中说的一样,先清空src目录里所有的其他文件,仅仅只留下空的入口文件index.js
,并在index.js
写入如下的代码:
JavaScript
1
2
3
4
5
6
7
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
const root = document.querySelector('#root');
render(<div>Hello World!</div>, root);
|
保存之后,结果如下:
如何你能轻松看懂这四行代码,那么说明你离掌握react已经不远了。至少你已经掌握了ES6的相关知识。我来解释一下这些代码的作用。
-
import React from 'react';
在我们通过npm install
指令安装依赖包的时候,就已经安装好了react,因此我们可以直接import。这句话的作用就在于,能够让构建工具在当前模块中识别jsx。而jsx,是一种类似于html标签的模板语言,我们只需要懂得html标签,就不必花费额外的精力去了解jsx,因为我们可以直接理解为它就是html标签,但是在此基础上,扩展了更多的能力。例如这里,程序能够识别Hello World!,正是这句话的作用。
-
import { render } from 'react-dom';
这是利用ES6的解析结构的语法,仅仅引入了react-dom
的render
方法。render方法的作用,就是将react组件,渲染进DOM结构中,它的第一个参数就是react 组件,第二个参数则是一个DOM元素对象。 -
const root = document.querySelector('#root');
这句话就很简答了,如果你理解不了,那么说明你的基础还不足以支撑你学习react, – -。 -
render(<div>Hello World!</div>, root);
这是最核心的一步,通过render
方法,将写好的react组件渲染进DOM元素对象。而这里的root
,则是在index.html
中写好的一个元素。这里的div
,可以理解为一个最简单的react组件。
OK,理解了这些,那么我们就可以开始学习react最核心的内容组件
了。
react组件
曾经,创建react组件有三种方式,但是既然都决定在ES6的基础上来学习react了,那么我也就只介绍其中的两种方式了。反正另外一种方式也已经被官方废弃。
当一个组件,并没有额外的逻辑处理,仅仅只是用于数据的展示时,我们推荐使用函数式的方式来创建一个无状态组件。
我们结合简单的例子来理解。在项目的src目录里创建一个叫做helloWorld.jsx
的文件。在该文件中,我们将创建一个正式的react组件,代码如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
|
// src/helloWorld.jsx
import React from 'react';
const HelloWorld = () => {
return (
<div>Hello World!</div>
)
}
export default HelloWorld;
|
并在index.js
中引入该组件。修改index.js
代码如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
// 引入HelloWorld组件
import HelloWorld from './helloWorld';
const root = document.querySelector('#root');
render(<HelloWorld />, root);
|
保存后运行,我们发现结果一样。
在helloWorld.jsx
中,我们仍然引入了react
,是因为所有会涉及到jsx模板的组件,我们都要引入它,这样构建工具才会识别得到。
组件里只是简单的创建了一个HelloWorld函数,并返回了一段html(jsx模板)。并在最后将HelloWorld函数作为对外的接口暴露出来export default HelloWorld
。
接下来我们通过一点一点扩展HelloWorld组件能力的方式,来学习组件相关的基础知识。
向组件内部传递参数
向组件内部传递参数的方式很简单,这就和在html标签上添加一个属性一样。
例如我们希望向HelloWorld组件内传递一个name属性。那么只需要我们在使用该组件的时候,添加一个属性即可。
1
2
|
<HelloWorld name="Tom" />
<HelloWorld name="Jake" />
|
如果我们希望组件最终渲染的结果是诸如:Tom say: Hello, world!
其中的名字可以在传入时自定义。那么我们在组件中应该如何接收传递进来的参数呢?
我们修改HelloWorld.jsx
如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
// src/helloWorld.jsx
import React from 'react';
const HelloWorld = (props) => {
console.log(props);
return (
<div>{ props.name } say: Hello World!</div>
)
}
export default HelloWorld;
|
并在index.js中修改render方法的使用,向组件中传入一个name属性
1
2
|
// src/index.js
render(, root);
|
结果如下:
在HelloWorld
组件中,我使用了一个叫做props的参数。而通过打印出来props可以得知,props正是一个组件在使用时,所有传递进来属性组合而成的一个对象。大家也可以在学习时多传入几个额外的参数,他们都会出现在props对象里。
而在jsx模板中,通过
这样的方式来将变量传入进来。这是jsx模板语言支持的一种语法,大家记住能用即可。
大家要记住,使用这种方式创建的无状态组件,会比另外一种方式性能更高,因此如果你的组件仅仅只是用于简单的数据展示,没有额外的逻辑处理,就要优先选择这种方式。
那么我们继续升级HelloWorld组件的能力。现在我希望有一个点击事件,当我们点击该组件时,会在Console工具中打印出传入的name值。这就涉及到了另外一种组件的创建,也就是当我们的组件开始有逻辑处理,之前的那种方式胜任不了时索要采取的一种形式。
修改helloWorld.jsx
文件如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.props);
console.log(this.props.name);
}
render () {
return (
<div onClick={this.clickHander}>{ this.props.name } say: Hello World!</div>
)
}
}
export default HelloWorld;
|
如果,你同时熟知第一种react组件的创建方式,与ES6语法的话,相信上面的代码,并不会对你造成多少困扰。
没错,这种方式创建的组件,正是通过继承react的Component
对象而来。所以创建的方式也是利用ES6的class语法来生成。也正因为如此,其中的很多实用方式,也就跟class的使用一样了。
上面的render方法,则是Component中,专门提供的用来处理jsx模板的方法。
与第一种方式不同的是,我们接收传入进来的参数,使用的是this.props
,第一种方式将props放置于函数参数中,而这种方式则是将props挂载与实例对象上,因此会有所不同。
而我们想要给一个组件添加点击事件,方式也与html标签中几乎一致
react事件相关的知识大家可以当做一个进阶课程去研究,这里就暂时不多说,详情可以参考官方文档 https://facebook.github.io/react/docs/events.html
好了,现在大家初步认识了react的第二种组件的创建方式,那么我们继续搞事情,现在我想要的效果,是传入两个名字,name1=Tom, name2='Jason'
,我希望第一次点击时,log出Tom,第二次log出Jason,第三次Tom…
这个时候,我们就需要引入react组件非常核心的知识状态state
。
修改helloWorld.jsx
代码如下:
JavaScript
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
|
// helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
state = {
switch: 0,
name: this.props.name1
}
clickHander = () => {
const { name1, name2 } = this.props;
if (this.state.switch === 0) {
console.log(name1);
this.setState({
switch: 1,
name: name2
})
} else {
console.log(name2);
this.setState({
switch: 0,
name: name1
})
}
}
render () {
return (
<div onClick={this.clickHander}>{ this.state.name } say: Hello World!</div>
)
}
}
export default HelloWorld;
|
先来说说state相关的基础知识。首先了解ES6 class语法的同学都应该知道,当我们通过这种方式来写的时候,其实是将state写入了构造函数之中。
1
2
3
4
|
state = {}
// 等同于ES5构造函数中的
this.state = {}
|
因此深入掌握class语法对于学习react组件的帮助非常巨大,我们需要清楚的知道什么样的写法会放入对象的什么位置,是构造函数中,还是原型中等。这也是为什么开篇我会强调一定要先对我的前两篇文章所介绍的知识有一定了解才行。
因此,在对象中,我们可以通过this.state
的方式来访问state中所存储的属性。同时,react还提供了如下的方式来修改state的值
1
2
3
|
this.setState({
name: 'newName'
})
|
setState
接收一个对象,它的运行结果类似于执行一次assign方法。会修改传入的属性,而其他的属性则保持不变。
react赋予state的特性,则是当state被修改时,会引起组件的一次重新渲染。即render方法会重新执行一次。也正是由于这个特性,因此当我们想要改变界面上的元素内容时,常常只需要改变state中的值就行了。这也是为什么结合render方法,我们可以不再需要jquery的原因所在。
而setState
也有一个非常重要的特性,那就是,该方法是异步的。它并不会立即执行,而会在下一轮事件循环中执行。
说到这里,基础薄弱的同学就开始头晕了,这就是为什么我在前面的文章都反复强调基础知识的重要性,基础扎实,很多东西稍微一提,你就知道是怎么回事,不扎实,到处都是让你头晕的点,不知道的没关系,读我这篇文章 http://www.jianshu.com/p/12b9f73c5a4f。
相信不理解这个点的同学肯定会遇到很多坑,所以千万要记住了。
1
2
3
4
5
6
7
|
// 假设state.name的初始值为Tom,我们改变它的值
this.setState({
name: 'Jason'
})
// 然后立即查看它的值
console.log(this.state.name) // 仍然为Tom,不会立即改变
|
refs
我们知道,react组件其实是虚拟DOM,因此通常我们需要通过特殊的方式才能拿到真正的DOM元素。大概说一说虚拟DOM是个什么形式存在的,它其实就是通过js对象的方式将DOM元素相关的都存储其实,比如一个div元素可能会是这样:
1
2
3
4
5
6
7
8
9
|
// 当然可能命名会是其他的,大概表达一个意思,不深究哈
{
nodeName: 'div',
className: 'hello-world',
style: {},
parentNodes: 'root',
childrenNodes: []
...
}
|
而我们想要拿到真实的DOM元素,react中提供了一种叫做ref
的属性来实现这个目的。
修改helloWorld.jsx
如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref="world" className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
为了区分ES6语法中的class关键字,当我们在jsx中给元素添加class时,需要使用
className
来代替
我们在jsx中,可以给元素添加ref
属性,而这些拥有ref属性的元素,会统一放在组件对象的refs
中,因此,当我们想要访问对应的真实DOM时,则通过this.refs
来访问即可。
当然,ref的值不仅仅可以为一个名字,同时还可以为一个回调函数,这个函数会在render渲染时执行,也就是说,每当render函数执行一次,ref的回调函数也会执行一次。
修改helloWorld.jsx
如下,感受一下ref回调的知识点
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// src/helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
refCallback = (elem) => {
console.log(elem);
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref={this.refCallback} className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
大概介绍一下我暂时能想到的ref使用的一个场景。例如我们要实现元素拖拽的时候,或者写一个slider组件。我们可能会非常频繁的改动元素的位置。这个时候,如果我们仍然通过react组件的state来存储元素的位置,那么就会导致react组件过于频繁的渲染,这就会引发一个严重的性能问题。所以这个时候我们不得不获取到真实DOM,并通过常规的方式来做。
同样的道理也适用于vue中,我们要尽量避免将可能会变动频率非常高的属性存放于vue组件的data中。
组件生命周期
所谓组件的生命周期,指的就是一个组件,从创建到销毁的这样一个过程。
而react为组件的生命周期提供了很多的钩子函数。很多地方也为生命周期画了很清晰明了的图帮助大家理解。但是我在初学的时候其实并没有看懂,还是在我懂得了生命周期之后,才看懂的那些图。所以呢,这里我也就不去找图了。我们这样理解。
通俗来说,react为一个组件,划分了如下的时刻。
- 组件第一次渲染完成的前后时刻,
componentWillMount
渲染完成之前componentDidMount
渲染完成之后
所谓的渲染完成,即组件已经被渲染成为真实DOM并插入到了html之中。
- 组件属性(我们前面提到的props与state)更新的前后时刻
componentWillReceiveProps
接收到一个新的props时,在重新render之前调用shouldComponentUpdate
接收到一个新的state或者props时,在重新render之前调用componentWillUpdate
接收到一个新的state或者props时,在重新render之前调用componentDidUpdate
组件完成更新之后调用 - 组件取消挂载之前(取消之后就没必要提供钩子函数了)
componentWillUnmount
在学习之初你不用记住这些函数的具体名字,你只需要记住这三个大的时刻即可,第一次渲染完成前后,更新前后,取消之前。当你要使用时,再查具体对应的名字叫什么即可。
而且根据我的经验,初学之时,其实也不知道这些钩子函数会有什么用,会在什么时候用,这需要我们在实践中慢慢掌握,所以也不用着急。当我们上手写了几个稍微复杂的例子,自然会知道如何去使用他们。
所以这里我只详细介绍一下,我们最常用的一个生命周期构造函数,组件第一次渲染完成之后调用的componentDidMount
。
既然是组件第一次渲染完成之后才会调用,也就是说,该函数在react组件的生命周期中,只会调用一次。而渲染完成,则表示组件已经被渲染成为真实DOM插入了html中。所以这时候就可以通过ref获取真实元素。记住它的特点,这会帮助我们正确的使用它。
修改helloWorld.jsx
如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
// 这时已经可以获取到真实DOM,而componentWillMount则不行
componentDidMount (props) {
console.log(this.refs)
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref="world" className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
我们在实际开发中,常常需要通过ajax获取数据,而数据请求的这个行为,则最适合放在componentDidMount
中来执行。
通常会在首次渲染改变组件状态(state)的行为,或者称之为有副作用的行为,都建议放在
componentDidMount
中来执行。主要是因为state的改动会引发组件的重新渲染。
组件之间的交互
作为react学习中的一个非常重要的点,组件之间的交互还是需要我们认真掌握的。这个时候hello world就满足不了我们学习的欲望了,所以我们可以先把它给删掉。
那么组件之间的交互,大概可以分为如下两种:
- 父组件与子组件之间交互
- 子组件与子组件之间交互
当然可能有的人会问,2个不相干的组件之间如何交互?如果,你的代码里,出现了两个不相干的组件还要交互,那说明你的组件划分肯定是有问题的。这就是典型的给自己挖坑找事儿。即使确实有,那也是通过react-redux把他们变成子组件对吧。但是,通常情况下,不到万不得已,并不建议使用react-redux,除非你的项目确实非常庞大了,需要管理的状态非常多了,已经不得不使用,一定要记住,react-redux这类状态管理器是最后的选择。
我们来想想一个简单常见的场景:页面里有一个submit提交按钮,当我们点击提交后,按钮前出现一个loading图,并变为不可点击状态,片刻之后,接口请求成功,飘出一个弹窗,告诉你,提交成功。大家可以想一想,这种场景,借助react组件应该如何做?
首先可以很简单的想到,将按钮与弹窗分别划分为两个不同的组件:
。然后创建一个父组件来管理这两个子组件
。
那么在父组件中,我们需要考虑什么因素?Button的loading图是否展示,弹窗是否展示对吧。
OK,根据这些思考,我们开始来实现这个简单的场景。
首先创建一个Button
组件。在src目录下创建一个叫做Button.jsx
的文件,代码如下:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/Button.jsx
import React from 'react';
const Button = (props) => {
const { children, loading, submit } = props
return (
<button onClick={submit} disabled={ loading ? 'disabled' : null }>
{ loading && <i className="loading"></i> }
{ children }
</button>
)
}
export default Button;
|
注意,当你引入了一个新创建的文件时,可能需要重新启动服务才会找得到新的组件
由于这里的Button组件仅仅是简单的展示,并无额外的逻辑需要处理,因此我们使用无状态的组件。在这个组件里,出现了一个新的知识点:children
JavaScript
1
2
3
4
5
6
7
8
9
|
// 假如我们这样使用Button组件时
<Button>确认</Button>
// 那么标签中间的确认二字就会放入props的children属性中
// 无状态组件中
props.children = '确认'
// 有状态组件中
this.props.children = '确认'
|
当然,children还可以是更多的元素,这和我们熟知的DOM元素的children保持一致。
还有一个需要注意的知识点,则是在jsx模板中,我们可以使用JavaScript表达式来执行简单的逻辑处理
我们可以列举一些常见的表达式:
JavaScript
1
2
3
4
5
6
7
|
<div>{ message }</div>
<Button disabled={ loading ? 'disabled' : null }></Button>
{ dialog && <Dialog /> }
{ pending ? <Aaaa /> : <Bbbb /> }
|
如果对于JavaScript表达式了解不够多的朋友,建议深入学习一下相关的知识。
理解了这些知识之后,相信对于上面的Button组件所涉及到的东西也就能够非常清楚知道是怎么回事了。接下来,我们需要创建一个弹窗组件,Dialog.jsx
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// src/Dialog.jsx
import React, { Component } from 'react';
const Dialog = (props) => {
const { message, close } = props;
return (
<div className="dialog-backdrop">
<div className="dialog-container">
<div className="dialog-header">提示</div>
<div className="dialog-body">{ message }</div>
<div className="dialog-footer">
<button className="btn" onClick={ close }>确定</button>
</div>
</div>
</div>
)
}
export default Dialog;
|
这个组件没有太多特别的东西,唯一需要关注的一点是,我们也可以通过props传递一个函数给子组件。例如这里的close方法。该方法在父组件中定义,但是却在子组件Dialog中执行,他的作用是关闭弹窗。
我们很容易知道父组件想要修改子组件,只需要通过改变传入的props属性即可。那么子组件想要修改父组件的状态呢?正是父组件通过向子组件传递一个函数的方式来改变。
该函数在父组件中定义,在子组件中执行。而函数的执行内容,则是修改父组件的状态。这就是close的原理,我们来看看父组件中是如何处理这些逻辑的。
创建一个父组件App.jsx
JavaScript
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
54
55
56
57
58
|
// src/App.jsx
import React, { Component } from 'react';
import Button from './Button';
import Dialog from './Dialog';
import './style.css';
class App extends Component {
state = {
loading: false,
dialog: false,
message: 'xxx'
}
submit = () => {
this.setState({
loading: true
})
// 模拟数据请求的过程,假设数据请求会经历1s得到结果
setTimeout(() => {
// 通过随机数的方式模拟可能出现的成功与失败两种结果
const res = Math.random(1);
if (res > 0.5) {
this.setState({
dialog: true,
message: '提交成功!'
})
} else {
this.setState({
dialog: true,
message: '提交失败!'
})
}
this.setState({ loading: false })
}, 1000)
}
close = () => {
this.setState({
dialog: false
})
}
render () {
const { loading, dialog, message } = this.state;
return (
<div className="app-wrap">
<Button loading={ loading } submit={ this.submit }>提交</Button>
{ dialog && <Dialog message={ message } close={ this.close } /> }
</div>
)
}
}
export default App;
|
App组件的state中,loading用于判断Button按钮是否显示loading图标,dialog用于判断是否需要显示弹窗,message则是表示弹窗的提示内容。
我们自定义的钩子函数submit
和close
则分别是与子组件Button与Dialog交互的一个桥梁。前面我们说过了,想要在子组件中改变父级的状态,就需要通过在父组件中创建钩子函数,并传递给子组件执行的方式来完成。
在App.jsx中我们还看到代码中引入了一个css文件。这是构建工具帮助我们整合的方式,我们可以直接将css文件当做一个单独的模块引入进来。我们还可以通过同样的方式引入图片等资源。
style.css也是在src目录下创建的。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
// src/style.scss
button {
background: none;
border: none;
outline: none;
width: 100px;
height: 30px;
border: 1px solid orange;
border-radius: 4px;
font-size: 16px;
display: block;
margin: 20px auto;
}
.loading {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #ccc;
border-radius: 10px;
margin-right: 10px;
border-bottom: transparent;
border-top: transparent;
animation-name: loading;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.dialog-backdrop {
background: rgba(0, 0, 0, 0.2);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.dialog-container {
width: 300px;
background: #FFFFFF;
border-radius: 4px;
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
}
.dialog-header {
height: 20px;
text-align: center;
line-height: 20px;
}
.dialog-body {
line-height: 1.6;
text-align: center;
margin-top: 20px;
}
.dialog-footer {
margin-top: 20px;
}
.dialog-footer button {
margin: 0 auto;
border: none;
background: orange;
color: #fff;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
|
最后修改index.js,即可将程序运行起来。
1
2
3
4
5
6
7
8
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
import App from './App';
const root = document.querySelector('#root');
render(, root);
|
那么总结一下组件之间的交互。
父组件改变子组件,通过改变传入的props属性值即可。
而子组件改变父组件的状态,则需要在父组件中创建钩子函数,然后让钩子函数通过props传递给子组件,并在子组件中执行。
那么子组件与子组件之间的交互方式,也就是通过影响共同的父组件来进行交互的。正如我们这个例子中的点击按钮,出现弹窗一样。这就是react组件之间交互的核心。
异步组件
在学习异步组件之前,可能还需要大家去折腾一下如何禁用浏览器的跨域限制。禁用跨域限制可以让我们使用更多的公共api进行学习,但是很多人并不知道还可以这样玩。总之一句话,知道了如何禁用浏览器的跨域限制,会让你的学习速度提升很多,很多项目你就可以动手自己尝试了。
我这里只能提供在mac环境下如何禁用chrome浏览器的跨域限制。在命令行工具中输入以下指令启动chrome即可。
1
|
> open -a "Google Chrome" --args --disable-web-security --user-data-dir
|
在safari浏览器中则更加简单。
windows环境下如何做需要大家自己去研究。
OK,禁用跨域限制以后,我们就可以自如的请求别人的接口。这个时候再来学习异步组件就能轻松很多。
异步组件并不是那么复杂,由于接口请求会经历一点时间,因此在组件第一次渲染的时候,并不能直接将我们想要的数据渲染完成,那么就得再接口请求成功之后,重新渲染一次组件。上面的知识已经告诉大家,通过使用this.setState
修改state的值可以达到重新渲染的目的。
所以我们通常的做法就是在接口请求成功之后,使用this.setState
。
为了降低学习难度,我们暂时先使用jquery中提供的方法来请求数据。
目前比较常用的是axios
首先在我们的项目中,安装jquery库。我们通常都会使用这样的方式来安装新的组件和库。
1
|
> npm install jquery
|
然后在src目录下创建一个News.jsx,借助知乎日报的api,我们来尝试完成一个简单的异步组件。
JavaScript
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
54
55
56
57
|
// src/News.jsx
import React, { Component } from 'react';
import $ from 'jquery';
class News extends Component {
state = {
stories: [],
topStories: []
}
componentDidMount() {
$.get('http://news-at.zhihu.com/api/4/news/latest').then(resp => {
console.log(resp);
this.setState({
stories: resp.stories,
topStories: resp.top_stories
})
})
}
render() {
const { stories, topStories } = this.state;
// 观察每一次render数据的变化
console.log(this.state);
return (
<div className="latest-news">
<section className="part1">
<div className="title">最热</div>
<div className="container">
{
topStories.map((item, i) => (
<div className="item-box" key={i}>
<img src={ item.image } alt=""/>
<div className="sub-title">{ item.title }</div>
</div>
))
}
</div>
</section>
<section className="part2">
<div className="title">热门</div>
<div className="container">
{
stories.map((item, i) => (
<div className="item-box" key={i}>
<img src={ item.images[0] } alt=""/>
<div className="sub-title">{ item.title }</div>
</div>
))
}
</div>
</section>
</div>
)
}
}
export default News;
|
在style.css
中简单补上相关的css样式
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
|
// src/style.css
.latest-news {
width: 780px;
margin: 20px auto;
}
.latest-news section {
margin-bottom: 20px;
}
.latest-news .title {
height: 40px;
line-height: 40px;
font-size: 16px;
padding: 0 10px;
}
.latest-news .container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.latest-news .item-box {
width: 30%;
overflow: hidden;
margin-bottom: 20px;
}
.latest-news .item-box img {
width: 100%;
height: 200px;
}
.latest-news .item-box .sub-title {
font-size: 12px;
line-height: 1.6;
margin-top: 10px;
}
|
并在App.jsx中引入使用即可。
1
2
3
4
5
|
// src/App.jsx
import News from './News';
// 将下面这一句放于render函数的jsx模板中即可
|
这个组件除了获取数据,没有额外的逻辑处理,但仍然有几个需要非常注意的地方。
1、 若非特殊情况,尽量保证数据请求的操作在componentDidMount
中完成。
2、 react中的列表渲染通常通过调用数组的原生方法map方法来完成,具体使用方式可参考上例。
3、为了确保性能,被渲染的每一列都需要给他配置一个唯一的标识,正入上栗中的key={i}
。我们来假想一个场景,如果我们在数组的最前面新增一条数据,如果没有唯一的标识,那么所有的数据都会被重新渲染,一旦数据量过大,这会造成严重的性能消耗。唯一标识会告诉react,这些数据已经存在了,你只需要渲染新增的那一条就可以了。
4、如果你想要深入了解该组件的具体变化,你可以在render方法中,通过console.log(this.state)
的方式,观察在整个过程中,组件渲染了多少次,已经每一次this.state
中的具体值是什么,是如何变化的。
高阶组件
很多人写文章喜欢把问题复杂化,因此当我学习高阶组件的时候,查阅到的很多文章都给人一种高阶组件高深莫测的感觉。但是事实上却未必。我们常常有一些口头俗语,比如说“包一层”就是可以用来简单解释高阶组件的。在普通组件外面包一层逻辑,就是高阶组件。
在进一步学习高阶组件之前,我们来回顾一下new与构造函数之间的关系。在前面我有文章提到过为什么构造函数中this在运行时会指向new出来的实例,不知道还有没有人记得。我将那段代码复制过来。
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
|
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
var res = {};
if (func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.__proto__ = func.prototype;
}
// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res;
}
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true
|
在上面的例子中,首先我们定义了一个本质上与普通函数没区别的构造函数,然后将该构造函数作为参数传入New函数中。我在New函数中进行了一些的逻辑处理,让New函数的返回值为一个实例,正因为New的内部逻辑,让构造函数中的this能够指向返回的实例。这个例子就是一个“包一层”的案例。
再来看一个简单的例子:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import React, { Component } from 'react';
class Div extends Component {
componentDidMount() {
console.log('这是新增的能力');
}
render () {
return (
<div>{ this.props.children }</div>
)
}
}
export default Div;
|
在上面的例子中,我们把html的DIV标签作为基础元件。对他新增了一个输出一条提示信息的能力。而新的Div组件,就可以理解为div标签的高阶组件。所以到这里希望大家已经理解了包一层的具体含义。
react组件的高阶组件,就是在基础react组件外面包一层,给该基础组件赋予新的能力。
OK,我们来试试定义第一个高阶组件,该高阶组件的第一个能力,就是向基础组件中传入一个props参数。
在例子中,传入的参数可能没有任何实际意义,但是在实际开发中,我们可以传入非常有必要的参数来简化我们的代码和逻辑。
先来定义一个拥有上述能力的高阶组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/Addsss.jsx
import React from 'react';
// 定义一个接受一个react组件作为参数的函数
function Addsss(Container) {
// 该函数返回一个新的组件,我们可以在该组件中进行新能力的附加
return class Asss extends React.Component {
componentDidMount() {}
render() {
return (
{ this.props.children }
)
}
}
}
export default Addsss;
|
尽管这个高阶组价足够简单,但是他已经呈现了高阶组件的定义方式。现在我们在一个基础组件中来使用该高阶组件。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/basic.jsx
import React, { Component } from 'react';
import Addsss from './Addsss';
class Basic extends Component {
componentDidMount() {
console.log(this.props.name);
}
render() {
return (
<div className={this.props.name}>{ this.props.children }</div>
)
}
}
export default Addsss(Basic);
|
我们看到其实在基础组件中,对外抛出的接口是Addsss(Basic)
,这是高阶组件里定义的函数运行的结果。也就是说,其实基础组件中返回的是高阶组件中定义的Asss中间组件。这和new的思路几乎完全一致。
当然,想要理解,并熟练使用高阶组件并不是一件容易的事情,大家初学时也不用非要完全掌握他。当你对react慢慢熟练之后,你可以尝试使用高阶组件让自己的代码更加灵活与简练。这正是向函数式编程思维转变的一个过程。
在进步学习的过程中,你会发现无论是路由组件react-router,或者react-redux都会使用高阶组件来实现一些功能。只要你遇到他们的时候,你能明白,哦,原来是这么回事儿就行了。
react路由
react提供了react-router组件来帮助我们实现路由功能。
但是react-router是一个不太好讲的知识点。因为由于react-router 4进行了颠覆性的更新,导致了react-router 3与react-router 4的使用方式大不一样。也正是由于变化太大,所以很多项目仍然正在使用react-router3,并且没有过渡到react-router4的打算。
因此这里我就不多讲,提供一些参考学习资料。
- react-router 3: react-router 3 使用教程
- react-router 4: react-router 4 全攻略
未完待续
由于时间关系,暂时就只能写到这里了。
本来还写了一个比较完整的例子也在这篇文章里逐步分析如何实现的,但是时间确实不够。所以如果觉得看了上面的知识还想进一步学习的话,可以先去https://github.com/yangbo5207/advance15 看看这个完整例子的样子。
另外我曾经写了一篇如何快速掌握一门前端框架,希望大家可以参考参考。
按照我的计划,只要理解了上面我所提到的知识,并把我准备的这个完整例子理解了。那么你的react掌握程度也算是小有所成了。至少应届毕业生找工作能提到这些思维方式应该会很有帮助。