深入理解React Hooks

前言:Hook 是 React 16.8的新增特性。它可以让你在不编写 class 的情况下使用state以及其他的 React 特性。即react hook只存在于函数组件中,在类组件中不受用.

1.首先为什么要使用Hook?

动机

Hook 解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。无论你正在学习 React,或每天使用,或者更愿尝试另一个和 React 有相似组件模型的框架,你都可能对这些问题似曾相识。

痛点问题:

1.在组件之间复用状态逻辑很难
a.之前的解决方案是:render props 和高阶组件
b.缺点是难理解、存在过多的嵌套形成“嵌套地狱”
2. 复杂组件变的难以理解
a.生命周期函数中充斥着各种状态逻辑和副作用
b. 这些副作用难以复用,且很零散
3.难以理解的Class
a.this指针问题
b.组件预编译技术(组件折叠)会在class中遇到优化失效的case
c.class不能很好的压缩
d.class在热重载时会出现不稳定的情况

2.它有什么优点?

1.代码复用变的更简单
2.不用记很多生命周期方法
3.更干净的代码
4.学习成本低
5.对齐React Class组件已经具备的能力

3.它的使用规则

Hook 本质就是JavaScript函数,但是在使用它时需要遵循两条规则。
1.只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们
2. 只在 React 函数中调用 Hook
不要在普通的 JavaScript函数中调用 Hook。你可以:
a.在 React 的函数组件中调用 Hook
b.在自定义 Hook 中调用其他 Hook

4.常用的Hook API

React目前提供的Hook
hook    用途
useState    设置和改变state,代替原来的state和setState
useEffect   代替原来的生命周期,componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版
useLayoutEffect 与 useEffect 作用相同,但它会同步调用 effect
useMemo 控制组件更新条件,可根据状态变化控制方法执行,优化传值
useCallback useMemo优化传值,usecallback优化传的方法,是否更新
useRef  跟以前的ref,一样,只是更简洁了
useContext  上下文爷孙及更深组件传值
useReducer  代替原来redux里的reducer,配合useContext一起使用
useDebugValue   在 React 开发者工具中显示自定义 hook 的标签,调试使用。
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
1.useState
初识:

const [num, setNum] = useState(0)

调用 useState方法做了什么?
定义一个“state变量”

useState需要什么参数?
其接收一个参数,作为变量初始化的值

useState方法的返回值是什么?
返回当前 state以及更新 state的函数。

使用useState

React 会确保 setState函数的标识是稳定的,并且不会在组件重新渲染时发生变化。
首先我们先通过 useState 方法定义三个变量其中有基本类型和引用类型的数据分别为:num、message、info,然后对它们的值进行修改。

const [num, setNum] = useState(0)

const [messge, setMessge] = useState(
  {name: 'zhangsan', age: 18, gender: '男'}
)
const [info,setInfo] = useState([
  { id: 1, name: 'zhangsan' },
  { id: 2, name: 'lisi' }
])

修改 num值为100

setNum(100)

修改message 对象的 age 属性,值为 22;并添加 weight 属性,值为 55kg

setMessage({
  ...message,//进行解构
  age: 22,//覆盖
  weight: "55kg"
})

修改info数组的第二项的 name属性,值为wangwu;

// 深拷贝
let temp = JSON.parse(JSON.stringify(info))
temp[1].name = 'wangwu';
setInfo(temp);
函数式更新

setState接收一个函数。
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <View>
      Count: {count}
      <Button onClick={() => setCount(initialCount)}>Reset</Button>
      <Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
      <Button onClick={() => setCount(prevCount => prevCount - 1)}>-</Button>
    </View>
  );
}

“+” 和 “-” 按钮采用函数式形式,因为被更新的state需要基于之前的state。但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值
注意如果你的更新函数返回值当前 state 完全相同,则随后的重渲染会被完全跳过

惰性初始 state​

initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
与class组件setState 有什么不同

useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

const [state, setState] = useState({});
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});
2.useEffect()
初识:

useEffect(()=>{},[])
调用 useEffect()方法做了什么?
使用 useEffect完成副作用操作。赋值给useEffect的函数会在组件渲染到屏幕之后执行。

它的作用?
useEffect()的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。

useEffect()需要什么参数?
useEffect()的第一个参数是一个函数,它就是所要完成的副效应。组件加载以后,React 就会执行这个函数。

useEffect()的第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项。只有该变量发生变化时,副效应函数才会执行。

如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入DOM执行一次,后面组件重新渲染,就不会再次执行。

useEffect()方法的返回值是什么?
useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

关于useEffect()的调用渲染
useEffect会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect
每一次渲染
重点:关于每一次渲染(rendering),组件都会拥有自己的:
Props and State
事件处理函数

使用useEffect()

我们希望组件加载以后,网页标题会随之改变。那么,改变网页标题这个操作,就是组件的副效应,必须通过useEffect()来实现。

import React, { useEffect } from 'react';
function Welcome(props) {
  useEffect(() => {
    document.title = '加载完成';
  });
  return <h1>Hello, {props.name}</h1>;
}

清除effect​
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除

effect 的执行时机

componentDidMountcomponentDidUpdate不同的是,传给useEffect的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
然而,并非所有effect都可以被延迟执行

虽然 useEffect会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect

设置正确的依赖

实际应用中,我们不需要在每次组件更新时,都去执行某些 effects,这个时候我们可以给 useEffect 设置依赖,告诉 React 什么时候去执行 useEffect。

看下面这个例子,只有在 name 发生改变时,才会执行这个 useEffect。如果将依赖设置为空数组,那么这个 useEffect 只会执行一次。

useEffect(() => {
  document.title = 'Hello, ' + name
}, [name])
useEffect() 的用途

1.获取数据
2.事件监听或订阅
3.改变 DOM
4.输出日志

useEffect小结

1.执行时机相当于componentDidMountcomponentDidUpdate,有return就相当于加了componentWillUnmount
2.主要用来解决代码中的副作用,提供了更优雅的写法
3.多个effect通过一个单向循环链表来存储,执行顺序是按照书写顺序依次执行
4.每一次组件渲染,都会完整地执行一遍清除、创建effect。如果有return一个清除函数的话。
5.清除函数会在创建函数之前执行

3.useContext()
初识:

如果需要在组件之间共享状态,可以使用useContext()。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

useContext的出现是为了解决什么问题

​ Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。如果我们在一个项目中只会出现一个context,那么consumer订阅provider的方式完全可以使用contextType来代替。但事实上,在一个项目势必会涉及到很多context,如果当前用户,国际化,主题颜色等。这么多的context,如果在某一个组件中同时使用了这三者,那么会造成很深的嵌套,不仅后期维护困难,而且开发体验也不会太好。
所以为了解决这个问题,就有useContext的出现。

useContext作用

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

使用

栗子:父后代通信 【Context 上下文】

  • 父组件中
    a. 初始化一个上下文组件

      const BaseContext = React.createContext({});
    

    b. 将上下文组件嵌套在根组件外部

        <BaseContext.Provider value={ 变量 }>
          <div>
          </div>
        </BaseContext.Provider>
  • 子组件中
    函数组件
      const context = useContext(BaseContext)
      context.xxx
    

