函数组件

大纲链接 §

[toc]


函数组件比 class 组件简单

1. 函数组件创建方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Arrow Function Component
const Hello = (props) => {
  return (
    <div>
      {props.message}
    </div>
  )
}

// 简写
const Hello = props => <div>{props.message}</div>

// Function Component
function Hello(props) {
  return  (
    <div>
      {props.message}
    </div>
  )
}

函数组件完全替代 Class 组件需要解决的两个问题

  • 函数组件没有 state 怎么办
  • 函数组件没有 生命周期 怎么办

函数组件没有 state 怎么办

  • React v16.8+ 推出 Hooks API
    • 其中的一个 API 叫做 useState 可以解决 state 的问题
  • 示例,实现 +1 的功能

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    import {useState} from 'react'
    cosnt App = props => {
    const [n, setN] = useState(0)
    const onClick = () => { setN(n + 1) }
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    
    export default App
  • 消除了this,大大降低了心智负担,简化代码量


函数组件没有 生命周期 怎么办

  • React v16.8+ 推出 Hooks API
    • 其中的一个 API 叫做 useEffect 可以解决 生命周期 的问题
    • useEffect 用来解决副作用
  • 模拟compnentDidMount
    • useEffect( () => {console.log('第一次渲染')}, [] )
  • 模拟compnentDidUpdate
    • useEffect( () => {console.log('任意属性变更')})
    • useEffect( () => {console.log('属性n变了')}, [n, ...] )
  • 模拟compnentWillUnmount
    • useEffect( console.log('第一次渲染后'); return () => {console.log('组件将要卸载')} )

模拟其他生命周期

  • 模拟constructor:函数组件执行的时候,就相当于constructor
  • 模拟shouldComponentUpdate:使用React.memouseMemo可以解决
  • 模拟render:函数组件的返回值就是 render 的返回值

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    cosnt App = props => {
    console.log('这里相当于执行了 constructor')
    const X = () => {
    const n = 1
    return <div>xxx</div>
    }
    const Y = () => {
    const n = 2
    return <div>yyy</div>
    }
    
    return (
    {/* 这里可以写一些逻辑 */}
    <>
      <X /> <Y />
      这里相当于执行了 render
    </>
    )
    }
    
    export default App

2. 详解函数组件模拟生命周期

模拟componentDidMount

  • 不能直接在函数内访问nconsole.log(n),否则每次渲染都会调用,不能做到只有第一次调用
  • 使用 useEffect(() => {}, [])

    • 默认只有第一个参数的情况下,效果和不使用hooks写在外部一样,每次渲染都执行
    • 传第二个参数为 [] 空数组,则可以实现只在第一次时调用

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      import {useState, useEffect} from 'react'
      cosnt App = props => {
      const [n, setN] = useState(0)
      const onClick = () => { setN(n + 1) }
      // console.log(n) // 每次渲染都会调用,不能做到只有第一次调用
      
      useEffect(() => {
      console.log('调用 useEffect')
      }, [])
      
      return (
      <div>
      {n}
      <button onClick={onClick}> +1 </button>
      </div>
      )
      }
      
      export default App

部分模拟componentDidUpdate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import {useState, useEffect} from 'react'
cosnt App = props => {
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  const onClickN = () => { setN(n + 1) }
  const onClickM = () => { setM(m + 1) }
  useEffect(() => {
    console.log('n, m 更新了') // 第一次也会执行
  }, [n, m])

  return (
    <div>
      {n}
      {m}
      <button onClick={onClickN}> n+1 </button>
      <button onClick={onClickM}> m+1 </button>
    </div>
  )
}

export default App
  • 使用 useEffect(() => {}, [n, m, ...])
    • 第一次也会执行,而 componentDidUpdate 第一次不会执行
    • 将需要追踪变化的属性写在第二个参数的数组中
    • 不写第二个参数,则追踪所有属性变化 useEffect( () => {} )

模拟componentWillUnmount

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {useState, useEffect} from 'react'
cosnt App = props => {
  const [childVisible, setChildVisible] = useState(true)
  const hide = () => { setChildVisible(false) }
  const show = () => { setChildVisible(true) }
  return (
    <div>
      {childVisible
        ? <button onClick={hide}>hide</button>
        : <button onClick={show}>show</button>
      }
      {childVisible
        ? <Child />
        : null
      }
    </div>
  )
}

const Child = props => {
  useEffect( () => {
    console.log('渲染或变化了')
    return () => {
      console.log('Child销毁了')
    }
  })
  return (<div>Child</div>)
}

export default App
  • 使用 useEffect(() => () => {} )
    • 在回调中返回一个箭头函数,会在组件销毁时调用

函数组件模拟生命周期钩子函数小结

  • 能用函数组件就用函数组件,更简单
  • React 通过使用 useEffect 不同的参数与返回值组合,来模拟实现类组件中的三个生命周期

3.自定义 HooksuseUpdate

引入一个计数状态

useEffect 会在数据第一次生成时就调用,如何在数据第二次发生变化才调用?

  • 通过useState引入一个计数状态 [nUpdateCount, setNUpdateCount] = useState(0)
  • 当组件挂载后,计数状态 +1
    • 通过useEffect模拟componentDidMount已挂载
  • nUpdateCount > 1 说明已挂载
  • 判断 nUpdateCount > 1 时,才执行后续逻辑

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    import {useState, useEffect} from 'react'
    
    const App = () => {
    const [n, setN] = useState(0)
    const onClick = () => {
    setN(n + 1)
    }
    // 引入一个计数状态
    const [nUpdateCount, setNUpdateCount] = useState(0)
    // 挂载后 计数
    useEffect(() => {
    // setNUpdateCount(nUpdateCount + 1)
    setNUpdateCount(x => x + 1)
    }, [n])
    
    // 计数状态大于 1 时,才显示 n 变化了
    useEffect(() => {
    nUpdateCount > 1 && console.log('n变了')
    }, [nUpdateCount])
    
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    // ...
  • 目前就实现当n第一次变化时,才执行 console.log('n变了')


通过将以上逻辑封装为自定义钩子函数组合来实现

封装自定义钩子函数

  • 这个自定义函数 必须是以use开头,例如:useX
    • 需要接受初始状态:const useX = (n) => {/*...*/}
    • 返回状态(可选):return {/*...*/}
  • 用解构赋值来接受变量(可选):const {nUpdateCount} = useX(n)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    import {useState, useEffect} from 'react'
    
    const App = () => {
    const [n, setN] = useState(0)
    const onClick = () => {
    setN(n + 1)
    }
    
    // 封装为钩子函数
    const useX = (n) => {
    const [nUpdateCount, setNUpdateCount] = useState(0)
    
    // 挂载后 计数状态 +1 说明已挂载
    useEffect(() => {
      // setNUpdateCount(nUpdateCount + 1)
      setNUpdateCount(x => x + 1)
    }, [n])
    
    return {
      nUpdateCount
    }
    }
    
    // 调用 useX() 执行之前相同的逻辑,获取 nUpdateCount
    const {nUpdateCount} = useX(n)
    
    // 计数状态 nUpdateCount 大于 1 时,才显示 n 变化了
    useEffect(() => {
    nUpdateCount > 1 && console.log('n变了')
    }, [nUpdateCount])
    
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    // ...

将后续的逻辑放到useX的第二个参数fn,为一个回调函数

  • 在封装的钩子函数中
    • 第一个参数接受 状态;第二个参数接受 回调函数
    • 判断计数状态是否大于1,大于1,说明已挂载
  • 在回调中执行具体的逻辑(即将具体执行的逻辑放在回调中,通过参数传入即可)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    import {useState, useEffect} from 'react'
    
    const App = () => {
    const [n, setN] = useState(0)
    const onClick = () => {
    setN(n + 1)
    }
    
    // 第一个参数接受 状态;第二个参数接受 回调函数
    const useX = (n, fn) => {
    // 计数状态
    const [nUpdateCount, setNUpdateCount] = useState(0)
    // 挂载后 计数状态 +1 说明已挂载
    useEffect(() => {
      // setNUpdateCount(nUpdateCount + 1)
      setNUpdateCount(x => x + 1)
    }, [n])
    
    // 判断计数状态是否大于1
    // 计数状态 nUpdateCount 大于 1 时,才显示 n 变化了
    useEffect(() => {
      // nUpdateCount > 1 && console.log('n变了')
      nUpdateCount > 1 && fn() // fn() 为传入的第二个参数,执行具体的逻辑
    }, [nUpdateCount])
    
    // 当无需暴露计数状态时,可以不写返回值
    return {
      nUpdateCount
    }
    }
    
    // 调用 useX() 执行之前相同的逻辑,获取 nUpdateCount
    // 并且将逻辑写在第二个参数 回调函数中
    // 当无需暴露计数状态时,可以不解构出返回值 直接 useX(n, () => {/*...*/})
    const {nUpdateCount} = useX(n, () => {
    console.log('n变了') // 将具体执行的逻辑放在回调中,通过参数传入即可
    })
    
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    // ...

模仿useEffect,交换参数位置 useX(fn, n)

  • 也可以将第二个参数改为数组的形式 useX(fn, stateArr)
  • 计数状态随便取名
    • const [nUpdateCount, setNUpdateCount] = useState(0)
    • 改为const [count, setCount] = useState(0)
  • useX改为useUpdate

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    import {useState, useEffect} from 'react'
    
    const App = () => {
    const [n, setN] = useState(0)
    const onClick = () => {
    setN(n + 1)
    }
    
    // const useX = (fn, n) => {
    // const useX = (fn, stateArr) => { // 交换参数位置
    // 更名为 useUpdate
    const useUpdate = (fn, stateArr) => {
    // 计数状态
    // const [nUpdateCount, setNUpdateCount] = useState(0)
    const [count, setCount] = useState(0)
    // 挂载后 计数状态 +1 说明已挂载
    useEffect(() => {
      // setNUpdateCount(x => x + 1)
      setCount(x => x + 1)
    }, stateArr)
    
    useEffect(() => {
      // nUpdateCount > 1 && fn()
      count > 1 && fn()
    }, [count])
    }
    
    // useX(() => { // 更名为 useUpdate
    useUpdate(() => {
    console.log('变了') // 将具体执行的逻辑放在回调中,通过参数传入即可
    }, [n])
    
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    // ...

useUpdate 放在外部

  • 第二参数为状态依赖,通过扩展运算符解构为数组,解决警告(可选)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    import {useState, useEffect} from 'react'
    
    const useUpdate = (fn, stateArr) => {
    // 计数状态
    const [count, setCount] = useState(0)
    // 挂载后 计数状态 +1 说明已挂载
    useEffect(() => {
    // setNUpdateCount(x => x + 1)
    setCount(x => x + 1)
    }, [stateArr])
    // }, [...stateArr])
    
    useEffect(() => {
    // nUpdateCount > 1 && fn()
    count > 1 && fn()
    }, [count])
    }
    
    const App = () => {
    const [n, setN] = useState(0)
    const onClick = () => {
    setN(n + 1)
    }
    
    useUpdate(() => {
    console.log('变了') // 将具体执行的逻辑放在回调中,通过参数传入即可
    }, n)
    
    return (
    <div>
      {n}
      <button onClick={onClick}> +1 </button>
    </div>
    )
    }
    // ...

封装为导出一个单独的文件,导入使用

useUpdate.js

1
2
3
export const useUpdate = (fn, stateArr) => {
  /*...*/
}
  • 其他复用逻辑的地方导入使用 import {useUpdate} from './useUpdate'

useUpdate小结

  • 第一次由 undefined 变为 0 时不执行函数
    • 第二次变化时,执行函数
    • 第n次变化时,执行函数
  • useUpdate接受一个 回调 和 一个 依赖
  • 将传入的依赖放入内部的 useEffect
  • useEffect模拟挂载时,依赖改变,将内部计数状态+1
  • 再次使用useEffect模拟第一次发生改变也执行逻辑
    • 此时使用 计数状态 作为 依赖
    • 判断内部计数状态大于1,说明已挂载,立即执行传入的回调

4. 测试:函数组件


·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名