一、理解 Hooks: React 为什么要发明 Hooks
react 组件得本质
Modal => View 得映射,其中 Modal 对应 react 中的 state + props。
react 通过 JSX 将 Modal 与 View 进行 数据绑定。
1)Class 组件的问题
在于 类 最主要的特性并没有被利用。
-
React 组件之间是不会互相继承的。 比如说,你不会创建一个 Button 组件,然后再创建一个 DropdownButton 来继承 Button。所以说,React 中其实是没有利用到 Class 的继承特性的。
-
因为所有 View 都是由状态驱动 的,因此很少会在外部去调用一个类实例(即组件)的方法。要知道,组件的所有方法都是在内部调用,或者作为生命周期方法被自动调用的。
因此,相比较 Class 组件,函数组件反而更适合去描述 Modal => View 的映射。但是函数组件没有 state,也没有 生命周期方法。
2)Hooks的诞生
为了解决 函数组件 没有 状态 和 监听状态变化,从而能够触发 函数组件的重新渲染。
- 定义: 把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。
Hooks的好处
-
简化了逻辑复用 在 Class组件 中 要实现逻辑复用,必须借助于 高阶组件 等设计模式。
-
有助于关注分离: 即 针对同一个业务逻辑的代码 尽可能的 聚合到一块。把业务逻辑清晰的分离开。 比如: useEffect() 集合了 Class组件中的 componentDidMount 、 componentWillUnmount、componentDidUpdate 三个生命周期得功能。
二、内置 Hooks:
内置Hook得特性如下: 各Hooks 得具体描述和用法,请参照官网
1)useEffect: 每次组件 render 完后判断依赖并执行。
执行时机:
- 每次 render 后执行:不提供第二个依赖项参数。
useEffect(() => {});
- 仅第一次 render 后执行:提供一个空数组作为依赖项
useEffect(() => {}, []);
- 第一次以及依赖项发生变化后执行:提供依赖项数组
useEffect(() => {}, [deps]);
- 组件 unmount 后执行:返回一个回调函数
useEffect(() => {
return () => {}
}, []);
2)useCallback: 缓存回调函数。
用法:
const memoizedCallback = useCallback(fn, deps);
fn: 定义的回调函数
deps:依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。
举例说明问题:
import React , { useState } from 'react';
function Counter () {
const [count, setCount] = useState(0);
const handleCountChange = () => setCount(count + 1);
return <Button onClick={handleCountChange}>{count}</Button>
}
每一次的 UI 变化,都是通过重新执行整个函数来完成的。这和 Class组件 的区别在于:函数组件没有一个直接的方式在多次渲染之间维持一个状态。 这就导致了:即使 count 没有发生变化,但是函数组件 每次因为其他状态的变化而重新渲染时,都会创建一个新的事件处理函数 handleCountChange,从而导致,接收事件处理函数的组件,需要重新渲染。
上面例子的优化:
import React , { useState, useCallback } from 'react';
function Counter () {
const [count, setCount] = useState(0);
const handleCountChange = useCallback(() => setCount(count + 1), [count]);
return <Button onClick={handleCountChange}>{count}</Button>
}
3)useMemo:缓存计算结果
用法:
const memoizedValue = useMemo(fn, deps);
fn: 产生所需数据的一个计算函数
deps:通常来说,fn 会使用 deps 中声明的一些变量来生成一个结果,用来渲染出最终的UI
使用场景:如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,即依赖的数据发生变化的时候,才需要重新计算。
好处:
- 可以避免在用到的数据没有发生变化时进行重新计算。
- 可以当数据未发生变化时,避免子组件的重复渲染。
举个例子:搜索关键词,展示用户列表
1、未使用 useMemo
import React, { useState, useEffect } from "react";
export default function SearchUserList() {
const [users, setUsers] = useState(null);
const [searchKey, setSearchKey] = useState("");
useEffect(() => {
const doFetch = async () => {
// 组件首次加载时发请求获取用户数据
const res = await fetch("https://reqres.in/api/users/");
setUsers(await res.json());
};
doFetch();
}, []);
let usersToShow = null;
if (users) {
// 无论组件为何刷新,这里一定会对数组做一次过滤的操作
usersToShow = users.data.filter((user) =>
user.first_name.includes(searchKey),
);
}
return (
<div>
<input
type="text"
value={searchKey}
onChange={(evt) => setSearchKey(evt.target.value)}
/>
<ul>
{usersToShow &&
usersToShow.length > 0 &&
usersToShow.map((user) => {
return <li key={user.id}>{user.first_name}</li>;
})}
</ul>
</div>
);
}
2、使用 useMemo
//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => {
return user.first_name.includes(searchKey));
}
}, [users, searchKey]);
//...
4)useRef:
1、在多次渲染之间共享数据
2、保存某个 DOM 节点的引用
用法
const myRefContainer = useRef(initialValue);
通过 myRefContainer.current 在多次渲染之间共享这个值。
举个例子 1、在多次渲染之间共享数据
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 保存的数据一般与 UI 的渲染无关,所以当 ref 的值发生变化时,不会触发组件的重新渲染。
2、保存某个 DOM 节点的引用
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>
</>
);
}
总结:结合 React 的 ref 属性和 useRef(), 就可以获取到真实的 DOM 节点,并对这个节点进行操作。
3、自定义Hooks
1)定义:声明一个名字以 use 开头的函数,且在其内部使用了其他 Hooks。
其他 Hooks: 可以是内置 Hooks,也可以是其他自定义 Hooks。这样才能够让组件刷新,或者去产生副作用。
与 工具类 的区别比较:
Hooks 可以管理当前组件的state,而在 工具类 中无法直接修改组件的 state,也就无法在数据改变的时候触发组件的重新渲染。 工具类 必须在数据改变后,调用setState 更新state之后 组件才会重新渲染。
2)三种典型的使用场景
-
封装通用逻辑
比如:发起异步请求获取数据并显示界面UI (请求成功、请求失败、loading 三种UI界面的显示)
-
监听浏览器状态
比如:
- 界面需要根据窗口大小的变化重新布局
- 在页面滚动时,需要根据滚动条的位置,来决定是否显示一个“返回顶部”的按钮
-
拆分复杂组件
尽量将相关的逻辑做成独立的 Hooks,然后再函数组件中使用这些 Hooks,通过参数传递和返回值让 Hooks之间完成交互。
这里拆分的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。
4、全局状态管理:Redux
特点:
-
Redux Store 是全局唯一的:即整个应用程序一般只有一个 Store。
-
Redux Store 是树状结构:可以更天然地映射到组件树的结构,虽然不是必须的。
两个典型使用场景:
-
跨组件的状态共享:当某个组件发起一个请求时,将某个 Loading 的数据状态设为 True,另一个全局状态组件则显示 Loading 的状态。
-
同组件多个实例的状态共享:某个页面组件初次加载时,会发送请求拿回了一个数据,切换到另外一个页面后又返回。这时数据已经存在,无需重新加载。设想如果是本地的组件 state,那么组件销毁后重新创建,state 也会被重置,就还需要重新获取数据。
React 与 Redux 是如何建立联系的呢?
主要是两点:
-
React 组件能够在依赖的 Store 的数据发生变化时,重新 Render;
-
在 React 组件中,能够在某些时机去 dispatch 一个 action,从而触发 Store 的更新。
要实现这两点,就需要 react-redux 库 建立桥梁,实现 react 与 redux 的互通。
在 react-redux 的实现中,为了确保需要绑定的组件能够访问到全局唯一的 Redux Store,利用了 React 的 Context 机制去存放 Store 的信息。通常我们会将这个 Context 作为整个 React 应用程序的根节点。因此,作为 Redux 的配置的一部分,我们通常需要如下的代码:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
异步Action:通过 Middleware 机制来实现拦截器。在 reducer 处理 action 之前被调用。在这个拦截器中,可以自由处理获得的 action,无论是把这个 action 直接传递到 reducer,或者构建新的 action 发送到 reducer,都是可以的。
总结:利用这个机制,Redux 提供了 redux-thunk 这样一个中间件,它如果发现接受到的 action 是一个函数,那么就不会传递给 reducer,而是执行这个函数,并把 dispatch 作为参数传给这个函数,从而在这个函数中你可以自由决定 何时,如何发送 action。
受控组件和非受控组件
受控组件:组件的展示完全由传入的属性决定。
比如: 在HTML中,标签<input>、<textarea>、<select>的值的改变通常是根据用户输入进行更新。在React中,可变状态通常保存在组件的状态属性中,并且只能使用 setState() 更新,而呈现表单的React组件也控制着在后续用户输入时该表单中发生的情况,以这种由React控制的输入表单元素而改变其值的方式,称为:“受控组件”。
总结:受控组件值的变化 是通过 React 组件的 状态属性的更新实现的。
对于每一个表单元素,的受控组件处理都遵循两个步骤: 1、设置一个 State 用于绑定到表单元素的 value 2、监听表单元素的 onChange 事件,将表单值同步到 value 这个 state。
非受控组件: 表单数据由 DOM 本身处理。不受 React 组件的 state 控制。
比如: input 输入就显示最新的值。(可以使用 ref 获取 表单值)
用 Hooks 实现 维护整个表单的状态,并提供根据名字去取值和设值的方法,从而方便表单在组件中的使用。
import { useState, useCallback } from 'react';
const useForm = (initialValues = {}) => {
// 设置整个 form 的状态 values
const [values, setValues] = useState(initialValues);
// 定义一个 errors 状态
const [errors, setErrors] = useState({});
// 提供方法 用于设置 form 表单上某个字段的值
const setFieldValues = useCallback((name, value) => {
setValues((values) => ({
...values,
[name]: value
}))
// 如果存在验证函数,则调用验证用户输入
if (validators[name]) {
const errMsg = validators[name](value);
setErrors((errors) => ({
...errors,
// 如果返回错误信息,则将其设置到 errors 状态,否则清空错误状态。
[name]: errMsg || null
}))
}
}, [validators]);
return { values, errors, setFieldValues }
}
5、React 中的事件处理机制
事件分为两种:
1、原生 DOM 事件:只要原生 DOM 有的事件,在 React 中基本都可以使用,只是写法上采用驼峰法。
React 原生事件的原理: 合成事件。
由于虚拟 DOM 的存在,在 React 中即使绑定一个事件到原生的 DOM 节点,事件也并不是绑定在对应的节点上,而是所有的事件都是绑定在根节点上。然后由 React 统一监听和管理,获取事件后再分发到具体的虚拟 DOM 节点上。
在 React17 之前,所有的事件都是绑定在 document 上的,而从 React17 开始,所有的事件都绑定在整个 App 上的根节点上(利用浏览器事件的冒泡模型实现根节点接收所有的事件),这主要是为了以后页面上可能存在多版本 React 的考虑。
具体来说,React 这么做的原因主要有两个。
-
虚拟 DOM render 的时候,DOM 可能还没有真实的 render 到页面上,所以无法绑定事件。
-
React 可以屏蔽底层事件的细节,避免浏览器的兼容问题。同时,对于 React Native 这种不是通过浏览器 render 的运行时,也能提供一致的 API。
2、自定义事件:利用属性传递回调函数给子组件,实现事件的触发。是纯组件实现的一种机制。