详细设计思路。

   const { useState, useEffect, useReducer, useContext }=React
      const BaseContext = React.createContext();
      function App(){
        const [ state, setState]=useState([])
        useEffect(()=>{
            let arr=[
                { id: 1, name: 'zhangsan', weight: '52kg', height: '172cm' },
                { id: 2, name: 'lisi', weight: '53kg', height: '182cm' },
                { id: 3, name: 'wangwu', weight: '50kg', height: '192cm' }
            ]
            setState(arr)
        },[])
        return (
            <BaseContext.Provider value={{state}}>
             <AppChild1/>
             <AppChild2/>
            </BaseContext.Provider>
        )
      }
      function AppChild1(props){
        const value = useContext(BaseContext)
        return (
              <div>
                {
                  value.state.map((item,index)=>{
                     return (
                         <ul key={index}>
                            <li>{item.name}</li>
                            <li>{item.height}</li>
                         </ul>
                     )
                  })
                }
              </div>
        )
      }
      function AppChild2(props){
        const value = useContext(BaseContext)
        return (
              <div>
                {
                  value.state.map((item,index)=>{
                     return (
                         <ul key={index}>
                            <li>{item.name}</li>
                            <li>{item.weight}</li>
                         </ul>
                     )
                  })
                }
              </div>
        )
      }
      ReactDOM.render(<App/>,document.getElementById('app'))
    </script>
值得注意的是:

1.调用了useContext的组件总会在 context值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization来优化。
2.使用Context一定程度上会使组件的复用性降低,我们需要合理的取舍。

4.useCallback
初识

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回值:返回一个 memoized回调函数。
什么是 memoized 函数?
简单来说记住函数的计算结果,提高函数计算的性能
传入的参数
useCallback第一个参数是一个函数,返回一个 memoized回调函数useCallback的第二个参数是依赖(deps),当依赖改变时才更新 memoizedCallback,也就是在依赖未改变时(或空数组无依赖时), memoizedCallback总是指向同一个函数,也就是指向同一块内存区域。当把 memoizedCallbac当作 props 传递给子组件时,子组件就可以通过shouldComponentUpdate等手段避免不必要的更新。
注意:

useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

它与useMemo非常的相似
useCallback(fn, deps)相当于useMemo(() => fn, deps)
所以用useMemo就能实现useCallback

5.useMemo​
初识
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回值

返回一个 memoized值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算memoized值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入useMemo的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据

自己懒的想栗子,网上找了个栗子,不过它的有点问题,我自己改了一下。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.development.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
    <script src="https://cdn.bootcss.com/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/babel">
   const {useState, useMemo }=React
   function Example() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    function getNum() {
        return Number(count)+Number(val)
    }
 
    return <div>
        
        <h4>总和:{getNum()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <input value={val} onChange={event => {setValue(event.target.value)
            console.log(event.target.value)}}/>
        </div>
    </div>;
   }
   ReactDOM.render(<Example/>,document.getElementById('app'))
    </script>
</body>
</html>

在这里插入图片描述
上面这个组件,维护了两个state,可以看到getNum的计算仅仅跟count有关,但是现在无论是count还是val变化,都会导致getNum重新计算,所以这里我们希望val修改的时候,不需要再次计算.

这种情况下我们可以使用useMemo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.development.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
    <script src="https://cdn.bootcss.com/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/babel">
   const {useState, useMemo }=React
   function Example() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
     const getNum=useMemo(()=>{
        return Number(count)+Number(val)
 
    },[count]) 
    return <div>
        
        <h4>总和:{getNum}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <input value={val} onChange={event => {setValue(event.target.value)
            console.log(event.target.value)}}/>
        </div>
    </div>;
   }
   ReactDOM.render(<Example/>,document.getElementById('app'))
    </script>
</body>
</html>

在这里插入图片描述
在这里插入图片描述
使用useMemo后,并将count作为依赖值传递进去,此时仅当count变化时才会重新执行getNum

useMemo与 useCallback相同点:

仅仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。

不同区别:

1.useMemo计算结果是return返回值, 主要用于 缓存计算结果的值 ,应用场景如: 需要计算的状态
2.useCallback计算结果是函数, 主要用于缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
一个是返回的值,一个是计算函数。

6.useRef

建议直接参考:useRef的理解
const refContainer = useRef(initialValue);
什么是useRef?

  • 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数
  • 返回的 ref 对象在组件的整个生命周期内保持不变
  • 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方
  • 更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里
  • useRef 类似于类组件的 this

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

