React Hooks核心原理与实战

一、Hooks的优点

1.1 Hooks的含义

	Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。它在一定程度上更好地体现了 React 的开发思想,即从 State => View 的函数式映射。
	![在这里插入图片描述](https://img-blog.csdnimg.cn/6ee28a321a1a4b78b8bbef7afcfc15ed.webp#pic_center)

1.2 优点

  1. 逻辑复用
  2. 有助于关注分离

二、常用的Hooks

2.1 useState

	useState:让函数具有维持状态的能力。
  1. useState(initialState) 的参数 initialState 是创建 state 的初始值,它可以是任意类型,比如数字、对象、数组等等。
  2. useState() 的返回值是一个有着两个元素的数组。第一个数组元素用来读取 state 的值,第二个则是用来设置这个 state 的值。
  3. 建议只有需要触发 UI 更新的状态才放到 state 里。
  4. 如果要创建多个 state,那么我们就需要多次调用 useState。比如要创建多个 state,使用的代码如下:
// 定义一个年龄的 state,初始值是 42
const [age, setAge] = useState(42);
// 定义一个水果的 state,初始值是 banana
const [fruit, setFruit] = useState('banana');
// 定一个一个数组 state,初始值是包含一个 todo 的数组
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

2.2 useEffect

useEffect:执行副作用, 在useEffect中执行的代码是不影响渲染出来的 UI 的。useEffect 是每次组件 render 完后判断依赖并执行的。用法如下
useEffect(callback, dependencies)
  1. 没有依赖项,则每次render后都会重新执行
  2. 空数组作为依赖项,则只在首次执行时触发,对应到 Class 组件就是 componentDidMount。例如:
useEffect(() => {
  // 组件首次渲染时执行,等价于 class 组件中的 componentDidMount
  console.log('did mount');
}, [])
  1. useEffect还允许返回一个函数,用于在组件销毁时做一些清理的操作。这个机制几乎等价于类组件中的componentWillUnmount。
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({});
useEffect(() => {
  // 窗口大小变化事件处理函数
  const handler = () => {
    setSize(getSize());
  };
  // 监听 resize 事件
  window.addEventListener('resize', handler);
  
  // 返回一个 callback 在组件销毁时调用
  return () => {
    // 移除 resize 事件
    window.removeEventListener('resize', handler);
  };
}, []);
  1. 注意,在useEffect的callback要避免直接使用async/await,需要封装一下
useEffect(() => { // useEffect 的 callback 要避免直接的 async 函数,需要封装一下 
	const doAsync = async () => { // 当 id 发生变化时,将当前内容清楚以保持一致性 
	setBlogContent(null); // 发起请求获取数据 
	const res = await fetch(`/blog-content/${id}`); 
	// 将获取的数据放入 state 
	setBlogContent(await res.text()); }; 
	doAsync(); 
}, [id]);

2.3 useCallback:缓存回调函数

useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建!!
每一个被useCallBack的函数都将被加入useCallBack内部的管理队列。而当我们大量使用useCallBack的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack的组件重新渲染的时候都需要去便利useCallBack内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。

在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。

import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
    const [parentState,setParentState] = useState(0);  //父组件的state
    // 未被useCallback包裹 需要传入子组件的函数
    // const toChildFun = () => {
    //    console.log("需要传入子组件的函数");    
    // }
    // 使用useCallback包裹
    const toChildFun = useCallback(() => {
	  console.log("需要传入子组件的函数");
	}, []);
    return (<div>
          <Button onClick={() => setParentState(val => val+1)}>
              点击我改变父组件中与Child组件无关的state
          </Button>
          //将父组件的函数传入子组件
          <Child fun={toChildFun}></Child>
    <div>)
}
/**被memo保护的子组件**/
const Child = memo(() => {
    console.log("我被打印了就说明子组件重新构建了")
    return <div><div>
})
 

总结

  1. useCallBack不要每个函数都包一下,否则就会变成反向优化,useCallBack本身就是需要一定性能的
  2. useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变
  3. React.memo()是通过校验props中的数据是否改变的来决定组件是否需要重新渲染的一种缓存技术,具体点说React.memo()其实是通过校验Props中的数据的内存地址是否改变来决定组件是否重新渲染组件的一种技术。
  4. useCallBack需要配合React.memo使用

