学习React Hooks

1. Hook 的定义

React Hooks 设计的目的:加强版函数组件,让函数组件也拥有类组件的功能。

“Hook”的意思是钩子,React Hooks 想要达到的效果就是在尽量使用纯函数,且如需要外部功能副作用,就用 Hooks 将它们“钩”进来。

React 默认提供了一些常用的钩子,钩子一律使用use前缀命名,常用的钩子如下:

useState();
useReducer();
useContext();
useEffect();

2. useState

State Hook使用的例子:

const MyCount = () => {
  // 声明了count这个state变量
  const [count, setCount] = useState(0); // 数组解构

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

调用useState时 React 为我们定义了一个state变量,我们将这个变量命名为countuseState还会返回一个更新 state 的函数,同样我们将之命名为setCount

在函数组件中,函数退出后变量就会消失,而 React 就通过 useState 替我们保存了变量,让函数也拥有保存state的能力

问题:React 是怎么知道 useState 对应的是哪个函数组件?

3. useEffect

Effect Hook 可以让你在函数组件中执行副作用操作,这里的副作用有点像生命周期

可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

3.1 无需清除的 effect 操作

在使用 React class 中,我们通常将副作用放在componentDidMountcomponentDidUpdate 中,如下面计时器实例:

class MyCountClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们需要在两个生命周期函数中编写重复的代码,因为我们希望组件在初次加载和更新时执行同样的操作。

而我们使用Hook Effect,如下:

const MyCountHook = () => {
  const [count, setCount] = useState(0);

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

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

通过使用useEffect这个 Hook,我们可以告诉 React 组件在渲染后执行哪些操作,React 会在 Dom 更新后执行我们传入的函数(这个函数称之为effect)。

useEffect首次渲染后和每次更新后都会执行,我们可以简单理解为effect发生在渲染之后,React 保证每次运行effect时,Dom 都已经更新完毕。

effect中我们可以获取到最新的count值,所以传给useEffect Hook的函数(effect)在每次渲染中都是不同的;每次重新渲染都会生成新的effect——每个effect都属于某一次特定的渲染。

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕;使用useLayoutEffect则会在 Dom 更新完毕前调度 effect,会触发页面阻塞。

3.2 需要清除的 effect 操作

有一些副作用是需要清除的(如clearInterval),这种情况下清除工作可以防止内存泄露。

如下面的例子:

const MyCountHook = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    // 清除interval的"副作用"
    return () => {
      clearInterval(interval);
    };
  });

  return (
    <div>
      <p>Now, the count is {count}</p>
    </div>
  );
};

effect返回函数effect中可选的清除机制,这样可以有效地将添加副作用与清除工作绑定在一起。

React 会在组件卸载时执行effect的清除操作,effect在每次渲染时都会执行,所以 React 在执行当前effect时会对上一个effect进行清除。

3.3 useEffect 的第二个参数

useEffect(() => {...}, [count]);

第二个参数传入一个数组,只有数组中的“state 变量”发生变化时,传入的effect才会被执行。

当第二个参数为空数组([])时,传入的effect只有在第一次渲染时会执行,之后函数组件更新时均不会执行,当然也不会有 effect 的卸载。

4. useReducer

useState的替代方案,它接收形如(state, action) => newState的 reducer,并返回当前最新的 state,以及配套的工作方法dispatch,用法与 redux 类似。

下面是使用useReducer的计数器例子:

// 初始化状态值
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();
  }
}

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

  useEffect(() => {
    const interval = setInterval(() => {
      dispatch({ type: "increment" });
    }, 1000);
    return () => {
      clearInterval(interval);
    };
  });

  return (
    <div>
      <p>Now, the count is {state.count}</p>
    </div>
  );
};

5. useRef

const refContainer = useRef(initialValue);

在函数式组件中,我们可以通过useRef这个 Hook 来实现访问 Dom。

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