如果你将ref对象以 <div ref={myRef} />形式传入组件,则无论该节点如何改变,React都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。而且 useRef可以很方便地保存任何可变值,且它会在每次渲染时返回同一个 ref 对象
一个常见的用例便是命令式地访问子组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <View>
      <Input ref={inputEl} type="text" />
      <Button onClick={onButtonClick}>Focus the input</Button>
    </View>
  );
}
7.useLayoutEffect​

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

它们的主要区别在于 useLayoutEffect会阻塞渲染,而useEffect不会。

8.useReducer

const [state, dispatch] = useReducer(reducer, initialState);

初识:

reducer的概念是伴随着Redux的出现逐渐在JavaScript中流行起来。但我们并不需要学习Redux去了解Reducer。简单来说 reducer是一个函数(state, action) => newState:接收当前应用的state和触发的动作action,计算并返回最新的state

参数

接收两个参数:
第一个参数:reducer函数,没错就是我们上一篇文章介绍的。第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。

使用

以下是用 reducer 重写 useState 一节的计数器示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

注意React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。

最后

由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux。

5.Taro Hooks

微信小程序开发时。

Taro Hooks

首先Taro有自己的专享API(例如usePageScroll,useReachBottom)从 @tarojs/taro中引入,框架自己的 Hooks (例如 useEffect, useState)从对应的框架引入。

import { usePageScroll, useReachBottom } from '@tarojs/taro' // Taro 专有 Hooks
import { useState, useEffect } from 'react' // 框架 Hooks (基础 Hooks)
useRouter​

等同于Class ComponentgetCurrentInstance().router

const router = useRouter()
useReady

等同于页面的 onReady 生命周期钩子
从此生命周期开始可以使用 createCanvasContextcreateSelectorQuery等 API 访问小程序渲染层的 DOM 节点

useReady(() => {
  const query = wx.createSelectorQuery()
})
useDidShow​

页面显示/切入前台时触发。
等同于 componentDidShow页面生命周期钩子。

useDidShow(() => {
  console.log('componentDidShow')
})
useDidHide—页面隐藏/切入后台时触发。

等同于 componentDidHide页面生命周期钩子。

useDidHide(() => {
  console.log('componentDidHide')
})
usePullDownRefresh

监听用户下拉动作(用户下拉时触发)
等同于 onPullDownRefresh页面生命周期钩子。

useReachBottom​

监听用户上拉触底事件(用户上拉的时候触发)
等同于 onReachBottom页面生命周期钩子。

usePageScroll​

监听用户滑动页面事件(用户滚动页面的时候的进行触发)
等同于 onPageScroll页面生命周期钩子。

useResize

小程序屏幕旋转时触发
等同于 onResize页面生命周期钩子

useShareAppMessage

监听用户点击页面内转发按钮(Button 组件 openType='share')或右上角菜单“转发”按钮的行为,并自定义转发内容。等同于 onShareAppMessage页面生命周期钩子。

useTabItemTap

点击 tab时触发。
等同于 onTabItemTap页面生命周期钩子。

useAddToFavorites

监听用户点击右上角菜单“收藏”按钮的行为,并自定义收藏内容
等同于 onAddToFavorites页面生命周期钩子。

useShareTimeline

监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容
等同于 onShareTimeline页面生命周期钩子。

说明:其实这个文章是我个人梳理的思路,很多东西都直接参考别人的,只不过,我觉得它们说的太罗嗦了,讲的东西有些不太对劲,就自己改了一下,个人看法哈,如果有问题,还请指出哈。

参考推荐:
Hook官方中文文档
Hooks|Taro中文档
阮一峰React Hooks 入门教程
useRef的总结
useMemo和useCallback
无意识设计-复盘React Hook的创造过程(深度好文)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值