React Hooks解析(看这一篇就够了)

React Hooks 是 React v16.8版本引入的全新API,这个 API 是 React 的未来,有必要深入理解。

类组件和函数组件

Hooks之前我们写组件方式,主要包括两种:类组件和函数组件。

一个React App 由多个类按照层级,一层层构成,复杂度成倍增长。再加入 Redux,就变得更复杂。

组件类有一下几个缺点:

大型组件很难拆分和重构,也很难测试。

业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

组件类引入了复杂的编程模式,比如 render props 和高阶组件。

React 团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。 组件的最佳写法应该是函数,而不是类。

但是,这种写法有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。

React Hooks 的设计目的,就是加强版函数组件,完全不使用”类”,就能写出一个全功能的组件。

Hook

Hook 这个单词的意思是”钩子”。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用 use 前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。

useState()

状态钩子 useState() 用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

这个钩子函数比较简单,看一下例子:

import React, { useState } from 'react';

export default function Home() {

  const [age, setAge] = useState(0);

  return (
    <div>
      <h2>当前年龄: {age}</h2>
      <button onClick={e => setAge(age + 1)}>age+1</button>
    </div>
  )
}

userState()的参数是状态的初始值。

setAge 时会触发整个组件的重新渲染。

需要注意的是, React Hooks不能出现在条件判断语句中

提倡的做法是每个状态都单独维护,而不是把所有的状态都维护到一个状态对象中。如果维护到状态对象中,这样再修改状态时就必须要小心翼翼,哪些修改哪些不修改都要注意。

// 假如state对象中包含了所以需要的状态
const [state, setState] = useState({});
// 那么修改的时候时候就比较麻烦了,这是不提倡的做法
setState({
	...state,
	num:num+1
})

useEffect

副作用函数 useEffect 用来代替常用的生命周期函数。

import React, { useState , useEffect } from 'react';
function Example(){
    const [ count , setCount ] = useState(0);
    
    useEffect(()=>{
        console.log(`useEffect=>You clicked ${count} times`)
    })

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={()=>{setCount(count+1)}}>click me</button>
        </div>
    )
}
export default Example;
  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect要求我们传入一个 回调函数 ,在React执行完更新DOM操作之后,就 会回调这个函数

  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

如果要要实现类似 componentDidMonut 的功能,第二个参数可以传一个空数组,代表不添加任何依赖。

// 这样只会在页面第一次渲染后调用一次,之后不会再调用。
useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
},[])

假如需要有两个状态,当其中一个状态改变时才更新组件,而另一个状态更新时,不更新组件,这个时候就需要第二个参数依赖了。

const [ count , setCount ] = useState(0);
const [ num , setNum ] = useState(0);

// 只有当count状态改变时,才会更新组件。num改变时,不会更新组件。
useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
},[count])

useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而 componentDidMonutcomponentDidUpdate 中的代码都是同步执行的。

如何实现类似 componentWillUnmount 的功能,在其中做一些解绑或清除的操作呢。

useEffect传入的 回调函数A本身 可以有一个返回值,这个返回值是 另外一个回调函数B

还是用上面的代码例子中的userEffect:

useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
    return ()=> {
    	console.log("effect清除机制");
    }
})

React 会在组件更新和卸载的时候执行清除操作。除了组件第一次渲染的时候不会调用清除操作,之后页面的每一次重新更新渲染都会触发这个操作。

我们可以使用多个userEffect来区分开不同的操作。

useEffect(() => {
  console.log("操作1");
});

useEffect(() => {
  console.log("操作2");
})

useEffect(() => {
  console.log("操作3");

  return () => {
    console.log("清除操作3");
  }
})

useEffect()的第二个参数,决定该userEffect的哪些state变化时,才重新执行渲染。当没有写第二个参数时候,默认支持所有的状态。

在实际验证中注意一下执行顺序的问题:

