class 组件存在的问题
- 复杂组件变得难以理解:
- 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
- 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在
componentWillUnmount中移除); - 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
- 难以理解的class: 很多人发现学习ES6的class是学习React的一个障碍。
- 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this; 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
- 组件复用状态很难:
- 在前面为了一些状态的复用我们需要通过高阶组件或render props;
- 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
- 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
- 这些代码让我们不管是编写和设计上来说,都变得非常困难;
为什么需要 Hooks
- Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
- Hooks 是很多 Hook 技术的组合,比如 useState 是一个 hook,useEffect 也是一种 hook;
- 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
- class组件可以定义自己的state,用来保存组件自己内部的状态;
- 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
- 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
- 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
- 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
- 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
- Hook 的出现可以让我们在不编写class的情况下使用state以及其他的React特性;我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
- 函数式组件结合 hooks 可以让整个代码更简洁,并且再也不用考虑 this 相关问题;
Hook 使用规则
- Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)
useState
- useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一
般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。 - useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。这个参数既可以是确定的泛型值,也可以是返回一个该泛型值的箭头函数
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
(如果没有传递参数,那么初始化值为
undefined)。 - useState返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。
- 通过源码我们可以看到,返回数组中的第二个元素是一个
Dispatch<SetStateAction<S>>
类型, 该类型定义为type SetStateAction<S> = S | ((prevState: S) => S);
,也就当我们使用返回的第二个元素修改 改状态时,即可以传入一个泛型值,也可以传入一个箭头函数,当传入箭头函数时,会将上一次的状态值传递给我们。 - 当我们箭头函数时,它就和我们使用 setState 传箭头函数修改状态一样
Effect Hook
- Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
- 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect 要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
- useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:
type EffectCallback = () => (void | (() => void | undefined)); 在这里还函数里我们可以做一些取消的操作。 - React 会在组件更新和卸载的时候执行清除操作;
- useEffect可以写多份,其调用顺序是按照先订阅先执行的规则
Effect 优化
- useEffect的回调函数会在每次渲染时都重新执行, 但有些代码我们只希望执行一次即可,那么使用useEffect 如何完成呢?
- useEffect实际上有两个参数:
- 参数一:执行的回调函数
- 参数二: 是个只读数组,数组里的元素就是一个个state,当其中的任一个 state 发生变化时就重新执行回调函数;当不传时,所有的状态发生改变都会执行该回调函数,若不想依赖任何状态,可以传一个空数组;
useContext
- 使用createContext创建 context,并导出
- 将需要共享数据的子组件包裹在创建的 context.Provider 中, 并设置要传递的数据
- 在需要共享数据的子组件中使用useContext接收需要的 context进行使用
import React, { useState, createContext } from 'react';
export const UserContext = createContext();
export const ThemeContext = createContext();
export default function App() {
return (
<div>
<UserContext.Provider value={{name: "why", age: 18}}>
<ThemeContext.Provider value={{fontSize: "30px", color: "red"}}>
<ContextHookDemo/>
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
function ContextHookDemo(props) {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
console.log(user, theme);
return (
<div>
<h2>ContextHookDemo</h2>
<h3>{user.name}</h3>
<h3>{theme.fontSize}</h3>
</div>
)
}
useReducer
- useReducer 不是 redux 的替代品,它是 useState 的一种替代方案
- 在某些场景下,如果 useState 的处理逻辑比较复杂,可以通过 useReducer 来对其进行拆分
- 它接收三个参数:
- 第一个是 reducer 函数
- 第二个是 初始值
- 第三个是一般是在我们对初始值进行一些操作的时候使用的,一般很少用
export default function Home() {
const [state, dispatch] = useReducer(reducer, {counter: 0});
return (
<div>
<h2>Home当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
function reducer(state, action) {
switch(action.type) {
case "increment":
return {...state, counter: state.counter + 1};
case "decrement":
return {...state, counter: state.counter - 1};
default:
return state;
}
}
useCallback
- useCallback 实际的目的是为了进行性能的优化,它是如何做到的呢?
- useCallback 会返回一个函数的 memoized(记忆的)值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
- 通常使用 useCallback 的目的是不希望子组件进行多次渲染,而不是为了函数进行缓存
import React, {useState, useCallback, memo} from 'react';
/**
* useCallback使用场景: 在将一个组件中的函数, 传递给子元素进行回调使用时, 使用useCallback对函数进行处理.
*/
const HYButton = memo((props) => {
console.log("HYButton重新渲染: " + props.title);
return <button onClick={props.increment}>HYButton +1</button>
});
export default function CallbackHookDemo02() {
console.log("CallbackHookDemo02重新渲染");
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
const increment1 = () => {
console.log("执行increment1函数");
setCount(count + 1);
}
const increment2 = useCallback(() => {
console.log("执行increment2函数");
setCount(count + 1);
}, [count]); // 这里useCallback依赖 count 状态
return (
<div>
<h2>CallbackHookDemo01: {count}</h2>
<HYButton title="btn1" increment={increment1}/>
<HYButton title="btn2" increment={increment2}/>
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
上面代码,当我们点击切换时 CallbackHookDemo02 组件会重新渲染,increment1函数会被重新生成,所以每次传给 btn1 的 props 是不一样的,这就导致 btn1每次都会被重新渲染; 而对于 btn2来说,由于 useCallback 依赖的 count 状态没有发生变化,所以increment2 函数不会改变,所以每次传给 btn2 的 props 是不变的,props 变,对高阶组件 memo 来说不会重新渲染。
useMemo
- useMemo 也是用来做性能优化的,它是如何做到的呢?
- 同 useCallBack 一样,useMemo 返回的也是一个 memoized 值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
- 比如在我们进行大量计算的时候,是否有必要每次渲染都重新计算
import React, {useState, useMemo} from 'react';
function calcNumber(count) {
console.log("calcNumber重新计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total;
}
export default function MemoHookDemo01() {
const [count, setCount] = useState(10);
const [show, setShow] = useState(true);
// 如果不使用useMemo,那么下面的这行代码在每次渲染时都会重新执行;
// const total = calcNumber(count);
// 下面我们使用了useMemo, 在依赖的 count 值不变的情况下,重新渲染并不会再去执行calcNumber函数;
const total = useMemo(() => {
return calcNumber(count);
}, [count]);
return (
<div>
<h2>计算数字的和: {total}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
- 还有就是组件传递相同内容的对象时,使用 useMemo 进行性能优化
import React, { useState, memo, useMemo } from 'react';
const HYInfo = memo((props) => {
console.log("HYInfo重新渲染");
return <h2>名字: {props.info.name} 年龄: {props.info.age}</h2>
});
export default function MemoHookDemo02() {
console.log("MemoHookDemo02重新渲染");
const [show, setShow] = useState(true);
// 如果不使用 useMemo 下面的代码,在每次渲染时都会执行,这样每次生成的 info 就不一样,导致传给HYInfo的 props 不一样,从而导致HYInfo组件的重新渲染
// const info = { name: "why", age: 18 };
// 当使用useMemo时,由于每次依赖的都是一个空数组,没有发生变化, 所以info也没有变化,所以在MemoHookDemo02重新渲染时,由于传递给HYInfo的 props 没有发生变化,所以HYInfo组件不会重新渲染
const info = useMemo(() => {
return { name: "why", age: 18 };
}, []);
return (
<div>
<HYInfo info={info} />
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
useCallback 和 useMemo 的区别
- useCallback是对传入的函数做优化的,而useMemo是对返回值做优化的
- 可以当useMemo传入的函数返回值是一个函数时,可以实现useCallback, 比如下面increment2和increment3是等效的
const increment2 = useCallback(() => {
console.log("执行increment2函数");
setCount(count + 1);
}, [count]);
const increment3 = useMemo(() => {
return ()=>{
console.log("执行increment2函数");
setCount(count + 1);
}
}, [count]);
useRef
- useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变,
const titleRef = useRef();
- 将创建的 ref 赋值给组件的 ref 属性,
<h2 ref={titleRef}>RefHookDemo</h2>
- 通过 ref 修改 DOM,
titleRef.current.innerHTML = "Hello World";
- 可以利用 useRef 保存上次的值
useImperativeHandle
- 函数式组件中我们如果想使用 ref,需要结合 forwardRef 将 ref 转发到子组件中,然后子组件拿到父组件中创建的 ref,绑定到自己的某个元素中使用;
const HYInput = forwardRef((props, ref) => {
return <input ref={ref} type="text"/>
})
export default function ForwardRefDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
- forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件,这样会存在以下问题:
- 直接暴露给父组件带来的问题是某些情况下的不可控
- 父组件可以拿到 DOM 后进行任意的操作
- 但是,事实上有时候只希望父组件做特定的操作,而不是可以任意操作
- 解决上面问题,我们就可以通过useImperativeHandle对父组件只暴露固定的操作
- 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
- 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
- 比如我调用了 focus函数,甚至可以调用 printHello函数;
const HYInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}), [inputRef])
return <input ref={inputRef} type="text"/>
})
export default function UseImperativeHandleHookDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
useLayoutEffect
- useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
-
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
自定义 Hook
- 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
- 自定义 Hook 本质上只是一种函数代码逻辑的抽取,并不算 React 的特性
- 比如我们想所有的组件在创建和销毁的时候都做一些处理,那么我们就可以使用自定义 hook 了, 如果定义的是普通函数(非 use 开头)的话,函数内是不能使用 hook 技术的
const Home = (props) => {
// 使用自定义 hook 函数
useLoggingLife("Home");
return <h2>Home</h2>
}
// 使用自定义 hook 函数
const Profile = (props) => {
useLoggingLife("Profile");
return <h2>Profile</h2>
}
export default function CustomHookDemo() {
useLoggingLife("CustomLifeHookDemo01");
return (
<div>
<h2>CustomHookDemo</h2>
<Home/>
<Profile/>
</div>
)
}
// 自定义 hook, 在函数式组件创建和销毁的时候都做处理
function useLoggingLife(name) {
useEffect(() => {
console.log(`${name}组件被创建出来了`);
return () => {
console.log(`${name}组件被销毁掉了`);
}
}, []);
}
- 可以将获取屏幕滚动位置抽取到一个 hook 函数中
export default function CustomScrollPositionHook() {
const position = useScrollPosition();
return (
<div style={{padding: "1000px 0"}}>
<h2 style={{position: "fixed", left: 0, top: 0}}>CustomScrollPositionHook: {position}</h2>
</div>
)
}
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
}
document.addEventListener("scroll", handleScroll);
return () => {
document.removeEventListener("scroll", handleScroll)
}
}, []);
return scrollPosition;
}
- 可以将与界面有关的内容抽取到一个 hook 函数中读取
export default function CustomDataStoreHook() {
const [name, setName] = useLocalStorage("name");
return (
<div>
<h2>CustomDataStoreHook: {name}</h2>
<button onClick={e => setName("kobe")}>设置name</button>
</div>
)
}
function useLocalStorage(key) {
const [name, setName] = useState(() => {
const name = JSON.parse(window.localStorage.getItem(key));
return name;
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(name));
}, [name]);
return [name, setName];
}