react Hooks释义

组件类的几个缺点。

  • 大型组件很难拆分、重构、测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑、关联逻辑。
  • 组件类引入了复杂的编程模式,比如 render props 和高阶组件。

hook的好处

  • 在函数组件里“钩入” state 及生命周期等特性的函数
  • 不需要很深的组件树嵌套
  • 避免了 class 创建类实例和在构造函数中绑定事件处理器的成本。
  • 在你不编写 class 的情况下使用 state 以及其他的 React 特性
  • 使你在无需修改组件结构的情况下复用状态逻辑
  • 将组件中相互关联的部分拆分成更小的函数

使用规则

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook

hook解决性能问题的方法

  • 传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。
  1. useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作:
// 除非 `a` 或 `b` 改变,否则函数引用地址不会变
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  1. useMemo Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要
    useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
  2. 最后,useReducer Hook 减少了对深层传递回调的依赖。

useState():状态钩子

  • 声明一个可以在重渲染时保持状态的变量
  • useState 唯一的参数就是初始 state,它会返回一对值:当前状态和一个让你更新它的函数。
  • 那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。如果我们想要有条件地执行一个 effect,可以将判断放到 effect 的内部
import React, { useState } from "react";
export default function  Button()  {
  const  [buttonText, setButtonText] =  useState("Click me,   please");
  function handleClick()  {
    return setButtonText("Thanks, been clicked!");
  }
  return  <button  onClick={handleClick}>{buttonText}</button>;
}
  • setState 的函数式更新形式结合展开运算符来达到合并更新对象的效果
const [state, setState] = useState({});
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});
  • setState 的函数式更新形式也允许我们指定 state 该 如何 改变而不用引用 当前 state
function Counter() {
  // 下面的setCount不引用此处的count
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量

  return <h1>{count}</h1>;
}
  • useState的函数形式可以惰性创建昂贵的初始state
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

function Table(props) {
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

useEffect():副作用钩子

  • 渲染结束之后开始执行
  • 用来连接外部系统(不受react控制的),比如定时器、连接服务器、事件订阅等
  • 使其状态与 React 组件的当前状态相匹配,比如用户设置放大缩小,通过effect同步到Map组件,Map.setZoom(level)
  • 将不相关逻辑分离到不同的 effect 中,这样删除一个不会影响另一个
  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
  • 它会在调用一个新的 effect 之前对前一个 effect 进行清理(比如切换连接聊天室、切换连接摄像头,数据请求(消除竞争条件的影响))
  • 只有依赖项变化effect才会重新运行,如果传空数组那就只会调用一次
  • 它的依赖包括 props 和直接在组件内声明的所有变量和函数。把函数或者对象移动到你的 effect 内部,减少对其依赖(每次渲染的对象都是不同的,容易导致effect频繁运行)。
  • 我的 Effect 做了一些视觉相关的事情,在它运行之前我看到了一个闪烁,如果 Effect 一定要阻止浏览器绘制屏幕,使用 useLayoutEffect 替换 useEffect。请注意,绝大多数的 Effect 都不需要这样。只有当在浏览器绘制之前运行 Effect 非常重要的时候才需要如此:例如,在用户看到 tooltip 之前测量并定位它。
  • useLayoutEffect 可能会影响性能。尽可能使用useEffect
const Person = ({ personId }) => {
  const [loading, setLoading] = useState(true);
  const [person, setPerson] = useState({});

  useEffect(() => {
    setLoading(true); 
    fetch(`https://swapi.co/api/people/${personId}/`)
      .then(response => response.json())
      .then(data => {
        setPerson(data);
        setLoading(false);
      });
  }, [personId])

  if (loading === true) {
    return <p>Loading ...</p>
  }

  return (
    <div>
      <p>You're viewing: {person.name}</p>
      <p>Height: {person.height}</p>
      <p>Mass: {person.mass}</p>
    </div>
  )
}

useLayoutEffect

  • 在react更新dom之后,浏览器绘制屏幕之前触发。
  • 在浏览器重新绘制屏幕之前进行布局测量,比如toolTip的展示位置
function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0); // 你还不知道真正的高度

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // 现在重新渲染,你知道了真实的高度
  }, []);

  // ... 在下方的渲染逻辑中使用 tooltipHeight ...
}

useCallback

  • 缓存一个函数
  • 在组件顶层调用 useCallback 以便在多次渲染中缓存函数,该回调函数仅在某个依赖项改变时才会更新
  • 默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件
  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
  • useCallback 只应作用于性能优化。
  • 请注意,useCallback 不会阻止创建函数。你总是在创建一个函数,但是如果没有任何东西改变,React 会忽略它并返回缓存的函数。
  • memo包裹的组件在props没有变化时会跳过重新渲染
  • 使用场景-优化子组件渲染速度
    • 父组件传递给子组件一个函数,子组件使用了React.memo()包裹,如果父组件的其他state变化引发重新渲染,导致每次传递给子组件的函数都不一样(每次渲染生成新的函数)导致子组件也重新渲染,此时给父组件中的该函数使用useCallback包裹,使得函数地址没有变化(缓存起来的地址一样),子组件将跳过重新渲染。
