React Hooks 深入浅出

目录

  1. 引言:React Hooks 的革命
  2. 基础 Hooks
    • useState:状态管理的新方式
    • useEffect:组件生命周期的替代方案
    • useContext:简化 Context API
  3. 额外的 Hooks
    • useReducer:复杂状态逻辑的管理
    • useCallback 与 useMemo:性能优化利器
    • useRef:引用 DOM 和保存变量
  4. 自定义 Hooks:逻辑复用的最佳实践
  5. Hooks 使用规则与陷阱
  6. 实战案例:使用 Hooks 重构传统组件
  7. 总结与展望

引言:React Hooks 的革命

React Hooks 是 React 16.8 版本中引入的特性,它彻底改变了 React 组件的编写方式。在 Hooks 出现之前,我们需要使用类组件来管理状态和生命周期,而函数组件则被视为"无状态组件"。Hooks 的出现使得函数组件也能够拥有状态和生命周期功能,从而简化了组件逻辑,提高了代码的可读性和可维护性。

Hooks 解决了 React 中的一些长期存在的问题:

  • 组件之间难以复用状态逻辑:在 Hooks 之前,我们通常使用高阶组件(HOC)或 render props 模式来复用组件逻辑,但这些方法往往导致组件嵌套过深,形成"嵌套地狱"。
  • 复杂组件变得难以理解:生命周期方法中常常混杂着不相关的逻辑,而相关逻辑却分散在不同的生命周期方法中。
  • 类组件的困惑:类组件需要理解 JavaScript 中 this 的工作方式,这对新手不太友好。

接下来,我们将深入探讨 React Hooks 的各个方面,从基础用法到高级技巧,帮助你全面掌握这一强大特性。

基础 Hooks

useState:状态管理的新方式

useState 是最基础也是最常用的 Hook,它让函数组件能够拥有自己的状态。

import React, { useState } from 'react';function Counter() {// 声明一个叫 "count" 的 state 变量,初始值为 0const [count, setCount] = useState(0);return (<div><p>你点击了 {count} 次</p><button onClick={() => setCount(count + 1)}>点击我</button></div>);
}

useState 返回一个数组,包含两个元素:当前状态值和一个更新该状态的函数。我们使用数组解构来获取这两个值。

useState 的高级用法

  1. 函数式更新:当新的状态依赖于之前的状态时,推荐使用函数式更新。
// 不推荐
setCount(count + 1);// 推荐
setCount(prevCount => prevCount + 1);
  1. 惰性初始化:如果初始状态需要通过复杂计算获得,可以传递一个函数给 useState
const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);return initialState;
});

useEffect:组件生命周期的替代方案

useEffect 让你在函数组件中执行副作用操作,如数据获取、订阅或手动更改 DOM 等。它统一了 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期方法。

import React, { useState, useEffect } from 'react';function Example() {const [count, setCount] = useState(0);// 类似于 componentDidMount 和 componentDidUpdateuseEffect(() => {// 更新文档标题document.title = `你点击了 ${count} 次`;// 返回一个清理函数,类似于 componentWillUnmountreturn () => {document.title = 'React App';};}, [count]); // 仅在 count 更改时更新return (<div><p>你点击了 {count} 次</p><button onClick={() => setCount(count + 1)}>点击我</button></div>);
}

useEffect 的依赖数组

  • 空数组 []:效果只在组件挂载和卸载时执行一次,类似于 componentDidMountcomponentWillUnmount
  • 有依赖项 [a, b]:效果在组件挂载时以及依赖项变化时执行。
  • 无依赖数组:效果在每次渲染后执行。

useContext:简化 Context API

useContext 让你可以订阅 React 的 Context,而不必使用 Context.Consumer 组件。

import React, { useContext } from 'react';// 创建一个 Context
const ThemeContext = React.createContext('light');function ThemedButton() {// 使用 useContext 获取当前主题const theme = useContext(ThemeContext);return (<button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>我是一个主题按钮</button>);
}function App() {return (<ThemeContext.Provider value="dark"><ThemedButton /></ThemeContext.Provider>);
}

useContext 接收一个 Context 对象(由 React.createContext 创建)并返回该 Context 的当前值。当 Provider 更新时,使用该 Context 的组件会重新渲染。

额外的 Hooks

useReducer:复杂状态逻辑的管理

useReduceruseState 的替代方案,适用于有复杂状态逻辑的场景,特别是当下一个状态依赖于之前的状态时。

