关于javascript:React系列六-从HOC再到HOOKS

44次阅读

共计 19749 个字符,预计需要花费 50 分钟才能阅读完成。

系列文章

React 系列(一)– 2013 起源 OSCON – React Architecture by vjeux

React 系列(二)– React 根本语法实现思路

React 系列(三)– Jsx, 合成事件与 Refs

React 系列(四)— virtualdom diff 算法实现剖析

React 系列(五)— 从 Mixin 到 HOC

React 系列(六)— 从 HOC 再到 HOOKS

在线调试

React 在线运行网址: https://codesandbox.io/s/blue…

Hook 是 React 16.8 的新增个性。它能够让你在不编写 class 的状况下应用 state 以及其余的 React 个性。

动机

Hook 解决了咱们五年来编写和保护成千上万的组件时遇到的各种各样看起来不相干的问题。

在组件之间复用状态逻辑很难

如果你应用过 React 一段时间,你兴许会相熟一些解决此类问题的计划,

比方 render props高阶组件。然而这类计划须要从新组织你的组件构造,这可能会很麻烦,使你的代码难以了解。

如果你在 React DevTools 中察看过 React 利用,你会发现由 providers,consumers,高阶组件,render props 等 其余形象层组成的组件会造成“嵌套天堂”。

你能够应用 Hook 从组件中提取状态逻辑,使得这些逻辑能够独自测试并复用。Hook 使你在无需批改组件构造的状况下复用状态逻辑。

简单组件变得难以了解

咱们常常保护一些组件,组件起初很简略,然而逐步会被状态逻辑和副作用充斥。每个生命周期经常蕴含一些不相干的逻辑。互相关联且须要对照批改的代码被进行了拆分,而齐全不相干的代码却在同一个办法中组合在一起。如此很容易产生 bug,并且导致逻辑不统一。

Hook 将组件中互相关联的局部拆分成更小的函数(比方设置订阅或申请数据)

难以了解的 class

你必须去了解 JavaScript 中 this 的工作形式,这与其余语言存在微小差别。还不能遗记绑定事件处理器。没有稳固的语法提案,这些代码十分冗余。

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

Hook 使你在非 class 的状况下能够应用更多的 React 个性。

HOOKS 标准

在顶层调用 HOOKS

不要在循环, 条件, 或者内嵌函数中调用. 这都是为了保障你的代码在每次组件 render 的时候会依照雷同的程序执行 HOOKS, 而这也是可能让 React 在多个 useState 和 useEffect 执行中正确保留数据的起因

只在 React 函数调用 HOOKS

  • React 函数组件调用
  • 从自定义 HOOKS 中调用

能够确保你源码中组件的所有有状态逻辑都是清晰可见的.

State Hook

const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值雷同。

setState 函数用于更新 state。它接管一个新的 state 值并将组件的一次从新渲染退出队列。

惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被疏忽。如果初始 state 须要通过简单计算取得,则能够传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);
  return initialState;
});

跳过 state 更新

调用 State Hook 的更新函数并传入以后的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 应用 Object.is 比拟算法 来比拟 state。)

须要留神的是,React 可能仍须要在跳过渲染前渲染该组件。不过因为 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必放心。如果你在渲染期间执行了高开销的计算,则能够应用 useMemo 来进行优化。

示例

import React, {useState} from 'react';

function Example() {
  // 申明一个叫“count”的 state 变量。const [count, setCount] = useState(0);

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

export default Example;

等价于上面 Class 写法

import React from 'react';

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

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

export default Example;

从下面能够看出 useState 实际上就是在 state 里申明一个变量并且初始化了一个值而且提供一个能够扭转对应 state 的函数. 因为在纯函数中没有 this.state.count 的这种用法, 所以间接应用 count 代替, 下面的 count 就是申明的变量,setCount 就是扭转变量的办法.

须要留神的一点是 useStatethis.state有点不同, 它 只有在组件第一次 render 才会创立状态, 之后每次都只会返回以后的值.

如果扭转须要依据之前的数据变动, 能够通过函数接管旧数据, 例如

setCount(prevCount => prevCount + 1)

如果是想申明多个 state 的时候, 就须要应用屡次 useState

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{text: 'Learn Hooks'}]);
}

或者通过组合对象一次合并多个数据

Effect Hook

useEffect(didUpdate);

