Hooks的核心原理梳理
我们前端都在诟病专业版,它的组件,它的耦合嵌套之深,它的性能。
我们希望改善,我们认为,如果……就好了。
如果重构就好了,如果技术栈统一就好了,如果有规范就好了。
其实,不用等,我们只要在写代码,就可以进行优化。
关键的一点,就是,如何写正确的代码。如果不能写正确的代码,以前的老问题没解决,又会加上新的问题。
我们现在基本上能达成一个共识,就是函数组件比类组件要好(参见https://reactjs.bootcss.com/docs/hooks-intro.html难以理解的 class)
所以,我们在写新组件的时候,优先会写成函数组件。
基于以上,我们来好好讨论,了解一下函数组件中常用的hooks。
react
特性
-
虚拟dom机制
-
diff算法
-
-
jsx语法
原理/本质
-
-
数据驱动UI
-
从 Model 到 View 的映射
React 本身正是为动态的状态变化而设计的,而可能引起状态变化的原因基本只有两个:用户操作产生的事件,比如点击了某个按钮。副作用产生的事件,比如发起某个请求正确返回了。这两种事件本身并不会导致组件的重新渲染,但我们在这两种事件处理函数中,一定是因为改变了某个状态,这个状态可能是 State 或者 Context,从而导致了 UI 的重新渲染。
-
元素
-
组件
-
形式-树状结构
-
内置组件
div input等 小写字母
-
自建组件
大写字母开头
-
-
-
状态
-
props
-
state
-
useState
-
-
-
JSX
-
语法糖
-
数据驱动UI变化
-
数据绑定
-
UI 的展现看成一个函数的执行过程
Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。
-
-
-
其他
-
Fiber
hooks
特点
可以实现class组件的所有能力
-
状态管理
-
生命周期
-
函数式思想
区别于dialog.show(),对象方式,细粒度控制UI
目的
-
了解hooks的边界
-
能做什么
-
不能做什么
-
原理
-
为什么要发明hooks
-
hooks 钩子 定义
-
State => View 映射
-
Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。
-
模型
-
-
简化逻辑复用
-
Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用
-
替代高阶组件
-
eg 窗口大小变化
-
-
-
关注分离
-
高内聚,低耦合
Class 组件中,你不得不把同一个业务逻辑的代码分散在类组件的不同生命周期的方法中。
-
代码区别
图的左侧是 Class 组件,右侧是函数组件结合 Hooks。蓝色和黄色代表不同的业务功能。可以看到,在 Class 组件中,代码是从技术角度组织在一起的,例如在 componentDidMount 中都去做一些初始化的事情。而在函数组件中,代码是从业务角度组织在一起的,相关代码能够出现在集中的地方,从而更容易理解和维护。
-
解决了 Class 组件代码冗余、难以逻辑复用的问题
-
-
-
useEffect就是生命周期函数吗
与class组件差异
-
思考方式差异
-
class
-
思考方式:某个生命周期方法中我要做什么
-
-
class BlogView extends React.Component { // ... componentDidMount() { // 组件第一次加载时去获取 Blog 数据 fetchBlog(this.props.id); } componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { // 当 Blog 的 id 发生变化时去获取博客文章 fetchBlog(this.props.id); }
-
-
hooks
-
思考方式:当某个状态发生变化时,我要做什么
function BlogView({ id }) { useEffect(() => { // 当 id 变化时重新获取博客文章 fetchBlog(id); }, [id]); // 定义了依赖项 id }
-
忘掉生命周期概念
-
-
-
-
state差异
-
class
-
constructor
-
state-一个对象
-
定义类的实例成员
-
-
-
hooks
-
useState可以有多个(更好语义化)
-
state改变,重新渲染
-
-
-
-
设计模式
-
class
-
面向对象开发
-
构造函数
-
在所以其它代码执行之前的一次性初始化工作
-
-
-
-
hooks
-
函数式开发
-
hooks内部进行初始化
import { useRef } from 'react'; // 创建一个自定义 Hook 用于执行一次性代码 function useSingleton(callback) { // 用一个 called ref 标记 callback 是否执行过 const called = useRef(false); // 如果已经执行过,则直接返回 if (called.current) return; // 第一次调用时直接执行 callBack(); // 设置标记为已执行过 called.current = true; } import useSingleton from './useSingleton'; const MyComp = () => { // 使用自定义 Hook useSingleton(() => { console.log('这段代码只执行一次'); }); return ( <div>My Component</div> ); };
-
-
-
-
代码
-
class
-
不同生命周期
-
松散
-
-
-
hooks
-
内聚
-
-
-
其他
-
hooks没法实现这几个生命周期
-
getSnapshotBeforeUpdate, componentDidCatch, getDerivedStateFromError
-
用的少
-
-
-
hooks和class可以存于同一个项目中
-
class没必要一定重构成hooks
-
能正确工作的代码就是好代码
-
-
类组件和函数组件可以互相引用
-
Hooks 很容易就能转换成高阶组件,并供类组件使用
-
介绍
-
useState
-
总结
useState(initialState) 的参数 initialState 是创建 state 的初始值,它可以是任意类型,比如数字、对象、数组等等。
useState() 的返回值是一个有着两个元素的数组。第一个数组元素用来读取 state 的值,第二个则是用来设置这个 state 的值。在这里要注意的是,state 的变量(例子中的 count)是只读的,所以我们必须通过第二个数组元素 setCount 来设置它的值。 如果要创建多个 state,那么我们就需要多次调用 useState。
-
与class组件区别
-
类组件中的 state 只能有一个(一个对象),useState可以有多个(更好语义化)
-
-
注意
-
state 中永远不要保存可以通过计算得到的值
-
props传递过来的值
-
URL获取的值
-
从 cookie、localStorage 中读取的值
-
-
清空state
-
编辑或者创建完成以后
-
-
-
-
useEffect
-
理解
-
副作用
-
useEffect(callback, dependencies)
-
-
useEffect 是每次组件 render 完后判断依赖并执行
对应到 Class 组件,那么 useEffect 就涵盖了 ComponentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期方法。不过如果你习惯了使用 Class 组件,那千万不要按照把 useEffect 对应到某个或者某几个生命周期的方法。你只要记住,useEffect 是每次组件 render 完后判断依赖并执行就可以了。
-
-
用法
每次 render 后执行:不提供第二个依赖项参数。比如useEffect(() => {})。 仅第一次 render 后执行:提供一个空数组作为依赖项。比如useEffect(() => {}, [])。 第一次以及依赖项发生变化后执行:提供依赖项数组。比如useEffect(() => {}, [deps])。 组件 unmount 后执行:返回一个回调函数。比如useEffect() => { return () => {} }, [])。
-
无依赖项
-
每次 render 后都会重新执行
-
-
依赖项[ ]
-
只在首次执行时触发
-
= componentDidMount
-
-
-
依赖项[id]
-
首次以及id变化执行
-
-
return 卸载
import React, { useEffect } from 'react'; import comments from './comments'; function BlogView({ id }) { const handleCommentsChange = useCallback(() => { // 处理评论变化的通知 }, []); useEffect(() => { // 获取博客内容 fetchBlog(id); // 监听指定 id 的博客文章的评论变化通知 const listener = comments.addListener(id, handleCommentsChange); return () => { // 当 id 发生变化时,移除之前的监听 comments.removeListener(listener); }; }, [id, handleCommentsChange]) } useEffect 接收的返回值是一个回调函数,这个回调函数不只是会在组件销毁时执行,而且是每次 Effect 重新执行之前都会执行,用于清理上一次 Effect 的执行结果。
-
-
依赖项
-
依赖项中定义的变量一定是会在回调函数中用到的,否则声明依赖项其实是没有意义的。
-
依赖项一般是一个常量数组,而不是一个变量。
-
死循环
-
-
React 会使用浅比较来对比依赖项是否发生了变化,所以要特别注意数组或者对象类型。
如果你是每次创建一个新对象,即使和之前的值是等价的,也会被认为是依赖项发生了变化。这是一个刚开始使用 Hooks 时很容易导致 Bug 的地方。
-
-
规则
-
只能在顶级作用域使用
-
顺序执行
React 组件内部,其实是维护了一个对应组件的固定 Hooks 执行列表的,以便在多次渲染之间保持 Hooks 的状态,并做对比。
如果if,第一次和第二次hook不一样,就会报错。 if,else不会报错
-
所有hook必须被执行到
-
不能在循环,条件判断,函数内执行
-
-
只能在函数组件使用
-
或者自定义hook使用
-
-
安装eslint-plugin-react-hooks
npm install eslint-plugin-react-hooks --save-dev 然后在你的 ESLint 配置文件中加入两个规则:rules-of-hooks 和 exhaustive-deps。如下: { "plugins": [ // ... "react-hooks" ], "rules": { // ... // 检查 Hooks 的使用规则 "react-hooks/rules-of-hooks": "error", // 检查依赖项的声明 "react-hooks/exhaustive-deps": "warn" } }
-
-
-
useCallback
-
缓存回调函数
-
多次渲染间,维持一个状态
-
避免,每次创建新函数,让接收事件处理函数的组件重新渲染
-
-
-
只有当依赖项目变化,才重新定义回调函数
useCallback(fn, deps) fn 是定义的回调函数,deps 是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。 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);
-
-
-
useRef
-
在多次渲染之间共享数据
-
唯一的 current 属性
-
-
保存某个 DOM 节点的引用
-
-
useContext
自定义hook
-
定义
-
函数中用到hooks
-
以use开头的函数
-
-
作用
-
复用逻辑
-
语义化
-
-
应用
-
抽取业务逻辑
-
封装通用逻辑
-
useAsync
import { useState } from 'react'; const useAsync = (asyncFunction) => { // 设置三个异步逻辑相关的 state const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 定义一个 callback 用于执行异步逻辑 const execute = useCallback(() => { // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态 setLoading(true); setData(null); setError(null); return asyncFunction() .then((response) => { // 请求成功时,将数据写进 state,设置 loading 为 false setData(response); setLoading(false); }) .catch((error) => { // 请求失败时,设置 loading 为 false,并设置错误状态 setError(error); setLoading(false); }); }, [asyncFunction]); return { execute, loading, data, error }; }; 应用: import React from "react"; import useAsync from './useAsync'; export default function UserList() { // 通过 useAsync 这个函数,只需要提供异步逻辑的实现 const { execute: fetchUsers, data: users, loading, error, } = useAsync(async () => { const res = await fetch("https://reqres.in/api/users/"); const json = await res.json(); return json.data; }); return ( // 根据状态渲染 UI... ); }
-
-
监听浏览器状态
-
窗口大小,滚动条位置,cookies,localStorage, URL
-
-
拆分复杂组件
-
代码过长
function BlogList() { // 获取文章列表... // 获取分类列表... // 组合文章数据和分类数据... // 根据选择的分类过滤文章... // 渲染 UI ... } --------- import React, { useEffect, useCallback, useMemo, useState } from "react"; import { Select, Table } from "antd"; import _ from "lodash"; import useAsync from "./useAsync"; const endpoint = "https://myserver.com/api/"; const useArticles = () => { // 使用上面创建的 useAsync 获取文章列表 const { execute, data, loading, error } = useAsync( useCallback(async () => { const res = await fetch(`${endpoint}/posts`); return await res.json(); }, []), ); // 执行异步调用 useEffect(() => execute(), [execute]); // 返回语义化的数据结构 return { articles: data, articlesLoading: loading, articlesError: error, }; }; const useCategories = () => { // 使用上面创建的 useAsync 获取分类列表 const { execute, data, loading, error } = useAsync( useCallback(async () => { const res = await fetch(`${endpoint}/categories`); return await res.json(); }, []), ); // 执行异步调用 useEffect(() => execute(), [execute]); // 返回语义化的数据结构 return { categories: data, categoriesLoading: loading, categoriesError: error, }; }; const useCombinedArticles = (articles, categories) => { // 将文章数据和分类数据组合到一起 return useMemo(() => { // 如果没有文章或者分类数据则返回 null if (!articles || !categories) return null; return articles.map((article) => { return { ...article, category: categories.find( (c) => String(c.id) === String(article.categoryId), ), }; }); }, [articles, categories]); }; const useFilteredArticles = (articles, selectedCategory) => { // 实现按照分类过滤 return useMemo(() => { if (!articles) return null; if (!selectedCategory) return articles; return articles.filter((article) => { console.log("filter: ", article.categoryId, selectedCategory); return String(article?.category?.name) === String(selectedCategory); }); }, [articles, selectedCategory]); }; const columns = [ { dataIndex: "title", title: "Title" }, { dataIndex: ["category", "name"], title: "Category" }, ]; export default function BlogList() { const [selectedCategory, setSelectedCategory] = useState(null); // 获取文章列表 const { articles, articlesError } = useArticles(); // 获取分类列表 const { categories, categoriesError } = useCategories(); // 组合数据 const combined = useCombinedArticles(articles, categories); // 实现过滤 const result = useFilteredArticles(combined, selectedCategory); // 分类下拉框选项用于过滤 const options = useMemo(() => { const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({ value: c.name, label: c.name, })); arr.unshift({ value: null, label: "All" }); return arr; }, [categories]); // 如果出错,简单返回 Failed if (articlesError || categoriesError) return "Failed"; // 如果没有结果,说明正在加载 if (!result) return "Loading..."; return ( <div> <Select value={selectedCategory} onChange={(value) => setSelectedCategory(value)} options={options} style={{ width: "200px" }} placeholder="Select a category" /> <Table dataSource={result} columns={columns} /> </div> ); }
-
需要保持每个函数到短小
尽量将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互。
拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。所以在这个场景下,我们不一定要把 Hooks 放到独立的文件中,而是可以和函数组件写在一个文件中。这么做的原因就在于,这些 Hooks 是和当前函数组件紧密相关的,所以写到一起,反而更容易阅读和理解。
-
-
-
注意
-
state
-
1、状态最小化原则,避免冗余状态
-
2、唯一数据源原则,避免中间状态
-
本博客其他有关文章