import React, { useReducer } from 'react';// 定义 reducer 函数
function reducer(state, action) {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };default:throw new Error();}
}function Counter() {// 使用 useReducerconst [state, dispatch] = useReducer(reducer, { count: 0 });return (<div>Count: {state.count}<button onClick={() => dispatch({ type: 'increment' })}>+</button><button onClick={() => dispatch({ type: 'decrement' })}>-</button></div>);
}

useReducer 返回当前状态和 dispatch 函数。dispatch 函数用于触发状态更新,它接收一个 action 对象,通常包含 type 属性和可选的 payload。

useCallback 与 useMemo:性能优化利器

这两个 Hooks 主要用于性能优化,避免不必要的计算和渲染。

useCallback:返回一个记忆化的回调函数,只有当依赖项变化时才会更新。

import React, { useState, useCallback } from 'react';function ParentComponent() {const [count, setCount] = useState(0);// 使用 useCallback 记忆化回调函数const handleClick = useCallback(() => {console.log(`Button clicked, count: ${count}`);}, [count]); // 只有当 count 变化时,handleClick 才会更新return (<div><ChildComponent onClick={handleClick} /><button onClick={() => setCount(count + 1)}>Increment</button></div>);
}// 使用 React.memo 优化子组件
const ChildComponent = React.memo(({ onClick }) => {console.log('ChildComponent rendered');return <button onClick={onClick}>Click me</button>;
});

useMemo:返回一个记忆化的值,只有当依赖项变化时才重新计算。

import React, { useState, useMemo } from 'react';function ExpensiveCalculation({ a, b }) {// 使用 useMemo 记忆化计算结果const result = useMemo(() => {console.log('Computing result...');// 假设这是一个耗时的计算return a * b;}, [a, b]); // 只有当 a 或 b 变化时,才重新计算return <div>Result: {result}</div>;
}

useRef:引用 DOM 和保存变量

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。

引用 DOM 元素

import React, { useRef, useEffect } from 'react';function TextInputWithFocusButton() {// 创建一个 refconst inputRef = useRef(null);// 点击按钮时聚焦输入框const focusInput = () => {inputRef.current.focus();};return (<div><input ref={inputRef} type="text" /><button onClick={focusInput}>聚焦输入框</button></div>);
}

保存变量

import React, { useState, useRef, useEffect } from 'react';function Timer() {const [count, setCount] = useState(0);// 使用 useRef 保存 interval IDconst intervalRef = useRef(null);useEffect(() => {// 设置定时器intervalRef.current = setInterval(() => {setCount(c => c + 1);}, 1000);// 清理定时器return () => {clearInterval(intervalRef.current);};}, []); // 空依赖数组,只在挂载和卸载时执行// 停止计时器const stopTimer = () => {clearInterval(intervalRef.current);};return (<div><p>计数: {count}</p><button onClick={stopTimer}>停止</button></div>);
}

useState 不同,useRef.current 属性变化不会触发组件重新渲染。

自定义 Hooks:逻辑复用的最佳实践

自定义 Hooks 是 React Hooks 最强大的特性之一,它允许你将组件逻辑提取到可重用的函数中。自定义 Hook 是一个以 “use” 开头的 JavaScript 函数,可以调用其他 Hooks。

示例:创建一个 useLocalStorage Hook

import { useState, useEffect } from 'react';// 自定义 Hook:使用 localStorage 持久化状态
function useLocalStorage(key, initialValue) {// 初始化状态const [storedValue, setStoredValue] = useState(() => {try {// 尝试从 localStorage 获取值const item = window.localStorage.getItem(key);// 如果存在则解析并返回,否则返回初始值return item ? JSON.parse(item) : initialValue;} catch (error) {console.log(error);return initialValue;}});// 更新 localStorage 的函数const setValue = value => {try {// 允许值是一个函数,类似于 useStateconst valueToStore = value instanceof Function ? value(storedValue) : value;// 保存到 statesetStoredValue(valueToStore);// 保存到 localStoragewindow.localStorage.setItem(key, JSON.stringify(valueToStore));} catch (error) {console.log(error);}};return [storedValue, setValue];
}// 使用自定义 Hook
function App() {const [name, setName] = useLocalStorage('name', 'Bob');return (<div><inputtype="text"value={name}onChange={e => setName(e.target.value)}/></div>);
}

示例:创建一个 useWindowSize Hook

