React组件内部数据定义
React组件中的数据称之为状态state, 而定义数据其实就在组件内部创建一个state对象来管理储存数据
但是, 因为React组件有两种写法,分别是类式组件和函数式组件,而这两种组件最大的区别就是有无状态和生命周期, 而函数式组件是没有状态也没有生命周期, 所以也称无状态组件, 所以我们这里先从类式组件开始学习
PS:
在react16.8中新添加了一个特性Hook, 它能让我们不通过 class类的方式去写一个组件,也就是用函数的方式去写,它提供一系列的hook方法去代替state、生命周期等react特性
类组件
在components内新建two.jsx
import { Component } from "react";
export default class Two extends Component {
constructor(){
super();
this.state = {
num:10
}
}
changeNum(){
//在函数中要使用this需要在调用的时候通过bind传入this
this.state.num++;
console.log(this.state.num);
}
render(){
return (
<>
//在JSX中调用组件内部数据,需要通过this
<p>{this.state.num}</p>
<button onClick={this.changeNum.bind(this)}>按钮</button>
</>
)
}
}
代码分析:
我们通过在constructor内部声明一个state状态对象,来定义当前组件内部的数据,然后我们有定义了一个changeNum的方法,这种定义方式与ES6语法一样,只不过数据我们一定要定义在state对象内
在changeNum方法中我们尝试对state中的num值进行修改,所以我们把该方法通过onClick绑定到了button上面,这里在button的onClick上执行的是js代码,不要忘记套在大括号里面,同时因为这里的标签并不是真正的html标签,所以changeNum中的this是无法直接指向当前对象的,这里就需要通过bind的方式将this传入到方法体内,从而可以让changeNum方法可以正常调用state并执行一系列操作
当我们点击按钮执行changeNum的时候会发现,num的数据确实被修改了,但是页面渲染的结果并没有一并修改,因为React的数据是单向绑定的,如果想实现双向绑定,那么需要使用setState方法来修改state,如下
changeNum(){ this.setState({ num:this.state.num + 1 //注意这里要实现累加不能使用++ }) console.log(this.state.num); }
这样就可以实现数据的双向绑定,这点和小程序很像
setState方法
通过上面的changeNum方法执行,我们可以看到一个情况, 我们通过setState修改了数据,但是紧接着打印的num却是修改之前的值, 这里其实说明了一个情况, setState方法是一个异步方法
如果我们想让数据修改之后再执行后面的语句, 该方法为我们提供了第二个参数, 一个回调函数, 写在这个回调函数内的语句, 会在修改完成之后再执行
changeNum(){
this.setState({
num:this.state.num + 1 //注意这里要实现累加不能使用++
},() => {
console.log(this.state.num);
})
}
setState有两种写法,分别是对象式写法和函数式写法
对象式写法
对象式写法就是我们上面例子中的写法
this.setState({
num:this.state.num + 1
},() => {
console.log(this.state.num);
})
函数式写法
将传入的第一个参数对象换成一个函数传入
this.setState((state,props) => {
//在这里可以写一些需要执行的语句
return {
num:state.num + 1
}
},() => {
console.log(this.state.num)
})
注意点
setState是一个同步的方法,但是setState引起React后续更新状态的动作是异步的
举例
import React, { Component } from 'react' export default class SetState extends Component { state = { num:1 } changeNum(){ this.state.num += 1; this.setState((state,props) => { console.log(state.num) return { num:state.num + 2 //注意这里要实现累加不能使用++ } }) } render() { return ( <div> <h2>{this.state.num}</h2> <button onClick={this.changeNum.bind(this)}>按钮</button> </div> ) } }
按照上面的业务逻辑,在changeNum方法开始执行的时候,如果setState方法本身是一个异步方法,那么,修改num加2的这个操作应该回被提到与
this.state.num += 1;
同时执行,而this.state.num += 1;
是直接调用状态值修改无法实现双向绑定,所以修改的结果是不会同时渲染把页面上,那么,渲染把页面上的应该是通过setState修改的加1操作,渲染结果应该是3,但是页面的渲染结果却是4,同时,我们再setState的函数语法内部还加入了一个console.log打印的结果是2,那就说明,执行到console.log为止,num只被加了1,如果同时执行了起码num现在在控制台打印出来应该是3或者4才对结论:
在setState中,只有修改React组件状态的部分才是执行的,按照上面函数式写法的例子就是return 后面执行的部分才是异步的
两种方式的使用原则
- 如果新状态不依赖于原状态,使用对象式
- 如果新状态依赖于原状态,使用函数式
- 如果需要在 setState() 执行后获取最新的状态数据,要在第二个 callback 函数中读取
- 对象式的 setState 是函数式 setState 的简写方式(语法糖)
事件绑定的三种方式
我们在上面的代码中,在给onClick绑定方法的时候使用了bind来重新定义changeNum方法内this的指向, 如果不通过bind修改this的指向, 那么根据我们以前学过关于this的指向的解释, this永远指向当前调用它的对象, 如果不写bind改this指向, 那么当前changeNum方法在调用的时候this应该是没有指向的, 导致我们无法调用当前组件的类中的state
基于以上的情况我们还可以通过一些其他的方式来修改this指向
第一种: 通过bind
<button onClick={this.changeNum.bind(this)}>按钮</button>
第二种: 在构造器函数内声明一个属性, 事先把this指向进行修改
export default class Two extends Component {
constructor(){
//......
this.changeNum = this.changeNum.bind(this)
}
changeNum(){
//.......
}
render(){
return (
<>
<button onClick={this.changeNum}>按钮</button>
</>
)
}
}
第三种: 利用bind修改this指向的原理
<button onClick={() => this.changeNum()}>按钮</button>
bind()函数会创建一个新的绑定函数,这个绑定函数包装了原函数的对象。调用绑定函数通常会执行包装函数。
一般我们更多采用第一种或第三种方式
React实现双向数据绑定
我们在vue中可以通过v-model指令来完成表单数据的双向绑定, 而react中实现需要我们自己来手写过程
在components中新建组件Data.jsx
扩展:
我们可以在VScode中安装ES7 React/Redux/GraphQL/React-Native snippets插件, 可以帮助我们快速输入类式组件的基础代码结构
import React, { Component } from 'react'
export default class Data extends Component {
constructor(){
super();
this.state = {
textVal:"用户名"
}
}
valChange(e){
this.setState({
textVal:e.target.value
})
}
render() {
return (
<div>
<input type="text" placeholder={this.state.textVal} onChange={this.valChange.bind(this)} />
<p>{this.state.textVal}</p>
</div>
)
}
}
代码分析:
双向绑定的主要实现手段是通过事件对象中的target来获取到当前input的value值, 然后再通过setState方法实现textVal的实时渲染
受控组件与不受控组件
这是一个react中对于组件状态state进行操作时操控方式区别的一种组件概念,简单来说就是组件中的状态值在渲染到标签结构中时,实现的是单向数据绑定,还是双向数据绑定
- 状态实现了双向数据绑定的就是受控组件
- 状态实现的是单向数据绑定的就是不受控组件
具体举例说明:
上面的双向数据绑定的例子就已经说明了受控组件使用 setState() 进行更新,而呈现表单的React组件也控制着在后续用户输入时该表单中发生的情况,以这种由React控制的输入表单元素而改变其值的方式,称为受控组件,所以就不专门用代码举例
而对于不受控组件表单数据由DOM本身处理。即不受setState()的控制,如果想要得到表单数据作为状态值,我们需要如下操作:
import React, { Component } from 'react'
export default class Data extends Component {
constructor(){
super();
this.state = {
textVal:"用户名"
}
}
valChange(){
console.log(this.refs.user.value)
}
render() {
return (
<div>
<input type="text" placeholder={this.state.textVal} onChange={this.valChange.bind(this)} ref="user" />
<p>{this.state.textVal}</p>
</div>
)
}
}
React列表渲染
在vue中我们可以通过v-for指令来进行渲染, 而react中跟上面的双向绑定一样,也需要我们自己来手写
在components新建list.jsx,然后把list组件导入到顶层组件中渲染
import React, { Component } from 'react'
export default class list extends Component {
constructor(){
super();
this.state = {
arrData:["zhangsan",'lisi','wangwu','zhaoliu']
}
}
render() {
return (
<div>
<ul>
{
this.state.arrData.map((item,index) => {
return (
<li key={index}>{item}</li>
)
})
}
</ul>
</div>
)
}
}
代码分析:
通过调用state中的数组进行遍历来完成列表渲染, 这里列表渲染也需要key来记录状态
在map方法回调的return中我们又使用了( ) 因为渲染渲染li标签, 所以我们可以得到一个结论, 在react组件的render方法中需要执行的语句,如果是js就套在大括号里面,如果是虚拟DOM就套在小括号里面
上面的map方法我们还可以通过ES6中箭头函数的语法进一步简化去掉map中的return
render() { return ( <div> <ul> { this.state.arrData.map((item,index) => <li key={index}>{item}</li> ) } </ul> </div> ) }
以上情况下return去掉之后, 依然还是执行的虚拟DOM 但是可以不在map里面虚拟DOM添加小括号, 加上也没有问题
React列表数据操作
现在我想修改arrData中的数据, 并且同步修改页面渲染数据, 按照上面的情况, 这个操作过程也需要我们手动通过原生JS来进行数组方法的使用
import React, { Component } from 'react'
export default class list extends Component {
constructor(){
super();
this.state = {
arrData:["zhangsan",'lisi','wangwu','zhaoliu']
}
}
//制作一个修改数组的方法
changeArr(){
let newArr = [...this.state.arrData];
//这里可以根据你自己实际需求的修改方式使用不同的数组方法来操作数据
newArr.push('haha');
//通过setState方法将新数组赋值
this.setState({
arrData:newArr
})
}
render() {
return (
<div>
<ul>
{
this.state.arrData.map((item,index) =>
<li key={index}>{item}</li>
)
}
</ul>
<!-- 添加一个按钮点击执行修改数组的方法 -->
<button onClick={this.changeArr.bind(this)}>按钮</button>
</div>
)
}
}
代码分析:
以上写法会有一个小问题, 如果我现在的操作并不是往数组的最后面添加一个元素, 而是在最前面或者是中间某个位置, 我们可以通过浏览器的控制台发现, 它会把后面不需要修改的元素也一并修改了, 这个问题主要就出在key的状态记录使用的是索引的问题上
这个问题我们在vue课程中就已经详细解释过了, 可以翻看下之前的笔记, 这里我们直接把key的值修改成item就没有问题了
this.state.arrData.map((item,index) => <li key={item}>{item}</li> )
但是如果点击多次添加还是会报个错, 这个错误我们之前在将vue项目案例的时候就讲过, 因为多次添加key的值是一样的
列表渲染的注意事项
因为我们现在采用的修改方式是直接赋值的方式, 而数组是引用数据类型, 现在是个一维数组其实还好, 但是如果是个多维数组就可能会有问题, 所以我们最好在这里对数组进行深拷贝, 沈拷贝有很多方式 , 我们以前有自己写的封装好的深度拷贝方法, 我们也可以有一些其他另辟蹊径的方法
changeArr(){
//先调用原始的state中的数组进行修改
this.state.arrData.push("haha");
//将原来的arrData进行序列化转换成字符串,再转回成一个数组赋值给newArr,相当于创建了一个新数组,而不是把arrData的内存地址赋值给newArr
let newArr = JSON.parse(JSON.stringify(this.state.arrData))
this.setState({
arrData:newArr
})
}
React中操作DOM
在React中我们通过ref来标记一个标签将其作为DOM使用,因为React支持原生html标签,所以ref既可以挂载到html元素上,也可以挂载到React元素上
举例:
import React, { Component } from 'react'
export default class Ref extends Component {
printDom(){
console.log(this.refs.htmlElement);
console.log(this.refs.reactElement)
}
render() {
return (
<div>
<input type="text" ref={'htmlElement'} />
<Child ref={'reactElement'}></Child>
<button onClick={this.printDom.bind(this)}>按钮</button>
</div>
)
}
}
class Child extends Component {
render(){
return (
<div>
<h2>我是child</h2>
</div>
)
}
}
代码分析:
上面的代码中,htmlElement会把input作为一个原生的DOM对象获取,而reactElement因为获取的是React组件的虚拟标签,所以会作为一个ReactDOM对象获取
如果想把一个ReactDOM作为一个原生DOM获取可以从react-dom中导入findDOMNode方法,将获取到的ReactDOM作为参数传入即可返回一个完整的原生DOM
import { findDOMNode } from 'react-dom' //.... printDom(){ //..... console.log(findDOMNode(this.refs.reactElement)) } //.....
使用ref的三种方式
1、字符串的方式
这种方式就是上面例子中的方式,但是官方目前已经不推荐使用这种方式,后期还有可能废弃掉
2、函数方式
import React, { Component } from 'react'
export default class Ref extends Component {
printDom(){
console.log(this.htmlElement);
}
render() {
return (
<div>
<input type="text" ref={ref => this.htmlElement = ref} />
<button onClick={this.printDom.bind(this)}>按钮</button>
</div>
)
}
}
代码分析:
通过ref标记赋值一个函数,参数ref会被注入当前DOM元素,
this.htmlElement = ref
相当于在当前类中声明一个属性叫做htmlElement,然后将注入到参数ref中的DOM对象赋值到此变量中,之后我们就可以在组件内通过this调用
3、通过React.createRef方法获取
import React, { Component } from 'react'
export default class Ref extends Component {
inputDOM = React.createRef();
printDom(){
console.log(this.inputDOM.current);
}
render() {
return (
<div>
<input type="text" ref={this.inputDOM} />
<button onClick={this.printDom.bind(this)}>按钮</button>
</div>
)
}
}
注意:
获取到的DOM对象在inputDOM的current属性中
父子组件
React的类式组件是通过在一个js文件中写入class来创建的, 所以我们可以一个文件一个类, 也可以一个文件多个类, 也就是说我们可以在一个js文件内完成父子组件
components文件夹内新建father.jsx
import React, { Component } from 'react'
export default class father extends Component {
//这里我们可以省略构造器,因为构造器主要是为了做类的继承使用,但是我们这里类式组件中的类不存在继承关系
state = {
fatherData:"zhangsan"
}
render() {
//在father父组件中调用Child子组件并将父组件中fatherData传入到子组件中
return (
<div>
<Child num={this.state.fatherData}></Child>
<Child></Child>
</div>
)
}
}
//Child是子组件
class Child extends Component {
//在子组件中可以通过定义静态属性方式定义props的默认值
static defaultProps = {
num:50
}
render(){
//在子组件的类中默认声明了一个props属性对象,用于存放父组件传递过来的数据
return (
<>
<h1>我是child组件</h1>
<h2>{this.props.num}</h2>
</>
)
}
}
代码分析:
其实类式组件中的类, 在构造器中都会默认设置一个props并通过super来继承, 完整写出来如下
class Child extends Component { constructor(props){ super(props) } //...... }
只不过这个过程React可以自动帮我们完成, 所以不写也行
设置props接收的期望数据类型
之前在vue中, 我们是可以设置需要接收的数据类型的, 在react中也可以
//......
import defaultTypes from 'prop-types' //导入设置期望类型的对象
class Child extends Component {
//于设置props默认值一样也通过静态属性设置props的期望数据类型
static propTypes = {
num:defaultTypes.number
}
static defaultProps = {
num:50
}
render(){
return (
<>
<h1>我是child组件</h1>
<h2>{this.props.num}</h2>
</>
)
}
}
export default class father extends Component {
state = {
fatherData:"zhangsan"
}
render() {
return (
<div>
<Child num={this.state.fatherData}></Child>
<!-- 在第二次调用中传入一个字符串,但是期望类型是number,虽然可以渲染,但是会报警告 -->
<Child num={'haha'}></Child>
</div>
)
}
}
子组件修改父组件数据
import React, { Component } from 'react'
import defaultTypes from 'prop-types'
export default class father extends Component {
state = {
fatherData:"zhangsan"
}
//在父组件中创建一个修改自己内部数据的方法
changeFatherData(){
this.setState({
fatherData:"lisi"
})
}
render() {
return (
<div>
<h2>{this.state.fatherData}</h2>
<!-- 将修改父组件内部数据的方法传递给子组件 -->
<Child num={this.state.fatherData} changNum={this.changeFatherData.bind(this)}></Child>
</div>
)
}
}
class Child extends Component {
static defaultProps = {
num:50
}
//子组件声明一个方法,在该方法内部调用父组件传递过来的修改父组件数据的方法
changNum(){
this.props.changNum()
}
render(){
return (
<>
<h1>我是child组件</h1>
<h2>{this.props.num}</h2>
<!-- 在子组件中的按钮上绑定用于修改父组件数据的子组件方法 -->
<button onClick={this.changNum.bind(this)}>修改父级</button>
</>
)
}
}
代码分析:
上述的制作方式类似vue的自定义事件, 只不过执行逻辑上react更加直接一些, 在父组件中声明修改自己数据的方法, 然后将该方法传递给子组件, 然后, 子组件再自己声明一个方法, 从props中调出父组件传递过来的修改数据的方法, 最后事件触发执行子组件的方法, 从而执行父组件传递过来的修改父组件数据的方法
使用context进行跨级传递数据
React组件之间的通信是基于props的数据传递,但是这种传递方式只能一层一层从上至下传递,如果说组件的层级嵌套的太多就会过于麻烦,举例:
现在要从父组件传递一个数据到子组件的子组件,如下:创建一个Father类式组件,在内部再创建两个子类组件
import React, { Component } from 'react'
class GrandSon extends Component {
render(){
return (
<div>
{this.props.str}
</div>
)
}
}
class Son extends Component {
render(){
return (
<div>
<GrandSon str={this.props.str}></GrandSon>
</div>
)
}
}
export default class Father extends Component {
state = {
str:"haha"
}
render() {
return (
<div>
<Son str={this.state.str}></Son>
</div>
)
}
}
代码分析:
上面代码我们可以看到,在father组件中的str,需要先传递给son,然后son再从自己的props中调出传递给GrandSon,如果层级更多,那么这个过程只会更加麻烦
React中Context配置可以解决组件跨级的数据传递
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class GrandSon extends Component {
//声明静态的contextTypes,设置接收的数据类型
static contextTypes = {
fstr:PropTypes.string
}
render(){
return (
<div>
<!-- 从上下文中调出父组件传递过来的数据 -->
{this.context.fstr}
</div>
)
}
}
class Son extends Component {
render(){
return (
<div>
<GrandSon></GrandSon>
</div>
)
}
}
export default class Father extends Component {
state = {
str:"hahahaha"
}
//声明静态的childContextTypes,设置需要在上下文环境中传递的数据类型
static childContextTypes = {
fstr:PropTypes.string
}
//通过getChildContext方法return需要传递的数据到上下文环境中
getChildContext(){
return {
fstr:this.state.str
}
}
render() {
return (
<div>
<Son></Son>
</div>
)
}
}
代码分析:
在上面代码中,我们一共使用了2个静态属性,和一个方法完成了通过context传递数据的操作
static contextTypes
设置在需要接收的数据的子组件上static childContextTypes
设置在要向下传递数据的父组件上getChildContext()
方法设置在需要向下传递数据的父组件上,在方法体内return需要传递的数据到上下文环境中基于上面三项操作,只要是出于当前父组件上下文环境中的子组件都可以调用到环境中的数据
注意:
以上三个名称不是随便取的,是固定的