React Hooks

class 组件存在的问题

  • 复杂组件变得难以理解:
  • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
  • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在
    componentWillUnmount中移除);
  • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  • 难以理解的class: 很多人发现学习ES6的class是学习React的一个障碍。
  • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this; 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
  • 组件复用状态很难:
  • 在前面为了一些状态的复用我们需要通过高阶组件或render props;
  • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
  • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

为什么需要 Hooks

  • Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
  • Hooks 是很多 Hook 技术的组合,比如 useState 是一个 hook,useEffect 也是一种 hook;
  • 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
  • class组件可以定义自己的state,用来保存组件自己内部的状态;
  • 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
  • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
  • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
  • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
  • 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
  • Hook 的出现可以让我们在不编写class的情况下使用state以及其他的React特性;我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
  • 函数式组件结合 hooks 可以让整个代码更简洁,并且再也不用考虑 this 相关问题;

Hook 使用规则

  • Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)

useState

  • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一
    般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。这个参数既可以是确定的泛型值,也可以是返回一个该泛型值的箭头函数function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];(如果没有传递参数,那么初始化值为
    undefined)。
  • useState返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。
  • 通过源码我们可以看到,返回数组中的第二个元素是一个Dispatch<SetStateAction<S>>类型, 该类型定义为type SetStateAction<S> = S | ((prevState: S) => S);,也就当我们使用返回的第二个元素修改 改状态时,即可以传入一个泛型值,也可以传入一个箭头函数,当传入箭头函数时,会将上一次的状态值传递给我们。
  • 当我们箭头函数时,它就和我们使用 setState 传箭头函数修改状态一样

Effect Hook

  • Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
  • 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect 要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
  • useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:
    type EffectCallback = () => (void | (() => void | undefined)); 在这里还函数里我们可以做一些取消的操作。
  • React 会在组件更新和卸载的时候执行清除操作;
  • useEffect可以写多份,其调用顺序是按照先订阅先执行的规则
Effect 优化
  • useEffect的回调函数会在每次渲染时都重新执行, 但有些代码我们只希望执行一次即可,那么使用useEffect 如何完成呢?
  • useEffect实际上有两个参数:
  • 参数一:执行的回调函数
  • 参数二: 是个只读数组,数组里的元素就是一个个state,当其中的任一个 state 发生变化时就重新执行回调函数;当不传时,所有的状态发生改变都会执行该回调函数,若不想依赖任何状态,可以传一个空数组;

useContext

  • 使用createContext创建 context,并导出
  • 将需要共享数据的子组件包裹在创建的 context.Provider 中, 并设置要传递的数据
  • 在需要共享数据的子组件中使用useContext接收需要的 context进行使用
import React, { useState, createContext } from 'react';

export const UserContext = createContext();
export const ThemeContext = createContext();

export default function App() {
  return (
    <div>
      <UserContext.Provider value={{name: "why", age: 18}}>
        <ThemeContext.Provider value={{fontSize: "30px", color: "red"}}>
          <ContextHookDemo/>
        </ThemeContext.Provider>
      </UserContext.Provider> 
    </div>
  )
}

function ContextHookDemo(props) {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  console.log(user, theme);
  return (
    <div>
      <h2>ContextHookDemo</h2>
      <h3>{user.name}</h3>
      <h3>{theme.fontSize}</h3>
    </div>
  )
}

useReducer

  • useReducer 不是 redux 的替代品,它是 useState 的一种替代方案
  • 在某些场景下,如果 useState 的处理逻辑比较复杂,可以通过 useReducer 来对其进行拆分
  • 它接收三个参数:
  • 第一个是 reducer 函数
  • 第二个是 初始值
  • 第三个是一般是在我们对初始值进行一些操作的时候使用的,一般很少用
export default function Home() {
  const [state, dispatch] = useReducer(reducer, {counter: 0});

  return (
    <div>
      <h2>Home当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

function reducer(state, action) {
  switch(action.type) {
    case "increment":
      return {...state, counter: state.counter + 1};
    case "decrement":
      return {...state, counter: state.counter - 1};
    default:
      return state;
  }
}

useCallback

  • useCallback 实际的目的是为了进行性能的优化,它是如何做到的呢?
  • useCallback 会返回一个函数的 memoized(记忆的)值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 通常使用 useCallback 的目的是不希望子组件进行多次渲染,而不是为了函数进行缓存
import React, {useState, useCallback, memo} from 'react';

/**
 * useCallback使用场景: 在将一个组件中的函数, 传递给子元素进行回调使用时, 使用useCallback对函数进行处理.
 */
const HYButton = memo((props) => {
  console.log("HYButton重新渲染: " + props.title);
  return <button onClick={props.increment}>HYButton +1</button>
});

export default function CallbackHookDemo02() {
  console.log("CallbackHookDemo02重新渲染");

  const [count, setCount] = useState(0);
  const [show, setShow] = useState(true);

  const increment1 = () => {
    console.log("执行increment1函数");
    setCount(count + 1);
  }

  const increment2 = useCallback(() => {
    console.log("执行increment2函数");
    setCount(count + 1);
  }, [count]); // 这里useCallback依赖 count 状态

  return (
    <div>
      <h2>CallbackHookDemo01: {count}</h2>
      <HYButton title="btn1" increment={increment1}/>
      <HYButton title="btn2" increment={increment2}/>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}

上面代码,当我们点击切换时 CallbackHookDemo02 组件会重新渲染,increment1函数会被重新生成,所以每次传给 btn1 的 props 是不一样的,这就导致 btn1每次都会被重新渲染; 而对于 btn2来说,由于 useCallback 依赖的 count 状态没有发生变化,所以increment2 函数不会改变,所以每次传给 btn2 的 props 是不变的,props 变,对高阶组件 memo 来说不会重新渲染。

useMemo

  • useMemo 也是用来做性能优化的,它是如何做到的呢?
  • 同 useCallBack 一样,useMemo 返回的也是一个 memoized 值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 比如在我们进行大量计算的时候,是否有必要每次渲染都重新计算
import React, {useState, useMemo} from 'react';

function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}

export default function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);
  // 如果不使用useMemo,那么下面的这行代码在每次渲染时都会重新执行;
  // const total = calcNumber(count);
  // 下面我们使用了useMemo, 在依赖的 count 值不变的情况下,重新渲染并不会再去执行calcNumber函数;
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);

  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}
  • 还有就是组件传递相同内容的对象时,使用 useMemo 进行性能优化