import { useState, useEffect } from 'react';// 自定义 Hook:获取窗口尺寸
function useWindowSize() {// 初始化状态const [windowSize, setWindowSize] = useState({width: undefined,height: undefined,});useEffect(() => {// 处理窗口大小变化的函数function handleResize() {setWindowSize({width: window.innerWidth,height: window.innerHeight,});}// 添加事件监听器window.addEventListener('resize', handleResize);// 初始调用一次以设置初始值handleResize();// 清理函数return () => window.removeEventListener('resize', handleResize);}, []); // 空依赖数组,只在挂载和卸载时执行return windowSize;
}// 使用自定义 Hook
function ResponsiveComponent() {const size = useWindowSize();return (<div>{size.width < 768 ? (<p>在小屏幕上显示</p>) : (<p>在大屏幕上显示</p>)}</div>);
}

Hooks 使用规则与陷阱

使用 Hooks 时,必须遵循两条重要规则:

  1. 只在最顶层使用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks,确保 Hooks 在每次组件渲染时都以相同的顺序被调用。
// ❌ 错误:在条件语句中使用 Hook
function Form() {const [name, setName] = useState('Mary');if (name !== '') {useEffect(() => {localStorage.setItem('name', name);});}// ...
}// ✅ 正确:将条件放在 Hook 内部
function Form() {const [name, setName] = useState('Mary');useEffect(() => {if (name !== '') {localStorage.setItem('name', name);}});// ...
}
  1. 只在 React 函数组件或自定义 Hooks 中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hooks。

常见陷阱与解决方案

  1. 依赖数组遗漏
// ❌ 错误:依赖数组遗漏了 count
function Counter({ count }) {useEffect(() => {document.title = `Count: ${count}`;}, []); // 依赖数组为空,但使用了 count// ...
}// ✅ 正确:添加所有依赖项
function Counter({ count }) {useEffect(() => {document.title = `Count: ${count}`;}, [count]); // 正确添加了 count 作为依赖项// ...
}
  1. 闭包陷阱
// ❌ 问题:使用过时的状态值
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {console.log(`Current count: ${count}`);setCount(count + 1); // 这里的 count 是闭包捕获的初始值}, 1000);return () => clearInterval(timer);}, []); // 依赖数组为空,导致 count 始终为初始值 0// ...
}// ✅ 解决方案:使用函数式更新
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {setCount(prevCount => prevCount + 1); // 使用函数式更新获取最新状态}, 1000);return () => clearInterval(timer);}, []); // 现在可以安全地使用空依赖数组// ...
}
  1. 过度依赖 useEffect
// ❌ 不必要的 useEffect
function Form() {const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');// 不必要的 useEffect,可以直接计算const [fullName, setFullName] = useState('');useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);// ...
}// ✅ 更好的方式:直接计算派生状态
function Form() {const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');// 直接计算派生值,无需额外的状态和 useEffectconst fullName = `${firstName} ${lastName}`;// ...
}

实战案例:使用 Hooks 重构传统组件

让我们通过一个实际例子,展示如何将类组件重构为使用 Hooks 的函数组件。

原始类组件

import React, { Component } from 'react';class UserProfile extends Component {constructor(props) {super(props);this.state = {user: null,loading: true,error: null};}componentDidMount() {this.fetchUserData();}componentDidUpdate(prevProps) {if (prevProps.userId !== this.props.userId) {this.fetchUserData();}}fetchUserData = async () => {this.setState({ loading: true });try {const response = await fetch(`https://api.example.com/users/${this.props.userId}`);const data = await response.json();this.setState({ user: data, loading: false });} catch (error) {this.setState({ error, loading: false });}};render() {const { user, loading, error } = this.state;if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);}
}

使用 Hooks 重构后的函数组件

import React, { useState, useEffect } from 'react';function UserProfile({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {async function fetchUserData() {setLoading(true);try {const response = await fetch(`https://api.example.com/users/${userId}`);const data = await response.json();setUser(data);setLoading(false);} catch (error) {setError(error);setLoading(false);}}fetchUserData();}, [userId]); // 依赖项数组,只有当 userId 变化时才重新获取数据if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);
}

进一步优化:提取自定义 Hook

import React, { useState, useEffect } from 'react';// 自定义 Hook:获取用户数据
function useUserData(userId) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {async function fetchUserData() {setLoading(true);try {const response = await fetch(`https://api.example.com/users/${userId}`);const data = await response.json();setUser(data);setLoading(false);} catch (error) {setError(error);setLoading(false);}}fetchUserData();}, [userId]);return { user, loading, error };
}// 使用自定义 Hook 的组件
function UserProfile({ userId }) {const { user, loading, error } = useUserData(userId);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);
}

