React 要做的工作就是每当 Model(state+props)
发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。
而 class
组件并一定是最合适的实现方式,比如说继承和实例两个重要的特性,在 react 中是很少被使用到,毕竟 React 组件之间是不会互相继承的,同时因为所有 UI 都是由状态驱动的,因此很少会在外部去调用一个类实例(即组件)的方法。
因为 React 推崇声明式编程范式,即所有的 UI 都是声明出来的,不用处理细节的变化过程。因此,通过函数去描述一个组件才是最为自然的方式。Hooks 带来的最大好处:逻辑复用,随着 React Hooks 的引入,函数组件变得更加强大和灵活,可以完成类组件的所有功能,并且更易于理解和编写。
useState
函数组件中并没有一个直接的方式在多次渲染之间维持一个状态,因此 Hook 通过 useState
来管理 state ,它可以让函数组件具有维持状态的能力,和类组件中的 setState 非常类似。
js
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}
useEffect
同样的,函数组件也是没有生命周期的,useEffect 应运而生,它可以接收两个参数
js
useEffect(callback, dependencies)
第一个为要执行的函数 callback,第二个是可选的依赖项数组 dependencies。
在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染出来的 UI 的。
useEffect 是每次组件 render 完后判断依赖并执行的:
- 没有依赖项,则每次 render 后都会重新执行,
- 空数组作为依赖项,则只在首次执行时触发,
js
import React, { useState, useEffect } from "react";
function BlogView({ id }) {
// 设置一个本地 state 用于保存 blog 内容
const [blogContent, setBlogContent] = useState(null);
useEffect(() => {
// useEffect 的 callback 要避免直接的 async 函数,需要封装一下
const doAsync = async () => {
// 当 id 发生变化时,将当前内容清楚以保持一致性
setBlogContent(null);
// 发起请求获取数据
const res = await fetch(`/blog-content/${id}`);
// 将获取的数据放入 state
setBlogContent(await res.text());
};
doAsync();
}, [id]); // 使用 id 作为依赖项,变化时则执行副作用
// 如果没有 blogContent 则认为是在 loading 状态
const isLoading = !blogContent;
return <div>{isLoading ? "Loading..." : blogContent}</div>;
}
useEffect 还允许返回一个函数,用于在组件销毁的时候做一些清理的操作。比如移除事件的监听。这个机制就几乎等价于类组件中的 componentWillUnmount。举个例子,在组件中,我们需要监听窗口的大小变化,以便做一些布局上的调整
js
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({});
useEffect(() => {
// 窗口大小变化事件处理函数
const handler = () => {
setSize(getSize());
};
// 监听 resize 事件
window.addEventListener('resize', handler);
// 返回一个 callback 在组件销毁时调用
return () => {
// 移除 resize 事件
window.removeEventListener('resize', handler);
};
}, []);
useCallback
每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍,这个时候,函数组件中的方法都会被重新创建。这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。
js
useCallback(fn, deps)
这里 fn 是定义的回调函数,deps 是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。这样就能避免重复创建函数组件中的方法
js
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
如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算
js
useMemo(fn, deps);
这里的 fn 是产生所需数据的一个计算函数。通常来说,fn 会使用 deps 中声明的一些变量来生成一个结果,用来渲染出最终的 UI。
如果 deps 中声明的一些变量没有变化,那么函数组件重新渲染时,不会重新触发fn这个计算函数。
js
//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => {
return user.first_name.includes(searchKey));
}
}, [users, searchKey]);
//...
users、searchKey不变化,就不会重新算usersToShow。除了避免重复计算之外,useMemo 还有一个很重要的好处:避免子组件的重复渲染。
useRef
在类组件中,我们可以定义类的成员变量,以便能在对象上通过成员属性去保存一些数据。但是在函数组件中,是没有这样一个空间去保存数据的。因此,React 让 useRef 这样一个 Hook 来提供这样的功能。
js
const myRefContainer = useRef(initialValue);
我们可以把 useRef 看作是在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。
js
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0);
// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);
// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
这里可以看到,我们使用了 useRef 来创建了一个保存 window.setInterval 返回句柄的空间,从而能够在用户点击暂停按钮时清除定时器,达到暂停计时的目的。
同时你也可以看到,使用 useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。
除了存储跨渲染的数据之外,useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。
js
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>
</>
);
}
useContext
React 提供了 Context 这样一个机制,能够让所有在某个组件开始的组件树上创建一个 Context。
js
// 创建content
const MyContext = React.createContext(initialValue);
//使用content
const value = useContext(MyContext);
具体地
js
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>
);
}
优点:
- 能够进行数据的绑定,让 React 应用具备定义全局的响应式数据的能力。
- 当这个 Context 的数据发生变化时,使用这个数据的组件就能够自动刷新。
缺点:
- 会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
- 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。
如何创建自定义 Hooks?
自定义 Hooks 在形式上其实非常简单,就是声明一个名字以 use 开头的函数,并且在函数中要用到其它 Hooks。
js
import { useState, useCallback }from 'react';
function useCounter() {
// 定义 count 这个 state 用于保存当前数值
const [count, setCount] = useState(0);
// 实现加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count]);
// 实现减 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count]);
// 重置计数器
const reset = useCallback(() => setCount(0), []);
// 将业务逻辑的操作 export 出去供调用者使用
return { count, increment, decrement, reset };
}
使用这个 Hook
js
import React from 'react';
function Counter() {
// 调用自定义 Hook
const { count, increment, decrement, reset } = useCounter();
// 渲染 UI
return (
<div>
<button onClick={decrement}> - </button>
<p>{count}</p>
<button onClick={increment}> + </button>
<button onClick={reset}> reset </button>
</div>
);
}