const MyCountHook = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [name, setName] = useState("xuanxiao");
  const inputEl = useRef(null);

  useEffect(() => {
    console.log(inputEl); // {current: input}
    console.log("effect invoked");
    return () => console.log("effect detached");
  }, [state.count, name]);

  return (
    <div>
      <input
        ref={inputEl}
        value={name}
        onChange={e => setName(e.target.value)}
      ></input>
      <button onClick={() => dispatch({ type: "increment" })}>
        {state.count}
      </button>
    </div>
  );
};

6. 使用 Hooks 后的渲染优化

使用了 Hooks 后子组件的重复渲染问题该如何优化呢?接上面的代码,我们改写成一个例子:

const MyCountFunc = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [name, setName] = useState("xuanxiao");

  const config = {
    text: `count is ${state.count}`,
    color: state.count > 3 ? "red" : "blue"
  };

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)}></input>
      <Child
        config={config}
        onButtonClick={() => dispatch({ type: "increment" })}
      ></Child>
    </div>
  );
};

const Child = ({ onButtonClick, config }) => {
  console.log("child render");
  return (
    <button onClick={onButtonClick} style={{ color: config.color }}>
      {config.text}
    </button>
  );
};

可以看到,无论我们点击 button 改变state.count,还是改变name的值,子组件Child都会重新渲染,这样显然是不高效的,因为name的改变并不会对Child产生任何影响,子组件Child不应该被重新渲染。

我们使用React.memo来优化这种重复渲染的情况

我们用高阶组件React.memo包裹子组件Child

...
const Child = React.memo(({ onButtonClick, config }) => {
  console.log('child render');
  return (
    <button onClick={onButtonClick} style={{ color: config.color }}>
      {config.text}
    </button>
  );
});

发现子组件依旧重复渲染了,为什么?

因为name改变的时候,整个MyCountFunc函数组件会重新渲染,因为 js 闭包的关系,即使常量config并没有改变,也会生成一个新的来代替它,所以子组件Child依然会重新渲染。

我们希望config及其中的state.count 这些并未改变的值能在父组件重新渲染时得到保留而不是重新生成,而单纯靠函数组件肯定做不到,于是我们又得使用 Hooks 中的特性。

我们使用useMemo,把“创建”函数和依赖项数组作为参数传入,它会在某个依赖项改变时才重新计算memoizedValue

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

如果没有提供依赖项数组,useMemo在每次渲染时都会重新计算值。

我们修改config的代码如下:

const config = React.useMemo(
  () => ({
    text: `count is ${state.count}`,
    color: state.count > 3 ? "red" : "blue"
  }),
  [state.count]
);

说明仅在state.count发生变化时,才会重新计算并生成新的const config

我们发现子组件依旧重复渲染?

因为这样做还不够,因为每次父函数组件重新渲染,会生成新的dispath函数,这也会造成子组件的重复渲染(在onClick方法处)

我们使用useCallback,把内联回调函数及依赖项数组作为参数传入,它将返回回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

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

我们对 onClick 的事件处理函数进行修改:

// 修改onClick函数
const handleButtonClick = React.useCallback(() => {
  dispatch({ type: 'increment' });
}, [dispatch]);
...
...
<Child config={config} onButtonClick={handleButtonClick}></Child>

注意,useCallback(fn, deps)等价于 useMemo(() => fn, deps)

所以上面的修改等价于:

const handleButtonClick = React.useMemo(
  () => () => {
    dispatch({ type: "increment" });
  },
  [dispatch]
);

这样,当<input>的 onChange 事件改变name时,子组件就不会重复渲染了,因为const config没有重新生成,handleButtonClick也没有更新。


文章作者: 玄霄
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 玄霄 !
评论
 上一篇
Go基本语法 Go基本语法
变量声明声明变量一般使用var关键字,变量声明的标准格式如下: var name type // example var a,b *int Go 中变量的基本类型有: bool string int、int8、int16、int32、int
2019-09-29
下一篇 
Nextjs初探 Nextjs初探
Nextjs 目录结构 pages下所有文件的文件名对应页面的子路径(理解为 nextjs 中的路由体系,区别于 KOA 的路由) 两个例外:_app.js、__document.js components:组件 lib:utils
2019-09-23
  目录