STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

electron+hooks+ts实现互动直播大班课(四)

本篇文章概要:

  • 类组件 和 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_lifecycle

先从react常用的五个生命周期说起:
1.constructor:类组件初始化state时

1
2
3
4
5
6
7
constructor(props) {
super(props)
this.state = {
uploading: false,
imgPath: '',
}
}

替换成hooks写法,使用useState

1
2
cosnt [uploading, setUploading] = useState(false)
cosnt [imgPath, setImgPath] = useState('')

2.render:类组件渲染时

1
2
3
4
5
class ImgUplod extends PureComponent {
render() {
return (<Upload/>)
}
}

替换成hooks写法,就是函数组件本身,直接使用return

1
2
3
function ImgUplod () {
return (<Upload/>)
}

3.componentDidMount:类组件渲染完毕,即DOM加载完成时
有点像DOM的onLoad方法,在挂载的时候执行,且只会执行一次,常用来做一些请求数据、事件监听等。

1
2
3
4
5
6
componentDidMount() {
dispatch({
type: `login/${type}`
})
window.addEventListener("resize", onResize, false)
}

替换成hooks写法,使用useEffect

1
2
3
4
5
6
useEffect(() => {
dispatch({
type: `login/${type}`
})
window.addEventListener("resize", onResize, false)
}, [])

4.componentDidUpdate:类组件重新渲染时
注意首次渲染是不会执行此方法的,可以获取到该组件上次的props或state。
也是react具有时间旅行这一特点的原因。

1
2
3
4
5
6
7
8
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

1
2
3
4
5
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配合,来实现时间旅行:

1
2
3
4
const preProps = useRef<number>()
useEffect(() => {
preProps.current = current
}, [current])

5.componentWillUnmount:类组件销毁时
当路由跳转后,组件销毁时触发。
可以操作,如:移除事件监听、移除 localStorage 持久化数据等。

1
2
3
4
componentWillUnmount() {
window.removeEventListener("resize", onResize, false)
window.localStorage.removeItem('userid')
}

替换成hooks写法,使用useEffect

1
2
3
4
5
6
7
useEffect(() => {
...
return () => {
window.removeEventListener("resize", onResize, false)
window.localStorage.removeItem('userid')
}
}, [])

通过hooks的useEffectuseStateuseRef就能把react常用的生命周期都实现一遍,是不是很神奇?不常用的生命周期,如:getDerivedStateFromProps、shouldComponentUpdate、getSnapshotBeforeUpdate,在这里就不做考虑了。


3.常用的hooks

3.1 useState

函数组件useState:等同于类组件this.setState。
举例:
this.setState方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

调用一次副作用:

1
2
3
4
5
6
7
// 相当于,componentDidMount 和 componentWillUnmount
useEffect(() => {
window.addEventListener("resize", onResize, false)
return () => {
window.removeEventListener("resize", onResize, false)
}
}, [])

调用多次副作用:

1
2
3
4
// 相当于,componentDidMount 和 componentDidUpdate
useEffect(() => {
document.title = xxx
}, [xxx])

多说一下useEffect,毕竟它太重要了。
可以发现[]是useEffect的精髓所在,正确的使用好useEffect,减少不必要的逻辑错误。
useEffect是在render之后调用的,即组件DOM渲染完成之后调用。
每个useEffect只处理一种副作用。这种模式,就是关注点分离。不同的副作用,分开放!


3.3 useContext

useContext是为了解决props层层传递的问题,可以实现多层级的数据共享。
用法:
1.通过createConntext创建Context对象

1
2
3
import { createContext } from 'react';
const Context = createContext(0)
export default Context

2.父组件:用Context.Provider包裹子组件,其包含的所有子组件共享该数据

1
2
3
4
5
6
7
import Context from 'hooks/useContext'

<Context.Provider value={current}>
<Child1 />
<Child2 />
...
</Context.Provider>

3.子组件:通过useContext获取父组件的值

1
2
3
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

1
2
3
4
5
6
7
8
9
function Parent() {
return (
<div>
<p>{current} Page</p>
<div>Child: <Child1 /> </div>
<div>Child: <Child2 /></div>
</div>
)
}

当父组件的 current 发生变化时,子组件Child2会不停被渲染,尽管它没做任何的变化;而加了memo的子组件Child1只会被渲染一次。

render


再用useMemo举例:
子组件Child1:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
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解决的是传入组件的函数属性导致其他组件渲染问题

说的可能有点绕,下面来举例说明:
父组件:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
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)

render_ok

尽管没有点击执行onClick事件,但是还是会让子组件Child2渲染,因为Child2的onClick函数属性,每次都会创造成新的函数。

我们改下onClick事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 改造前
const onClick = () => {
console.log('Click-----')
}

// 用useMemo改造后
const onClick = useMemo(() => {
return () => {
console.log('Click-----')
}
}, [])

// 用useCallback改造后
const onClick = useCallback(() => {
console.log('Click-----')
}, [])

改造后,不会执行Child2的任何渲染。

render_yes

useMemo 和 useCallback 都是作性能优化之用,与业务逻辑无关


3.6 useRef

useRef 不仅仅是用来管理 DOM ref的,它还相当于this, 可以存放任何变量。
不需要引起组件重新渲染的变量,都可以放在ref里。

举例:管理DOM ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>
)
}

render_input

点击组件Child2,实现input聚焦。


再举例:存放任何变量
使用useRef存放渲染前一个的变量:

1
2
3
4
5
6
7
const preProps = useRef<number>()

useEffect(() => {
console.log('pre', preProps.current)
console.log('cur', current)
preProps.current = current
}, [current])

使用useRef存放一个变量,阻止多次点击导致重复请求接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

render_useRef

以上只是举例,按理说useRef还有更多的玩法。


还有一种阻止多次点击导致重复请求接口的方案,通过dva-loading 控制 disabled 属性,从而控制点击事件。

1
2
3
(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还有useReduceruseImperativeHandleuseLayoutEffectuseDebugValue。不怎么常用,就不一一说明了。

react官方hook api:
https://reactjs.org/docs/hooks-reference.html


4.什么时候使用 useMemo 和 useCallback

这个不太好定性,因为项目不同、业务不同,什么时候使用一时半会也说不清。
先重点说下为什么React会引入useMemo、useCallback这两个hook的原因?
原因一:引用不相等
原因二:重复计算

原因二重复计算在上一节介绍useMemo的时候说过,在这就不多说什么了。
下面重点说下原因一引用相等的问题。

如果你是编程人员,你很快就会明白为什么会这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
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的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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时,[]传入的值为对象、数组、函数

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
import { useEffect } from 'react'

const useTitle = (title: string) => {
useEffect(() => {
document.title = title
}, [title])
return
}

export default useTitle

在Home.tsx和Login.tsx中分别引入并使用该hook:

1
2
3
4
5
6
7
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技术栈)