import React, { useState, memo, useMemo } from 'react';

const HYInfo = memo((props) => {
  console.log("HYInfo重新渲染");
  return <h2>名字: {props.info.name} 年龄: {props.info.age}</h2>
});

export default function MemoHookDemo02() {
  console.log("MemoHookDemo02重新渲染");
  const [show, setShow] = useState(true);
  // 如果不使用 useMemo 下面的代码,在每次渲染时都会执行,这样每次生成的 info 就不一样,导致传给HYInfo的 props 不一样,从而导致HYInfo组件的重新渲染
  // const info = { name: "why", age: 18 };
  // 当使用useMemo时,由于每次依赖的都是一个空数组,没有发生变化, 所以info也没有变化,所以在MemoHookDemo02重新渲染时,由于传递给HYInfo的 props 没有发生变化,所以HYInfo组件不会重新渲染
  const info = useMemo(() => {
    return { name: "why", age: 18 };
  }, []);

  return (
    <div>
      <HYInfo info={info} />
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}
useCallback 和 useMemo 的区别
  • useCallback是对传入的函数做优化的,而useMemo是对返回值做优化的
  • 可以当useMemo传入的函数返回值是一个函数时,可以实现useCallback, 比如下面increment2和increment3是等效的
const increment2 = useCallback(() => {
    console.log("执行increment2函数");
    setCount(count + 1);
}, [count]);

const increment3 = useMemo(() => {
    return ()=>{
        console.log("执行increment2函数");
        setCount(count + 1);
    }
 }, [count]);

useRef

  • useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变,const titleRef = useRef();
  • 将创建的 ref 赋值给组件的 ref 属性,<h2 ref={titleRef}>RefHookDemo</h2>
  • 通过 ref 修改 DOM, titleRef.current.innerHTML = "Hello World";
  • 可以利用 useRef 保存上次的值

useImperativeHandle

  • 函数式组件中我们如果想使用 ref,需要结合 forwardRef 将 ref 转发到子组件中,然后子组件拿到父组件中创建的 ref,绑定到自己的某个元素中使用;
const HYInput = forwardRef((props, ref) => {
  return <input ref={ref} type="text"/>
})

export default function ForwardRefDemo() {
  const inputRef = useRef();
  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
    </div>
  )
}
  • forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件,这样会存在以下问题:
  • 直接暴露给父组件带来的问题是某些情况下的不可控
  • 父组件可以拿到 DOM 后进行任意的操作
  • 但是,事实上有时候只希望父组件做特定的操作,而不是可以任意操作
  • 解决上面问题,我们就可以通过useImperativeHandle对父组件只暴露固定的操作
  • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
  • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
  • 比如我调用了 focus函数,甚至可以调用 printHello函数;
const HYInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }), [inputRef])

  return <input ref={inputRef} type="text"/>
})

export default function UseImperativeHandleHookDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
    </div>
  )
}

useLayoutEffect

  • useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
  • 如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。


自定义 Hook

  • 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
  • 自定义 Hook 本质上只是一种函数代码逻辑的抽取,并不算 React 的特性
  • 比如我们想所有的组件在创建和销毁的时候都做一些处理,那么我们就可以使用自定义 hook 了, 如果定义的是普通函数(非 use 开头)的话,函数内是不能使用 hook 技术的
const Home = (props) => {
 // 使用自定义 hook 函数
  useLoggingLife("Home");
  return <h2>Home</h2>
}
 // 使用自定义 hook 函数
const Profile = (props) => {
  useLoggingLife("Profile");
  return <h2>Profile</h2>
}

export default function CustomHookDemo() {
  useLoggingLife("CustomLifeHookDemo01");
  return (
    <div>
      <h2>CustomHookDemo</h2>
      <Home/>
      <Profile/>
    </div>
  )
}
// 自定义 hook, 在函数式组件创建和销毁的时候都做处理
function useLoggingLife(name) {
  useEffect(() => {
    console.log(`${name}组件被创建出来了`);

    return () => {
      console.log(`${name}组件被销毁掉了`);
    }
  }, []);
}
  • 可以将获取屏幕滚动位置抽取到一个 hook 函数中
export default function CustomScrollPositionHook() {
  const position = useScrollPosition();

  return (
    <div style={{padding: "1000px 0"}}>
      <h2 style={{position: "fixed", left: 0, top: 0}}>CustomScrollPositionHook: {position}</h2>
    </div>
  )
}

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll)
    }
  }, []);

  return scrollPosition;
}
  • 可以将与界面有关的内容抽取到一个 hook 函数中读取
export default function CustomDataStoreHook() {
  const [name, setName] = useLocalStorage("name");

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={e => setName("kobe")}>设置name</button>
    </div>
  )
}

function useLocalStorage(key) {
  const [name, setName] = useState(() => {
    const name = JSON.parse(window.localStorage.getItem(key));
    return name;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name));
  }, [name]);

  return [name, setName];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容