React(二)- React组件
前言
先来说下模块和组件的差别:
模块:向外提供特定功能的JS程序,一般是一个JS文件。
作用:复用JS。
组件:用来实现局部功能效果的代码和资源的集合(Html、Css、Js等)。
作用:复用编码。
安装React调试工具
1.打开谷歌浏览器,选择扩展程序:
2.点击左侧的扩展程序,并打开Chrome网上应用商店:
3.搜索React,添加对应组件,注意提供方是Facebook
4.固定插件于右上角,方便使用:
5.打开用React框架编写的页面,进行检查,如美团的首页:
以服务的方式打开HTML文件
有些小伙伴通过Vscode软件打开某个HTML,并且也下载了上面的React调试工具,但是那个图标还是不显示,是因为我们是以本地的方式打开这个文件,而不是打开一个运行于某个服务上的页面。
解决方案如下:
1.下载Live Server插件:
2.安装完成后,鼠标右键打开文件:
3.完成,其中默认的端口是5500。
一. React组件
React的组件有两种:
- 函数式组件
- 类式组件
我们从简单的函数式组件来讲起
1.1 函数式组件
案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函数式组件</title>
</head>
<body>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<div id="test"></div>
<script type="text/babel">
// 1. 创建函数式组件,函数式组件的名称必须是大写开头,在React系列一种有提及到
// 若以大写字母开头,React就会去渲染对应的组件
function DEMO() {
// 此处为undefined,因为经过babel编译后,开启了严格模式
console.log(this);
return <h2>我是函数定义的组件</h2>
}
// 2.渲染组件到页面,第一个参数需要用标签来修饰
ReactDOM.render(<DEMO/>, document.getElementById('test'))
</script>
</body>
</html>
页面效果如下:
倘若组件名字是小写,则报错:
那么执行ReactDOM.render()
代码后,发生了什么?
- React解析组件的标签,找到了xxx组件。
- 发现该组件是由函数定义的,随后定义了该函数,将返回的虚拟DOM转化为真实DOM。
- 页面渲染对应的DOM,展示。
1.2 类式组件
1.2.1 JS有关类的基本语法
1.定义类、构造、方法。
<script type="text/javascript">
// 定义一个对象
class Person {
// 构造器方法
constructor(name, age) {
// 构造器中的this指的是:类的实例对象
this.name = name
this.age = age
}
// 定义一个一般方法,该方法处于类的原型对象上,供给实例对象使用
say(){
console.log(`我是${this.name},年龄是${this.age}`)
}
}
// 创建一个对象
const p1 = new Person("Tom", 20);
console.log(p1);
p1.say();
</script>
结果如下:
2.继承
class Student extends Person {
// 若有自己新增的属性,构造器可以这么写
// 通过super来代替this.xxx=xxx这种重复的代码
constructor(name, age, grade) {
super(name, age)
this.grade = grade
}
}
const s1 = new Student("Tom", 20, 'msg');
接下来来看下如何使用类式组件:
<body>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<div id="test"></div>
<script type="text/babel">
// 1.创建类式组件,有两个条件。1.需要继承React内置的类。2.类中有render()方法,且有返回值
class MyComponent extends React.Component {
render() {
return <h2>我是类定义的组件</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
</script>
</body>
那么对于类式组件,执行ReactDOM.render()
代码后,发生了什么?
- React解析组件的标签,找到了xxx组件。
- 发现该组件是由类定义的,随后new了一个该类的实例,并通过该实例调用到原型上的render方法。
- 将render返回的虚拟DOM转化为真实DOM,渲染页面。
1.3 函数式和类式组件的区别
函数式组件 | 类式组件 | |
---|---|---|
定义方式 | function xxx(){} | class xxx extends React.Component{} |
this的指向 | undefined | 组件实例对象 |
注意的点 | 一定要有render() 方法,并且有返回值 | |
解析过程 | 随后定义了该函数,将返回的虚拟DOM转化为真实DOM | 随后new了一个该类的实例,并通过该实例调用到原型上的render 方法。 |
二. 组件实例的三大核心
2.1 state
React中的每个组件实例对象都有一个“状态”:state
,值是对象(可以包含多个key-value组合),一般我们通过更新组件的state
的值来更新对应的页面显示。
注意:
- 这里的组件实例对象指的是类式组件。
Demo1:通过修改state
值,来更新页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS</title>
</head>
<body>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<div id="test"></div>
<script type="text/babel">
class Weather extends React.Component {
// 这里的this指的是组件实例对象
constructor(props) {
super(props)
// 2.初始化状态
this.state = { isHot: true }
// 5.解决change方法中的this指向问题
this.method = this.change.bind(this)
}
// 3.定义render方法
render() {
// 这里的this指的是组件实例对象
// React中的点击事件的绑定,需要用到onClick,并且后面的函数,不需要加括号,因为这不是一个赋值语句。而是指定一个函数
return <h2 onClick={this.method}>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h2>
}
// 4.定义一个绑定事件函数
change() {
// 该方法中,只有通过Weather实例对象去调用change()方法的时候,这里面的this才是实例对象
// 由于change是作为onClick的回调,因此不是通过实例的方式去调用的,而是直接调用。
// 类中的方法默认开启了局部的严格模式,因此console.log(this)这里输出是undefined
// console.log(this)
// 5.类组件中的状态不可以直接修改,而是需要一个内置的方法进行修改
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
}
// 渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById('test'))
</script>
</body>
</html>
页面效果:
点击后:
注意:
- 若鼠标点击N次,那么代码中,构造器只会执行一次,而render()方法会执行1+N次。
Demo2:Demo1的精简写法
<script type="text/babel">
class Weather extends React.Component {
// 不用写构造了,直接赋值初始化就行
state = { isHot: true }
render() {
return <h2 onClick={this.method}>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h2>
}
// 箭头函数有一个特点就是没有自己的this,如果箭头函数中使用了this关键字,那么就会去找外层的this来当做自己的this
// 因此这里的this指的是类组件实例对象
// 以后一个类组件中的方法一般都要写成赋值语句的格式,并且写成箭头函数
change = () => {
const isHot = this.state.isHot
this.setState({ isHot: !isHot })
}
}
// 渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById('test'))
</script>
小总结1
- 类式组件中,比如绑定一个单击事件,需要用
bind
函数去解决this
的指向问题。 - 类式组件中,构造和
render()
方法中的this
指的是组件实例对象,而其他的内部方法的this
是undefined
。 - 类组件中的状态不可以直接修改,而是需要一个内置的方法
setState()
进行修改。 - 箭头函数有一个特点就是没有自己的
this
,如果箭头函数中使用了this
关键字,那么就会去找外层的this
来当做自己的this
。 - 实际开发中,建议使用Demo2的方式去编写类式组件。state是一个对象,直接内部赋值即可。结构:
state = {key:value,key1:value2...}
2.2 props
每个组件对象都会有props
属性,组件标签的所有属性都保存在props
中。
Demo1:props
的简单使用:
<div id="test"></div>
<script type="text/babel">
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person name="Hello" age="30" sex="男" />, document.getElementById('test'))
</script>
React调试工具结果如下:
当然,若一个对象的属性有非常多,我们不可能像上面一样将属性一一列出来,因此,React还提供了一个语法糖,如下:
const p = { name: 'Tom', age: 19, sex: '男' }
ReactDOM.render(<Person {...p} />, document.getElementById('test'))
2.2.1 语法糖扩展
语法糖:展开运算符...数组
<script type="text/javascript">
let arr1 = [1, 2, 3, 4, 5]
let arr2 = [6, 7, 8, 9]
console.log(...arr1)
let arr3 = [...arr1, ...arr2]
console.log(...arr3)
function sum(...numbers) {
// 第一个参数是上一个值,第二个参数是当前值,那么求和就是上面的和+当前值
return numbers.reduce((pre, cur) => {
return pre + cur
})
}
console.log(sum(...arr3))
</script>
结果如下:
对象的复制操作:
let obj1 = { a: "str", b: "str" }
// 复制一个对象,若想更改某个属性,直接后面跟上key:value即可
let obj2 = { ...obj1 }
// 改变引用
let obj3 = obj1
obj1.a = "a"
console.log(obj2)
console.log(obj3)
结果如下:
2.2.2 对props进行限制
这是Demo1当中的部分代码:
ReactDOM.render(<Person name="Hello" age="30" sex="男" />, document.getElementById('test'))
我们可以发现,一般年龄这个属性,其字段类型应该是整形,但是我们在传参的时候,把它当做了字符串类型,如果想要传入一个整形,应该这么改:
ReactDOM.render(<Person name="Hello" age={30} sex="男" />, document.getElementById('test'))
但是这并不是最好的一种方案,我们应该对标签属性类型进行限制:
引用prop-types.js
文件:一旦引用后,全局就多了一个对象PropTypes
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
Demo1:类式组件使用props
并定义规则
<body>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<div id="test"></div>
<div id="test1"></div>
<div id="test2"></div>
<script type="text/babel">
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age + 1}</li>
</ul>
)
}
}
// 定义标签属性规则
Person.propTypes = {
name: PropTypes.string.isRequired,// 限制name必传,且为string类型
say: PropTypes.func// 限制say标签的类型必须是函数
}
// 默认值
Person.defaultProps = {
sex: '不男不女'
}
ReactDOM.render(<Person name={30} age={30} sex="男" say="2"/>, document.getElementById('test'))
ReactDOM.render(<Person age={30} sex="男" />, document.getElementById('test1'))
ReactDOM.render(<Person name="Str" age={30} />, document.getElementById('test2'))
</script>
</body>
结果如下:
Demo2:Demo1的精简写法(将标签规则以及默认值放到类组件中)
<script type="text/babel">
class Person extends React.Component {
// 定义标签属性规则
static propTypes = {
// ...
}
// 默认值
static defaultProps = {
// ...
}
render() {
//...
}
}
</script>
2.2.3 类式组件和函数式组件的构造与props
官网中有提到,通常在React中,构造函数仅用于以下两种情况:
- 通过
this.state
赋值对象来初始化内部state
。 - 为事件处理函数绑定实例。
注意:
1.在构造函数里面,不要使用
setState()
方法,若要初始化,直接this.state=xxx
即可。
2.若Reat.Component
的子类有构造器,若构造器中用到了this.props
,那么必须往构造器中传入props
,且必须调用super()
。
3.props
是只读的属性,修改值相关的操作会报错。
函数式组件使用props
:
<div id="test"></div>
<script type="text/babel">
function Person(props) {
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age + 1}</li>
</ul>
)
}
Person.propTypes = {
name: PropTypes.string.isRequired,
}
// 默认值
Person.defaultProps = {
sex: '男',
age: 18
}
ReactDOM.render(<Person name="小明" />, document.getElementById('test'))
</script>
小总结2
- 每个组件标签的所有属性都会保存到
props
中。 - 往组件中传入属性的时候,可以通过
{...对象}
展开运算符的方式来传参。 props
的属性限制需要引入prop-types.js
文件。规则格式:xxx.propTypes={}
,默认值格式xxx.defaultProps={}
。props
是只读的属性,修改值相关的操作会报错。
2.3 refs
Demo1:点击按钮,提示第一个输入框中的值,点击第二个输入框失去焦点时,提示输入框中的内容。
<body>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<div id="test"></div>
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
render() {
return (
<div>
<input ref="p1" type="text" placeholder="点击按钮提示数据" />
<button ref="b" onClick={this.showData}>点击我提示左侧的数据</button>
<input onBlur={this.showData2} ref="p2" type="text" placeholder="失去焦点提示数据" />
</div>
)
}
// 展示左侧输入框的数据
showData = () => {
const { p1 } = this.refs
alert(p1.value)
}
// 展示右侧输入框的数据
showData2 = () => {
const { p2 } = this.refs
alert(p2.value)
}
}
// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
</body>
结果如下:
其实这种refs的使用方式并不好,上述案例中是string类型的,而该类型的refs存在一些问题。官方建议用回调函数或者createRef API
的方式去代替。
2.3.1 回调方式使用refs
render() {
return (
<div>
// 将当前的节点赋给this对象,并且命名为input1
<input ref={(curNode) => this.input1 = curNode} type="text" placeholder="点击按钮提示数据" />
<button ref="b" onClick={this.showData}>点击我提示左侧的数据</button>
</div>
)
}
// 展示左侧输入框的数据
showData = () => {
const { input1 } = this
alert(input1.value)
}
其中,回调ref
这种方式还有一个问题需要考虑:回调执行次数,如果ref
回调函数是以内联函数的方式去定义的(如上面的案例中,直接在标签里面定义了一个函数),那么在更新过程中他会被执行两次:
- 第一次传入参数null。
- 第二次传入参数DOM元素。
这是因为在每次渲染时会创建一个新的函数实例,所以 React会清空旧的 ref
并且设置新的。
案例如下:
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
state = { isHot: true }
render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input type="text" ref={(curNode) => { this.input1 = curNode; console.log('@', curNode) }} />
<button onClick={this.showInfo}>点我提示输入的数据</button>
<button onClick={this.change}>切换</button>
</div>
)
}
showInfo = () => {
const { input1 } = this
alert(input1.value)
}
change = () => {
const { isHot } = this.state
this.setState({ isHot: !isHot })
}
}
// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
结果如下:可以发现,每点击一次切换按钮,就会调用2次内联函数
那么如何避免这种问题的发生呢?
解决:通过将 ref
的回调函数定义成 class
的绑定函数的方式。
改动部分如下:
render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
{/*<input type="text" ref={(curNode) => { this.input1 = curNode; console.log('@', curNode) }} />*/}
<input type="text" ref={this.saveInput} />
<button onClick={this.showInfo}>点我提示输入的数据</button>
<button onClick={this.change}>切换</button>
</div>
)
}
saveInput = (curNode) => {
this.input1 = curNode; console.log('@', curNode)
}
最后无论点击多少次切换按钮,都不会多次调用函数:
这两种本质上是没有区别的,内联函数产生的2次调用也不会有什么影响,因此没有必要特意将内联函数的写法改成第二种。
2.3.2 createRef方式使用refs
案例如下:
class Demo extends React.Component {
/**
* React.createRef()调用后会返回一个容器,该容器可以存储被ref所标识的节点。
* 但是该容器是只能够存储一个节点。
*/
myRef = React.createRef()
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
<button ref={this.myRef} ref="b" onClick={this.showData}>点击我提示左侧的数据</button>
</div>
)
}
// 展示左侧输入框的数据
showData = () => {
// current指的是当前节点,也就是<input xxx>框 那么自然而然的current.value也就是输入框的值了
console.log(this.myRef.current.value)
}
}
小总结3
refs
引用:可以将带有ref="xxx"
标识的所有标签给当前组件实例对象中的refs
属性。- 官方不建议使用string类型的ref引用,推荐使用回调函数或者
createRef API
。 - 若是回调函数使用,使用格式
ref="内联函数"
,但是这种方式在更新操作时会造成2次调用,第一次传入参数null,第二次传入DOM元素。(实际并不会影响到什么) - API方式,则可以通过
var xxx = React.createRef()
来返回一个容器,容器用来存储被ref
标识的节点,但是只能对应一个节点。 可以通过this.xxx.current.value
来获得当前节点的值(比如输入框)