之前我们一直用类组件写demo,类组件相对于函数组件有什么优势?
import React, { PureComponent } from 'react'
// 函数组件
function HelloHooks() {
// 就算函数重新执行, 又会重新赋值, 无意义
let message = "Hello Hooks"
return(
<div>
<h2>{message}</h2>
</div>
)
}
export class App extends PureComponent {
render() {
return (
<div>
<HellWorld/>
<HelloHooks/>
</div>
)
}
}
export default App
componentDidMount
中发送网络请求,并且该生命周期函数只会执行一次。但是函数组件在哪里发请求呢?如果在函数中发请求,那么每次重新渲染又重新调用函数又重新发请求,这样反复发请求很沙雕。class组件
可以在状态改变时, 只重新执行render函数
以及我们希望重新调用的生命周期函数(如componentDidUpdate)
;函数式组件
在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们内部的某段代码只执行一次。
所以在Hook之前通常都用类组件写项目。
随着业务的增多,我们的class组件会变得越来越复杂;
比如componentDidMount
中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount
中移除);
而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
最显著的问题就是this的指向问题,如果this不处理好,可能会出现一些错误。
比如我们之前使用redux中的数据需要用connect这样的高阶组件,或者如果想实现什么功能还需要自己封装高阶组件给类组件注入一些东西,这还是非常费劲的。
如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
Hook只能在函数组件中使用,不能在类组件或者函数组件之外的地方使用;
之前用类组件的写法我们都很熟知了:
import React, { PureComponent } from 'react'
export class Counter1 extends PureComponent {
constructor() {
super()
this.state = {
counter: 10
}
}
changeNumber(num) {
this.setState({
counter: this.state.counter + num
})
}
render() {
const { counter } = this.state
return (
<div>
<h2>当前计数: {counter}</h2>
<button onClick={() => this.changeNumber(-1)}>-1</button>
<button onClick={() => this.changeNumber(1)}>+1</button>
</div>
)
}
}
export default Counter1
我们再看看用函数式组件,这个东西怎么写:
import React, { memo, useState } from 'react';
const Counter = memo(() => {
let [counter, setCounter] = useState(99);
return (
<div>
<h2>Counter:{counter}</h2>
<button onClick={() => setCounter(counter+1)}>点击让Counter+1</button>
</div>
)
})
export default Counter
真的是非常的简洁,而且没有this的指向问题。
它与class组件里面的 this.state
提供的功能完全相同,就是用来保存状态(数据)的。
上面的代码点击按钮会发生两件事情:
1、调用setCounter
修改counter
2、组件重新渲染(函数重新执行),根据新值返回DOM结构
一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。我盲猜这里用到了闭包:setCounter
函数内部保存了state中的原始变量,等到setCounter
函数被调用后原始变量才会被销毁,数据更改后再重新执行函数组件并把新值传过去,此时数据就有了最新的值。(当然只是盲猜)
useState
只有一个参数: 接收一个初始化状态的值
(设置初始值),在第一次组件被调用时使用来作为初始化值
(如果不设置则默认为undefined
)。
useState
的返回值: 返回一个数组,数组包含两个元素:
一般我们会对数组进行解构(名字当然是自己取):
let [counter, setCounter] = useState(99);
通过上面的讲解,可以感受到hook顾名思义就是把要用的东西钩过来用一下子。
使用Hook的规则:
1、只能在函数组件的
顶层
调用 Hook。不能在循环语句、条件判断语句或者子函数中调用。
2、只能在 React 的函数组件和自定义hook
中调用 Hook。不能在其他 JavaScript 函数中调用。
当然,我们可以定义更多类型的数据。
也可以把修改的操作单独封装一个函数。
import React, { memo, useState } from 'react';
const Counter = memo(() => {
let [counter, setCounter] = useState(99);
let [newsList, setNewsList] = useState(['体育','娱乐']);
let [userInfo, setUserInfo] = useState({name:'zzy',age:18});
function changeNum() {
setCounter(counter - 1);
}
return (
<div>
<h2>Counter:{counter}</h2>
<button onClick={() => setCounter(counter+1)}>点击让Counter+1</button>
<button onClick={changeNum}>点击让Counter-1</button>
<h3>{newsList}</h3>
<h3>{userInfo.name}-{userInfo.age}</h3>
</div>
)
})
export default Counter
在类组件中是可以有生命周期函数的, 那么如何在函数组件中定义类似于生命周期这些函数呢? 答案就是:useEffect
钩子
顾名思义,其实我们平时的网络请求、手动更新DOM、一些事件的监听、订阅redux数据变化等操作,都是除了更新DOM之外需要做的操作,也就是一些副作用effect
比如一个这样的案例:修改counter时,实现页面标题和counter同步变化
类组件实现:
import React, { PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
counter: 100
}
}
// 进入页面时, 标题显示counter
componentDidMount() {
document.title = this.state.counter
}
// 数据发生变化时, 让标题一起变化
componentDidUpdate() {
document.title = this.state.counter
}
render() {
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={() => this.setState({counter: counter+1})}>+1</button>
</div>
)
}
}
export default App
不难看出,类组件想要实现这个效果,需要写两个生命周期钩子,componentDidMount
只有第一次挂载完成会执行,componentDidUpdate
在每次更新结束后会执行。
那么函数组件怎么实现这个效果呢?答案是使用useEffect
import React, { memo, useEffect, useState } from 'react'
const App = memo(() => {
const [counter, setCounter] = useState(200)
// useEffect传入一个回调函数, 在页面渲染完成后自动执行
useEffect(() => {
// 一般在该回调函数在编写副作用的代码(网络请求, 操作DOM, 事件监听)
document.title = counter
})
return (
<div>
<h2>{counter}</h2>
<button onClick={() => setCounter(counter+1)}>+1</button>
</div>
)
})
export default App
useEffect传入一个回调函数, 这个回调函数在每次
页面渲染完成后自动执行。也就是说,每次在函数式组件执行的顺序是:
执行函数组件 => 定义初始状态 => 渲染DOM => 执行useEffect
中的回调
=> 修改数据 => 重新执行函数组件 => 更新状态 => 渲染最新DOM => 执行useEffect
中的回调
不难看出,其实useEffect
中的回调
相当于完成了componentDidMount
和componentDidUpdate
做的事情。
在类组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount
中进行清除,比如事件总线
或redux的数据订阅
,那这在函数式组件中怎么做呢?
useEffect
传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B
useEffect(() => {
console.log('订阅了redux,绑定了某个事件A')
return () => {
console.log('取消订阅redux,解绑某个事件A')
}
})
返回的这个回调函数执行的时机有两个:组件即将更新数据、组件即将卸载。
什么意思呢?如果我们在useEffect
回调中绑定了某个事件,那么每次更新数据,每次都要渲染DOM,每次渲染DOM就会引起每次useEffect
回调重新执行,这样的话相当于只要更新数据就反复绑定事件,嘚儿嘚儿嘚儿绑定一堆事件,这显然有点奥里给啊。
所以说我们可以协商这个返回的回调函数,每次更新数据前先执行回调解绑事件,再更新再绑定事件,也就是同样的事件每次只绑定一个
就够啦。
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:
比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
一个函数组件中可以使用多个Effect Hook,我们可以将逻辑分离到不同的useEffect中:
import React, { memo } from 'react';
import { useState, useEffect } from 'react';
const Counter = memo(() => {
let [counter, setCounter] = useState(99);
useEffect(() => {
//当前回调会在页面渲染完成后重新执行,每次都重新执行
document.title = counter;
})
useEffect(() => {
console.log('订阅了redux,绑定了某个事件A')
return () => {
console.log('取消订阅redux,解绑某个事件A')
}
})
useEffect(() => {
console.log('发送网络请求的逻辑')
})
return (
<div>
<h2>Counter:{counter}</h2>
<button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
</div>
)
})
export default Counter
React将按照 effect 声明的顺序
依次调用组件中的每一个 effect;
默认情况下,useEffect的回调函数会在每次渲染都重新执行,但是某些代码我们只是希望执行一次即可(比如网络请求,绑定事件、订阅redux)。另外,多次执行也会导致一定的性能问题。
我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
useEffect实际上有两个参数:
useEffect
在哪些state
发生变化时,才重新执行(受谁的影响才会重新执行)我们来看下面这个案例,就明白了:
import React, { memo } from 'react';
import { useState, useEffect } from 'react';
const Counter = memo(() => {
let [counter, setCounter] = useState(99);
let [message, setMessage] = useState('DJ drop');
useEffect(() => {
//当前回调会在页面渲染完成后重新执行,每次都重新执行
document.title = counter;
console.log('counter的值变了,我瞅见了!')
},[counter])
useEffect(() => {
console.log('message的值改变了!我瞅见了!')
},[message])
useEffect(() => {
console.log('counter和message的值都变了!我瞅见了!')
},[counter,message])
return (
<div>
<h2>Counter:{counter}</h2>
<button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
<button onClick={() => setMessage('the beat')}>改变message</button>
</div>
)
})
export default Counter
在上面这个案例中,页面第一次渲染每个useEffect
都会先执行,然后第一个useEffect
只有当counter
的值改变时会重新调用,第二个useEffect
只有当message
的值改变时会重新调用,第三个useEffect
当counter
和message
的值任意一个改变时会重新调用。
当然啊,如果要模拟生命周期如componentDidMount
这种只在挂载时执行一次,那么第二个参数写个空数组[]
就行了,表示我只调用第一次,后面任何数据的修改都不会引起我的重新调用。
import React, { memo } from 'react';
import { useState, useEffect } from 'react';
const Counter = memo(() => {
let [counter, setCounter] = useState(99);
useEffect(() => {
console.log('订阅了redux,绑定了某个事件A')
return () => {
console.log('取消订阅redux,解绑某个事件A')
}
},[])
useEffect(() => {
console.log('发送网络请求的逻辑')
},[])
return (
<div>
<h2>Counter:{counter}</h2>
<button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
</div>
)
})
export default Counter
那么这样的话,其实回调中返回值的那个回调,也只有在组件即将销毁时调用,相当于componentWillUnmount
了,非常奈斯。