// 没有缓存时,每次渲染重新生成函数
const submit = (a,b) => {
  // doSomething....
}
// 使用useCallback缓存后,如果依赖没有变化,本次渲染的函数与上次渲染的函数地址一样(是同一个函数)
const submit = useCallback(
  (a,b) => {
    // doSomething....
  },
  [a, b],
);
// useCallback与useMemo的关系,在 React 内部的简化实现
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
  • 场景-effect的依赖中存在函数时,将该函数使用useCallback包裹
  • 场景-自定义hook
    • 如果你正在编写一个 自定义 Hook,建议将它返回的任何函数包裹在 useCallback 中,这确保了 Hook 的使用者在需要时能够优化自己的代码。

useMemo

  • 它在每次重新渲染的时候能够缓存计算的结果
  • 把高开销的函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新执行函数。这种优化有助于避免在每次渲染时都进行高开销的计算。
// 将函数computeExpensiveValue执行结果缓存,而useCallback缓存的是函数
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useMemo 也允许你跳过一次子节点的昂贵的重新渲染
  • 手动将 JSX 节点包裹到 useMemo 中并不方便,比如你不能在条件语句中这样做。这就是为什么通常会选择使用 memo 包装组件而不是使用 useMemo 包装 JSX 节点
    const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos])
  • 不允许在循环中使用useMemo,可以抽出一个新组件,在新组件的顶层使用useMemo,或者你把新组件使用memo包裹,2种办法都可以使新组件跳过重新渲染
function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

useDeferredValue

  • 延迟更新 UI 的某些部分
  • useDeferredValue 更适合优化渲染
  • useDeferredValue 执行的延迟重新渲染默认是可中断的(输入改变后会中断这次渲染开始最新值的渲染),比如输入关键字后查询展示一个长list组件,输入时会卡顿(长列表渲染导致),此时可以延迟渲染list,可以立即更新输入框,list组件需要使用memo包裹,否则将会重新渲染
  • 与防抖或节流不同,该方法可以自适应渲染速度,不必规定间隔时间(高性能电脑自动渲染快),并且可以避免让输入由于渲染列表而变得卡顿
function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
  • 本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
  • 请记住,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
  • 改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息
  • 在 渲染期间 读取或写入 ref 会破坏这些预期行为,你可以在 事件处理程序或者 effects 中读取和写入 ref
function MyComponent() {
  // ...
  // 🚩 不要在渲染期间写入 ref
  myRef.current = 123;
  // ...
  // 🚩 不要在渲染期间读取 ref
  return <h1>{myOtherRef.current}</h1>;
}

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  • 避免重新创建 useRef() 的初始值
// bad
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被创建
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
// good
function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }

  // 当你需要时,调用 getObserver()
  // ...
}

  • 回调ref
// 在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用
// 注意到我们传递了 [] 作为 useCallback 的依赖列表。
// 这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

useContext():共享状态钩子

  • 深层传递值给子孙组件,最好提供一个默认值(即创建context时传入的值)
  • 为了确定 context 值,React 搜索组件树,为这个特定的 context 向上查找最近的 context provider。
  • 通过传递一个set函数给子孙组件以在合适的时机修改context(声明为一个state)
  • 在传递对象和函数时优化重新渲染,比如传递的value为obj= {login:login,userId:userId}
    • 使用useCallback缓存login函数
    • 使用useMemo缓存对象obj
  • 通过在 provider 中使用不同的值(下层使用不同的value即可覆盖上层的value)包装树的某个部分,可以覆盖该部分的 context。(类似css的color属性,下层会覆盖上层的属性)
const AppContext = React.createContext({});
<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>
// 任何子组件都可读取AppContext
const Navbar = () => {
  const { username } = useContext(AppContext);
  return (
    <div className="navbar">
      <p>{username}</p> // superawesome,值与Provider 的value有关
    </div>
  )
}
// 任何子组件都可读取AppContext
const Messages = () => {
  const { username } = useContext(AppContext)
  return (
    <div className="messages">
      <p>1 message for {username}</p> // superawesome,值与Provider 的value有关
    </div>
  )
}

useReducer():action 钩子

    • dispatch 函数 是为下一次渲染而更新 state。因此在调用 dispatch 函数后读取 state 并不会拿到更新后的值,也就是说只能获取到调用前的值。
  • 通过给 useReducer 的第三个参数传入 初始化函数 来解决 重新创建初始值的 问题。
    const [state, dispatch] = useReducer(reducer, initState, createInitialState);
  • 如果你需要获取更新后的 state,可以手动调用 reducer 来得到结果
// reducer.js
const myReducer = (state, action) => {
  switch(action.type)  {
    case('countUp'):
      // 要返回一个新对象,而不是修改对象(state.count = state.count + 1)
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}
// app.js
function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}

useImperativeHandle

  • 它能让你自定义由 ref 暴露出来的句柄。
  • useImperativeHandle 应当与 forwardRef 一起使用
  • 使用场景:例如,滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等
// 父组件
import ProTable from './ProTable';

const tableRef = useRef();
const refreshTable = () => {
  // 父组件可以调用暴露出来的方法
  tableRef.current?.refresh();
};
<ProTable ref={tableRef} onClick={() => refreshTable} />

//子组件暴露一个refresh方法
const ProTable = (props,ref) => {
  useImperativeHandle(ref, () => ({
    refresh,
  }));
return xxx
}
export default forwardRef(ProTable);

useId

  • 可以生成传递给无障碍属性的唯一 ID。
 const id = useId()
 <label htmlFor={id + '-firstName'}>名字:</label>
 <input id={id + '-firstName'} type="text" />

自定义 Hook

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