函数组件

大纲链接 §

[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. 测试:函数组件

  • 函数组件直接通过参数获取 props
  • 函数组件通过 React.useState
  • React v16.8 正式推出了 Hooks API
  • useEffect(()=>{}, []) 模拟 componentDidMount


参考文章

相关文章


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