执行有副作用的函数, 你能够把 useEffect Hooks 视作 componentDidMountcomponentDidUpdatecomponentWillUnmount 的联合,useEffect 会 在浏览器绘制后提早执行,但会保障在任何新的渲染前执行,React 将在组件更新前刷新上一轮渲染的 effect。

在函数组件主体内(这里指在 React 渲染阶段)扭转 DOM、增加订阅、设置定时器、记录日志以及执行其余 蕴含副作用的操作都是不被容许的,因为这可能会产生莫名其妙的 bug 并毁坏 UI 的一致性

React 组件中的 side effects 大抵能够分为两种

不须要清理

有时咱们想要 在 React 更新过 DOM 之后执行一些额定的操作。比方网络申请、手动更新 DOM、以及打印日志都是常见的不须要清理的 effects

import React from 'react';

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

  componentDidMount() {console.log(`componentDidMount: You clicked ${this.state.count} times`);
  }

  componentDidUpdate() {console.log(`componentDidUpdate: 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>
    );
  }
}

export default Example;

componentDidMount: You clicked 0 times

// 点击按钮

componentDidUpdate: You clicked 1 times

然而如果咱们换成 HOOKS 的写法

import React, {useState, useEffect} from 'react';

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

  useEffect(() => {
      // 初始化默认输入: You clicked 0 times 
      // 后续点击才会输入
    console.log(`You clicked ${count} times`);
  });

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

export default Example;

You clicked 0 times

// 点击按钮

You clicked 1 times

useEffect 做了什么?

通过这个 Hook,React 晓得你想要这个组件在每次 render 之后做些事件。React 会记录下你传给 useEffect 的这个办法,而后在进行了 DOM 更新之后调用这个办法。但咱们同样也能够进行数据获取或是调用其它必要的 API。

为什么 useEffect 在组件外部调用?

useEffect 放在一个组件外部,能够让咱们在 effect 中,即可取得对 count state(或其它 props)的拜访,而不是应用一个非凡的 API 去获取它。

useEffect 是不是在每次 render 之后都会调用?

默认状况下,它会在 第一次 render 和 之后的每次 update 后运行 。React 保障 每次运行 effects 之前 DOM 曾经更新了

应用上还有哪些区别?

不像 componentDidMount 或者 componentDidUpdateuseEffect 中应用的 effect 并 不会阻滞浏览器渲染页面。咱们也提供了一个独自的 useLayoutEffect 来达成这同步调用的成果。它的 API 和 useEffect 是雷同的。

须要清理的 Effect

比拟常见的就相似挂载的时候监听事件或者开启定时器, 卸载的时候就移除.

import React from 'react';

class Example extends React.Component {constructor(props) {super(props);
  }

  componentDidMount() {document.addEventListener('click', this.clickFunc, false);
  }

  componentWillUnmount() {document.removeEventListener('click', this.clickFunc);
  }

  clickFunc(e) {// doSomethings}

  render() {return <button>click me!</button>;}
}

export default Example;

换成 HOOKS 写法相似, 只是会返回新的函数

import React, {useEffect} from 'react';

function Example() {useEffect(() => {document.addEventListener('click', clickFunc, false);
    return () => {document.removeEventListener('click', clickFunc);
    };
  });

  function clickFunc(e) {// doSomethings}

  return <button>click me!</button>;
}

export default Example;

咱们为什么在 effect 中返回一个函数

这是一种可选的清理机制。每个 effect 都能够返回一个用来在晚些时候清理它的函数。这让咱们让增加和移除订阅的逻辑彼此凑近。它们是同一个 effect 的一部分!

React 到底在什么时候清理 effect?

React 在 每次组件 unmount 的时候执行清理 。然而,正如咱们之前理解的那样,effect 会在每次 render 时运行,而不是仅仅运行一次。这也就是为什么 React 也会在 执行下一个 effect 之前,上一个 effect 就已被革除

咱们能够批改一下代码看看 effect 的运行机制

import React, {useState, useEffect} from 'react';

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

  useEffect(() => {console.log('addEventListener:' + count);
    document.addEventListener('click', clickFunc, false);
    return () => {console.log('removeEventListener:' + count);
      document.removeEventListener('click', clickFunc);
    };
  });

  function clickFunc(e) {setCount(count + 1);
  }

  return <button>click me! {count}</button>;
}

export default Example;

addEventListener: 0

// 点击按钮

removeEventListener: 0

addEventListener: 1

// 点击按钮

removeEventListener: 1

addEventListener: 2

能够看到下面代码在每次更新都是从新监听, 想要防止这种状况不在 useEffect 里 return 函数即可

进阶应用

有时候咱们可能有多套逻辑写在不同的生命周期里, 如果换成 HOOKS 写法的话咱们能够按性能划分应用多个,React 将会依照指定的程序利用每个 effect。

import React, {useState, useEffect} from "react";

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

  useEffect(() => {console.log(`You clicked ${count} times`);
  });

  useEffect(() => {console.log('addEventListener');
    document.addEventListener("click", clickFunc, false);
    return () => {console.log('removeEventListener');
      document.removeEventListener("click", clickFunc);
    };
  });

  function clickFunc(e) {setCount(count + 1);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button>Click me</button>
    </div>
  );
}

export default Example;

You clicked 0 times

addEventListener

// 点击按钮

removeEventListener

You clicked 1 times

addEventListener

// 点击按钮

removeEventListener

You clicked 2 times

addEventListener

为什么 Effects 会在每次更新后执行

如果你们以前应用 class 的话可能会有纳闷, 为什么不是在卸载阶段执行一次. 从官网解释代码看

componentDidMount() {
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

componentWillUnmount() {
  ChatAPI.unsubscribeFromFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

它在挂载阶段监听, 移除阶段移除监听, 每次触发就依据 this.props.friend.id 做出对应解决. 然而这里有个暗藏的 bug 就是 当移除阶段的时候获取的 this.props.friend.id 可能是旧的数据 , 引起的问题就 是卸载时候会应用谬误的 id 而导致内存透露或解体, 所以在 class 的时候个别都会在componentDidUpdate 做解决

componentDidUpdate(prevProps) {
  // Unsubscribe from the previous friend.id
  ChatAPI.unsubscribeFromFriendStatus(
    prevProps.friend.id,
    this.handleStatusChange
  );
  // Subscribe to the next friend.id
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

然而如果咱们换成 HOOKS 的写法就不会有这种 bug

useEffect(() => {ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

这是因为HOOKS 会在利用下一个 effects 之前革除前一个 effects, 此行为默认状况下确保一致性,并避免因为短少更新逻辑而在类组件中常见的谬误

通过跳过 effects 晋升性能

就在下面咱们晓得每次 render 都会触发 effects 机制可能会有性能方面的问题, 在 class 的写法里咱们能够通过 componentDidUpdate 做抉择是否更新

componentDidUpdate(prevProps, prevState) {if (prevState.count !== this.state.count) {document.title = `You clicked ${this.state.count} times`;
  }
}

而在 useEffect 里咱们能够通过传递一组数据给它作为第二参数, 如果在下次执行的时候该数据没有发生变化的话 React 会跳过当次利用

import React, {useState, useEffect} from 'react';

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

  useEffect(() => {console.log('addEventListener:' + count);
    document.addEventListener('click', clickFunc, false);
    return () => {console.log('removeEventListener:' + count);
      document.removeEventListener('click', clickFunc);
    };
  }, [count]);

  function clickFunc(e) {}

  return <button>click me! {count}</button>;
}

export default Example;

所以下面提到的 bug 案例能够通过这个形式做解决

useEffect(() => {ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

留神

如果你想应用这种优化形式,请确保数组中蕴含了所有内部作用域中会发生变化且在 effect 中应用的变量, 否则你的代码会始终援用上一次 render 的旧数据.

如果你想要 effects只在挂载和卸载时各清理一次的话, 能够传递一个 空数组 作为第二参数. 相当于通知 React 你的 effects 不依赖于任何的 props 或者 state, 所以没必要反复执行.

effect 的执行机会

componentDidMountcomponentDidUpdate 不同的是,在 浏览器实现布局与绘制之后,传给 useEffect 的函数会提早调用 。这使得它实用于许多常见的副作用场景,比方设置订阅和事件处理等状况,因而 不应在函数中执行阻塞浏览器更新屏幕的操作

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

useLayoutEffect

其函数签名与 useEffect 雷同,但它会在所有的 DOM 变更之后同步调用 effect。能够应用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 外部的更新打算将被同步刷新。

提醒

如果你应用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无奈在 Javascript 代码加载实现之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,须要将代码逻辑移至 useEffect 中(如果首次渲染不须要这段逻辑的状况下),或是将该组件提早到客户端渲染实现后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的状况下)。

若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,能够通过应用 showChild && <Child /> 进行条件渲染,并应用 useEffect(() => { setShowChild(true); }, []) 提早展现组件。这样,在客户端渲染实现之前,UI 就不会像之前那样显示错乱了。

useMemo

const memoizedValue = useMemo(() => {doSomething(a, b);
  },
  [a, b],
);

返回一个 memoized 值。

把“创立”函数和依赖项数组作为参数传入 useMemo,它 仅会在某个依赖项扭转时才从新计算 memoized 值。这种优化有助于防止在每次渲染时都进行高开销的计算。(能够了解成 Vue 的 computed API)

记住,传入 useMemo 的函数会 在渲染期间执行。请不要在这个函数外部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的实用领域,而不是 useMemo

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

你能够把 useMemo 作为性能优化的伎俩,但不要把它当成语义上的保障。未来,React 可能会抉择“忘记”以前的一些 memoized 值,并在下次渲染时从新计算它们,比方为离屏组件开释内存。先编写在没有 useMemo 的状况下也能够执行的代码 —— 之后再在你的代码中增加 useMemo,以达到优化性能的目标。

留神

依赖项数组不会作为参数传给回调函数。尽管从概念上来说它体现为:所有回调函数中援用的值都应该呈现在依赖项数组中。将来编译器会更加智能,届时主动创立数组将成为可能。

useCallback

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

返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项扭转时才会更新。当你把回调函数传递给通过优化的并应用援用相等性去防止非必要渲染(例如 shouldComponentUpdate)的子组件时,它将十分有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

留神

依赖项数组不会作为参数传给回调函数。尽管从概念上来说它体现为:所有回调函数中援用的值都应该呈现在依赖项数组中。将来编译器会更加智能,届时主动创立数组将成为可能。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的代替计划。它接管一个形如 (state, action) => newState 的 reducer,并返回以后的 state 以及与其配套的 dispatch 办法。(相似 Redux 的工作形式)

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

import React, {useReducer} from "react";

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();}
}

function Counter() {const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

export default Counter;

留神

React 会确保 dispatch 函数的标识是稳固的,并且不会在组件从新渲染时扭转。这就是为什么能够平安地从 useEffectuseCallback 的依赖列表中省略 dispatch

指定初始 state

将初始 state 作为第二个参数传入 useReducer 是最简略的办法:

const [state, dispatch] = useReducer(reducer, { count: initialCount});

留神

React 不应用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因而须要在调用 Hook 时指定。如果你特地喜爱上述的参数约定,能够通过调用 useReducer(reducer, undefined, reducer) 来模仿 Redux 的行为,但咱们不激励你这么做。

惰性初始化

从语法上你们会看到还有一个 init 的入参, 是用来做惰性初始化, 将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做能够将用于计算 state 的逻辑提取到 reducer 内部,这也为未来对重置 state 的 action 做解决提供了便当

import React, {useReducer} from "react";

function init(initialCount) {return {count: initialCount};
}

function reducer(state, action) {switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();}
}

function Counter({initialCount = 0}) {const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

export default Counter;

跳过 dispatch

如果 Reducer Hook 的返回值与以后 state 雷同,React 将跳过子组件的渲染及副作用的执行。(React 应用 Object.is 比拟算法 来比拟 state。)

须要留神的是,React 可能仍须要在跳过渲染前再次渲染该组件。不过因为 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必放心。如果你在渲染期间执行了高开销的计算,则能够应用 useMemo 来进行优化。

useContext

const value = useContext(MyContext);

接管一个 context 对象(React.createContext 的返回值)并返回该 context 的以后值。以后的 context 值由下层组件中距离以后组件最近的 <MyContext.Provider>value prop 决定。

当组件下层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并应用最新传递给 MyContext provider 的 context value 值。即便先人应用 React.memoshouldComponentUpdate,也会在组件自身应用 useContext 时从新渲染。

调用了 useContext 的组件总会在 context 值变动时从新渲染 。如果重渲染组件的开销较大,你能够通过应用 memoization 来优化。

示例

举个例子,在下面的 useReducer 代码中,咱们通过一个 context 做直达:

import React, {useContext, useReducer} from "react";

const initialState = {count: 0};
// context 对象
const stateContext = React.createContext();

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 ContextProvider(props) {const [state, dispatch] = useReducer(reducer, initialState);
  return (<stateContext.Provider value={{ state, dispatch}}>
      {props.children}
    </stateContext.Provider>
  );
}

// 数据接管组件
function Counter() {const { state, dispatch} = useContext(stateContext);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement"})}>-</button>
      <button onClick={() => dispatch({ type: "increment"})}>+</button>
    </>
  );
}

// 造成关系
const App = () => {
  return (
    <ContextProvider>
      <Counter />
    </ContextProvider>
  );
};

export default App;

useRef

const refContainer = useRef(initialValue);

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

import React, {useRef} from "react";

function Example() {const inputEl = useRef(null);
    const onButtonClick = () => {
      // `current` 指向已挂载到 DOM 上的文本输出元素
      inputEl.current.focus();};
    return (
      <div>
        <input ref={inputEl} type="text" />
        <button onClick={onButtonClick}>Focus the input</button>
      </div>
    );
  }

export default Example;

你应该相熟 ref 这一种拜访 DOM 的次要形式。如果你将 ref 对象以 <div ref={myRef} /> 模式传入组件,则无论该节点如何扭转,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它能够很不便地保留任何可变值
这是因为它创立的是一个一般 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的惟一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会告诉你。变更 .current 属性不会引发组件从新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要应用回调 ref 来实现。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 能够让你在应用 ref 时自定义裸露给父组件的实例值。在大多数状况下,该当防止应用 ref 这样的命令式代码。useImperativeHandle 该当与 forwardRef一起应用:

import React, {useRef, useImperativeHandle, forwardRef} from "react";

const FancyButton = forwardRef((props, ref) => {const inputRef = useRef();
  useImperativeHandle(ref, () => ({focus: () => {inputRef.current.focus();
    },
  }));
  return <input ref={inputRef} />;
});

function Example() {const inputRef = useRef();
  return (
    <div>
      <FancyButton ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus the input</button>
    </div>
  );
}

export default Example;

在上述的示例中,React 会将 <FancyButton ref={inputRef}> 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 <input ref={inputRef}> 元素。

因而,当 React 附加了 ref 属性之后,ref.current 将间接指向 <button> DOM 元素实例。

自定义 HOOKS

咱们能够将相干逻辑抽取进去

function useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {setIsOnline(status.isOnline);
  }

  useEffect(() => {ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

我必须以“use”结尾为自定义钩子命名吗? 这项公约十分重要。如果没有它,咱们就不能主动查看钩子是否违反了规定,因为咱们无奈判断某个函数是否蕴含对钩子的调用。

应用雷同钩子的两个组件是否共享状态? 不。自定义钩子是一种重用有状态逻辑的机制(例如设置订阅并记住以后值),然而每次应用自定义钩子时,其中的所有状态和成果都是齐全隔离的。

自定义钩子如何取得隔离状态? 对钩子的每个调用都处于隔离状态。从 React 的角度来看,咱们的组件只调用 useStateuseEffect

问题

Hook 会代替 render props 和高阶组件吗?

通常,render props 和高阶组件只渲染一个子节点。咱们认为让 Hook 来服务这个应用场景更加简略。这两种模式仍有用武之地,(例如,一个虚构滚动条组件或者会有一个 renderItem 属性,或是一个可见的容器组件或者会有它本人的 DOM 构造)。但在大部分场景下,Hook 足够了,并且可能帮忙缩小嵌套。

生命周期办法要如何对应到 Hook?

  • constructor:函数组件不须要构造函数。你能够通过调用 useState 来初始化 state。如果计算的代价比拟低廉,你能够传一个函数给 useState。
  • getDerivedStateFromProps:改为在渲染时安顿一次更新。
  • shouldComponentUpdate:详见 React.memo.
  • render:这是函数组件体自身。
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 能够表白所有这些的组合。
  • componentDidCatch and getDerivedStateFromError:目前还没有这些办法的 Hook 等价写法,但很快会加上。

我能够只在更新时运行 effect 吗?

这是个比拟常见的应用场景。如果你需要的话,你能够 应用一个可变的 ref 手动存储一个布尔值来示意是首次渲染还是后续渲染,而后在你的 effect 中查看这个标识。

如何获取上一轮的 props 或 state?

目前,你能够通过 ref 来手动实现:

function Counter() {const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  );
}

function usePrevious(value) {const ref = useRef();
  useEffect(() => {ref.current = value;});
  return ref.current;
}

有相似 forceUpdate 的货色吗?

如果前后两次的值雷同,useState 和 useReducer Hook 都会放弃更新。原地批改 state 并调用 setState 不会引起从新渲染。
通常,你不应该在 React 中批改本地 state。然而,作为一条前途,你能够用一个增长的计数器来在 state 没变的时候仍然强制一次从新渲染:

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

function handleClick() {forceUpdate();
}

我该如何测量 DOM 节点?

要想测量一个 DOM 节点的地位或是尺寸,你能够应用 callback ref。每当 ref 被附加到另一个节点,React 就会调用 callback。

function MeasureExample() {const [rect, ref] = useClientRect();
  return (
    <div>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
    </div>
  );
}

function useClientRect() {const [rect, setRect] = useState(null);
  const ref = useCallback(node => {if (node !== null) {setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}

应用 callback ref 能够确保 即使子组件提早显示被测量的节点 (比方为了响应一次点击),咱们仍然可能在父组件接管到相干的信息,以便更新测量后果。

留神到咱们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时扭转,因而 React 不会在非必要的时候调用它。

我该如何实现 shouldComponentUpdate?

你能够用 React.memo 包裹一个组件来对它的 props 进行浅比拟:

const Button = React.memo((props) => {// 你的组件});

React.memo 等效于 PureComponent,但它只比拟 props。(你也能够通过第二个参数指定一个自定义的比拟函数来比拟新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比拟 state,因为没有繁多的 state 对象可供比拟。但你也能够让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。

如何惰性创立低廉的对象?

第一个常见的应用场景是当创立初始 state 很低廉时, 为防止从新创立被疏忽的初始 state,咱们能够传一个函数给 useState,React 只会在首次渲染时调用这个函数

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

你或者也会偶然想要防止从新创立 useRef() 的初始值。useRef 不会像 useState 那样承受一个非凡的函数重载。相同,你能够编写你本人的函数来创立并将其设为惰性的:

function Image(props) {const ref = useRef(null);

  //  IntersectionObserver 只会被惰性创立一次
  function getObserver() {
    let observer = ref.current;
    if (observer !== null) {return observer;}
    let newObserver = new IntersectionObserver(onIntersect);
    ref.current = newObserver;
    return newObserver;
  }

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

Hook 会因为在渲染时创立函数而变慢吗?

不会。在古代浏览器中,闭包和类的原始性能只有在极其场景下才会有显著的差异。
除此之外,能够认为 Hook 的设计在某些方面更加高效:

  • Hook 防止了 class 须要的额外开支,像是创立类实例和在构造函数中绑定事件处理器的老本。
  • 合乎语言习惯的代码在应用 Hook 时不须要很深的组件树嵌套。这个景象在应用高阶组件、render props、和 context 的代码库中十分广泛。组件树小了,React 的工作量也随之缩小。

传统上认为,在 React 中应用内联函数对性能的影响,与每次渲染都传递新的回调会如何毁坏子组件的 shouldComponentUpdate 优化无关。Hook 从三个方面解决了这个问题。

  • useCallback Hook 容许你在从新渲染之间放弃对雷同的回调援用以使得 shouldComponentUpdate 持续工作:
  • useMemo Hook 使管制具体子节点何时更新变得更容易,缩小了对纯组件的须要。
  • 最初,useReducer Hook 缩小了对深层传递回调的须要,就如上面解释的那样。

如何防止向下传递回调?

在大型的组件树中,咱们举荐的代替计划是通过 contextuseReducer 往下传一个 dispatch 函数:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提醒:`dispatch` 不会在从新渲染之间变动
  const [todos, dispatch] = useReducer(todosReducer);

  return (<TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

TodosApp 外部组件树里的任何子节点都能够应用 dispatch 函数来向上传递 actions

function DeepChild(props) {
  // 如果咱们想要执行一个 action,咱们能够从 context 中获取 dispatch。const dispatch = useContext(TodosDispatch);

  function handleClick() {dispatch({ type: 'add', text: 'hello'});
  }

  return <button onClick={handleClick}>Add todo</button>;
}

总而言之,从保护的角度来这样看更加不便(不必一直转发回调),同时也防止了回调的问题。像这样向下传递 dispatch 是解决深度更新的举荐模式。

React 是如何把对 Hook 的调用和组件分割起来的?

React 放弃对当先渲染中的组件的追踪。多亏了 Hook 标准,咱们得悉 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件外部都有一个「记忆单元格」列表。它们只不过是咱们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取以后的单元格(或在首次渲染时将其初始化),而后把指针挪动到下一个。这就是多个 useState() 调用会失去各自独立的本地 state 的起因。

正文完
 0