React组件三大属性
1. state
- 组件中
render
方法中的this
为组件实例对象 - 组件自定义方法中
this
为undefined
- 通过
bind
强制绑定this
- 箭头函数
- 通过
- 状态数据,不能直接修改或者更新
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
42class Weather extends React.Component {
// 构造器调用了 1 次
constructor(props) {
console.log("constructor")
super(props);
// 构造器中的this一定是当前的实例对象
this.state = {
isHot: true,
wind:'windy'
}
// bind 会返回一个新函数
this.demo = this.handleWeatherChange.bind(this); // = 后面的handleWeatherChange是原型对象上的
// 给实例对象自身又生成了一个 handleWeatherChange
}
handleWeatherChange() { // 只有通过实例调用,this才会指向实例对象
// 由于该函数是作为onClick的回调,所以不是通过实例调用的,是直接调用的
// 又因为类中定义的方法默认是局部严格模式,所以该方法中的this默认是undefined
console.log(this)
this.setState({
isHot: !this.state.isHot,
});
// state中的属性不能直接修改,是一种合并,不是替换
}
// render 调用了 1 + n 次 n是状态更新的次数
render() { // render 放在组件的原型对象上,通过实例对象进行调用
// console.log(this.state) // render中的this 代表的是实例对象
console.log("render")
const {isHot,wind} = this.state;
return (
// 调用的是实例对象上的demo。如果没有定义,则会顺着原型链找到原型对象上的 WeatherChange
// 但是原型对象上的WeatherChange中的this是undefined
<div onClick={this.demo}>
<h3 id="title">今天天气很{this.state.isHot ? 'hot' : 'cool'},{wind}</h3>
</div>
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 类中方法局部自动开启了严格模式,因此this默认不是window了
speak(){
// speak 放在了类的原型对象上,供实例对象使用
console.log(this); // this 指向person实例
}
}
const p1 = new Person('austyn', 18);
p1.speak(); // 通过实例对象调用speak方法
const x = p1.speak;
x(); // 直接调用 this是undefined
function demo() {
console.log(this);
}
const y = demo.bind({haha: 444})
y();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Weather extends React.Component {
state = {
isHot: true,
wind: 'Windy'
}
// 在实例对象上的自定义方法:赋值语句的形式+箭头函数
handleWeatherChange = () => { // 只有通过实例调用,this才会指向实例对象
console.log(this);
const isHot = this.state.isHot;
this.setState({isHot: !isHot})
}
render() { // render 放在组件的原型对象上,通过实例对象进行调用
const {isHot, wind} = this.state;
return (
<div onClick={this.handleWeatherChange}>
<h3 id="title">今天天气很{this.state.isHot ? 'hot' : 'cool'},{wind}</h3>
</div>
);
}
}
2. props
2.1 批量传递标签属性
1 | class Person extends React.Component { |
{...p}
通过解构赋值的语法进行批量传递。
展开运算符
1 | let person = {name: "jingxun", age:18, gender: "男"}; |
报错信息说对象类型没有iterator接口,也就是说不能直接把展开运算符直接运用到一个对象上,也就是说展开运算符不能展开一个对象。
1 | let person = { name: "jingxun", age: 18, gender: "男" }; |
展开运算符不能直接对对象进行展开,但是让我们在对象的{}内部的话是可以使用展开运算符来复制一个对象的,注意我这里说的是复制而不是展开对象。
所以说直接使用运算符作用在对象上是一个语法错误,如果在对象的{}内部用展开运算符展开作用在对象上则触发了一个新的语法,是复制一个对象。
注意
<Person {…p} />
- babel库和react的支持,使得我们可以在jsx语法环境下来用展开运算符来展开对象。但是在原生js语法中绝对是不允许展开对象的。
- babel库虽然支持展开对象,但是并不允许我们随便使用,仅仅只在标签属性传递时才允许展开一个对象,别的地方都不行。
- 这里的
{}
代表的仍然是对js
表达式的使用 - 这里的
...p
是展开对象,而不是复制对象了
总结
- props可以在标签中使用展开运算符展开一个对象来批量传入属性
- props在使用展开运算符时必须确保被展开对象中的属性和解构props时的变量一致才能正常取数
- 展开运算符在原生js语法中不能展开对象
- babel和react库支持仅在组件标签批量传入属性时展开对象
2.2 对props进行限制
对函数限制
1 | Person.propTypes = { |
Warning: Failed prop type: Person: prop type speak
is invalid; it must be a function, usually from the prop-types
package, but received undefined
in Person
1 | Person.propTypes = { |
限制
1 | Person.propTypes = { |
总结
- 限制并不是必须的
- 限制props必须引入prop-types
- 限制可以避免一些不必要的错误
- 限制有三种场景
- 限制属性是否必须非空
- 限制属性类型
- 限制属性默认值
- 限制属性类型为函数时,不能用function而要用func
2.3 简写props
- 定义类组件
- 对props进行类型以及必要性限制
- 给props添加默认值
- 渲染组件到页面
Person.propsTypes = {…}还有Person.defaultProps = {…}是给Person自身加了两个属性
- 在类里直接写赋值语句是什么意思?**是给这个类的实例对象添加属性 **但是我们是要给类本身添加属性啊。
- 为什么不是给实例对象添加呢?我们说了propsTypes和defaultProps是类本身的属性,react要在实例化组件之前去类本身上去查找这两个属性,然后再实例化对象,如果是给实例化对象上添加属性是不是要通过实例化之后的对象才能拿到啊?但是这一步查找实是在实例化对象之前的,都没有实例对象react上哪去拿实例对象的属性呢?所以说必须把这两个属性加在类的自身上。
在类没有被实例化就可以直接通过类名就调用到的属性和方法就是静态属性和静态方法。那么也就是说我们要把propsTypes和defaultProps作为Person类的静态属性。那么赋值语句前面加一个static关键字即可。
示例
1 | class Person extends React.Component { |
总结
- propsTypes和defaultProps必须是组件类自身的属性,不是实例对象的属性
- 用static关键字给类自身添加属性
- props是只读的
2.4 类组件中构造器和props
1 | constructor(props) { |
1 | constructor(props) { |
总结
- 类组件不写构造器完全可以,而且能不写就尽量不写
- 构造器中props传给super和不传给super的区别就是在构造器中能否通过this访问到props
2.5 函数式组件中props
- 函数中没有实例,因此无法使用state和refs
- 函数有参数,通过参数将props传入
- 函数没有static的概念,因此必须在函数体外进行限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function Person(props) {
const { name, age, gender } = props;
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{gender}</li>
<li>年龄:{age + 1}</li>
</ul>
);
}
Person.propTypes = {
name: PropTypes.string.isRequired,
gender: PropTypes.string,
age: PropTypes.number
}
Person.defaultProps ={
age: 18,
gender:"男"
}
ReactDOM.render(<Person name={123} />, document.getElementById("test"));
总结
- 函数式组件目前只能使用props
- 函数式组件使用参数来接收props
- 函数式组件可以限制props但是只能写在函数体外
2.6 总结props
基本使用
props其实就是标签属性,是可以批量传入的,而且在react和babel的支持下可以在组件标签中对一个对象使用展开运算符来完成批量传入。
对props进行限制
并不是必须对props进行限制,如果想限制就限制,不想限制随便传也可以。
限制也很简单,只需要给标签自身添加propTypes属性就行了。
如果说我们要给props添加默认值的话,可以通过给组件自身添加defaultProps属性来完成
构造器与props
构造器能省略就省略,实在不能省略的话,构造器一定要接收props并且传给super
refs
字符串形式的refs
1 | class Demo extends React.Component { |
总结
- refs是一个收集各个标签ref属性来作为标识的一个对象
key-value
中,key
代表人为定义的标识符/关键字,而val
则代表页面的节点 - refs可以代替document.getElementById方法来获取页面节点
- refs可以配合事件处理来完成相应的功能需求。
回调形式的refs
回调函数的特点:
- 程序员自定义的函数
- 程序员自己没有调用
- 最终函数被执行了
在ref
中定义的回调函数,其参数就是页面节点
回调函数的外层是render方法,render方法是react通过Demo组件的实例调用的,那么render方法中的this指向的就是组件的实例对象啊。因此,ref中定义的回调函数中的this就是render中的this,也就是组件的实例对象
1 | class Demo extends React.Component { |
为什么我们在回调函数里面返回了接到的参数refs却没有收集呢?
- 这里不是字符串ref啊,那肯定就不支持这种方法了。
- 而且跟你有没有return没有关系,因为这个是定义一个回调函数又不是定义一个函数并获取返回值。
- ref={ () => {} },这相当于ref=function,按照字符串ref的规则,是不是应该收集function: DOM然后存放到this.refs里面?这是一对key-value,但是函数不用来做key的
总结
- 回调式refs不会自动收集到this.refs中
- 字符串式refs有问题,后期可能会废弃
- 回调式refs所传入的ref属性是一个回调函数
- 回调式refs是将传入的标签节点传入回调函数,并通过回调函数将标签节点挂在实例自身
3.3 回调式 ref 被调用的次数
内联函数多次调用
1 | <input ref={val => { |
- 第一次渲染的时候,程序执行到这个input标签,发现指定了回调式的refs,那么程序就会自动调用这个回调函数。这是第一次渲染并执行了这一次。这一步没有任何问题。
- 但是当我们点击按钮更新了state之后出现了调用两次的情况。state会驱动页面更新,通过重新调用render方法。
那么render方法重新执行的时候,这个input标签这一行是不是也要被重新执行?然后程序又发现你这个标签里面有一个回调式的refs,是不是又要来执行这个函数?但是这个函数已经是一个新函数了,之前的那个函数已经执行完成了,那一块内存就自动释放了。所以这里是一个全新的回调函数。但是程序并没法确定之前那次回调函数都接到了什么。虽然我们都知道上一次函数接到的是当前所在的DOM节点,但是程序是不知道的,正是这个原因。所以程序必须要保证上一次这个回调函数的结果被清空,所以才会先调一次这个回调函数并且传入null来确保上次的结果清空了,然后再调一次这个回调函数来传入当前所在节点。
class的绑定函数
- 在构造器里调用this.method = this.method.bind(this)
- 在类中使用赋值语句配合箭头函数更新了状态也还是一次。所以说通过这种方法就可以解决回调式refs调用两次的情况。但是官方也明确说明了,即便调用两次这种内联函数的形式,其实对我们的程序没有任何影响。所以后期的开发或者是案例,我们还是以写内联函数居多。
1
<input ref={this.bindInput2} onBlur={this.blurData} placeholder="失去焦点"/>
总结
- 回调式refs会在页面第一次被渲染时调用一次
- 如果回调式refs是内联函数形式,那么在state更新之后,会被重新调用两次
- 内联函数形式的回调式refs调用次数对程序不会有影响
- 类组件绑定函数的方式可以解决refs被多次调用的问题
3.4 使用 createRef
使用
1 | class Demo extends React.Component { |
- 其实只要调用了React.createRef那么就会得到一个容器,这个容器里面会收集所有被ref属性标识的DOM节点。
- myRef = React.createRef();把我们调用React.createRef得到的容器放在组件实例对象自身上的myRef属性了。那么我们在标签里的ref属性就直接ref={this.myRef}。
- 当react通过组件实例对象调用了render方法的时候发现这个input标签里面有个ref属性,而且这个属性还是React.createRef返回的一个容器,就自动把当前的DOM节点传入到了这个容器中
- 如果有多个
ref = this.myRef
,会覆盖掉
总结
- createRef可以返回一个容器用于存放被ref属性标识的DOM节点
- createRef返回的容器“专人专用”
- 如果多个标签要被标识,就要创建多个容器
3.5 总结refs
字符串形式ref
在标签里直接写ref属性,然后给字符串形式的值
但是官方已经不在推荐使用字符串形式的ref所以说要尽量避免使用字符串形式的refs
回调式refs
一个回调函数,直接通过回调函数把被标识的DOM节点挂在组件实例对象自身,不用纠结是不是内联函数,不重要,没有影响。也是我个人最喜欢的方式
createRef
会返回一个容器,但是容器“专人专用”,多个标签被标识就要创建多个容器,最麻烦了。虽然官方最推荐这种。但是我不接受官方的推荐。