通过这个例子,我们可以看到 Hooks 带来的几个明显优势:

  1. 代码更简洁:函数组件比类组件代码量少,更易于阅读和理解。
  2. 关注点分离:通过自定义 Hook,我们可以将数据获取逻辑与 UI 渲染逻辑分离。
  3. 逻辑复用:自定义 Hook 可以在多个组件之间复用,而不需要使用 HOC 或 render props。

总结与展望

React Hooks 彻底改变了 React 组件的编写方式,使函数组件成为了 React 开发的主流。通过本文,我们深入探讨了 React Hooks 的基础知识、高级用法、常见陷阱以及最佳实践。

Hooks 的优势总结:

  1. 简化组件逻辑:使用 Hooks,我们可以将相关的逻辑放在一起,而不是分散在不同的生命周期方法中。
  2. 促进逻辑复用:自定义 Hooks 提供了一种比 HOC 和 render props 更简洁的逻辑复用方式。
  3. 更好的类型推断:在 TypeScript 中,Hooks 比类组件有更好的类型推断。
  4. 减少代码量:函数组件通常比等效的类组件代码量少。
  5. 更容易测试:纯函数更容易测试,Hooks 使得编写纯函数组件变得更加容易。

随着 React 的发展,Hooks 生态系统也在不断壮大。React 团队还在探索更多的 Hooks,如 useTransitionuseDeferredValue,以解决并发模式下的性能问题。同时,社区也开发了大量的第三方 Hooks 库,如 react-useuse-http 等,进一步扩展了 Hooks 的能力。


参考资料:

  1. React 官方文档 - Hooks 介绍
  2. React Hooks 完全指南
  3. 使用 React Hooks 的常见错误
  4. 深入理解 React useEffect

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/80349.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【应急响应】- 日志流量如何分析?

【应急响应】- 日志流量如何下手&#xff1f;https://mp.weixin.qq.com/s/dKl8ZLZ0wjuqUezKo4eUSQ

stm32 debug卡在0x1FFFxxxx

自己画的一个四轴飞机电路板&#xff0c;之前还能debug&#xff0c;改了一下mos管两端的电阻&#xff0c;还能正常下载&#xff0c;蓝牙接收也正常&#xff0c;但是debug出问题了&#xff0c;刚下载就自动运行&#xff0c;然后程序就在0x1FFFxxxx附近循环运行&#xff0c;这一块…

java-----------------多态

多态&#xff0c;当前指的是 java 所呈现出来的一个对象 多态 定义 多态是指同一个行为具有多个不同表现形式或形态的能力。在面向对象编程中&#xff0c;多态通过方法重载和方法重写来实现。 强弱类型语言 javascript 或者python 是弱类型语言 C 语言&#xff0c;或者 C…

Java 23种设计模式 - 结构型模式7种

Java 23种设计模式 - 结构型模式7种 1 适配器模式 适配器模式把一个类的接口变换成客户端所期待的另一种接口&#xff0c;从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 优点 将目标类和适配者类解耦增加了类的透明性和复用性&#xff0c;将具体的实现封…

Git clone时出现SSL certificate problem unable to get local issuer certificate

正确解决方法 git config --global http.sslVerify false错误解决方法&#xff1a;&#xff08;主要是看错了嘿嘿&#xff0c;但是如果是 OpenSSL SSL_read: Connection was reset, errno 10054 Failed to connect to github.com port 443: Timed out 原…

DevExpressWinForms-AlertControl-使用教程

文章目录 AlertControl-使用教程一、将 AlertControl 添加到 Form二、编辑 AlertControl 的 HtmlTemplateHTML Template Editor介绍编辑HTML Template 三、使用AlertControl弹出AlertAlert中的按钮事件获取 Alert 标题等信息向Alert传递参数 总结源码 AlertControl-使用教程 一…

制作项目进度表常用的 8 款项目管理工具分享

在数字化管理和高效协作的今天&#xff0c;项目进度表软件已经成为企业管理不可或缺的工具。无论是中小型企业还是大型机构&#xff0c;都需要通过精准的项目计划和实时的进度跟踪来确保业务目标的顺利达成。这篇文章将聚焦项目进度表软件&#xff0c;深入探讨市场上8款主流产品…

SecureCRT网络穿透/代理

场景 公司的办公VPN软件只有Windows系统版本&#xff0c;没有Macos系统版本&#xff0c;而日常开发过程中需要先登录VPN后&#xff0c;然后才能登录应用服务器。 目的&#xff1a;Macos系统在使用SecureCRT时&#xff0c;登录服务器&#xff0c;需要走Parallels Desktop进行网络…