useEffect(()=>{
    console.log("操作");
    return ()=> {
    	console.log("effect清除机制");
    }
})
// 打印的结果:
effect清除机制
操作

在看一个有意思的例子:

const [count,setCount] = useState(0)
useEffect(()=>{
	setCount(count+1)
},[])

想一下,最后count是几呢,会累加吗?

答案是1,不会一直累加。

因为当页面组件第一次加载渲染后,useEffect()会调用,执行到 setCount(count+1) 会重新触发页面组价的渲染,组件渲染后本来又该执行useEffect()的,但是该useEffect()没有添加任何依赖,因count状态改变引起的组件更新,不会触发该useEffect(),所以不会执行。

useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

最常用的ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素;
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;

获取Dom元素

这个比较简单,直接看例子:

import React, { useRef } from 'react';

export default function RefHookDemo() {
  const inputRef = useRef();
  const titleRef = useRef();

  const handleOperating = () => {
    titleRef.current.innerHTML = "我是coderperson";
    inputRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={inputRef}/>
      <h2 ref={titleRef}>默认内容</h2>
      <button onClick={e => handleOperating()}>操作</button>
    </div>
  )
}

可以利用该特性,获取到DOM后,给DOM绑定事件。

保存普通变量

  • useRef可以想象成在ref对象中保存了一个.current的可变盒子;
  • useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;
import React, { useState, useEffect, useRef } from 'react';

let preValue = 0;

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>前一次的值: {countRef.current}</h2>
      <h2>这一次的值: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
}

useMemo & momo

useMemo实际的目的也是为了进行性能的优化。(在使用上和useEffect类似,也是两个参数,第二个参数控制依赖)。

如何进行性能的优化呢?

  • useMemo返回的也是一个 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

通常用在当父组件更新时,如果子组件没有变化,使用useMemo控制子组件不用更新。类似于 shouldCompnentUpdate .

import React, { useState, useMemo } from 'react';

// 计算操作
function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("计算一遍");
  return total
}

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  // 如果不使用useMemo 当点击切换按钮时也会执行计算操作。现在只有当count改变时才会执行操作。
  const total = useMemo(() => {
    return calcNum(count);
  }, [count]);

  return (
    <div>
      <h2>数字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切换</button>
    </div>
  )
}

另外说一下React的 memo 。React的memo是一个高阶函数,内部会自动判断props的preprops和nextprops是否相同,如果相同则不更新组件,不过不相同才更新组件。

const Child = memo(()=>{
	return (
		<div>
			444
		</div>
	)
})

如果想自己控制条件的判断,可以使用memo的第二个参数,该参数是一个函数的形式。

const Child = memo(()=>{
	return (
		<div>
			444
		</div>
	)
},(prev,next)=> {
  // 如果props中的count前后相等则不更新,否则更新。
	return prev.count === next.count 
})

useCallback

useCallback实际的目的是为了进行性能的优化。

如何进行性能的优化呢?

  • useCallback会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

对比和useMemo的区别:

useMemo缓存的是值
useCallback缓存的是函数
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

(使用方法和useMemo、useEffect一样,第一个参数是函数,第二个参数是依赖对象)。

同样也是常用来控制子组件是否更新的。

// 使用useCallback生成的increment1传递给Child子组件,当父组件更新时,子组件实现不更新。
const increment1 = useCallback(function increment() {
    setCount(count + 1);
  }, []);
  
 return (
 	<div>
 		<Child increment={increment1}/>
 	</div>
 )

useReducer

reducer源于redux的兴起和广泛使用,本身是一个函数。

看一个简单的reducer的例子:

function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        default: 
            return state;
    }
}

你只需要理解的就是这种形式和两个参数的作用,一个参数是状态,一个参数是如何控制状态。

useReducer 它也是React hooks提供的函数,来实现reducer的功能。

利用上面的的reducer例子,在useReducer中使用:

