2022即将到来,前端发展非常迅速,现在的前端已经不再是写写简单的页面了,伴随着web大前端,工程化越来越火,也出现了很多框架。很多公司也不单单是使用单一的框架了,作为前端开发国内最火的两大框架,都是必须要掌握的。所以,我决定在这疫情肆虐的年底把React学习一下,也为自己将来找工作多一分竞争力...
学习阶段,如有不对之处,欢迎评论区留言,我及时更正
本文已连载,其它章节传送门⬇️
Props和State
React中使用props接收数据,state维护自己的数据。并且props是只读的属性,不能修改,如果修改数据,我们只能去修改父组件的state。
Props
💡 props可以接收多种类型的数据,甚至可以接收一个组件,非常的灵活,我们可以通过props传递任何数据
传递普通数据
import {Component} from 'react'
import ReactDOM from 'react-dom'
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
date: '2022-01-14'
}
}
render() {
return(
<Date date={this.state.date} />
)
}
}
function Date(props) {
console.log(props)
return (
<h1>{props.date}</h1>
)
}
ReactDOM.render(<Demo />, document.querySelector('#root'))
打印下props,我们可以看下它长什么样子
传递一段结构
import {Component} from 'react'
import ReactDOM from 'react-dom'
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
date: '2022-01-14'
}
}
render() {
const element = <h1>{this.state.date}</h1>
return(
<Date date={element} />
)
}
}
function Date(props) {
console.log(props)
return (
<div>
{props.date}
</div>
)
}
ReactDOM.render(<Demo />, document.querySelector('#root'))
我们这里改了一下代码,通过props传递一段结构看来也是可以的
这里可以看到props接收的date属性是一个react的dom元素,细心的小伙伴发现date属性还有一个props属性,并且里面有一个children属性,里面是我们父组件的date,再去看看父组件,这个值是渲染在 h1
标签里面的,也就是说写在中间的内容会自动给props增加一个属性。
props的children属性
接着上面的我们试着在组件中间传递一段结构或者文本试试
import {Component} from 'react'
import ReactDOM from 'react-dom'
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
date: '2022-01-14',
name: '小明'
}
}
render() {
return(
<Date date={this.state.date}>
{this.state.name}
</Date>
)
}
}
function Date(props) {
console.log(props)
return (
<div>
<h1>{props.date}</h1>
</div>
)
}
ReactDOM.render(<Demo />, document.querySelector('#root'))
我们可以看到,props身上自动多了一个children属性,由于我们传递的是字符串,这里children属性值就是一个普通的字符串,如果我们传递了一段结构或者一个组件,那么它就是一个对象,如果我们传递了多个结构或者组件,那么它的值就是一个数组
代码略了,直接截图console的结果:
如果children是数组的话,我们可以通过遍历的方法拿到所有的子节点喔~
State
💡 state提供用来维护组件的数据,类似于vue中的data函数
还是看上面的代码吧,只有类组件才有state,它提供了组件初始化的数据,我们可以通过this.state.属性名拿到。这里很简单就不多说了,接下来让我们看一下如何修改state数据吧
React提供唯一可以修改state的方法:setState()
比如一个小需求,实现一个计数器的功能,点击按钮可以加减数据
class Demo extends React.Component {
constructor() {
super()
this.state = {
counter: 0
}
}
handelClick = (num) => {
this.setState({
counter: this.state.counter + num
})
}
render() {
return(
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={() => {this.handelClick(1)}}>+ 1</button>
<button onClick={() => {this.handelClick(-1)}}>- 1</button>
</div>
)
}
}
ReactDOM.render(<Demo/>, document.querySelector('#app'))
暂且这么写,这里涉及到this问题,下面会讲,可以看到我们通过setState
方法成功修改了数据
setState使用
React中,setState方法是修改数据唯一的方法,它是一个回调函数,接收一个对象形式的参数或者接收一个回调函数作为参数。 👉关于setState的官网说明
对象参数
💡 问题:当我们调用setState后立马打印此时的数据,不会显示更新后的,而是显示上一次的,并且多个setState会合并为一个,后面的会覆盖上面的,如下图所示
add = () => {
const { count } = this.state
this.setState({
count: count + 1
})
this.setState({
count: count + 2
})
this.setState({
count: count + 1
})
console.log(count)
}
React会把setState接收的对象参数放到一个更新队列里面,把所有的对象提取出来合并的到一起,然后才会触发组件更新,所以多次调用setState只会触发一次重新渲染,毕竟dom更新成本是比较昂贵的
回调参数
💡 思考:如果我们需要多次调用,数据依赖于上一次的结果,有没有办法解决呢? — 除了对象形式的参数,setState还可以接收一个回调形式的参数,回调参数接收一个参数,此参数就是上一次执行的结果
add = () => {
const { count } = this.state
this.setState((pre) => {
return {
count: pre.count + 1
}
})
this.setState((pre) => {
return {
count: pre.count + 1
}
})
this.setState((pre) => {
return {
count: pre.count + 1
}
})
console.log(count)
}
需要注意的是,无论是对象形式的回调还是 函数形式的回调,setState调用后立马都是拿到上一次的数据,而不是这次的,那么我们就需要在组件更新完成后再去获取数据,而不是立即获取
setState可以接收二个函数作为参数,第二个函数参数会等待setState执行后在再执行,那么我们也可以在第二个回调参数中去获取数据
setState同步or异步
💡结论: setState是同步,表现异步行为是由于React的优化机制,setState只在React引发的事件处理(如onClick)和钩子函数中表现“异步”的,在原生事件(选定DOM 监听addEventListener)和setTimeout 中都是同步的
React事件处理
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
date: '2022-01-14',
name: '小明',
count: 0
}
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第一次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第二次log')
setTimeout(() => {
console.log(this.state.count, '第三次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第四次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第五次log')
})
}
render() {
return(
<div>
<h1>显示当前count:{this.state.count}</h1>
<button onClick={this.handleClick}>按钮</button>
</div>
)
}
}
我们可以看到第二次log的时候,由于刚调完setState
立即打印的话,拿到的还是初始值,这也说明了多次调用react会合并到一个队列中,并不会立即去更新state,而是一次性的更新state,也就是表现的像异步;接着看第三次log以后的就已经是立即更新state了,说明在setTimeout中是同步的。
React钩子函数
componentDidMount = () => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第一次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第二次log')
setTimeout(() => {
console.log(this.state.count, '第三次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第四次log')
this.setState({
count: this.state.count + 1
})
console.log(this.state.count, '第五次log')
})
}
我们把 handleClick
函数改为react生命周期钩子函数componentDidMount
,此时打印结果和事件处理中的是一样的,再次说明了上面的结论。
为什么 React 不同步地更新 this.state?
看了官网的说明,意思是为了高效的更新渲染,react把setState
放到队列中等待一次性执行,从而避免了组件会重复多次渲染的问题。试想,如果不这样处理,调用一次直接渲染一次,性能开销和体验肯定也是不好的,一次更新只需要重新渲染一次就可以解决的,没必要多次重新渲染导致重绘
setState不可变的力量
我们知道js中,简单类型的数据是直接占的内存,而复杂类型的数据只是一个引用,当我们修改简单类型的数据时,是直接可以修改掉的。而当我们修改复杂类型的数据时候,比如修改obj.name = ‘小明’,其实它的引用地址并没有修改,还是指向的同一块内存空间
export default class Index extends Component {
constructor(props) {
super(props)
this.state = {
list: [
{
name: '小明',
id: 1
},
{
name: '小红',
id: 2
}
]
}
}
shouldComponentUpdate(nextProps, nextState) {
console.log(nextState)
if(this.state.list !== nextState.list) {
return true
}
return false
}
addName = () => {
const obj = {name: '小刚', id: 3}
this.state.list.push(obj)
this.setState({
list: this.state.list
})
}
render() {
console.log('render函数执行了');
return (
<div>
<ul>
{
this.state.list.map((item) => {
return <li key={item.id}>{item.name}</li>
})
}
</ul>
<button onClick={this.addName}>按钮</button>
</div>
)
}
}
比如我们有一个小需求,点击按钮,数组新增一条数据,并且在shouldComponentUpdate
钩子函数里,通过对比list的值是否变化来决定是否更新组件,此时如果我们是直接往list里push一个对象的话,我们知道数组是引用类型,引用的那个地址并没有变化,在对比前后state的时候就不会有变化,导致return false,组件并不会更新。但是打印看nextState
此时实际是已经加进去了的,无论点击多少次组件都不会更新,因为引用地址始终没有变化
我们修改下代码:
addName = () => {
const arr = [...this.state.list]
arr.push({
name: '小刚',
id: 3
})
this.setState({
list: arr
})
}
运用es6的扩展运算符,把数组扩展到一个新数组里面,此时并不是之前的引用地址了,而是一个全新引用地址的数组,我们再添加数据后 重新设置list的值就会生效了,可以看到页面也更新重新渲染了。