摘要: 以前我们熟悉的jquery编程的方式是以面向对象的方式来进行编程的,而react将这种编程方式提升了一个层级,及面向对象---》 面向模块 ----》 面向组件。react面向组件编程无非两个步骤,定义组件和渲染组件。接下来将学习如何定义组件,以及渲染组件。
1. react组件的分类
- 函数式组件
- es6类组件
这两种组件接下来会一一了解到。
2. 自定义组件
2.1) 工厂函数组件(简单组件)如
// 函数式组件必须要有个return
function MyComponent () {
return <h2>函数式组件</h2>
}
以下用用函数式组件编写最简单的demo
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script type="text/babel">
// 定义组件
function MyComponent () {
return <h2>react 学习</h2>
}
// 渲染组件
ReactDOM.render(<MyComponent/>, document.getElementById('root'))
</script>
</body>
</html>
2.2) es6类组件(复杂组件)如
// 这种方式继承了React的核心组件,并重写了render方法,类实例通过调用render方法来创建虚拟dom
class MyComponent extends React.Component{
render () {
return <h2>学习react类组件</h2>
}
}
以下是基于类组件最简单的demo:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script type="text/babel">
// 定义组件
class MyComponent extends React.Component {
render () {
return <h2>react 类组件的学习</h2>
}
}
// 渲染组件
ReactDOM.render(<MyComponent/>, document.getElementById('root'))
</script>
</body>
</html>
这里分析类了组件如何定义使用,到这里心中应该有个疑问,既然是类,那么他的人实例应该长什么样子?类组件重写了React的render方法,这个render方法一定是由组件类的实例所调用的,因此在render的方法中是可以访问到这个实例的,其实就i是常见的this,所以有必要在render中打印this一探究竟,如下:
<script type="text/babel">
// 定义组件
class MyComponent extends React.Component {
render () {
console.log(this) // 查看组件实例对象
return <h2>react 类组件的学习</h2>
}
}
// 渲染组件
ReactDOM.render(<MyComponent/>, document.getElementById('root'))
</script>
通过打印this得到如下的结果:
可以看到有属性,有方法。接下来我们就来看看这些属性和方法的作用,(
红色框起来的是重点),究竟是用来干嘛的。
3. 组件三大属性
- state(react的思想是尽量少操作dom或者不操作dom,而是通过操作数据来改变视图,要操作的数据其实就是state,这个state其实就是组件对象最重要的属性,值是对象,可以包含多个数据,react组件也因此可以叫做状态机,通过更新组件的state来更新对应的页面显示)那么问题就来了,如何初始化这些状态?如何读取某个状态,如何更新某一个状态?这些问题通过以下编码demo的方式来阐述,。
/**
* 首先要先知道以下几点:
* 1. 在构造器中进行初始化操作,react 类组件提供了构造器
* 2. 要更新状态要通过事件触发,那么如何绑定事件
* 3. 通过setState来更新状态的值,更新状态前可能需要读取状态
*
*****/
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<div id="box">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script type="text/babel">
/**
* 需求: 自定义组件,功能说明如下:
*
* 1. 显示h2标题,初始化为本为: 我对代码如初恋
* 2. 点击标题更新为: 代码虐我千百遍
*/
// 定义组件
class MyLove extends React.Component {
constructor (props) {
super(props) // 这是固定写法,react库的常规套路
// 初始化state
this.state = {
isChange: false
}
// 绑定this
this.handleClick = this.handleClick.bind(this)
}
// 事件处理函数,函数内部的this并不是组件内部的this,要让this指向组件对象,可以在构造函数中绑定this
// 通过 this.handleClick = this.handleClick.bind(this)的方式来实现
handleClick () {
// 更新状态,注意react中更新状态的唯一方式是通过setState方法,接受一个对象
// 跟新状态前先获取一下(这里获取后直接取反了)
const isChange = !this.state.isChange
// 设置状态
this.setState({isChange}) // 这是es6的简写, 相当于 this.setState({isChange: isChange})
}
render () {
// 获取状态
const {isChange} = this.state
// 注意: onClick事件,很像原声的onclick,但是用了驼峰的写法,因为他并不是原生事件,而且后面不是引号括起来的字符串
// 而是用{} 括起来的js代码,点击的时候执行函数,在函数里面进行状态的更新,而且onClick={this.handleClick}中的handleClick后面没有加括号,因为react是声明式的,我们只需要告诉他我们要做什么,具体怎么做我们不管,加括号了就相当于我们手动调用了
return <h2 onClick={this.handleClick}>{isChange ? '代码虐我千百遍' : '我对代码如初恋'}</h2>
}
}
// 渲染虚拟dom到真实dom中
ReactDOM.render(<MyLove/>, document.getElementById('root'))
</script>
</body>
</html>
这个示例尽管简单,但是说明了几个问题,1.修改状态数据确实能改变视图,体现了react的数据驱动思想,2.设置更新状态唯一的方式是通过setState方法,3.通过this.state.属性可以直接获取状态。4.如何进行事件绑定以及this指向的问题的。
- props属性(state是组件内部的状态,这似乎在有些情况下只有state是不能满足需求的,比如说,自定义一个显示年龄信息的组件,要求显示姓名,性别,年龄,很明显这些要显示的信息不是组件内部特有的属性,而应该是传递给组件的属性,传递给组件的属性其实就是props,很显然这个组件是没有state的,那么可以通过工厂函数组件的方式来创建,函数组件是没有state的,函数组件比类组件效率高,因为函数可以直接调用,而类必须先创建对象,才能调用render函数),接下来就来编写这个年龄信息组件。
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<div id="box">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script type="text/babel">
/***
* 需求,自定义一个显示年龄信息的组件
* 要求显示姓名,年龄,性别
* 从这个需求可以分析出,当前组件应该是没有状态的,他的数据应该都来自外部,因此创建组件可以通过类组件, 也可以通过函数组件
************/
function Person (props) {
return (
<ul>
<li>姓名:{props.name}</li>
<li>年龄:{props.age}</li>
<li>性别:{props.sex}</li>
</ul>
)
}
// 定义人员信息数据
const person1 = {
name: '张三',
age: 20,
sex: '男'
}
// 类似dom标签属性一样给Person组件传值
ReactDOM.render(<Person name={person1.name} age={person1.age} sex={person1.sex}/>, document.getElementById('root'))
</script>
</body>
</html>
这个demo很明显的说明了props的作用,传递给组件Person的属性全部由props接收。但是上面的这个例子还有一些小缺陷,比如,如果age,sex没有传递给组件,呢么Person组件是拿不到age,和sex的。name能不能再age和sex没有被传递的情况下给一个默认值了?如果组件要求name必须传递,age需要传递数值类型的,该如何验证了?首先说明一下,组件是可以给默认值得,也可以做类型和唯一校验的,接下来就添加这些规则:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<div id="box">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
/***
* 需求,自定义一个显示年龄信息的组件
* 姓名必须指定,如果性别没有指定默认为男
* 如果年龄没有指定默认为18
* 从这个需求可以分析出,当前组件应该是没有状态的,他的数据应该都来自外部,因此创建组件可以通过类组件, 也可以通过函数组件,当然也可通过类组件
************/
// 定义组件
function Person (props) {
return (
<ul>
<li>姓名:{props.name}</li>
<li>年龄:{props.age}</li>
<li>性别:{props.sex}</li>
</ul>
)
}
// 添加默认值规则,通过给函数组件添加一个defaultProps属性来添加默认值
Person.defaultProps = {
age: 18,
sex: '男'
}
// 添加类型校验和唯一性校验,通过一个库prop-types来校验,该库是react提供的,通过脚本的方式在上面引入
// 现在用这种方式来指定
Person.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
// 定义人员信息数据
const person1 = {
name: '张三',
age: 20,
sex: '男'
}
const person2 = {
name: '里斯',
age: "20"
}
// 测试默认值,这里不传递sex和age
// ReactDOM.render(<Person name={person1.name}/>, document.getElementById('root'))
// 测试name属性唯一性和age的类型,
// ReactDOM.render(<Person/>, document.getElementById('root'))//Warning: Failed prop type: The prop `name` is marked as required in `Person`, but its value is `undefined`.in Person
ReactDOM.render(<Person name={person2.name} age={person2.age} sex="女"/>, document.getElementById('root')) // react.development.js:2721 Warning: Failed prop type: Invalid prop `age` of type `string` supplied to `Person`, expected `number`.
</script>
</body>
</html>
该demo虽然简单,但是从从如何传递props,如何做规则校验,如何给默认值都做了说明,可谓麻雀虽小。
- refs(react尽量不要操作dom,但是有时候不得不操作dom,那么,refs就是用来获取dom元素的,对于设置了ref属性的虚拟dom,可以通过refs属性来获取到dom)这里用一个小demo来阐述一下:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="root">
</div>
<div id="box">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
/***
* 自定义组件,功能说明下:
* 1. 点击按钮,提示第一个输入框中的值
*
* 2.当第二个输入框失去焦点的时候,提示这个输入框中的值
*
*
*******/
// 1. 定义组件
class MyComponent extends React.Component{
constructor (props) {
super(props)
this.showInput = this.showInput.bind(this)
this.handleBlur = this.handleBlur.bind(this)
}
// 这个是通过refs来获取dom元素的
showInput () {
const input = this.refs.content // 得到dom元素, 可以通过refs获取到dom对象,进而操作dom
// alert(input.value)
// 因为通过ref回调的方式保存了input,所以可以直接获取
alert(this.input.value)
}
// 所有的事件函数都可以接受一个event,这个是通过当前事件回调event来获取的
handleBlur (event) {
alert(event.target.value)
}
render () {
/**
//ref是个字符串
<input type="text" ref="content"/>
// ref可以是个回调函数,回调函数的参数就是当前dom元素,这里获取到dom元素input ,将其保存到组件自定义属性input中
<input type="text" ref={input => this.input = input}/>
****/
return (
<div>
<input type="text" ref="content"/>
<input type="text" ref={input => this.input = input}/>
<button onClick={this.showInput} >提示输入</button>
<input type="text" placeholder="失去焦点提示内容" onBlur={this.handleBlur}/>
</div>
)
}
}
// 2. 渲染组件标签
ReactDOM.render(<MyComponent/>, document.getElementById('root'))
</script>
</body>
</html>
这个小demo解释了refs属性的用法,通过ref标记dom元素,通过refs来获取dom元素,当然ref可以是字符串,也可以是回调函数,上面例子中的<input type="text" ref={input => this.input = input}/> 就是采用回调的方式,而且这种写法优雅,因为获取dom一次就被存起来了,不用每次都获取。
4. 组件组合使用(组件化)
组件化编程无非以下几个步骤:
- 拆分组件: 拆分界面,抽取组件
- 实现静态组件: 使用组件实现静态页面效果
- 实现动态组件: 动态显示初始化数据,交互功能(从绑定事件监听开始)
根据以上三个步骤进行编码工作 ,第一步其实就是图示的拆分方式,第二步编写三个静态组件,第三步做交互,改为动态组件。
// 编写静态组件
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="app">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
/**
* 实现一个简单的todolist案例
*
**/
// 1. 定义组件
class App extends React.Component{
render () {
return (
<div>
<h1>todo list</h1>
<Add/>
<List/>
</div>
)
}
}
// List组件
class List extends React.Component{
render () {
return (
<ul>
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
<li>ddd</li>
</ul>
)
}
}
// Add组件
class Add extends React.Component{
render () {
return (
<div>
<input type="text"/>
<button>add</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App/>, document.getElementById('app'))
</script>
</body>
</html>
至此,静态组件已经编写完成了,接下来就是改成动态组件,首先需要明确几个问题,这里有三个组件APP,Add,List。那么我们应该吧数据状态放在那个组件中?到这里我们必须明确知道到底需要哪些数据,其实这里只要一个list列表数据就可以了,我们可以称其为todos,这些数据是哪些组件使用的?很明显Add组件用,因为他要往里面添加数据,List也需要,因为他要读取。因此最好的方式是放在App中,因为App是他们共同的父组件,而且todos需要被Add和List共享。由此可以总结为:看数据是某一个组件需要还是某些组件需要,如果是某一个组件需要则放在组件内部,如果是某些组件需要,则放在他们公共的父组件中。
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="app">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
/**
* 实现一个简单的todolist案例
*
**/
// 1. 定义组件
class App extends React.Component{
constructor (props) {
super(props)
// 初始化组件状态
this.state = {
todos: ['吃饭', '睡觉', '打代码']
}
this.addTodos = this.addTodos.bind(this)
}
// 更新todos的行为方法,但是有个问题,调用addTodos是在子组件Add中调用的,那么只能通过将addTodos传到子组件执行
addTodos (todo) {
// this.state.todos.unshift(todo) 不能这么更改,因为我们知道更改state的唯一方式是setState,只有调用setState才能更新状态
const {todos} = this.state
todos.unshift(todo)
// 更新状态
this.setState({todos})
}
render () {
const {todos} = this.state
return (
<div>
<h1>todo list</h1>
<Add count={todos.length} addTodos={this.addTodos}/>
<List todos={todos}/>
</div>
)
}
}
// List组件
class List extends React.Component{
render () {
return (
<ul>
{
// this.props.todos 是一个数据数组,这里需要转为标签数组
this.props.todos.map((todo, index) => <li key={index}>{todo}</li>)
}
</ul>
)
}
}
// Add组件
class Add extends React.Component{
constructor (props) {
super(props)
this.add = this.add.bind(this)
}
add () {
// 需要在add 组件修改App组件的状态,即子组件中改变父组件的状态,这是相当困难的,子组件不能直接更改父组件状态,那么怎么改了?
// 也就是说状态在哪个组件,更新状态的行为就在哪个组件,这里因为todos在App组件中,那么修改todos的方法也在App中,那么在Add中怎么修改了,
// 1. 读取输入数据
const todo = this.todoInput.value.trim()
// 2. 检查合法性
if (!todo) {
return
}
// 3. 添加
this.props.addTodos(todo)
// 清除输入
this.todoInput.value = '' // this.todoInput其实就是dom的引用
}
render () {
return (
<div>
<input type="text" ref={input => this.todoInput = input}/>
<button onClick={this.add}>add#{this.props.count + 1}</button>
</div>
)
}
}
// List组件需要接受一个todos数据,通过这样的方式接受
List.propTypes = {
todos: PropTypes.array.isRequired // 表示必须传递一个数组
}
Add.propTypes = {
count: PropTypes.number.isRequired,
addTodos: PropTypes.func.isRequired
}
// 渲染组件
ReactDOM.render(<App/>, document.getElementById('app'))
</script>
</body>
</html>
此demo初步体现了组件化编程的思想。
5. 收集表单数据
表单在开发中都会用到,在react中,将表单组件分为受控和非受控两种,受控组件就是指表单项输入数据能够自动收集成状态,非受控组件是指需要表单项的值时手动读取表单输入框中的数据,以下通过demo来具体说明以下:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>react-demo1</title>
</head>
<body>
<div id="app">
</div>
<!--react的核心库-->
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!--是基于react的专门操作dom的扩展库-->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
/*
* 受控组件: 自己能够收集表单数据,我们只需要读取状态即可,需要定义状态,需要添加onChange事件,写起来比较麻烦
* 非受控组件: 我们通过手动收集表单输入框中的值
*/
class LoginForm extends React.Component{
constructor (props) {
super(props)
// 初始化状态
this.state = {
pwd: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleSubmit (event) {
const username = this.nameInput.value
const pwd = this.state.pwd
console('用户名为:'+username + '密码为:' + pwd)
event.preventDefault()
}
handleChange (event) {
// 读取输入的值
const pwd = event.target.value
// 更新pwd的状态
this.setState({pwd})
}
render () {
// 该组件中表单项input既有受控的,也有非受控的,因此当前这个组件既是受控组件也是非受控组件
return (
<form onSubmit={this.handleSubmit}>
用户名: <input type="text" ref={input => this.nameInput = input} />
密码: <input type="password" value={this.state.pwd} onChange={this.handleChange}/>
<input type="submit" value="登录"/>
</form>
)
}
}
// 渲染dom
ReactDOM.render(<LoginForm/>,document.getElementById('app'))
</script>
</body>
</html>
/**
* 这段代码中,受控项是密码输入框,在输入的时候,会自动收集输入框的值到state* 中,非受控项是用户名输入框,当需要输入框中的值的时候必须手动获取,如 const username = this.nameInput.value 就是在用到的时候才获取的
*
*
**/
以上就是对受控组件和非受控组件的阐述,从代码的简洁性上非受控组件简单,因为非受控组件不需要绑定onChange事件之类的。但是从react的思想出发,受控组件优于非受控组件,因为非受控组件其实就是在操作dom.
6.组件声明周期(组件从生到死的过程)
生命周期流程:
(一).第一次初始化渲染
- constructor(): 创建对象初始化state
- componentWillMount(): 将要插入虚拟dom的回调
- render(): 用于插入虚拟dom回调
- componentDidMount(): 已经插入虚拟dom的回调
(二).每次更新state: this.setState()
*componentWillUpdate(): 将要更新回调
*render(): 更新(重新渲染)
*componentDidUpdate(): 已经更新的回调
(三).移除组件ReactDom.unmountComponentAtNode(containerDom) - componentWillUnmount(): 组件将要被移除的回调
重要的几个钩子函数:
- render(): 初始化渲染或者更新渲染调用
- componentDidMount(): 开启监听,发送ajax请求
- componentWillUnmount(): 组建将要卸载,做一些收尾工作,如清理定时器
- componentWillReceiveProps(): 接收父组件属性的钩子