theme: qklhk-chocolate highlight: vs2015
一、为什么需要hook
class组件的优势
- 状态的优势
- 有自己内部的state,可以保存自己内部的状态
- 函数式组件就不行,因为每次调用都会产生新的临时变量
- 生命周期的优势
- class组件有自己的生命周期.
- 函数式组件在使用hook前,如果在函数中发送请求,那么每次重新渲染都会发送请求
- 渲染的优势
- class组件可以再状态改变时,只执行render函数以及我们希望执行的生命周期函数
- 函数式组件在重新渲染时,则会将内部所有函数都重新执行一遍
class组件存在的问题
- 组件变得复杂并且难以理解
- 起初编写的组件,逻辑往往比较简单,,不会特别复杂,但是业务增多以后,calss组件会越来越复杂。
- 比如在componentDidMount中,可能包含大量的逻辑代码:包括网络请求、一些事件监听(还需要在componentWillUnmount中移除)
- 对于这样的class其实非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会变得过度设计,增加代码逻辑。
- 组件复用状态很难:
- 一些组件的状态复用我们需要通过高阶组件或者render props(返回React元素并调用它的函数,而不是实现自己的渲染逻辑)
- 或者通过Provider和Consumer来共享一些状态,但是多次使用Consumer很容易造成嵌套过度
二、hook的出现
简单总结下hooks
- 它可以让我们在不编写class的情况下使用state以及其他的React特性
- 但是我们可以由此延伸出非常多的用法,来让解决上面提到的问题
三、hooks API
useState解析
- 只能在函数最外层调用Hook。不要在循环、条件判断或者钩子函数中调用。
- 只能在React的函数组件中调用Hook。不要再其他JavaScript函数中调用。
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState);
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
注意
React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。
import React, { useState } from "react";
export default function Counter() {
const [friends, setfriends] = useState(["李磊", "韩梅梅", "逍遥", "鬼谷子"]);
function deleteFriends(index) {
const newFriends = [...friends];
newFriends.splice(index, 1);
setfriends(newFriends);
}
return (
<div>
<h2>好友列表</h2>
<ul>
{friends.map((item, index) => {
return (
<div key={index}>
<li>{item}</li>
<button onClick={(e) => deleteFriends(index)}>删除好友</button>
</div>
);
})}
</ul>
</div>
);
}
补充
在源码中setState的定义
我们可以看到,setState还可以传入能拿到上一个的prevState的回调函数,这就意味着,我们可以避开setState在一次操作中,对相同的setState的合并处理:
import React, { useState } from "react";
export default function Counter() {
const [Counter, setCounter] = useState(10);
function handelButtonClick() {
setCounter(Counter + 10);
setCounter(Counter + 10);
setCounter(Counter + 10);
setCounter(Counter + 10);
}
function handelButtonClick2() {
setCounter((prevCounter) => prevCounter + 10);
setCounter((prevCounter) => prevCounter + 10);
setCounter((prevCounter) => prevCounter + 10);
setCounter((prevCounter) => prevCounter + 10);
}
return (
<div>
<h2>当前计数:{Counter}</h2>
<button onClick={handelButtonClick}>+10</button>
<button onClick={handelButtonClick2}>+10</button>
</div>
);
}
handelButtonClick每点击一次只会+10,因为react在同一次操作中对相同的setState做了合并处理。handelButtonClick2每点击一次则会+40
useEffect
Effect Hook出现的的需求
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用
useEffect完成副作用操作。赋值给useEffect的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
- Effect Hook可以让你来完成一些类似于class中生命周期的功能
- 事实上类似于网络请求、手动更新DOM、一些事件监听,都是React更新DOM的一些副作用(Side Effects)
- 所以对于完成这些功能的Hook被称为 Effect Hook
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
清除Effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。
useEffect,传入的回调函数本身有返回一个清除函数,我们可以在这个返回的清除函数中,处理我们需要清除的操作。
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
Effect执行时机
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。
虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
Effect的条件执行(优化)
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
别忘记 useContext 的参数必须是 context 对象本身:
- 正确: useContext(MyContext)
- 错误: useContext(MyContext.Consumer)
- 错误: useContext(MyContext.Provider) 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。).
import React, { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return state;
}
}
export default function Home() {
// const [count, setCount] = useState(0);
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<h2>当前计数:{state.count}</h2>
<button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
<button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
</div>
);
}
useCallback
useCallback的目的是进行新能优化
- useCallback会返回一个函数的memoized(记忆)的值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
但是它的优化,并不是体现在自己单独使用时,而是配合父组件渲染时,可以减少不必要的渲染次数。
import React, { memo, useCallback, useState } from "react";
const MyButton = memo((props) => {
console.log("MYbutton重新渲染了:" + props.title);
return <button onClick={props.increment}>+1</button>;
});
function UseCallBack2() {
console.log("UseCallBack2重新渲染了");
const [count, setCount] = useState(10);
const [login, setLogin] = useState(true);
const increment1 = () => {
console.log("执行了increment函数!");
setCount(count + 1);
};
const increment2 = useCallback(() => {
console.log("执行了increment函数!");
setCount(count + 1);
}, [count]);
return (
<div>
<h2>UseCallBack</h2>
<h2>当前计数:{count}</h2>
<MyButton increment={increment1} title={"btn1"} />
<MyButton increment={increment2} title={"btn2"} />
<button onClick={(e) => setLogin(!login)}>切换除login</button>
</div>
);
}
export default UseCallBack2;
上面代码中的UseCallBack2父组件,在点击切换button时,自身依赖的login发生了变化,那么React就会重新渲染,它所包含子组件也会重新渲染,但是它的子组件并没有发生变化,那么这次的渲染就是多余的,影响了性能。这个时候,我们使用useCallback就可以避免这种情况。
因为memo在比较时,是浅比较props的值,但是我们上面的increment2函数因为使用了useCallback,所以increment2函数的返回值,是没有变化的(因为我们只改变了login没有改变count),然后子组件收到props就没有变化,这时memo就可以判断出来,子组件没有变化,就会不会重新渲染子组件,从而提升了性能。
useMemo
- useMemo实际的目的也是为了进行性能的优化
- 如何进行性能的优化呢?
- useMemo返回的也是一个 memoized(记忆的) 值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
useMemo与useCallback补充:
这两个hook其实很相似,useMemo是返回相同的值,useCallback是返回相同的函数,所以其实useMemo可以实现useCallback的效果,只要return一个函数即可。
const myCallback = useMemo(() => {
return () => {
console.log("执行increment2函数");
setCount(count + 1);
}
}, [count]);
useRef
- useRef返回一个ref对象,并返回的ref对象,在整个应用的生命周期保持不变!
import React, { useRef } from "react";
export default function RefHookDemo01() {
const titleRef = useRef();
const inputRef = useRef();
function changeDOM() {
titleRef.current.innerHTML = "Hello World";
inputRef.current.focus();
}
return (
<div>
<h2 ref={titleRef}>RefHookDemo01</h2>
<input ref={inputRef} type="text" />
<button onClick={(e) => changeDOM()}>修改DOM</button>
</div>
);
}
- 除了常规用法,我们还以用它来存储上一次的值,因为它的特性是在整个生命周期内返回一个不会变化的ref对象。
import React, { useRef, useState, useEffect } from "react";
export default function RefHookDemo02() {
const [count, setCount] = useState(0);
const numRef = useRef(count);
useEffect(() => {
numRef.current = count;
}, [count]);
return (
<div>
<h2>count上一次的值: {numRef.current}</h2>
<h2>count这一次的值: {count}</h2>
<button onClick={(e) => setCount(count + 10)}>+10</button>
</div>
);
}
useImperativeHandle
- useImperativeHandle的出现是为了,在父组件转发ref给子组件,获得子组件的dom元素时,可以指定子组件需要暴露的元素,而不是全部暴露给父组件
- 需要配合forwardRef方法使用
import React, { forwardRef, memo, useRef, useImperativeHandle } from "react";
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current.focus();
},
}),
[inputRef]
);
return <input ref={inputRef} type="text"></input>;
});
const useImperativeHandleDemo = memo(() => {
const inputRef = useRef();
return (
<div>
<h2>useImperativeHandle</h2>
<MyInput ref={inputRef}></MyInput>
<button onClick={(e) => inputRef.current.focus()}>点击聚焦</button>
</div>
);
});
export default useImperativeHandleDemo;
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。
自定义hook
自定义 Hook 必须以 “use” 开头吗? 必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
在两个组件中使用相同的 Hook 会共享 state 吗? 不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
自定义获取页面位置的hook
import { useState, useEffect } from 'react';
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
}
document.addEventListener("scroll", handleScroll);
return () => {
document.removeEventListener("scroll", handleScroll)
}
}, []);
return scrollPosition;
}
export default useScrollPosition;