Blog Logo

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

写于2020-08-07 04:05 阅读耗时18分钟 阅读量


本篇文章概要:

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

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的useEffectuseStateuseRef就能把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只会被渲染一次。

render


再用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)

render_ok

尽管没有点击执行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的任何渲染。

render_yes

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>
    )
}

render_input

点击组件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:

render_useRef

以上只是举例,按理说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还有useReduceruseImperativeHandleuseLayoutEffectuseDebugValue。不怎么常用,就不一一说明了。

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技术栈)

Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。