【计算机网络-传输层】传输层协议-TCP核心机制与可靠性保障

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f152; C 语言 | &#x1f310; 计算机网络 上篇文章&#xff1a;传输层协议-UDP 下篇文章&#xff1a; 网络层 我们的讲解顺序是&…

OpenMagnetic的介绍与使用

1. Background OM&#xff08;OpenMagnetic&#xff09;OpenMagnetics&#xff0c;能涵盖气隙磁阻&#xff0c;磁导率&#xff0c;铁芯损耗、磁滞损耗、涡流电流损耗、涡流效应、漏感、温升的计算与仿真[1]。 铁损计算模型&#xff1a;改进的Steinmetz方程[2] 气隙阻抗计算&…

【JVM】从零开始深度解析JVM

本篇博客给大家带来的是JVM的知识点, 重点在类加载和垃圾回收机制上. &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; …

字符串---Spring字符串基本处理

一、String类的特性 不可变性 String对象一旦创建&#xff0c;内容不可更改&#xff0c;任何修改操作都会生成新对象。字符串常量池 字符串字面量&#xff08;如"abc"&#xff09;直接存储在常量池中&#xff0c;重复字面量共享同一内存地址。创建方式 虽然都是字符…

26考研——中央处理器_CPU 的功能和基本结构(5)

408答疑 文章目录 一、CPU 的功能和基本结构CPU 的功能CPU 的基本结构运算器控制器 CPU 的寄存器运算器中的寄存器控制器中的寄存器 八、参考资料鲍鱼科技课件26王道考研书 九、总结 一、CPU 的功能和基本结构 CPU 的功能 中央处理器&#xff08;CPU&#xff09;由运算器和控…

传统数据展示 vs 可视化:谁更打动人心?

数据&#xff0c;每天都在我们身边流动&#xff1a;从你手机里的健康步数&#xff0c;到企业财报中的营收增长&#xff0c;再到国家发布的经济指标。但问题是——你怎么“看”这些数据&#xff1f; 过去&#xff0c;我们习惯用表格、文字和报告来展示数据&#xff0c;这种方式…

Base64 编码原理详细解析

Base64 编码是一种常见的数据编码方式&#xff0c;它将二进制数据转化为可打印的 ASCII 字符串。Base64 编码广泛应用于电子邮件、URL 编码、HTTP 请求和响应中等场景。它的核心作用是让二进制数据可以通过仅支持文本的协议或媒介进行传输。本文将更深入地探讨 Base64 编码的原…

一周学会Pandas2 Python数据处理与分析-Pandas2数据排序操作

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili Pandas 2提供了多种灵活的数据排序方法&#xff0c;主要针对 DataFrame 和 Series 对象。 1. 按值排序&#xff1a;s…

计算机二级(C语言)已过

非线性结构&#xff1a;树、图 链表和队列的结构特性不一样&#xff0c;链表可以在任何位置插入、删除&#xff0c;而队列只能在队尾入队、队头出队 对长度为n的线性表排序、在最坏情况下时间复杂度&#xff0c;二分查找为O(log2n)&#xff0c;顺序查找为O(n)&#xff0c;哈希查…

Windows Server 2025开启GPU分区(GPU-P)部署DoraCloud云桌面

本文描述在ShareStation工作站虚拟化方案的部署过程。 将服务器上部署 Windows Server、DoraCloud&#xff0c;并创建带有vGPU的虚拟桌面。 GPU分区技术介绍 GPU-P&#xff08;GPU Partitioning&#xff09; 是微软在 Windows 虚拟化平台&#xff08;如 Hyper-V&#xff09;中…

Android RxJava框架分析:它的执行流程是如何的?它的线程是如何切换的?如何自定义RxJava操作符?

目录 RxJava是什么&#xff1f;为什么使用。RxJava是如何使用的呢&#xff1f;RxJava如何和Retrofit一起使用。RxJava源码分析。 &#xff08;1&#xff09;他执行流程是如何的。&#xff08;2&#xff09;map&#xff08;3&#xff09;线程的切换。 如何自定义RxJava操作符…

QT的初始代码解读及其布局和弹簧

this指的是真正的当前正在显示的窗口 main函数&#xff1a; Widget w是生成了一个主窗口&#xff0c;QT Designer是在这个主窗口里塞组件 w.show()用来展示这个主窗口 头文件&#xff1a; namespace Ui{class Widget;}中的class Widget和下面的class Widget不是一个东西 Ui…