import React, { useReducer } from 'react'
import { countReducer } from '../reducer/counter'
export default function Home() {
  const [state, dispatch] = useReducer(countReducer, {counter: 100});
  return (
    <div>
      <h2>当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({type: "add"})}>+1</button>
      <button onClick={e => dispatch({type: "sub"})}>-1</button>
    </div>
  )
}

useReducer有两个参数,第一个参数是控制状态的reducer函数,第二个参数是初始值。返回值是一个数组,第一个元素是state状态,第二个元素是 dispatch ,用来分发type, 实现state状态的改变。

看起来是不是和 useState 有点像,其实 useState 就是基于 useReducer 实现的。

下面利用 useReducer 来简单模拟一下 useState 的实现:

useState = (initState)=> {
	const [state,dispatch] = useReducer((state,action)=>(state||initState),initState)
	return [state,dispatch]
}

useContext

Context 的作用就是对它所包含的组件树提供全局共享数据的一种技术。

useContext可以实现父子组件之间的传值。

下面直接看代码:

// 创建Context
export const UserContext = createContext();
export const ThemeContext = createContext();
export default function App() {
  return (
    <div>
      <UserContext.Provider value={{name: "why", age: 18}}>
        <ThemeContext.Provider value={{color: "red", fontSize: "20px"}}>
          <ContextHook/>
        </ThemeContext.Provider>
      </UserContext.Provider>
    </div>
  )
}

在函数组件 ContextHook 中使用 useContext :

export default function ContextHook() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  console.log(user);
  console.log(theme);
  return (
    <div>
      ContextHook
    </div>
  )
}

通常 useReduceruseContext 一起结合来使用 。

还是用 useReducer 例子中计数器的reducer,搭配使用上 useContext 实现父子组件之间值的同步传递。

const Ctx = createContext();
// APP组件:
const [count,dispatch] = useReducer(reducer,10)
return (
	<Ctx.Provider value={[count,dispatch]}>
		<div>
			<Parent />
		</div>
	</Ctx.Provider>
)

// Parent
const [count] = useContext(Ctx)
return (
	<div>
		count:{count}
		<Child />
	</div>
)

// Child
const [count,dispatch] = useContext(Ctx)
return (
	<div>
		count:{count}
		{/* 实现count的加减 */}
		<button onClick={()=>dispatch({type:'add'})}>+1</buttton>
		<button onClick={()=>dispatch({type:'sub'})}>-1</buttton>
	</div>
)

你会发现,在Child中实现count的加减,Parent中的count也跟着改变了,实现了数据的共享。

自定义Hooks

自定义Hook本质上是一种函数代码逻辑的封装。它可以做到之前类组件做不到或比较难实现的功能逻辑的抽取封装。

自定义Hook函数偏向于功能,而组件偏向于界面和业务逻辑。

自定义Hook,函数必须以 use 开头,类似 useXxx .

比如如果要实现实时监听屏幕的宽高的功能,如果在类组件中,需要在 componentDidMount 中添加监听,然后在其它函数中获取 width 和 height,最后在 componentWillUnMount 中移除监听。流程处理分散在各个角落,不便于封装处理。

而自定义Hook,可以很好的实现该功能的封装。看一下代码:

function useWinSize(){
    const [ size , setSize] = useState({
        width:document.documentElement.clientWidth,
        height:document.documentElement.clientHeight
    })

    const onResize = useCallback(()=>{
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        })
    },[]) 
    useEffect(()=>{
        window.addEventListener('resize',onResize)
        return ()=>{
            window.removeEventListener('resize',onResize)
        }
    },[])

    return size;
}

然后在其它组件中引入使用即可。

小结

React Hooks 是以后的发展趋势,学好这一块还是很有必要的。写这篇文章花了我将近一天的时间,之后在公司还要做一个Hooks方面的技术分享,也算是一个技术铺垫吧。

Hooks虽然实现了生命周期的所有功能,但是对我而言 componentDidMount componentxxx 这种生命周期的流程形式,还是比较容易理解,也更容易被人所接受。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章