乐趣区

关于前端:了解React-Hooks及其常用的几个钩子函数

写在后面

React Hooks 是 React 团队在两年前的 16.8 版本推出的一套全新的机制。作为最支流的前端框架,React 的 API 非常稳固,这次更新的公布,让泛滥恐怖新轮子的前端大佬们虎躯一震,毕竟每一次更新又是高老本的学习,这玩意到底好使么?

答案是好用的,对于 React 的开发者而言,只是多了一个抉择。过来的开发方式是基于 Class 组件的,而 hooks 是基于函数组件,这意味着 这两种开发方式能够并存 ,而新的代码能够依据具体情况采纳 Hooks 的形式来实现就行了。这篇文章次要就来介绍一下 Hooks 的劣势 罕用的几个钩子函数

Hooks 的劣势

1. 类组件的有余

  • 代码量多

    相较于函数组件的写法,应用类组件代码量要略多一点,这个是最直观的感触。

  • this 指向

    类组件中总是须要思考 this 的指向问题,而函数组件则能够疏忽。

  • 趋势简单难以保护

    在高版本的 React 中,又更新了一些生命周期函数,因为这些函数相互解耦,很容易造成扩散不集中的写法,漏掉要害逻辑和多了冗余逻辑,导致前期 debug 艰难。相同,hooks 能够把要害逻辑都放在一起,不显得那么割裂,调试起来也易懂一点。

  • 状态逻辑难复用

    在组件之间复用状态逻辑很难,可能要用到 render props(渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(个别都是 div 元素),导致层级冗余。

2. Hooks 带来的益处

  • 逻辑复用

    在组件之前复用状态逻辑,往往须要借助高阶组件等简单的设计模式,这些高级组件会产生冗余的组件节点,让调试变得艰难,上面用一个 demo 来比照一下两种实现形式。

Class

在 class 组件场景下,定义了一个高阶组件,负责监听窗口大小变动,并将变动后的值作为 props 传给下一个组件。

const useWindowSize = Component => {
  // 产生一个高阶组件 HOC,只蕴含监听窗口大小的逻辑
  class HOC extends React.PureComponent {constructor(props) {super(props);
      this.state = {size: this.getSize()
      };
    }
    componentDidMount() {window.addEventListener("resize", this.handleResize); 
    }
    componentWillUnmount() {window.removeEventListener("resize", this.handleResize);
    }
    getSize() {return window.innerWidth > 1000 ? "large":"small";}
    handleResize = ()=> {const currentSize = this.getSize();
      this.setState({size: this.getSize()
      });
    }
    render() {
      // 将窗口大小传递给真正的业务逻辑组件
      return <Component size={this.state.size} />;
    }
  }
  return HOC;
};

接下来能够在自定义组件中能够调用 useWindowSize 这样的函数来产生一个新组件,并自带 size 属性,例如:

class MyComponent extends React.Component{render() {const { size} = this.props;
    if (size === "small") return <SmallComponent />;
    else return <LargeComponent />;
  }
}
// 应用 useWindowSize 产生高阶组件,用于产生 size 属性传递给真正的业务组件
export default useWindowSize(MyComponent); 

上面看下 Hooks 的实现形式

Hooks

const getSize = () => {return window.innerWidth > 1000 ? "large" : "small";}
const useWindowSize = () => {const [size, setSize] = useState(getSize());
  useEffect(() => {const handler = () => {setSize(getSize())
    };
    window.addEventListener('resize', handler);
    return () => {window.removeEventListener('resize', handler);
    };
  }, []);
  
  return size;
};

应用:

const Demo = () => {const size = useWindowSize();
  if (size === "small") return <SmallComponent />;
  else return <LargeComponent />;
};

从下面的例子中通过 Hooks 的形式对窗口大小进行了封装,从而将其变成一个可绑定的数据源。这样当窗口大小发生变化时,应用这个 Hook 的组件就都会从新渲染。而且代码也更加简洁和直观,不会产生额定的组件节点,也不显得那么冗余了。

  • 业务代码更加聚合

上面举一个最常见的计时器的例子。

class

let timer = null
componentDidMount() {timer = setInterval(() => {// ...}, 1000)
}
// ...
componentWillUnmount() {if (timer) clearInterval(timer)
}

Hooks

useEffect(() => {let timer = setInterval(() => {// ...}, 1000)
    return () => {if (timer) clearInterval(timer)
    }
}, [//...])

Hooks 的实现形式能让代码更加集中,逻辑也更清晰。

  • 写法简洁

这个就不举例了,能够从字面意思了解,应用函数组件的确能少些很多代码,懂得都懂,嘻嘻~

几个内置 Hooks 的作用以及应用思考

useState:让函数组件具备维持状态的能力

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

长处:

让函数组件具备维持状态的能力 ,即:在一个函数组件的屡次渲染之间,这个 state 是 共享 的。便于保护状态。

毛病:

一旦组件有本人状态,意味着组件如果从新创立,就须要有复原状态的过程,这通常会让组件变得更简单。

用法:

  1. useState(initialState) 的参数 initialState 是创立 state 的初始值。

它能够是任意类型,比方数字、对象、数组等等。

  1. useState() 的返回值是一个有着两个元素的数组。第一个数组元素用来读取 state 的值,第二个则是用来设置这个 state 的值。

在这里要留神的是,state 的变量(例子中的 count)是只读的,所以咱们必须通过第二个数组元素 setCount 来设置它的值。

  1. 如果要创立多个 state,那么咱们就须要屡次调用 useState。

什么样的值应该保留在 state 中?

通常来说,咱们要遵循的一个准则就是:state 中不要保留能够通过计算失去的值

  • 从 props 传递过去的值。有时候 props 传递过去的值无奈间接应用,而是要通过肯定的计算后再在 UI 上展现,比如说排序。那么咱们要做的就是每次用的时候,都从新排序一下,或者利用某些 cache 机制,而不是将后果间接放到 state 里。
  • 从 URL 中读到的值。比方有时须要读取 URL 中的参数,把它作为组件的一部分状态。那么咱们能够在每次须要用的时候从 URL 中读取,而不是读出来间接放到 state 里。
  • 从 cookie、localStorage 中读取的值。通常来说,也是每次要用的时候间接去读取,而不是读出来后放到 state 里。

useEffect:执行副作用

useEffect(fn, deps);

useEffect,顾名思义,用于执行一段副作用。

什么是副作用?

通常来说,副作用是指一段和以后执行后果无关的代码。比如说要批改函数内部的某个变量,要发动一个申请,等等。

也就是说,在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染进去的 UI 的。

对应到 Class 组件,那么 useEffect 就涵盖了 ComponentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期办法。不过如果你习惯了应用 Class 组件,那千万不要依照把 useEffect 对应到某个或者某几个生命周期的办法。你只有记住,useEffect 是每次组件 render 完后判断依赖并执行就能够了。

useEffect 还有两个非凡的用法:没有依赖项,以及依赖项作为空数组。咱们来具体分析下。

  1. 没有依赖项,则每次 render 后都会从新执行。例如:
useEffect(() => {
  // 每次 render 完肯定执行
  console.log('渲染...........');
});
  1. 空数组作为依赖项,则只在首次执行时触发,对应到 Class 组件就是 componentDidMount。例如:
useEffect(() => {
  // 组件首次渲染时执行,等价于 class 组件中的 componentDidMount
  console.log('did mount........');
}, []);

小结用法:

总结一下,useEffect 让咱们可能在上面四种机会去执行一个回调函数产生副作用:

  1. 每次 render 后执行:不提供第二个依赖项参数。

比方 useEffect(() => {})。

  1. 仅第一次 render 后执行:提供一个空数组作为依赖项。

比方 useEffect(() => {}, [])。

  1. 第一次以及依赖项发生变化后执行:提供依赖项数组。

比方 useEffect(() => {}, [deps])。

  1. 组件 unmount 后执行:返回一个回调函数。

比方 useEffect() => { return () => {}}, [])。

useCallback:缓存回调函数

useCallback(fn, deps)

为什么要应用 useCallback?

在 React 函数组件中,每一次 UI 的变动,都是通过从新执行整个函数来实现的,这和传统的 Class 组件有很大区别:函数组件中并没有一个间接的形式在屡次渲染之间维持一个状态。

function Counter() {const [count, setCount] = useState(0);
  const handleIncrement = () => setCount(count+1);
  return <button onClick={handleIncrement}>+</button>
}

思考下这个过程。每次组件状态发生变化的时候,函数组件实际上都会从新执行一遍。在每次执行的时候,实际上都会创立一个新的事件处理函数 handleIncrement

这也意味着,即便 count 没有发生变化,然而函数组件因为其它状态发生变化而从新渲染时(函数组件从新被执行),这种写法也会每次创立一个新的函数。创立一个新的事件处理函数,尽管不影响后果的正确性,但其实是没必要的。因为这样做不仅减少了零碎的开销,更重要的是:每次创立新函数的形式会让接管事件处理函数的组件,须要从新渲染

比方这个例子中的 button 组件,接管了 handleIncrement,并作为一个属性。如果每次都是一个新的,那么这个 React 就会认为这个组件的 props 产生了变动,从而必须从新渲染。因而,咱们须要做到的是:只有当 count 发生变化时,咱们才须要从新定一个回调函数。而这正是 useCallback 这个 Hook 的作用。

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

function Counter() {const [count, setCount] = useState(0);
  const handleIncrement = useCallback(() => setCount(count + 1),
    [count], // 只有当 count 发生变化时,才会从新创立回调函数
  );
  return <button onClick={handleIncrement}>+</button>
}

useMemo:缓存计算的后果

useMemo(fn, deps);

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

这里的 fn 是产生所需数据的一个 计算函数 。通常来说,fn 会应用 deps 中申明的一些变量来生成一个后果,用来渲染出最终的 UI

这个场景应该很容易了解:如果某个 数据 是通过其它数据计算失去的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该须要从新计算。

防止反复计算

通过 useMemo 这个 Hook,能够防止在用到的数据没发生变化时进行的反复计算。尽管例子展现的是一个很简略的场景,但如果是一个简单的计算,那么对于 晋升性能 会有很大的帮忙。

举个例子:

const calc = (a, b) => {
    // 假如这里做了简单的计算,临时用次幂模仿
    return a ** b;
}
const MyComponent = (props) => {const {a, b} = props;
    const c = calc(a, b);
    return <div>c: {c}</div>;
}

如果 calc 计算耗时 1000ms,那么每次渲染都要期待这么久,怎么优化呢?

a, b 值不变的状况下,得出的 c 定是雷同的。

所以咱们能够用 useMemo 把值给缓存起来,防止反复计算雷同的后果。

const calc = (a, b) => {
    // 假如这里做了简单的计算,临时用次幂模仿
    return a ** b;
}
const MyComponent = (props) => {const {a, b} = props;
    // 缓存
    const c = React.useMemo(() => calc(a, b), [a, b]);
    return <div>c: {c}</div>;
}

useCallback 的性能其实是能够用 useMemo 来实现的:

 const myEventHandler = useMemo(() => {
   // 返回一个函数作为缓存后果
   return () => {// 在这里进行事件处理}
 }, [dep1, dep2]);

小结一下:

感觉到这有这种感觉,其实 hook 就是建设了一个绑定某个后果到依赖数据的关系。只有当依赖变了,这个后果才须要被重新得到。

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

const myRefContainer =useRef(initialValue);

咱们能够把 useRef 看作是在函数组件之外创立的一个容器空间。在这个容器上,咱们能够通过惟一的 current 属设置一个值,从而在函数组件的屡次渲染之间共享这个值。

useRef 的重要的性能

1. 存储跨渲染的数据

应用 useRef 保留的数据个别是和 UI 的渲染无关的,因而当 ref 的值发生变化时,是不会触发组件的从新渲染的,这也是 useRef 区别于 useState 的中央。

举例:

 const [time, setTime] = useState(0);
 // 定义 timer 这样一个容器用于在跨组件渲染之间保留一个变量 
 const timer = useRef(null);

  const handleStart = useCallback(() => {
    // 应用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => { setTime((time) => time + 1); }, 100);
  }, []);

2. 保留某个 DOM 节点的援用

是在某些场景中,咱们必须要取得实在 DOM 节点的援用,所以联合 React 的 ref 属性和 useRef 这个 Hook,咱们就能够取得实在的 DOM 节点,并对这个节点进行操作。

React 官网例子:

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

了解:

能够看到 ref 这个属性提供了取得 DOM 节点的能力,并利用 useRef 保留了这个节点的利用。这样的话,一旦 input 节点被渲染到界面上,那咱们通过 inputEl.current 就能拜访到实在的 DOM 节点的实例了

useContext:定义全局状态

为什么要应用 useContext?

React 组件之间的状态传递只有一种形式,那就是通过 props。毛病:这种传递关系只能在父子组件之间进行。

那么问题呈现:跨档次,或者同层的组件之间要如何进行数据的共享?这就波及到一个新的命题:全局状态治理

react 提供的解决方案:Context 机制。

具体原理:

React 提供了 Context 这样一个机制,可能让所有在某个组件开始的组件树上创立一个 Context。这样这个组件树上的所有组件,就都能拜访和批改这个 Context 了。

那么在函数组件里,咱们就能够应用 useContext 这样一个 Hook 来治理 Context。

应用:(这儿用了官网例子)

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
// 创立一个 Theme 的 Context

const ThemeContext = React.createContext(themes.light);
function App() {
  // 整个利用应用 ThemeContext.Provider 作为根组件
  return (
    // 应用 themes.dark 作为以后 Context 
    <ThemeContext.Provider value={themes.dark}>
      <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>
  );
}

长处:

Context 提供了一个不便在多个组件之间共享数据的机制。

毛病:

Context 相当于提供了一个定义 React 世界中全局变量的机制,而全局变量则意味着两点:

1. 会让调试变得艰难,因为你很难跟踪某个 Context 的变动到底是如何产生的。

2. 让组件的复用变得艰难,因为一个组件如果应用了某个 Context,它就必须确保被用到的中央肯定有这个 ContextProvider 在其父组件的门路上。

理论利用场景

因为以上毛病,所以在 React 的开发中,除了像 Theme、Language 等高深莫测的须要全局设置的变量外),咱们很少会应用 Context 来做太多数据的共享。须要再三强调的是,Context 更多的是提供了一个弱小的机制,让 React 利用具备定义全局的响应式数据的能力。

此外,很多状态治理框架,比方 Redux,正是利用了 Context 的机制来提供一种更加可控的组件之间的状态管理机制。因而,了解 Context 的机制,也能够让咱们更好地去了解 Redux 这样的框架实现的原理。

最初

感觉这次的内容不多不少。其实理解学会了 useState 和 useEffect 这两个 外围 Hooks,根本能实现绝大多数 React 性能的开发了。

useCallback、useMemo、useRef 和 useContext。这几个 Hook 都是为了解决函数组件中遇到的特定问题。

还有几个比拟边缘的 hook 这里就不再写了,有趣味的大佬能够移步到官网文档上看看。

码字不易,也辛苦大佬们领导交换~

团队

TNTWeb – 腾讯新闻前端团队,TNTWeb 致力于行业前沿技术摸索和团队成员集体能力晋升。为前端开发人员整顿出了小程序以及 web 前端技术畛域的最新优质内容,每周更新 ✨,欢送 star,github 地址:https://github.com/tnfe/TNT-Weekly

退出移动版