Hooks的核心原理梳理
我们前端都在诟病专业版,它的组件,它的耦合嵌套之深,它的性能。
我们希望改善,我们认为,如果……就好了。
如果重构就好了,如果技术栈统一就好了,如果有规范就好了。
其实,不用等,我们只要在写代码,就可以进行优化。
关键的一点,就是,如何写正确的代码。如果不能写正确的代码,以前的老问题没解决,又会加上新的问题。
我们现在基本上能达成一个共识,就是函数组件比类组件要好(参见https://reactjs.bootcss.com/docs/hooks-intro.html难以理解的 class)
所以,我们在写新组件的时候,优先会写成函数组件。
基于以上,我们来好好讨论,了解一下函数组件中常用的hooks。
Hooks的核心原理和实战
react
特性
虚拟dom机制
diff算法
jsx语法
原理/本质
react中文含义-反应/响应
数据驱动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、唯一数据源原则,避免中间状态
本博客其他有关文章
Hooks的核心原理梳理的更多相关文章
- 服务治理演进剖析 & Service Mesh、 xDS核心原理梳理
基于XDS协议实现控制面板与数据面板通信分享 基于这段时间在同程艺龙基础架构部的蹲坑,聊一聊微服务治理的核心难点.历史演进.最新动态, 以上内容属自我思考,不代表同程艺龙技术水准.如理解有偏差.理解不 ...
- 《大型网站技术架构:核心原理与案例分析》【PDF】下载
<大型网站技术架构:核心原理与案例分析>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062557 内容简介 本书通过梳理大型网站 ...
- 新书介绍 -- 《Redis核心原理与实践》
大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- docker核心原理
容器概念. docker是一种容器,应用沙箱机制实现虚拟化.能在一台宿主机里面独立多个虚拟环境,互不影响.在这个容器里面可以运行着我饿们的业务,输入输出.可以和宿主机交互. 使用方法. 拉取镜像 do ...
- HDFS 核心原理
HDFS 核心原理 2016-01-11 杜亦舒 HDFS(Hadoop Distribute File System)是一个分布式文件系统文件系统是操作系统提供的磁盘空间管理服务,只需要我们指定把文 ...
- 剖析SSH核心原理(一)
在我前面的文章中,也试图总结过SSH,见 http://blog.csdn.net/shan9liang/article/details/8803989 ,随着知识的积累,总感觉以前说得比较笼统, ...
- 关于Ajax的技术组成与核心原理
1.Ajax 特点: 局部刷新.提高用户的体验度,数据从服务器商加载 2.AJax的技术组成 不是新技术,而是之前技术的整合 Ajax: Asynchronous Javascript And Xml ...
- Libevent核心原理
Libevent 是一个事件驱动框架, 不能仅说他是一个网络库. notejs就是采用与libevent类似的libev来做核心驱动的. Libevent支持三种事件:io事件.信号事件.时间事件 ...
- 高性能消息队列 CKafka 核心原理介绍(上)
欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:闫燕飞 1.背景 Ckafka是基础架构部开发的高性能.高可用消息中间件,其主要用于消息传输.网站活动追踪.运营监控.日志聚合.流式 ...
随机推荐
- 【Vue】父子组件传值、方法引用
父子组件值.方法引用 1.值 1.1 父组件获取子组件值 父组件 <template> <div> <button @click="getChildValue& ...
- hexo博客yilia主题_缺失模块_解决方案
hexo博客yilia主题,左侧栏目有一个全部文章的按钮,刚开始开始报错缺失模块,如下图: 我解决了这个问题着实不容易饶了弯路,但是跟着提示步骤,其实很简单,走起: 1.查看node版本 win键+R ...
- EC600U-4G模组,连接阿里云测试服务器和物联网平台
原博主视频:https://www.bilibili.com/video/BV1yT4y1P7Gw?share_source=copy_web 连接阿里云服务器 !!需要公网ip(服务器)才能远程,不 ...
- git关于分支的常用命令
上家公司实习,一个人干一个项目,没有用git管理代码,导致我以前学的命令都忘了 git checkout -b xxx 创建xxx分支 并切换到xxx分支 等价于 git branch xxx git ...
- Python类与面向对象
Python类与面向对象 一.面向对象 1.1 面向对象概述 面向对象与面向过程? 面向过程编程的基本思想是:分析解决问题的步骤,使用函数实现每步对应的功能,按照步骤的先后顺序依次调用函数.面向过程只 ...
- react中使用动画 react-transition-group
在React中通过react-transition-group使用过渡.动画,首先要有CSS3中的过渡和动画的相关知识储备,可以参考 过渡和2D变换.动画和3d变换. 我们自己通过css设置过渡.动画 ...
- Mybatis开发中的常用Maven配置
Mybatis导入Maven配置 <!-- MyBatis导入 --> <dependency> <groupId>org.mybatis</groupId& ...
- [k8s]使用nfs挂载pod的应用日志文件
前言 某些特殊场景下应用日志无法通过elk.grafana等工具直接查看,需要将日志文件挂载出来再处理.本文以nfs作为远程存储,统一存放pod日志. 系统版本:CentOS 7 x86-64 宿主机 ...
- Vue3 路由优化,使页面初次渲染效率翻倍
3996 条路由? addRoute函数用了大约1s才执行完毕.通过观察,发现居然有3996条路由记录. 可是项目并没有这么多的页面啊~ 重复路由 let routes: Array<Route ...
- linux 查找目录中的大文件
find是Linux系统中常用的文件查找命令.它可以在文件系统中查找指定条件的文件,并执行相应的操作.语法格式如下: find [pathname] [options] pathname: 指定查找的 ...