本篇文章概要:
- 类组件 和 hooks组件
- hooks 和 react生命周期 的对应关系
- constructor
- render
- componentDidMount
- componentDidUpdate
- componentWillUnmount
- 常用的hooks
- useState
- useEffect
- useContext
- useMemo
- useCallback
- useRef
- 什么时候使用 useMemo 和 useCallback
- 自定义hook
1.类组件 和 hooks组件
从类组件转变成hooks组件。这期间对于react研发来说,是个挑战,而且转变挺大的! 在讲hooks之前,说下类组件有哪些不足的地方? 1.类组件状态逻辑复用难 2.类组件趋向复杂难以维护 3.this指向困扰
hoos组件优势: 1.自定义hook方便复用状态逻辑 2.副作用的关注点分离 3.无this指向问题
后面会详细说明~
2.hooks 和 react生命周期对应关系
在没引入 hooks 之前,函数组件是没有state ,因此函数组件是不存在生命周期这一概念的; 但是引入 hooks 之后,函数组件支持了 state,所以就有了和类组件一样的生命周期这一概念。
下面来对比,函数组件hooks和类组件class生命周期的对应关系,这样习惯写类组件的react开发能很好的写出hooks组件。
先从react常用的五个生命周期说起:
1.constructor
:类组件初始化state时
constructor(props) {
super(props)
this.state = {
uploading: false,
imgPath: '',
}
}
替换成hooks写法,使用useState
:
cosnt [uploading, setUploading] = useState(false)
cosnt [imgPath, setImgPath] = useState('')
2.render
:类组件渲染时
class ImgUplod extends PureComponent {
render() {
return (<Upload/>)
}
}
替换成hooks写法,就是函数组件本身,直接使用return
:
function ImgUplod () {
return (<Upload/>)
}
3.componentDidMount
:类组件渲染完毕,即DOM加载完成时
有点像DOM的onLoad
方法,在挂载的时候执行,且只会执行一次,常用来做一些请求数据、事件监听等。
componentDidMount() {
dispatch({
type: `login/${type}`
})
window.addEventListener("resize", onResize, false)
}
替换成hooks写法,使用useEffect
:
useEffect(() => {
dispatch({
type: `login/${type}`
})
window.addEventListener("resize", onResize, false)
}, [])
4.componentDidUpdate
:类组件重新渲染时
注意首次渲染是不会执行此方法的,可以获取到该组件上次的props或state。
也是react具有时间旅行
这一特点的原因。
componentDidUpdate(preProps) {
const { data } = this.props;
if (data !== preProps.data) {
// because of charts data create when rendered
// so there is a trick for get rendered time
this.getLegendData();
}
}
替换成hooks写法,使用useEffect
:
useEffect(() => {
// because of charts data create when rendered
// so there is a trick for get rendered time
this.getLegendData();
}, [this.props.data])
准确来说,useEffect还是和componentDidUpdate有些区别。 componentDidUpdate是首次不执行,更新才执行; useEffect是首次会执行,更新也执行。
注意:为什么componentDidUpdate有if判断,而useEffect没有?因为useEffect监听的就是this.props.data
这个数据,只有当data发生变化时,才会执行useEffect里面的内容(自动if)。
还有一个疑问,那么在hooks中如何获取历史props和state呢?
除了useEffect
,还需要和useRef
配合,来实现时间旅行:
const preProps = useRef<number>()
useEffect(() => {
preProps.current = current
}, [current])
5.componentWillUnmount
:类组件销毁时
当路由跳转后,组件销毁时触发。
可以操作,如:移除事件监听、移除 localStorage 持久化数据等。
componentWillUnmount() {
window.removeEventListener("resize", onResize, false)
window.localStorage.removeItem('userid')
}
替换成hooks写法,使用useEffect
:
useEffect(() => {
...
return () => {
window.removeEventListener("resize", onResize, false)
window.localStorage.removeItem('userid')
}
}, [])
通过hooks的useEffect
、useState
、useRef
就能把react常用的生命周期都实现一遍,是不是很神奇?不常用的生命周期,如:getDerivedStateFromProps、shouldComponentUpdate、getSnapshotBeforeUpdate,在这里就不做考虑了。
3.常用的hooks
3.1 useState
函数组件useState:等同于类组件this.setState。
举例:
this.setState方式:
class DeivceTest extends PureComponent {
constructor(props) {
super(props)
this.state = {
visible: false
}
}
handleChange = () => {
this.setState({
visible: true
})
}
render() {
const { visible } = this.state
return (
<div onClick={this.handleChange}>
{ visible && <SettingCard/> }
</div>
)
}
}
useState方式:
const DeviceTest = () => {
const [visible, setVisible] = useState<boolean>(false)
const handleChange = () => {
setVisible(true)
}
return (
<div onClick={handleChange}>
{ visible && <SettingCard/> }
</div>
)
}
3.2 useEffect
上面提到副作用
这一概念,实现副作用就是使用useEffect
。
那什么是副作用呢?
除了数据渲染到视图外的操作,都可以是副作用
。
比如:发起网络请求,访问DOM元素,写本地持久化缓存、绑定解绑事件等。
副作用时机: Mount之后(componentDidMount)、 Update之后(componentDidUpdate)、 Unmount之前(componentWillUnmount)
调用一次副作用:
// 相当于,componentDidMount 和 componentWillUnmount
useEffect(() => {
window.addEventListener("resize", onResize, false)
return () => {
window.removeEventListener("resize", onResize, false)
}
}, [])
调用多次副作用:
// 相当于,componentDidMount 和 componentDidUpdate
useEffect(() => {
document.title = xxx
}, [xxx])
多说一下useEffect,毕竟它太重要了。
可以发现[]是useEffect的精髓所在
,正确的使用好useEffect,减少不必要的逻辑错误。
useEffect是在render之后
调用的,即组件DOM渲染完成之后调用。
每个useEffect只处理一种副作用
。这种模式,就是关注点分离。不同的副作用,分开放!
3.3 useContext
useContext是为了解决props层层传递的问题,可以实现多层级的数据共享。 用法: 1.通过createConntext创建Context对象
import { createContext } from 'react';
const Context = createContext(0)
export default Context
2.父组件:用Context.Provider包裹子组件,其包含的所有子组件共享该数据
import Context from 'hooks/useContext'
<Context.Provider value={current}>
<Child1 />
<Child2 />
...
</Context.Provider>
3.子组件:通过useContext获取父组件的值
import Context from 'hooks/useContext'
const pcount = useContext(Context)
注意:该hook,尽量别乱用,因为会破坏组件的独立性
。
3.4 useMemo
上一章提到过React.memo(),useMemo和memo对比就能知道其意义。
React.memo() 和 PureComponent,针对的是组件的渲染是否重复执行
。
useMemo,针对的是定义的函数逻辑是否重复执行
。
本质用的是同样的算法,判断依赖是否改变,进而决定是否触发特定逻辑。
输入输出是对等的,相同的输入一定产生相同的输出,就和数学的幂等
一样。
先用React.memo举例:
在父组件中,有两个子组件:
一个子组件export default React.memo(Child1)
,
另一个组件export default Child2
。
function Parent() {
return (
<div>
<p>{current} Page</p>
<div>Child: <Child1 /> </div>
<div>Child: <Child2 /></div>
</div>
)
}
当父组件的 current 发生变化时,子组件Child2会不停被渲染,尽管它没做任何的变化;而加了memo的子组件Child1只会被渲染一次。
再用useMemo举例: 子组件Child1:
import React from 'react';
interface ChildProps {
count: number
}
function Child({ count }: ChildProps) {
console.log('render----------', count * 2)
return (<span>Child1 {count * 2}</span>)
}
export default React.memo(Child)
子组件Child2:
import React, { useMemo } from 'react';
interface ChildProps {
count: number
}
function Child({ count }: ChildProps) {
const dcurrent = useMemo(() => {
return count * 2
}, [count])
console.log('render----------', dcurrent)
return (<span>Child1 {dcurrent}</span>)
}
export default React.memo(Child)
两者的区别在于:
子组件Child1,在render一次后,count * 2 是调用两次、执行两次计算
,一个是日志里的,一个是页面的;
而子组件Child2,在render一次后,count * 2是调用两次、执行一次计算
。
useMemo 相当于Vue中computed里的计算属性,当某个依赖项改变时才重新计算值,这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo作用:避免重复计算,减少资源浪费
。
useMemo和useEffect的调用时机不同:useMemo是在render之前,useEffect是在render之后。
3.5 useCallback
useCallback,也是针对的是定义的函数逻辑是否重复执行
。
useMemo解决的是避免在每次渲染时都进行高开销的计算问题
。
useCallback解决的是传入组件的函数属性导致其他组件渲染问题
。
说的可能有点绕,下面来举例说明: 父组件:
function Parent() {
const onClick = () => {
console.log('Click-----')
}
return (
<div>
<p>{current} Page</p>
<div>Child: <Child1 /> </div>
<div>Child: <Child2 onClick={onClick} /></div>
</div>
)
}
子组件Child2:
import React from 'react';
interface ChildProps {
onClick: (evt: any) => void
}
const Child: React.FC<ChildProps> = ({ onClick }) => {
return (<span onClick={onClick}>Child2</span>)
}
export default React.memo(Child)
尽管没有点击执行onClick事件,但是还是会让子组件Child2渲染,因为Child2的onClick函数属性,每次都会创造成新的函数。
我们改下onClick事件:
// 改造前
const onClick = () => {
console.log('Click-----')
}
// 用useMemo改造后
const onClick = useMemo(() => {
return () => {
console.log('Click-----')
}
}, [])
// 用useCallback改造后
const onClick = useCallback(() => {
console.log('Click-----')
}, [])
改造后,不会执行Child2的任何渲染。
useMemo 和 useCallback 都是作性能优化之用,与业务逻辑无关
。
3.6 useRef
useRef 不仅仅是用来管理 DOM ref
的,它还相当于this
, 可以存放任何变量。
不需要引起组件重新渲染的变量
,都可以放在ref里。
举例:管理DOM ref
function Parent() {
const inputElement = useRef<HTMLInputElement>(null)
const onClick = () => {
console.log('Click-----')
inputElement.current?.focus()
}
return (
<div>
<p>{current} Page</p>
<div>Child: <Child1 /> </div>
<div>Child: <Child2 onClick={onClick} /></div>
<input ref={inputElement} />
</div>
)
}
点击组件Child2,实现input聚焦。
再举例:存放任何变量 使用useRef存放渲染前一个的变量:
const preProps = useRef<number>()
useEffect(() => {
console.log('pre', preProps.current)
console.log('cur', current)
preProps.current = current
}, [current])
使用useRef存放一个变量,阻止多次点击导致重复请求接口:
const lock = useRef<boolean>(false)
const change = async (type: string) => {
if (lock.current) return
lock.current = true
console.log('Change-------start')
const delay = (timeout: number) => new Promise((resolve) => {
setTimeout(resolve, timeout);
})
await dispatch({
type: `login/${type}`
})
await delay(2000)
console.log('Change-------end')
lock.current = false
}
疯狂点击ing:
以上只是举例,按理说useRef还有更多的玩法。
还有一种阻止多次点击导致重复请求接口的方案,通过dva-loading 控制 disabled 属性,从而控制点击事件。
(loading: loading.effects['login/addASync']) => {
return (<button className="App-btn" disabled={loading} onClick={() => change('addASync')}>Add</button>)
})
3.7 hooks简单总结
useState:数据渲染到组件的操作
useEffect:数据渲染到组件之外的操作
useContext:需要props多层传递的操作
useMemo:避免重复计算的操作
useCallback:避免组件函数属性引起渲染的操作
useRef:存放不需要引起组件渲染的变量
执行时机: Context.Provider 是在render之前声明,useContext 是在render之后执行; useState 是在render之前声明,setState 是在render之后执行; useRef 是在render之前声明,ref.current 是在render之后执行;
useMemo、useCallback 是在render之前执行; useEffect 是在redner之后执行;
简单理解:执行除了优化性能的hook在render之前执行;其余hook的使用都是render之后执行。
除了以上说到的常见6个hook之外,官方提到的hook还有useReducer
、useImperativeHandle
、useLayoutEffect
、useDebugValue
。不怎么常用,就不一一说明了。
react官方hook api: https://reactjs.org/docs/hooks-reference.html
4.什么时候使用 useMemo 和 useCallback
这个不太好定性,因为项目不同、业务不同,什么时候使用一时半会也说不清。
先重点说下为什么React会引入useMemo、useCallback这两个hook的原因?
原因一:引用不相等
原因二:重复计算
原因二重复计算在上一节介绍useMemo的时候说过,在这就不多说什么了。 下面重点说下原因一引用相等的问题。
如果你是编程人员,你很快就会明白为什么会这样:
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false
const z = {}
z === z // true
注意:React实际上使用Object.is,但是它与===非常相似
当在React函数组件中定义一个对象时,尽管它跟上次定义的对象相同,引用是不一样的(即使它具有所有相同值和相同属性)
。
这会引起两个问题:
1.给组件添加行为事件和对象porps的时候
function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
}
function DualCounter() {
const [count1, setCount1] = useState(0)
const increment1 = () => setCount1(c => c + 1)
const [count2, setCount2] = useState(0)
const increment2 = () => setCount2(c => c + 1)
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
)
}
因为每次函数引用都是不一样的,肯定会引起其他组件的渲染。 点击第一个按钮,会引起第二个按钮的渲染。
解决方案:React.memo 和 useMemo/useCallback 配合使用
const CountButton = React.memo(function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
})
function DualCounter() {
const [count1, setCount1] = useState(0)
const increment1 = useMemo(() => {
return () => setCount1(c => c + 1)
}, [])
const [count2, setCount2] = useState(0)
const increment2 = useCallback(() => setCount2(c => c + 1), [])
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
)
}
2.使用useEffect时,[]传入的值为对象、数组、函数
function Blub() {
const fun = () => {}
const arr = [1, 2, 3]
return <Foo fun={fun} arr={arr} />
}
function Foo({arr, fun}) {
React.useEffect(() => {
fun(arr)
}, [arr, fun]) // 如果fun或arr更改,我们希望重新运行
return <div>foobar</div>
}
useEffect 将在每次渲染中对 arr、fun 进行引用相等性检查。 由于[]中传入的是数组、函数,尽管 arr、fun 里面的值不变,但每次渲染引用都是新的,所以还会执行useEffect的回调。
解决方案:useMemo/useCallback
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
return <Foo bar={bar} baz={baz} />
}
除了useEffect,同样的事情也适用于传递给 useLayoutEffect, useCallback, 和 useMemo 的依赖项。
最后总结一下什么时候useMemo、useCallback。
在解决重复计算
的场景上使用useMemo
;
在解决引用不相等
的场景上使用useCallback
。
5.自定义hook
通过自定义hook,可以将组件逻辑提取到可重用的函数中
。
一句话理解自定义hook:复用页面就组件化
、复用逻辑就自定义hook
。
下面来实现一个简单的自定义hook: useTitle:设置当前页面标题
创建一个useTitle.tsx:
import { useEffect } from 'react'
const useTitle = (title: string) => {
useEffect(() => {
document.title = title
}, [title])
return
}
export default useTitle
在Home.tsx和Login.tsx中分别引入并使用该hook:
import useTitle from 'hooks/useTitle'
// Home.tsx
useTitle('首页')
// Login.tsx
useTitle('登录页')
创建自定义 Hook 是不是特别简单呢。值得注意的是:
1.自定义 Hook 必须以 “use” 开头
2.我们可以在一个组件中多次调用自定义hook,因为它们是完全独立的
3.hook本质就是函数
本篇先介绍到这里。下一篇将继续讲解electron+hooks+ts项目,尽情期待。(本篇重点react的hooks,下篇重点ts技术栈)