2.4 useMemo: 缓存计算结果

  1. 如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。
  2. 避免子组件的重复渲染
// 子组件
import React from "react";
interface ChildProps {
  name: { name: string; color: string };
  onClick: Function;
}
const ChildUseMemo = ({ name, onClick }: ChildProps): JSX.Element => {
  console.log("子组件?");
  return (
    <>
      <div style={{ color: name.color }}>
        我是一个子组件,父级传过来的数据:{name.name}
      </div>

      <button onClick={onClick.bind(null, "新的子组件name")}>改变name</button>
    </>
  );
};
// 建议useMemo配合这memo使用
export default React.memo(ChildUseMemo);

import React, { useState, useCallback, useMemo } from "react";
import ChildUseMemo from "./childMemo";
const Father: React.FC = () => {
  const [count, setCount] = useState(0);  
  const [name, setName] = useState("Child组件2");
  return (
    <div>
      <p>父组件:{count}</p>
      <ChildUseMemo
      	// 使用useMemo,返回一个和原本一样的对象,第二个参数是依赖性,当name发生改变的时候,才产生一个新的对象
        name={useMemo(
          () => ({
            name,
            color: name.indexOf("name") !== -1 ? "red" : "green"
          }),
          [name]
        )}
        // name={{ name, color: name.indexOf("name") !== -1 ? "red" : "green" }}
        // 这里使用了useCallback优化了传递给子组件的函数,只初始化一次函数,下次不产生新的函数
        onClick={useCallback((newName: string) => setName(newName), [])}
      ></ChildUseMemo><button onClick={(e)=> {setCount(count + 1)}>+</button>
    </div>
  );
};

export default Example;

总结

在子组件不需要父组件的值和函数的情况下,只需要使用memo函数包裹子组件即可。而在使用函数的情况,需要考虑有没有函数传递给子组件使用useCallback。而在值有所依赖的项,并且是对象和数组等值的时候而使用useMemo(当返回的是原始数据类型如字符串、数字、布尔值,就不要使用useMemo了)。不要盲目使用这些hooks。

2.5 useRef:在多次渲染之间共享数据

  1. useRef:保存数据(保存的数据一般和UI无关),因此当 ref 的值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。

import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  // 定义 time state 用于保存计时的累积时间
  const [time, setTime] = useState(0);

  // 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
  const timer = useRef(null);

  // 开始计时的事件处理函数
  const handleStart = useCallback(() => {
    // 使用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  // 暂停计时的事件处理函数
  const handlePause = useCallback(() => {
    // 使用 clearInterval 来停止计时
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div>
      {time / 10} seconds.
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}
  1. useRef可以保存某个 DOM 节点的引用
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

2.6 useContext:定义全局状态

优点:
1. 提供了一个方便在多个组件之间共享数据的机制。context能够进行数据绑定,当Context的数据发生变化的时候,使用这个数据的组件就能够自动刷新。
缺点:
1. 会让调试变的困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
2. 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。


const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
// 创建一个 Theme 的 Context
// ThemeContext拥有一个Provider属性
const ThemeContext = React.createContext(themes.light);
function App() {
  // 使用 state 来保存 theme 从而可以动态修改 
  const [theme, setTheme] = useState("light");
  // 切换 theme 的回调函数,当state的值修改后,动态的切换Context的值,当用到这个context的地方都会自动刷新 
  const toggleTheme = useCallback(() => { 
  	setTheme((theme) => (theme === "light" ? "dark" : "light")); 
  }, []);
  // 整个应用使用 ThemeContext.Provider 作为根组件
  return (
    // 使用 themes.dark 作为当前 Context 
    <ThemeContext.Provider value={themes.dark}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{
      background: theme.background,
      color: theme.foreground
    }}>
      I am styled by theme context!
    </button>
  );
}

三、Hooks的使用规则

  1. 只能在函数组件的顶级作用域使用;

    Hooks不能在循环、条件判断或者嵌套函数内执行,而必须在顶层。同时Hooks在组件多次渲染之间,必须按顺序被执行。
    
  2. 只能在函数组件或其他Hooks中使用。如果一定要在Class组件中使用函数式组件,可以利用高级组件的模式,将Hooks封装成高阶组件,从而让类组件使用

import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';

export const withWindowSize = (Comp) => {
  return props => {
    const windowSize = useWindowSize();
    return <Comp windowSize={windowSize} {...props} />;
  };
};
import React from 'react';
import { withWindowSize } from './withWindowSize';

class MyComp {
  render() {
    const { windowSize } = this.props;
    // ...
  }
}

// 通过 withWindowSize 高阶组件给 MyComp 添加 windowSize 属性
export default withWindowSize(MyComp);

四、自定义Hooks

  • 优点:

    • 方便进行逻辑复用
    • 帮助分离
  • 定义:

    声明一个名字以use开头的函数。例如你创建了一个usexxx的函数,但是内部没有使用任何其他的hooks,那么这个函数就是一个普通函数,如果里面用了其他hooks,那么它就是一个hook

    // useCount.js
    import { useState, useCallback }from 'react';
    export default function useCounter() {
      // 定义 count 这个 state 用于保存当前数值
      const [count, setCount] = useState(0);
      // 实现加 1 的操作
      const increment = useCallback(() => setCount(count + 1), [count]);
      // 实现减 1 的操作
      const decrement = useCallback(() => setCount(count - 1), [count]);
      // 重置计数器
      const reset = useCallback(() => setCount(0), []);
      // 将业务逻辑的操作 export 出去供调用者使用
      return { count, increment, decrement, reset };
    }
    
    // index.js
    import React from 'react';
    import useCounter from './useCount.js'
    function Counter() {
      // 调用自定义 Hook
      const { count, increment, decrement, reset } = useCounter();
      // 渲染 UI
      return (
        <div>
          <button onClick={decrement}> - </button>
          <p>{count}</p>
          <button onClick={increment}> + </button>
          <button onClick={reset}> reset </button>
        </div>
      );
    }
    

五、函数组件设计模式

5.1 容器模式

把条件判断的结果放到两个组件之中,确保真正 render UI 的组件收到的所有属性都是有值的。

// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
  visible,
  ...rest, // 使用 rest 获取除了 visible 之外的属性
}) {
  // 如果对话框不显示,则不 render 任何内容
  if (!visible) return null; 
  // 否则真正执行对话框的组件逻辑
  return <UserInfoModal visible {...rest} />;
}

5.2 render props模式重用UI逻辑

render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容。

// counterRender.jsx
import { useState, useCallback } from "react";

function CounterRenderProps({ children }) {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  const decrement = useCallback(() => {
    setCount(count - 1);
  }, [count]);

  return children({ count, increment, decrement });
}


// counterReanderExample.jsx
function CounterRenderPropsExample() {
  return (
    <CounterRenderProps>
      {({ count, increment, decrement }) => {
        return (
          <div>
            <button onClick={decrement}>-</button>
            <span>{count}</span>
            <button onClick={increment}>+</button>
          </div>
        );
      }}
    </CounterRenderProps>
  );
}

六、React事件处理

  1. Reac原生事件的原理:合成事件。

由于虚拟 DOM 的存在,在 React 中即使绑定一个事件到原生的 DOM 节点,事件也并不是绑定在对应的节点上,而是所有的事件都是绑定在根节点上。然后由 React 统一监听和管理,获取事件后再分发到具体的虚拟 DOM 节点上。
在 React 17 之前,所有的事件都是绑定在 document 上的,而从 React 17 开始,所有的事件都绑定在整个 App 上的根节点上,这主要是为了以后页面上可能存在多版本 React 的考虑。
具体来说,React 这么做的原因主要有两个。
第一,虚拟 DOM render 的时候, DOM 很可能还没有真实地 render 到页面上,所以无法绑定事件。
第二,React 可以屏蔽底层事件的细节,避免浏览器的兼容性问题。同时呢,对于 React Native 这种不是通过浏览器 render 的运行时,也能提供一致的 API。这里有一点我要多解释下。
那就是为什么事件绑定在某个根节点上,也能触发实际 DOM 节点的事件。我们知道,在浏览器的原生机制中,事件会从被触发的节点往父节点冒泡,然后沿着整个路径一直到根节点,所以根节点其实是可以收到所有的事件的。这也称之为浏览器事件的冒泡模型。

  1. 创建自定义事件
    a.原生事件是浏览器的机制;
    b.而自定义事件则是纯粹的组件自己的行为,本质是一种回调函数机制。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值