2.1 基本理解和使用
2.1.1 函数式编程
概要总结
1、创建函数式组件
2、函数式组件运行原理
一、创建函数式组建
<script type="text/babel">
// 1.创建函数式组件
function demo() {
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
</script>
二、渲染组件到页面
<script type="text/babel">
// 1.创建函数式组件
function demo() {
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
// 2.渲染组件到页面
ReactDOM.render(demo, document.getElementById('test'))
</script>
注意:直接把函数放在渲染函数里,它会报错:函数类型是不能作为React节点的。
解决方案:对于函数式组件,渲染时应该使用标签。
<script type="text/babel">
// 1.创建函数式组件
function demo() {
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
// 2.渲染组件到页面
ReactDOM.render(<demo/>, document.getElementById('test'))
</script>
注意:此时用了标签,它还会报错:标签是不被浏览器所允许的。
解决方案:jsx的语法规则,如果标签首字母是小写,则该标签转为html的同名元素,很明显html是没有demo这个标签的,因此报错。所以如果是组件,那么首字母必须是大写,只需要把方法名和标签名的首字母都改为大写即可。
<script type="text/babel">
// 1.创建函数式组件
function Demo() {
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
// 2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
三、函数组件式原理
1、this指向
在函数里的this,是undefined,而不是window,因为它要经过babel翻译,会开启了严格模式。
2、React对函数式组件的工作流程
(1)React解析了组件标签,找到了MyComponent组件
(2)发现组件式使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中
2.1.2 类式组件
概要总结
1、创建类式组件
2、类式组件运行原理
一、创建类式组建
1、继承React.Component
2、类里必须要有render函数
3、render函数必须有return返回值
<script type="text/babel">
// 1.创建类式组件
class MyComponent extends React.Component {
render() {
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
</script>
二、渲染组件到页面
<script type="text/babel">
// 1.创建类式组件
class MyComponent extends React.Component {
render() {
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
// 2.渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>
三、类式组件原理
1、React对类式组件的工作流程
(1)React解析了组件标签,找到了MyComponent组件
(2)发现组件式使用类定义的,随后new出来该类的实例,并通过实例调用到原型上的render方法
(3)将render返回的虚拟DOM转为真实DOM,随后呈现在页面中
2、render函数里的this指向
由于render函数是在类组件内部,因此this指向类的实例对象。
<script type="text/babel">
// 1.创建类式组件
class MyComponent extends React.Component {
render() {
// render是放在哪里的? —— MyComponent的原型对象上,供实例使用
// render中的this是谁? —— MyComponent的实例对象。
console.log('render中的this', this)
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
// 2.渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>
2.2 组件三大核心属性1: state
2.2.1 初始化state
概要总结
1、初始化state属性
2、render函数读取state属性
一、初始化state属性
1、类式组件添加构造器
因为类式组件它是继承于React.Component,这个state属性是继承之后就已经存在了,它的默认值为null。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
render() {
console.log(this)
return <h1>今天天气很炎热</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
现在我们需要对state重新赋值,因此要先写一个构造器,把父组件的东西继承下来,然后再改写:
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props);
}
render() {
console.log(this)
return <h1>今天天气很炎热</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
2、重写state属性
React规定,state属性必须是一个对象。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {isHot: true}
}
render() {
console.log(this)
return <h1>今天天气很炎热</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
二、render函数读取state属性
由于render函数里的this就是指向组件类本身,而state是类的属性,因此直接在render函数里使用this.state就能读取成功。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
this.state = {isHot: true}
}
render() {
const {isHot} = this.state
return <h1>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
2.2.2 react的事件绑定
概要总结
1、react的3种绑定事件方式
2、react绑定与原生事件绑定的区别
一、react绑定事件的3种方式
1、使用addEventListener方法绑定
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 id="title">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
const title = document.getElementById('title')
title.addEventListener('click', () => {
console.log('标题被点击了')
})
</script>
2、使用onclick方法绑定
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 id="title">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
const title = document.getElementById('title')
title.onclick = () => {
console.log('标题被点击了')
}
</script>
3、在元素里绑定事件
前两个方法虽然可以,但是这毕竟都是原生的绑定方法,跟React无关。在元素里绑定事件是React推荐的。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onclick="demo()">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo() {
console.log('标题被点击了')
}
</script>
注意:它会报错:onclick是一个不被允许的属性,你是不是想用"onClick"?
解决:React把原生的事件都进行了一次重写,命名规则使用了驼峰式命名。例如onclick会被改写成onClick,onblur改写成onBlur
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick="demo()">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo() {
console.log('标题被点击了')
}
</script>
注意:它会报错:onClick监听者必须是一个函数,但是我得到了一个不想要的string类型。
解决:在React里跟原生js有所不同,React绑定事件需要传一个函数进去即可,可以将"demo()"写成{demo()}。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={demo()}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo() {
console.log('标题被点击了')
}
</script>
注意:绑定事件写成{demo()},现在的情况是还没触发事件自己就已经先执行了。
解决:React在渲染组件的时候,React会初始化一个实例,实例会调用render方法,然后执行return返回值。发现里面绑定了一个onClick事件,它的值是demo方法的返回值。现在就变成了把demo的返回值赋值给onClick事件,demo方法的返回值是undefined的。React做了处理,发现了是undefined赋值给事件的,它就不会进行处理。最终的解决办法就是把()去掉,把demo函数给onClick作为回调。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo() {
console.log('标题被点击了')
}
</script>
2.2.3 类中方法中的this
概要总结
1、通过类内部的this赋值给外部变量来获取类属性
2、类内部定义函数的this指向问题
一、在方法里指定this指向类实例对象
由于方法在类的外部,如果想在方法里操作类里的属性,首先要解决this的指向问题。因为在方法里的this是没办法指向类实例的,而类实例是由React自己创建的。可以通过外部定义一个变量,将类里的this赋值给变量,那么方法就可以操作类属性。
<script type="text/babel">
let that
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
that = this
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo() {
console.log(that)
}
</script>
说明:这样虽然可以操作类属性,但是由于里面的逻辑都属于类的内部,而方法定义在外面显然是不合理的。
二、将事件绑定的方法挪到类的内部
将方法写在类的内部,绑定的事件需要加上this.即可,而方法里可以通过this来获取类里的属性。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
// 通过Weather实例调用Weather时,changeWeather中的this就是Weather实例
console.log(this.state.isHot)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
注意:这里报错说的是在undefined里没有state属性,也就是说this是undefined。
说明:
(1)首先类的constructor和render两个函数,是React在构造的时候内部自己调用的,因此this一定是指向类的实例对象,而自己写的changeWeather方法并不是类实例调用的,因此它的this并不是指向类实例
(2)在绑定事件的过程中,onClick={this.changeWeather}这个写法就相当于把changeWeather这个函数赋值给了onClick作为回调而已,这只是一个赋值语句。所以在触发这个点击事件的时候,它就把这个changeWeather函数从堆里面找出来直接调用,那里面的this应该指向window而不是类实例。
(3)在类中定义的方法,它在局部都会开启严格模式,是类自己开的与babel没有关系,所以这个this就变成了undefined。
2.2.4 解决类中this的指向问题
概要总结
1、使用bind方法解决this的指向问题
一、使用bind方法解决this的指向问题
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
this.changeWeather = this.changeWeather.bind(this)
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
// 通过Weather实例调用Weather时,changeWeather中的this就是Weather实例
console.log(this.state.isHot)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
1、this是指向类实例,那么在this.changeWeather = this.changeWeather.bind(this)中,右边的this.changeWeather在实例属性是不存在的,但是它会顺着往原型链上找到该方法。
2、bind会生成一个新的函数,而且改变this指向。.bind(this)的意思是用这个传入的this来改变函数内部的this指向,传入的this就是类实例,而函数内部的this原本指向window,所以.bind(this)就把函数内部指向window的this变成指向了类实例。
3、把改变了this的新函数赋值给左边的this.changeWeather,这个this.changeWeather属于实例属性。
此时在自身属性和原型链都存在changeWeather,那么onClick绑定事件的时候,它会按照就近原则先去找实例,再找原型,因此它目前绑定的是实例上的changeWeather方法。
2.2.5 setState的使用
概要总结
1、直接改变state属性
2、使用setState改变状态
3、setState方法是合并操作
4、构造器、render函数以及自定义方法的调用次数
一、直接改变state内部属性
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
this.changeWeather = this.changeWeather.bind(this)
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// 获取原来的isHot值
const isHot = this.state.isHot
this.state.isHot = !isHot
console.log(this.state.isHot)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
注意:此时我们发现,state里面的值已经改变了,但是视图并没有切换内容,因为React不承认你直接修改状态属性的行为,也就是说不能直接改state。
二、使用setState方法修改状态
我们查看这个类就会发现,首先最外层的就是这个类的属性和原型,这个原型也就是这个类的原型。由于这个类是继承了React.Component,因此原型里的原型,就是React.Component,而这个setState方法就是从React.Component里继承过来的。
此时使用setState方法,传入对象进行修改对应的属性即可:
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
this.changeWeather = this.changeWeather.bind(this)
}
render() {
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// 获取原来的isHot值
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
三、setState方法是合并,而不是替换
setState修改的过程中,它是一个合并的操作,只会改变对应的属性值,其它的保持不变。
四、构造函数、render、自定义方法分别调用次数
1、constructor是类初始化的时候调用1次
2、render函数是1+n次,1是类初始化的时候调用,n是代表状态的改变次数,也就是说setState调用了几次,render函数就执行几次
3、自定义方法就是触发几次执行几次
2.2.6 setState的简写方式
概要总结
1、直接改变state属性
2、使用setState改变状态
3、setState方法是合并操作
4、构造器、render函数以及自定义方法的调用次数
一、类的constructor用法
<script type="text/babel">
class Car {
constructor(name, price) {
this.name = name
this.price = price
}
}
const c1 = new Car('奔驰c63', 199)
console.log(c1)
</script>
对于初始化传入的参数,那必须得在constructor里去接收。但是对于一些实例的公共参数,例如汽车有4个轮子这样的参数,是不需要外部传入的,我们也没必要写在constructor里,可以写在类的内部。
<script type="text/babel">
class Car {
constructor(name, price) {
this.name = name
this.price = price
}
wheel = 4
}
const c1 = new Car('奔驰c63', 199)
console.log(c1)
</script>
注意:在类里写赋值语句,它是给类的实例对象添加属性,而不是给原型添加属性!
二、精简constructor里的state初始化
我们在constructor里对state重新赋值,无非是React给我们初始化实例的state属性值为null,不符合我们需求。那我们可以把写在constructor里的state拿出来放在类的内部:
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
this.changeWeather = this.changeWeather.bind(this)
}
state = {isHot: false, wind: '微风'}
render() {
const {isHot, wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
changeWeather() {
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
三、精简constructor里对方法的this绑定语句
在constructor里对自定义方法调用.bind(this),目的是把方法里的this指向类的实例。此时自定义的changeWeather方法是定义在类的原型上,如果把它变成一个方法再赋值给changeWeather,那么这个changeWeather方法就变成了实例属性了。也就是把changeWeather() {}改成changeWeather = function() {}。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
}
state = {isHot: false, wind: '微风'}
render() {
const {isHot, wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
changeWeather = function() {
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
此时它仍然会报错this为undefined,因为现在的做法只是把changeWeather从原型挪到实例上,它给onClick绑定的时候,this仍然是undefined的。但只需要使用箭头函数,那么this的指向问题就迎刃而解:
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
constructor(props) {
super(props)
}
state = {isHot: false, wind: '微风'}
render() {
const {isHot, wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
changeWeather = () => {
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
四、删除constructor构造器
constructor构造器最大的作用就是初始化state以及改变自定义方法的this指向,然而目前这两个问题都不需要在constructor里处理,也就是说constructor可以去掉了。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component {
// 初始化状态
state = {isHot: false, wind: '微风'}
render() {
const {isHot, wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
changeWeather = () => {
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}
// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
2.3 组件三大核心属性2: props
2.3.1 props的基本使用
概要总结
1、创建组件传参
2、使用props读取传入参数
一、创建组件传参
在创建组件的时候,对组件直接传参,组件从React继承下来的实例属性props能接收到参数。
<script type="text/babel">
// 1.创建组件
class Persion extends React.Component {
render() {
return (
<ul>
<li>姓名:tom</li>
<li>性别:女</li>
<li>年龄:18</li>
</ul>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test'))
</script>
二、组件内部使用props读取传参属性
组件不需要做任何处理,直接使用this.props读取属性渲染即可。
<!-- 准备好一个"容器" -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>
<script type="text/babel">
// 1.创建组件
class Persion extends React.Component {
render() {
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.sex}</li>
<li>年龄:{this.props.age}</li>
</ul>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Persion name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test2'))
ReactDOM.render(<Persion name="老刘" age="30" sex="女"/>, document.getElementById('test3'))
</script>
三、优化props代码
<!-- 准备好一个"容器" -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>
<script type="text/babel">
// 1.创建组件
class Persion extends React.Component {
render() {
const {name, sex, age} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Persion name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test2'))
ReactDOM.render(<Persion name="老刘" age="30" sex="女"/>, document.getElementById('test3'))
</script>
2.3.2 批量传递props
概要总结
1、使用扩展运算符批量传递props
2、回顾扩展运算符对对象的使用
一、使用扩展运算符传递批量参数
const p = {name: '老刘', age: 30, sex: '女'}
ReactDOM.render({...p}/>, document.getElementById('test3'))
<script type="text/babel">
// 1.创建组件
class Persion extends React.Component {
render() {
const {name, sex, age} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const p = {name: '老刘', age: 30, sex: '女'}
ReactDOM.render(<Persion {...p}/>, document.getElementById('test3'))
</script>
二、关于扩展运算符
1、扩展运算符无法直接展开对象
扩展运算符可以直接对数组进行扩展,但是对象不行。
<script type="text/babel">
let person = {name: 'tom', age: 18}
console.log(...person)
</script>
2、扩展运算符字面量展开对象
虽然扩展运算符无法直接展开对象,但可以在外层包裹一个花括号,即变成ES6语法使用字面量展开对象。
<script type="text/babel">
let person = {name: 'tom', age: 18}
console.log({...person})
</script>
注意:使用扩展运算符可以实现拷贝或者合并对象的功能,但这种既不是深拷贝也不是浅拷贝,它只能深拷贝第一层,第二层开始又是浅拷贝。
3、React使用扩展运算符展开对象
通过React库和babel的结合,可以使用扩展运算符展开对象。
<script type="text/babel">
// 1.创建组件
class Persion extends React.Component {
render() {
const {name, sex, age} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const p = {name: '老刘', age: 30, sex: '女'}
ReactDOM.render(<Persion {...p}/>, document.getElementById('test3'))
</script>
注意:这里的{...p}的花括号,代表的意思是告诉React这里面写的是js表达式,而不是扩展运算符的对象字面量的花括号,所以在React这里,它是没有{}而直接对对象使用扩展运算符,这种只能在React与babel结合才可以使用。
2.3.3 对props进行限制
概要总结
1、使用PropTypes限制传参的类型和必填
2、使用defaultProps设置参数默认值
一、引入prop-types.js
在React15版本,它是通过React.PropTypes.xxx来进行限制。但是这种方式是在React身上加上PropTypes属性,随着内容越来越多,会导致React核心对象越来越重,而且这个限制也不是一定需要,所以直接挂在React上不太合适。
在16的版本里,PropTypes已经不再从React里读取了,通过引入prop-types.js来使用PropTypes。
<script type="text/javascript" src="../js/prop-types.js"></script>
二、使用PropTypes进行限制
1、限制类型
<script type="text/babel">
// 1.创建组件
class Person extends React.Component {
render() {
const {name, sex, age} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
Person.propTypes = {
name: PropTypes.string
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
ReactDOM.render(<Person name="tom" age="18" sex="女"/>, document.getElementById('test2'))
const p = {name: '老刘', age: 30, sex: '女'}
ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))
</script>
这里限制了name属性为字符串类型,如果传了其它类型,它也能正常显示,但会在控制台报错:
const p = {name: 100, age: 30, sex: '女'}
假设定了string类型,结果传的是number类型,它会提示它希望得到string类型,实际上得到number类型。
注意:对于函数类型,它的属性并不是function,而是func。因为function属于关键字,它改名为func。
2、限制必传
使用isRequired即可限制必传,而且它可以叠加在其它限制的后面,例如:PropTypes.string.isRequired
Person.propTypes = {
name: PropTypes.string.isRequired
}
// 2.渲染组件到页面
ReactDOM.render(<Person age="19" sex="男"/>, document.getElementById('test1'))
三、使用defaultProps设置默认值
Person.defaultProps = {
sex: '不男不女',
age: 18
}
ReactDOM.render(<Person name="jerry"/>, document.getElementById('test1'))
2.3.4 props的简写方式
概要总结
1、props属性不能修改
2、将propsType、defaultProps等类属性使用static放在类内部
一、props属性不能修改
props里的属性都是只读的,只可以拿来使用,不允许做修改。
<script type="text/babel">
// 1.创建组件
class Person extends React.Component {
render() {
const {name, sex, age} = this.props
this.props.name = 'jack'; // 此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
Person.propTypes = {
name: PropTypes.string
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
</script>
二、props的简写方式
这里定义了类之后,无论是对它进行类型的限制还是指定默认值,都是给类自身添加属性。
对类本身添加属性,可以在类的内部,使用static关键字进行添加:
<script type="text/babel">
// 1.创建组件
class Person extends React.Component {
// 对标签属性进行类型、必要性的限制
static propTypes = {
name: PropTypes.string.isRequired, // 限制name必传,且为字符串
sex: PropTypes.string, // 限制sex为字符串
age: PropTypes.number, // 限制age为数值
speak: PropTypes.func // 限制speak为函数
}
// 指定默认标签属性值
static defaultProps = {
sex: '男', // sex默认值为男
age: 18 // age默认值为18
}
render() {
const {name, sex, age} = this.props
// props是只读的
this.props.name = 'jack'; // 此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
</script>
2.3.5 类式组件中的构造器与props
概要总结
1、constructor的作用
2、constructor的使用场景
一、constructor的作用
官网描述:
通常,在React中,构造函数仅用于以下两种情况:
(1)通过给this.state赋值对象来初始化内部state。
(2)为事件处理函数绑定实例
这两个场景可以分别通过在类里初始化state以及使用箭头函数来解决,因此constructor可以省略。
二、constructor的使用场景
官网描述:
在React组件挂载之前,会调用它的构造函数。在为React.Component子类实现构造函数时,应在其他语句之前调用super(props)。否则,this.props在构造函数中可能会出现未定义的bug。
说明:constructor可以不写,如果写了之后,就必须要给super传入props,否则在constructor里使用this.props会undefined。但即使真的要在constructor里使用props,也根本没必要写this.props,直接用props就得了。
1、super传入props
<script type="text/babel">
// 1.创建组件
class Person extends React.Component {
constructor(props) {
super(props);
console.log('constructor', this.props);
}
......
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" />, document.getElementById('test1'))
</script>
2、super没传props
<script type="text/babel">
// 1.创建组件
class Person extends React.Component {
constructor() {
super();
console.log('constructor', this.props);
}
......
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" />, document.getElementById('test1'))
</script>
2.3.6 函数式组件使用props
概要总结
1、函数式组件接收props
2、函数式组件对props进行限制
一、函数式组件接收props
在类式组件中,只要在实例化组件的时候传入参数,类会自动收集到props里,通过this.props即可使用。而函数式组件是没有this的,但它是一个函数,可以通过接收参数的方式接收props。
<script type="text/babel">
// 1.创建组件
function Person(props) {
const {name, sex, age} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" sex="女" age={18} />, document.getElementById('test1'))
</script>
二、函数式组件对props进行限制
在类式组件中,可以使用static关键字在类的内部进行限制。在函数式组件只能放在外部,对函数名添加属性进行限制。
<script type="text/babel">
// 1.创建组件
function Person(props) {
const {name, sex, age} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
// 对标签属性进行类型、必要性的限制
Person.propTypes = {
name: PropTypes.string.isRequired, // 限制name必传,且为字符串
sex: PropTypes.string, // 限制sex为字符串
age: PropTypes.number, // 限制age为数值
}
// 指定默认标签属性值
Person.defaultProps = {
sex: '男', // sex默认值为男
age: 18 // age默认值为18
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="jerry" sex="女" age={18} />, document.getElementById('test1'))
</script>
2.4 组件三大核心属性3: refs与事件处理
2.4.1 字符串形式的ref
概要总结
1、给DOM节点添加ref属性
2、通过this.refs获取对应的dom节点
需求:自定义组件,功能说明如下:
1、点击按钮,提示第一个输入框中的值
2、当第2个输入框失去焦点时,提示这个输入框中的值
一、编写html并定义事件
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showData = () => {
console.log(this)
}
render() {
return (
<div>
<input type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
二、给DOM节点添加ref属性
在React中获取dom节点有两种方式:第一种就是用原生js的方法获取,这显然是不合理的;第二种是给dom节点添加一个ref属性。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showData = () => {
console.log(this)
}
render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button ref="button100" onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
React提供了一个内置的refs属性,它跟props一样,以键值对的形式收集所有的dom节点的ref属性,键名为ref的值,值为dom节点。
三、通过this.refs获取对应的dom节点
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showData = () => {
const {input1} = this.refs
alert(input1.value)
}
// 展示右侧输入框的数据
showData2 = () => {
const {input2} = this.refs
alert(input2.value)
}
render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button ref="button100" onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
2.4.2 回调形式的ref
概要总结
1、使用箭头函数传入ref属性
2、通过this来获取对应的dom节点
一、字符串形式的ref将被淘汰
官方说明:react.docschina.org/docs/refs-a…
二、ref属性传入箭头函数
1、ref传入箭头函数
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
render() {
return (
<div>
<input ref={() => {}} type="text" placeholder="点击按钮提示数据"/>
<button ref="button100" onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
<input ref={() => {}} />
回调函数要满足3个条件:定义函数;自己没有调用;函数最终被执行。
在这里ref使用箭头函数之后,可以测试组件内部是否执行它:
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
render() {
return (
<div>
<input ref={c => console.log(c)} type="text" placeholder="点击按钮提示数据"/>
<button ref="button100" onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
事实证明,React内部会帮我们执行ref里的函数,而且还传入了dom节点,因此它的第一个参数就是所在的dom。
2、定义变量存储ref返回dom节点
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showData = () => {
const {input1} = this
alert(input1.value)
}
// 展示右侧输入框的数据
showData2 = () => {
const {input2} = this
alert(input2.value)
}
render() {
return (
<div>
<input ref={c => this.input1 = c} type="text" placeholder="点击按钮提示数据"/>
<button ref="button100" onClick={this.showData}>点我提示左侧的数据</button>
<input ref={c => this.input2 = c} onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
把ref所在的节点挂在了实例自身上,并取一个名字叫input1和input2。那么在函数获取这两个dom节点直接在this里获取即可。
2.4.3 回调ref中调用次数的问题
概要总结
1、ref使用内联函数的执行次数问题
2、使用类定义函数替代内联函数
一、官方说明
官方说明:react.docschina.org/docs/refs-a…
所谓内联函数指的是在标签里传入函数,例如: this.input1 = c} type="text"/>。我们发现,在这里实际上并没有文档所说第一次获取了null,第二次才获取DOM元素,这是因为它说的是在更新过程中才会发生,而我们这是属于render函数初始化的第一次调用,因此能正常获取DOM元素。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showInfo = () => {
const {input1} = this
alert(input1.value)
}
render() {
return (
<div>
<input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/>
<button onClick={this.showInfo}>点我提示输入的数据</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
二、观察ref在更新过程的执行次数
更新过程指的是通过setState改变数据的时候产生的更新。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
state = {
isHot: true
}
// 展示左侧输入框的数据
showInfo = () => {
const {input1} = this
alert(input1.value)
}
changeWeather = () => {
const {isHot} = this.state
this.setState({isHot: !isHot})
}
render() {
const {isHot} = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/><br/><br/>
<button onClick={this.showInfo}>点我提示输入的数据</button>
<button onClick={this.changeWeather}>点我切换天气</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
上面的例子通过按钮切换天气,实现更新的过程。此时会发现,每次更新ref的函数会执行两次,第一次传入为null,第二次才是DOM节点:
官方说明:这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。
为了保证代码渲染到页面上,一定要先调用一次render函数。在第一次调用的时候,发现里面有函数式的ref,那么它帮我们调用。因此第一次调用是能拿到它对应的dom节点。
在更新状态的时候,会驱动页面显示。驱动页面显示无非是通过重新执行render函数,它发现里面有函数式的ref,注意它并不是直接执行,而是重新赋一个新的函数再执行,之前的函数执行完就释放掉了。它不确定之前的函数接收了什么,做了什么操作,它为了保证这个函数完美的被清空,因此会传入一个null。然后紧接着又调用第二次,才把当前的dom节点传入。
三、ref使用类函数替代内联函数
官方说明:这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。
简单来说,就是不要用内联函数,把函数定义在类里面,然后传给ref即可:
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
state = {
isHot: true
}
// 展示左侧输入框的数据
showInfo = () => {
const {input1} = this
alert(input1.value)
}
changeWeather = () => {
const {isHot} = this.state
this.setState({isHot: !isHot})
}
saveInput = (c) => {
this.input1 = c
console.log('@', c)
}
render() {
const {isHot} = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
{/*<input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/><br/><br/>*/}
<input ref={this.saveInput} type="text"/><br/><br/>
<button onClick={this.showInfo}>点我提示输入的数据</button>
<button onClick={this.changeWeather}>点我切换天气</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
<input ref={this.saveInput} type="text"/>。使用这种方式,无论render函数执行多少次,ref所绑定的函数始终没有变化,从而解决多次执行的问题。
2.4.4 createRef的使用
概要总结
1、使用createRef创建ref容器
2、创建多个createRef对应多个ref
注意:React.createRef方法必须要在16.3版本以后才能使用
一、React.createRef
React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点。
简单理解为,React.createRef调用了之后,React会在实例上创建一个容器,它能拿到所在元素的ref存进去,我们在获取ref的时候,在这个ref容器里拿就可以了。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 展示左侧输入框的数据
showInfo = () => {
const {input1} = this
alert(input1.value)
}
render() {
return (
<div>
<input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/>
<button onClick={this.showInfo}>点我提示输入的数据</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
二、使用多个createRef
React.createRef只能存一个ref,如果有多个ref则需要调用多个React.createRef方法来创建多个ref容器进行存储。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
/*
* React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点
* */
myRef = React.createRef()
myRef2 = React.createRef()
// 展示左侧输入框的数据
showData = () => {
alert(this.myRef.current.value)
}
// 展示右侧输入框的数据
showData2 = () => {
alert(this.myRef2.current.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点左侧的数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
2.4.5 react中的事件处理
概要总结
1、React使用的是自定义(合成)事件,而不是使用的原生DOM事件
2、React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
3、通过event.target得到发生事件的DOM元素对象
需求:
1、通过OnXxx属性指定事件处理函数(注意大小写)
(1)React使用的是自定义(合成)事件,而不是使用的原生DOM事件 —— 为了更好的兼容性
(2)React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —— 为了高效
2、通过event.target得到发生事件的DOM元素对象 —— 不要过度使用ref
一、React自定义合成事件
React把原生js的事件重新封装了一遍,目的就是为了更好的兼容性。
二、React事件通过事件委托最外层元素处理
假设在一个ul下需要绑定所有的li的点击事件,显然把点击事件委托给父级元素ul更加高效。
三、使用event.target获取DOM元素
实际上我们绑定点击事件的时候,它都会给每个事件传入原生的event对象。如果只是获取本元素的值或者属性,可以通过event.target进行获取,而没必要滥用ref。
<script type="text/babel">
// 1.创建组件
class Demo extends React.Component {
// 创建ref容器
myRef = React.createRef()
myRef2 = React.createRef()
// 展示左侧输入框的数据
showData = () => {
alert(this.myRef.current.value)
}
// 展示右侧输入框的数据
showData2 = (event) => {
alert(event.target.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} type="text" placeholder="失去焦点左侧的数据"/>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>
这里已经可以通过event.target.value就能替代ref的使用。
2.5 收集表单数据
2.5.1 非受控组件与受控组件
概要总结
1、非受控组件:页面中所有输入类的dom,包括input、checkbox、radio等等,属于现用现取的,也就是需要用到的时候才去获取dom节点的值
2、受控组件:页面中所有输入类的dom,随着输入就可以维护到状态里,等需要用到的时候直接从状态里取出来
需求:定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息
一、定义有表单的组件
<script type="text/babel">
// 1.创建组件
class Login extends React.Component {
handleSubmit = () => {
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input type="text" name="username"/>
密码:<input type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
二、非受控组件
我们在提交表单的时候,需要获取表单的每一项数据,可以通过ref的形式获取每一个输入框的值。
<script type="text/babel">
// 1.创建组件
class Login extends React.Component {
handleSubmit = event => {
event.preventDefault() // 阻止表单提交
const {username, password} = this
alert(`你输入的用户名是:${username.value}, 你输入的密码是:${password.value}`)
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username"/>
密码:<input ref={c => this.password = c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
非受控组件:页面中所有输入类的dom,包括input、checkbox、radio等等,属于现用现取的,也就是需要用到的时候才去获取dom节点的值。
三、受控组件
想要获取表单每一项,除了给每一项设置ref来获取以外,也可以给它们绑定onChange事件,在事件里把它们的值存在state里。在提交表单的时候,就不需要获取dom节点来取值了,直接从state里拿值即可。
<script type="text/babel">
// 1.创建组件
class Login extends React.Component {
state = {
username: '', // 用户名
password: '' // 密码
}
// 保存用户名到状态中
saveUsername = event => {
this.setState({username: event.target.value})
}
// 保存密码到状态中
savePassword = event => {
this.setState({password: event.target.value})
}
// 表单提交的回调
handleSubmit = event => {
event.preventDefault() // 阻止表单提交
const {username, password} = this.state
alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username"/>
密码:<input onChange={this.savePassword} ref={c => this.password = c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
受控组件:页面中所有输入类的dom,随着输入就可以维护到状态里,等需要用到的时候直接从状态里取出来。
总结:受控组件相比非受控组件更好一些,毕竟它没有设置ref,在性能上会更为优化。
2.5.2 高阶函数_函数柯里化
概要总结
1、高阶函数
(1)若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数
(2)若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数
常见的高阶函数有:Promise、setTimeout、arr.map()等等。
2、函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式
3、使用函数柯里化的方式,解决React绑定事件传参问题
需求:
定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息
一、优化受控组件
对于受控组件而言,它在每一个输入项绑定一个onChange事件来把值存储在state状态中。但如果输入项非常多,这显然是过于臃肿且不合理。此时我们可以尝试让所有输入项绑定同一个方法,然后根据类型作为参数传到方法里进行区分:
<script type="text/babel">
// 1.创建组件
class Login extends React.Component {
state = {
username: '', // 用户名
password: '' // 密码
}
// 保存表单数据到状态中
saveFormData = event => {
this.setState({password: event.target.value})
}
// 表单提交的回调
handleSubmit = event => {
event.preventDefault() // 阻止表单提交
const {username, password} = this.state
alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} ref={c => this.password = c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
注意:所有输入项的onChange绑定同一个saveFormData事件之后,需要传入输入项的类型作为参数。但这样一来会产生以下问题
(1)onChange={this.saveFormData('username')}这句代码的意思是把执行saveFormData方法的返回值作为onChange的回调。它的返回值明显是没有的,也就是undefined,这就相当于把undefined赋给onChange去绑定
(2)在onChange里传参就意味着已经自执行了,因此在初始化的时候它就会先把方法执行
(3)由于在绑定事件上传参了,因此把React自带的event参数给覆盖了,所以事件方法里的event已经不再是原来的event了,而是自己传过去的参数
二、绑定事件返回一个函数
React绑定事件它需要的是一个函数,当我们想传参的时候,它就变成了执行函数,把函数的返回值作为事件的绑定。例如:
onChange={this.saveFormData}这样写是正确的,React会帮我们传入一个event作为参数。但如果写成onChange={this.saveFormData('username')},那意思就是去调用saveFormData方法,把它的返回值赋给onChange进行绑定。之所以不成功原因是saveFormData没有返回值。
既然又想自己传参又想正常绑定,那么可以在saveFormData里返回一个函数,这样既能满足自己传参,也能满足React的事件绑定:
saveFormData = dataType => {
return () => {
console.log('@')
}
}
onChange={this.saveFormData('username')}它是将saveFormData的返回的函数进行事件绑定,因此实际上它所绑定的正是那个return () => {},所以React会帮我们把event传到这个匿名函数里:
saveFormData = dataType => {
return (event) => {
console.log(dataType, event.target.value)
}
}
目前已经拿到输入项的类型以及它的值,最后一步把它们的值存进state状态里即可。
saveFormData = dataType => {
return (event) => {
this.setState({[dataType]: event.target.value})
}
}
完整代码:
<script type="text/babel">
// 1.创建组件
class Login extends React.Component {
state = {
username: '', // 用户名
password: '' // 密码
}
// 保存表单数据到状态中
saveFormData = dataType => {
return (event) => {
this.setState({[dataType]: event.target.value})
}
}
// 表单提交的回调
handleSubmit = event => {
event.preventDefault() // 阻止表单提交
const {username, password} = this.state
alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} ref={c => this.password = c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
三、高阶函数与函数柯里化
1、高阶函数
(1)若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数
(2)若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数
常见的高阶函数有:Promise、setTimeout、arr.map()等等。
2、函数柯里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式。在上述的saveFormData方法已经使用了函数柯里化:
saveFormData = dataType => {
return (event) => {
this.setState({[dataType]: event.target.value})
}
}
2.5.3 不用柯里化的写法
概要总结
1、绑定事件使用箭头函数,通过箭头函数里执行自定义的方法,可以实现同时传自定义和event参数
2、自定义的方法不需要返回一个函数,取消柯里化的写法
需求:
定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息
一、同时传入自定义参数与React的event参数
1、绑定事件使用箭头函数
对于React绑定事件而言,它目的只有一个,它要的就是一个函数,不管是直接把类定义的函数给它,还是调用了函数之后再返回一个函数给它都可以。
我们可以在绑定事件的时候,直接给予一个箭头函数,先满足了它的需求:
<input onChange={() => {}} type="text" name="username"/>
在这里React会给绑定的函数传入event的,所以在这个箭头函数里我们就能获取到event:
<input onChange={(event) => {}} type="text" name="username"/>
而在箭头函数里就可以执行我们自己定义的函数了,可以传自己想要的参数,并且把event也顺带传过去:
<input onChange={(event) => {this.saveFormData('username', event)}} type="text" name="username"/>
二、不用函数柯里化
既然在React事件绑定的时候已经能传入自定义参数和event参数,那就没必要再返回一个函数了,直接把数据存到状态里即可:
saveFormData = (dataType, event) => {
this.setState({[dataType]: event.target.value})
}
完整代码:
<script type="text/babel">
/*
* 高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数
* 1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数
* 2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数
* 常见的高阶函数有:Promise、setTimeout、arr.map()等等
* 函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式
* */
// 1.创建组件
class Login extends React.Component {
state = {
username: '', // 用户名
password: '' // 密码
}
// 保存表单数据到状态中
saveFormData = (dataType, event) => {
this.setState({[dataType]: event.target.value})
}
// 表单提交的回调
handleSubmit = event => {
event.preventDefault() // 阻止表单提交
const {username, password} = this.state
alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
}
render() {
return (
<form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
用户名:<input onChange={event => this.saveFormData('username', event)} type="text" name="username"/>
密码:<input onChange={event => this.saveFormData('password', event)} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Login />, document.getElementById('test1'))
</script>
2.6 组件的生命周期
2.6.1 引出生命周期
概要总结
1、componentDidMount:组件完成挂载生命周期
2、componentWillUnmount:组件卸载前生命周期
需求:定义组件实现以下功能:
1、让指定的文本做显示/隐藏的渐变动画
2、从完全可见,到彻底消失,耗时2s
3、点击“不活了”按钮从界面中卸载组件
一、卸载组件
在React中提供了一个方法,叫做unmountComponentAtNode,它的意思很明确,就是指定dom节点卸载组件。
<div id="test1"></div>
<script type="text/babel">
// 1.创建组件
class Life extends React.Component {
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
render() {
return (
<div>
<h2>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life />, document.getElementById('test1'))
</script>
二、实现透明度渐变效果
1、在render函数添加计时器
从完全可见,到彻底消失,耗时2s。思路就是改变元素的透明度,从1变为0。假设每次减少0.1透明度,那就是200毫秒变化一次。
<script type="text/babel">
// 1.创建组件
class Life extends React.Component {
state = {opacity: 1}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
render() {
setInterval(() => {
// 获取原状态
let {opacity} = this.state
// 减少0.1
opacity -= 0.1
if (opacity <= 0) {
opacity = 1
}
// 设置新的透明度
this.setState({opacity})
}, 200)
return (
<div>
<h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life />, document.getElementById('test1'))
</script>
目前我们把定时器放在了render函数里,让React渲染组件的时候,执行render函数开启定时器。
注意:这里引发了一个无限的递归,也就是死循环。因为render函数的调用次数是1+n,组件初始化的时候调用一次,然后每次改变状态的时候它会调用。但是如果在render函数里改变状态,那就是一个死循环。
2、componentDidMount:组件完成挂载生命周期
componentDidMount从单词意义上来说,就是组件完成挂载。它跟render函数是兄弟。render函数的调用时机是初始化渲染和状态更新之后,而componentDidMount是在组件挂载完毕之后触发,只调用1次。
此时把render函数里的定时器,移到componentDidMount生命周期里执行即可:
<script type="text/babel">
// 1.创建组件
class Life extends React.Component {
state = {opacity: 1}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
// 组件挂载完毕
componentDidMount() {
setInterval(() => {
// 获取原状态
let {opacity} = this.state
// 减少0.1
opacity -= 0.1
if (opacity <= 0) {
opacity = 1
}
// 设置新的透明度
this.setState({opacity})
}, 200)
}
// 初始化渲染、状态更新之后
render() {
return (
<div>
<h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life />, document.getElementById('test1'))
</script>
3、componentWillUnmount:组件卸载前生命周期
当计时器在运行的时候,如果卸载组件,它会发出以下报错:
它的意思是:我不能在已经被卸载的组件执行React状态的更新。此时可以通过componentWillUnmount生命周期,在组件卸载前把定时器去除掉即可:
<script type="text/babel">
// 1.创建组件
class Life extends React.Component {
state = {opacity: 1}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
// 组件挂载完毕
componentDidMount() {
this.timer = setInterval(() => {
// 获取原状态
let {opacity} = this.state
// 减少0.1
opacity -= 0.1
if (opacity <= 0) {
opacity = 1
}
// 设置新的透明度
this.setState({opacity})
}, 200)
}
// 组件将要卸载
componentWillUnmount() {
clearInterval(this.timer)
}
// 初始化渲染、状态更新之后
render() {
return (
<div>
<h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life />, document.getElementById('test1'))
</script>
2.6.2 生命周期(旧)_组件挂载流程
概要总结
1、组件挂载流程
(1)constructor:构造器
(2)componentWillMount:组件将要挂载的钩子
(3)render:render函数
(4)componentDidMount:组件挂载完毕的钩子
(5)componentWillUnmount:组件将要卸载的钩子
一、组件挂载流程
挂载流程的顺序是:constructor => componentWillMount => render => componentDidMount => componentWillUnmount
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 组件将要挂载的钩子
componentWillMount() {
console.log('Count---componentWillMount')
}
// 组件挂载完毕的钩子
componentDidMount() {
console.log('Count---componentDidMount')
}
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
2.6.3 生命周期(旧)_setState流程
概要总结
1、setState流程
(1)shouldComponentUpdate:组件是否应该被更新的钩子
(2)componentWillUpdate:组件将要更新的钩子
(3)render:render函数
(4)componentDidUpdate:组件更新完毕的钩子
setState流程:
一、shouldComponentUpdate:组件是否应该被更新的钩子
setState更新状态之后,它会先执行shouldComponentUpdate钩子。这个组件的意思是:组件是否应该被更新。它是一个阀门,如果shouldComponentUpdate钩子返回了false,那么后面的钩子不会再执行,也就不会帮我们更新。所以每次执行setState的时候,它都要先去执行shouldComponentUpdate钩子判断是否应该更新。
shouldComponentUpdate钩子如果不写,React会给我们补上它,并且永远返回true。如果自己写了,那就以自己写的为准,并且一定要返回一个boolean值,否则会报错:
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
}
render() {
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
显然shouldComponentUpdate一定要返回一个boolean值,如果返回true,则正常往下执行:
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return true
}
如果返回false,则不往下执行:
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return false
}
二、componentWillUpdate:组件将要更新的钩子
经过shouldComponentUpdate钩子之后,下一个钩子执行的就是componentWillUpdate,它的意思就是组件将要被更新。
三、componentDidUpdate:组件更新完毕的钩子
经过componentWillUpdate钩子之后,那么组件就会执行render函数去更新,更新完毕之后再调componentDidUpdate钩子,它的意思就是组件更新完毕。
四、setState流程
setState流程的顺序是:shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 控制组件更新的"阀门"
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return true
}
// 组件将要更新的钩子
componentWillUpdate() {
console.log('Count---componentWillUpdate')
}
// 组件更新完毕的钩子
componentDidUpdate() {
console.log('Count---componentDidUpdate')
}
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
2.6.4 生命周期(旧)_forceUpdate流程
概要总结
1、forceUpdate流程
(1)componentWillUpdate:组件将要更新的钩子
(2)render:render函数
(3)componentDidUpdate:组件更新完毕的钩子
forceUpdate流程:
一、forceUpdate:强制更新
forceUpdate与setState的区别在于,它比setState少走了一个shouldComponentUpdate钩子。它不需要经过shouldComponentUpdate钩子决定要不要更新,它直接就往下执行更新流程。它可以在没有状态更新的情况下,强制更新或者刷新一下。
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 强制更新按钮的回调
force = () => {
this.forceUpdate()
}
// 控制组件更新的"阀门"
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return false
}
// 组件将要更新的钩子
componentWillUpdate() {
console.log('Count---componentWillUpdate')
}
// 组件更新完毕的钩子
componentDidUpdate() {
console.log('Count---componentDidUpdate')
}
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
此时在shouldComponentUpdate钩子里返回false,它没有办法改变状态。但调用this.forceUpdate()方法之后,它依然会执行componentWillUpdate以下的更新钩子流程。
2.6.5 生命周期(旧)_父组件render流程
概要总结
1、父组件render流程
(1)componentWillReceiveProps:组件将要接收新的props的钩子
(2)shouldComponentUpdate:组件是否应该被更新的钩子
(3)componentWillUpdate:组件将要更新的钩子
(4)render:render函数
(5)componentDidUpdate:组件更新完毕的钩子
父组件render流程:
一、父子组件
这里创建一个A与B组件,通过A组件调用B组件形成父子组件。
<script type="text/babel">
class A extends React.Component {
// 初始化状态
state = {carName: '奔驰'}
changeCar = () => {
this.setState({carName: '奥拓'})
}
render() {
return (
<div>
<div>我是A组件</div>
<button onClick={this.changeCar}>换车</button>
<B carName={this.state.carName} />
</div>
)
}
}
class B extends React.Component {
render() {
return (
<div>我是B组件,接收到的车是:{this.props.carName}</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<A />, document.getElementById('test1'))
</script>
二、componentWillReceiveProps:组件将要接收新的props钩子
1、componentWillReceiveProps钩子在第二次接收props才执行
componentWillReceiveProps钩子,直接翻译就是组件将要接收参数,这个钩子显然是在子组件接收参数的时候调用的。
<script type="text/babel">
class A extends React.Component {
// 初始化状态
state = {carName: '奔驰'}
changeCar = () => {
this.setState({carName: '奥拓'})
}
render() {
return (
<div>
<div>我是A组件</div>
<button onClick={this.changeCar}>换车</button>
<B carName={this.state.carName} />
</div>
)
}
}
class B extends React.Component {
componentWillReceiveProps() {
console.log('B---componentWillReceiveProps')
}
render() {
return (
<div>我是B组件,接收到的车是:{this.props.carName}</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<A />, document.getElementById('test1'))
</script>
这里子组件B显然已经接收到A的参数,而且已经显示在页面上,但是并没有执行componentWillReceiveProps钩子。
注意:componentWillReceiveProps钩子对于第一次传入props是不会执行的,从第二次以后传入才会执行。
当父组件A点击换车按钮,改变了状态。对于B组件而言,这是属于第二次传入props,那么它就会执行componentWillReceiveProps钩子。
2、componentWillReceiveProps钩子接收props参数
<script type="text/babel">
class A extends React.Component {
......
}
class B extends React.Component {
componentWillReceiveProps(props) {
console.log('B---componentWillReceiveProps', props)
}
render() {
return (
<div>我是B组件,接收到的车是:{this.props.carName}</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<A />, document.getElementById('test1'))
</script>
2.6.6 总结生命周期(旧)
概要总结
1、初始化阶段:由ReactDom.render()触发--初次渲染
2、更新阶段:由组件内部this.setState()或父组件render触发
3、卸载组件:由ReactDOM.unmountComponentAtNode()触发
一、初始化阶段:由ReactDom.render()触发--初次渲染
1、constructor()
2、componentWillMount()
3、render()
4、componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
二、更新阶段:由组件内部this.setState()或父组件render触发
1、shouldComponentUpdate()
2、componentWillUpdate()
3、render() =====> 必须使用的一个
4、componentDidUpdate()
三、卸载组件:由ReactDOM.unmountComponentAtNode()触发
1、componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
2.6.7 对比新旧生命周期
概要总结
1、升级React到最新版本
2、即将废弃三个生命周期:componentWillMount、componentWillUpdate、componentWillReceiveProps
3、新增两个生命周期:getDerivedStateFromProps、getSnapshotBeforeUpdate
一、升级React版本为17.0.1
官网地址:zh-hans.reactjs.org/docs/cdn-li…
二、即将废弃三个生命周期:componentWillMount、componentWillUpdate、componentWillReceiveProps
1、新版本的警告信息
在新的版本中,是可以使用旧版本的生命钩子的,但是会提出警告信息。
(1)componentWillMount has been renamed, and is not recommended for use.
翻译:componentWillMount钩子被重命名了,而且不被推荐使用了。
(2)Rename componentWillMount to UNSAFE_componentWillMount
翻译:componentWillMount改名为UNSAFE_componentWillMount
(3)In React 18.x, only the UNSAFE_ name will work.
翻译:在18版本里,只有写UNSAFE_这种形式才能工作。
说明:现在componentWillMount没有加UNSAFE_,仍然能正常调用,但在18版本后就必须要加UNSAFE_才能用了。
2、给componentWillMount、componentWillUpdate、componentWillReceiveProps前面加上UNSAFE_
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 卸载组件按钮的回调
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
// 强制更新按钮的回调
force = () => {
this.forceUpdate()
}
// 组件将要挂载的钩子
UNSAFE_componentWillMount() {
console.log('Count---componentWillMount')
}
// 组件将要更新的钩子
UNSAFE_componentWillUpdate() {
console.log('Count---componentWillUpdate')
}
......
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
这3个生命钩子加上UNSAFE_之后,警告已经消失了。
总结:所有带will的钩子,在新版本里都希望你加上UNSAFE_,除了componentWillUnmount钩子。
3、详述加上UNSAFE_原因
官方文档:zh-hans.reactjs.org/blog/2018/0…
综上所述:React在为以后铺路,上面用了"预计"、"未来版本"这些词,其实就是提前打好预防针,预测这3个钩子会出问题。现在提高这3个钩子的使用成本,必须加上UNSAFE_前缀。在实际的开发中,这3个并不是核心的钩子,也有不少人说React即将要废弃这3个钩子。
官网标明在未来的版本已经给它们开启废弃的告警,我们看到的警告就是它们开启的废弃警告。在17版本以后就必须要加上UNSAFE_前缀,但实际上我们没有加前缀还是能使用,所以React在进行一些旧东西的变化时候,它是非常小心翼翼的。
三、新增两个生命周期:getDerivedStateFromProps、getSnapshotBeforeUpdate
1、对比挂载流程
旧版本挂载流程:constructor => componentWillMount => render => componentDidMount
新版本挂载流程:constructor => getDerivedStateFromProps => render => componentDidMount
总结:新版本挂载流程,对比旧版本而言,把componentWillMount生命钩子改成getDerivedStateFromProps生命钩子
2、对比更新流程
旧版本更新流程:componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate
新版本更新流程:getDerivedStateFromProps => shouldComponentUpdate => render => getSnapshotBeforeUpdate => componentDidUpdate
总结:新版本更新流程,对比旧版本而言,把componentWillReceiveProps生命钩子改成getDerivedStateFromProps生命钩子,废弃componentWillUpdate生命钩子,在render函数之后增加了getSnapshotBeforeUpdate生命钩子。
2.6.8 getDerivedStateFromProps
概要总结
1、getDerivedStateFromProps使用方法
2、getDerivedStateFromProps接收参数
3、getDerivedStateFromProps注意事项
4、使用constructor代替getDerivedStateFromProps
一、getDerivedStateFromProps使用方法
getDerivedStateFromProps生命钩子,直译就是从props得到一个派生的状态。
1、添加getDerivedStateFromProps生命钩子
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 卸载组件按钮的回调
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
// 强制更新按钮的回调
force = () => {
this.forceUpdate()
}
getDerivedStateFromProps() {
console.log('getDerivedStateFromProps')
}
......
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
这里并没有执行getDerivedStateFromProps生命钩子,反而提示一个警告:getDerivedStateFromProps方法不能给实例使用,请定义它为一个静态方法。
解决方案:在getDerivedStateFromProps前面加一个static关键字。
2、static getDerivedStateFromProps
static getDerivedStateFromProps() {
console.log('getDerivedStateFromProps')
}
这次getDerivedStateFromProps生命钩子执行成功了,但又提示另一个告警:它必须返回一个状态对象或者null。
3、getDerivedStateFromProps的状态对象
在React里,状态就是state。而getDerivedStateFromProps要求返回的状态对象,这个状态对象实际上就是state,而且state会以这个状态对象为准,会把自己定义的state覆盖掉。
例如state与getDerivedStateFromProps返回的状态对象同时存在一个count属性:
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
static getDerivedStateFromProps() {
console.log('getDerivedStateFromProps')
return {count: 108}
}
......
render() {
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
此时状态里的count属性以getDerivedStateFromProps返回的状态对象为准,而且它无法更新这个count状态属性。因为每次更新是必须执行getDerivedStateFromProps生命钩子,而getDerivedStateFromProps又返回了一个固定的count状态值,因此无论怎么setState都无法改变。
二、getDerivedStateFromProps接收参数
1、props
getDerivedStateFromProps接收的第一个参数就是props,props参数就是在调用组件的时候传入的参数:
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
static getDerivedStateFromProps(props) {
console.log('getDerivedStateFromProps', props)
return props
}
......
}
// 2.渲染组件到页面
ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>
它把接收到的props作为状态对象来返回,它就已经是当成状态来使用。简单来说,这个state不是自己写的,是从props传递过来的,所以就叫做从props得到一个派生的状态。
2、state
getDerivedStateFromProps接收的第二个参数是state,这个state是我们自己定义的state:
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps', props, state)
return props
}
......
}
// 2.渲染组件到页面
ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>
我们可以在这里进行一些判断,例如传递过来的props的某一个属性,与state里的某一个属性进行对比,然后更新state值,最后返回一个null。那么state就可以根据props进行一些初始化的改变。
三、getDerivedStateFromProps注意事项
官方文档:zh-hans.reactjs.org/docs/react-…
四、使用constructor代替getDerivedStateFromProps
如果需要state完全取决于props,其实是可以在constructor里进行操作。constructor接收到props之后,把它赋值给state就可以了,因此getDerivedStateFromProps可以忽略。
2.6.9 getSnapshotBeforeUpdate
概要总结
1、getSnapshotBeforeUpdate使用方法
2、getSnapshotBeforeUpdate的作用
3、getSnapshotBeforeUpdate使用场景
一、getSnapshotBeforeUpdate使用方法
getDerivedStateFromProps生命钩子,直译就是在更新之前获取快照。
1、添加getSnapshotBeforeUpdate生命钩子
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props);
// 初始化状态
this.state = {count: 0}
}
// 加1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count + 1})
}
// 卸载组件按钮的回调
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
// 强制更新按钮的回调
force = () => {
this.forceUpdate()
}
getSnapshotBeforeUpdate() {
console.log('getSnapshotBeforeUpdate')
}
......
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
在更新的时候,已经执行了getSnapshotBeforeUpdate生命钩子,但提示一个警告:它必须返回一个快照值或者null。
2、componentDidUpdate接收参数
componentDidUpdate生命钩子能接收3个参数的,前两个参数是prevProps、prevState,意思是上一个props和上一个state。
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
// 组件更新完毕的钩子
componentDidUpdate(prevProps, prevState) {
console.log('Count---componentDidUpdate', prevProps, prevState)
}
......
render() {
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test1'))
</script>
此时状态里的count属性值已经是1了,而prevState输出的count却是0,证明这确实是上一个state的状态值。
3、getSnapshotBeforeUpdate的快照值
官方文档:zh-hans.reactjs.org/docs/react-…
文档明确说明,getSnapshotBeforeUpdate的返回值会传给componentDidUpdate生命钩子,这正是对应componentDidUpdate的第三个参数:
<script type="text/babel">
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
}
getSnapshotBeforeUpdate() {
console.log('getSnapshotBeforeUpdate')
return 'atguigu'
}
// 组件更新完毕的钩子
componentDidUpdate(prevProps, prevState, snapshotValue) {
console.log('Count---componentDidUpdate', prevProps, prevState, snapshotValue)
}
......
}
// 2.渲染组件到页面
ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>
二、getSnapshotBeforeUpdate的作用
如果componentDidUpdate生命钩子执行完毕,那就意味着组件完成更新了,而在getSnapshotBeforeUpdate环节是在更新之前。在更新的时候,页面肯定要发生一些变化,一旦完成更新,那更新之前的东西就获取不到了。我们可以利用getSnapshotBeforeUpdate的快照就可以在它马上更新之前获取一些信息,就像亲戚聚餐合影拍照留念一样,而组件就在更新之前来个快照是同一个道理。
至于getSnapshotBeforeUpdate的快照值是什么,你想返回什么就是什么,根据需求返回即可。
三、getSnapshotBeforeUpdate使用场景
场景:
(1)每隔1s推送一条新闻,出现新闻列表滚动条
(2)手动滚动到某一条新闻时,不被新推送的新闻影响滚动条的位置,也就是说要把滚动条固定在这里
<script type="text/babel">
// 1.创建组件
class NewsList extends React.Component {
state = {newsArr: []}
componentDidMount() {
setInterval(() => {
// 获取原状态
const {newsArr} = this.state
// 模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
this.setState({newsArr: [news, ...newsArr]})
}, 1000)
}
render() {
return (
<div className="list">
{
this.state.newsArr.map((n, index) => {
return <div key={index} className="news">{n}</div>
})
}
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<NewsList/>, document.getElementById('test'))
</script>
这里实现了1s推送一条新闻,接下来可以使用getSnapshotBeforeUpdate来动态计算推送新闻前后的滚动条差值,然后再进行对滚动条的高度进行控制,实现固定滚动条的效果。
<script type="text/babel">
// 1.创建组件
class NewsList extends React.Component {
state = {newsArr: []}
componentDidMount() {
setInterval(() => {
// 获取原状态
const {newsArr} = this.state
// 模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
this.setState({newsArr: [news, ...newsArr]})
}, 1000)
}
getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight
}
componentDidUpdate(prevProps, prevState, height) {
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}
render() {
return (
<div className="list" ref="list">
{
this.state.newsArr.map((n, index) => {
return <div key={index} className="news">{n}</div>
})
}
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<NewsList/>, document.getElementById('test'))
</script>
思路:
(1)在getSnapshotBeforeUpdate钩子把当前的滚动条的总高度作为快照传给componentDidUpdate钩子
(2)在componentDidUpdate的列表高度减去getSnapshotBeforeUpdate传过来的高度,得到的是一条新闻记录的高度
(3)把一条新闻记录的高度加到当前滚动条的scrollTop上,这样就可以保持滚动条的位置不变了
2.6.10 总结生命周期(新)
概要总结
1、初始化阶段:由ReactDom.render()触发--初次渲染
2、更新阶段:由组件内部this.setState()或父组件render触发
3、卸载组件:由ReactDOM.unmountComponentAtNode()触发
一、初始化阶段:由ReactDom.render()触发--初次渲染
1、constructor()
2、getDerivedStateFromProps
3、render()
4、componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
二、更新阶段:由组件内部this.setState()或父组件render触发
1、getDerivedStateFromProps
2、shouldComponentUpdate()
3、render() =====> 必须使用的一个
4、getSnapshotBeforeUpdate
5、componentDidUpdate()
三、卸载组件:由ReactDOM.unmountComponentAtNode()触发
1、componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
2.7 虚拟DOM与DOM Diffing算法
2.7.1 DOM的diffing算法
概要总结
1、验证diffing算法
2、key的作用
3、存在输入类的DOM
4、用index作为key可能会引发的问题
一、验证diffing算法
<script type="text/babel">
class Time extends React.Component {
state = {
date: new Date()
}
componentDidMount() {
setInterval(() => {
this.setState({
date: new Date()
})
}, 1000)
}
render() {
return (
<div>
<h1>hello</h1>
<input type="text" />
<span>现在是:{this.state.date.toTimeString()}</span>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Time />, document.getElementById('test1'))
</script>
在这个案例中,date时间属性每隔1s更新一次,也就意味着1s执行一次render函数。render函数里有和里面的3个标签,每次执行render就意味着它就会生成整个的虚拟DOM。
在<div>标签中,有3个子标签,分别是、、。在生成出来新的虚拟DOM之后,它就会跟旧的虚拟DOM做一个比较,发现实际上发生改变的只有标签,所以、对应的真实DOM没有发生变化,就只有的真实DOM产生改变。
注意:它对比的最小粒度是标签,它并不能精确到标签里的某个内容的变化。例如现在是:{this.state.date.toTimeString()},它的比较只能检测到标签发生了改变,而不能精确到里面的时间发生改变。但是在标签内既有内容又有子标签,在改变内容的时候,子标签是不会发生改变的。
render() {
return (
<div>
<h1>hello</h1>
<input type="text" />
<span>
现在是:{this.state.date.toTimeString()}
<input type="text" />
</span>
</div>
)
}
二、key的作用
/**
经典面试题:
1). react/vue中的key有什么作用?(key的内部原理是什么?)
2). 为什么遍历列表时,key最好不要用index?
1. 虚拟DOM中key的作用:
1). 简单的说: key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用
2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后React进行【新的虚拟DOM】与【旧的虚拟DOM】的diff比较,比较规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1).若虚拟DOM中内容没变,直接使用之前的真实DOM
(2).若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
根据数据创建新的真实DOM,随后渲染到页面
2. 用index作为key可能会引发的问题:
1). 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低
2). 如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题
3). 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
3. 开发中如何选择key?:
1). 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
2). 如果确定只是简单的展示数据,用index也是可以的
* */
案例:在数组前插入一条数据:
<script type="text/babel">
class Person extends React.Component {
state = {
persons: [
{id: 1, name: '小张', age: 18},
{id: 2, name: '小李', age: 19}
]
}
add = () => {
const {persons} = this.state
const p = {id: persons.length + 1, name: '小王', age: 20}
this.setState({persons: [p, ...persons]})
}
render() {
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
this.state.persons.map((personObj, index) => {
return <li key={index}>{personObj.name} --- {personObj.age}</li>
})
}
</ul>
</div>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Person />, document.getElementById('test1'))
</script>
1、使用index索引值作为key
(1)初始数据
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
(2)初始的虚拟DOM
小张 ---- 18
小李 ---- 19
(3)更新后的数据
{id: 3, name: '小王', age: 20}
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
(4)更新后的虚拟DOM
小王 ---- 20
小张 ---- 18
小李 ---- 19
2、旧虚拟DOM中找到了与新虚拟DOM相同的key(索引值)
我们可以发现,在新的虚拟DOM与旧的虚拟DOM当中,存在key为0和1是相同的,但通过对比发现内容是不同的,因此它们需要重新生成新的真实DOM来替换。
注意:在这里其实只是添加了一条数据,原来的两条数据是没有变化,按理是可以复用的,但由于key使用了index,而新数据偏偏又是从最前面插入,导致所有的索引值错乱,所以就产生了两条没有必要的真实DOM更新。如果数据量庞大,则会产生严重的效率问题。
3、使用id唯一标识作为key
(1)初始数据
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
(2)初始的虚拟DOM
小张 ---- 18
小李 ---- 19
(3)更新后的数据
{id: 3, name: '小王', age: 20}
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
(4)更新后的虚拟DOM
小王 ---- 20
小张 ---- 18
小李 ---- 19
4、旧虚拟DOM中找到了与新虚拟DOM相同的key(id唯一标识)
我们可以发现,在新的虚拟DOM与旧的虚拟DOM当中,存在key为1和2是相同的,而且内容也相同,因此可以复用。
注意:使用id作为key,无论新数据从哪里插入,只要是key相同内容都没有发生变化,这里只需要新建一条新的真实DOM,旧的可以完全复用。性能比index索引值作为key要高得多。
三、存在输入类的DOM
render() {
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加一个小王</button>
<h3>使用index(索引值)作为key</h3>
<ul>
{
this.state.persons.map((personObj, index) => {
return <li key={index}>{personObj.name} --- {personObj.age}<input type="text" /></li>
})
}
</ul>
<hr/>
<h3>使用id(数据的唯一标识)作为key</h3>
<ul>
{
this.state.persons.map((personObj, index) => {
return <li key={personObj.id}>{personObj.name} --- {personObj.age}<input type="text" /></li>
})
}
</ul>
</div>
)
}
当存在输入类的DOM的时候,使用index作为key会产生错乱的现象,而id为key就没问题。
1、使用index索引作为key
(1)初始的虚拟DOM
小张 ---- 18
小李 ---- 19
(2)更新后的虚拟DOM
小王 ---- 20
小张 ---- 18
小李 ---- 19
首先它拿key为0的新旧虚拟DOM做一个对比,对比后发现里面的内容并不一样,但是大家里面都有一个相同的,那么它们就会直接复用。但是万万没想到旧的真实DOM里面残留着之前的输入信息,因此React直接复用就会产生这种错乱的情况。以此类推后面的节点也会产生同样的错乱问题。
2、使用id唯一标识作为key
在这种情况下,如果使用id唯一标识作为key,就完美的避免这种问题了。
(1)初始的虚拟DOM
小张 ---- 18
小李 ---- 19
(2)更新后的虚拟DOM
小王 ---- 20
小张 ---- 18
小李 ---- 19
首先它拿key为0的新旧虚拟DOM做一个对比,对比后发现内容是一样的,因此是整个标签复用,只有key为3的节点需要重新生成,因此不会产生错乱问题。
四、用index作为key可能会引发的问题
1、若对数据进行:逆序添加、逆序删除等破坏顺序的操作
会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低
2、如果结构中还包含输入类的DOM
会产生错误DOM更新 ==> 界面有问题
3、注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的