第1期 Hook

346 阅读4分钟

什么是Hook

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。

使用Hook的目的

1、在class中代码需要按照生命周期去划分代码,但是在hook中则是按照代码的用途去划分,hook避免把相关的逻辑拆分到不同的地方。

2、Hook 使你在无需修改组件结构的情况下复用状态逻辑。

3、class 也给目前的工具带来了一些问题。例如,class不能很好的压缩,并且会使热重载出现不稳定的情况。

使用Hook的规则

1、只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。因为React 靠的是 Hook 调用的顺序来知道哪个 state 对应哪个 useState。

2、只能在 React 的函数组件中调用 Hook或者自定义的 Hook 中使用。在class组件内不起作用。不要在其他 JavaScript 函数中调用。

useState

state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。

useState返回的是一个数组 const [fruit,setFruit]是一种数组解构的方式。

const [fruit, setFruit] = useState('banana');
// 等同于
var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
var fruit = fruitStateVariable[0]; // 数组里的第一个值
var setFruit = fruitStateVariable[1]; // 数组里的第二个值

setFruit的时候,如果值是{},则每次都会render,即使每次都是{},也会render,但是如果数据类型是string等,则不会render。因为{}相当于一个新的对象,{} != {}。

使用单个还是多个 state 变量

state不进行合并而是覆盖。我们可以选择写一个自定义Hook来专门做合并这件事情,也可以选择把state切分成多个state变量。

切分成多个state使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易。

const [position, setPosition] = useState({ left: 0, top: 0, width: 100, height: 100 });
// 把state切分成多个state
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });

useEffect

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
}

副作用

在 React 组件中执行过数据获取、订阅或者手动修改过DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

Effect Hook 可以让你在函数组件中执行副作用操作。

执行时机

仅在组件挂载、组件更新和卸载时执行。可以理解为对应class组件生命周期的componentDidMount,componentDidUpdate 和 componentWillUnmount时执行。

为什么传给useEffect的是不同的函数

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  // 为什么不选择下面的方式?
  let f1 = () => {
    document.title = `You clicked ${count} times`;
  };
  useEffect(f1);

事实上这正是React可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

useEffect是异步还是同步

使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

为什么在组件内部调用 useEffect

将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

如何清除副作用

虽然这个例子不需要清除副作用,但是还是用跟这个例子进行举例。通过return一个函数进行清除副作用。

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
    
    return ()=>{
        console.log("此处执行清除副作用的代码")
    }
  });
}

何时清除副作用

像前面所说的,传给useEffect的是不同的函数,所以每一次在执行effect之前,React 会对上一个 effect 进行清除。

之所以在每一次执行effect之前清除上一个effect,是因为此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

如何跳过 Effect 进行性能优化

有些情况是不需要每次渲染都去执行effect的,我们通过给它传第二个参数来控制何时进行渲染。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []); // 仅在组件挂载和卸载时执行 对应class组件的componentDidMount 和 componentWillUnmount执行

自定义Hook

有时候我们会想要在组件之间重用一些状态逻辑。 目前为止,有两种主流方案来解决这个问题:高阶组件和render props。自定义Hook可以让你在不增加组件的情况下达到同样的目的。

自定义 Hook 更像是一种约定而不是功能。useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

useContext

在类组件中使用context

const ThemeContext = React.createContext(themes.light);
class MyClass extends React.Component {
    render() {
    let value = this.context;// class中的获取context数据的方式
  }
}
MyClass.contextType = ThemeContext;

在函数组件中使用context

const ThemeContext = React.createContext(themes.light);
function MyClass() {
  const theme = useContext(ThemeContext);// 函数组件中
}

useReducer

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

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, {count:0});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useRef

// 类组件
const refContainer = React.createRef();
// 函数组件
const refContainer = useRef(initialValue);

<input ref={refContainer} type="text" />

FAQ

为什么我会在我的函数中看到陈旧的 props 和 state

组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。

function Example() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);// 此处count是在点击Show alert的时候的count
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

有类似 forceUpdate 的东西吗

可以考虑如下方式,但尽量避免。

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
forceUpdate();
}