React 生命周期与 Hook 理解解析

从生命周期到 Hook:React 组件演进之路

React 组件的本质是管理渲染与副作用的统一体。Class 组件通过生命周期方法实现这一目标,而函数组件则依靠 Hook 系统达成相同效果。

Class 组件生命周期详解

生命周期完整流程

Class 组件生命周期可分为三大阶段:挂载、更新和卸载。

class Clock extends React.Component {constructor(props) {super(props);this.state = { date: new Date() };console.log('constructor: 组件初始化');}componentDidMount() {console.log('componentDidMount: 组件已挂载');this.timerID = setInterval(() => this.tick(), 1000);}componentDidUpdate(prevProps, prevState) {console.log('componentDidUpdate: 组件已更新');if (prevState.date.getSeconds() !== this.state.date.getSeconds()) {document.title = `当前时间: ${this.state.date.toLocaleTimeString()}`;}}componentWillUnmount() {console.log('componentWillUnmount: 组件即将卸载');clearInterval(this.timerID);}tick() {this.setState({ date: new Date() });}render() {return <div>当前时间: {this.state.date.toLocaleTimeString()}</div>;}
}
挂载阶段执行顺序
  1. constructor(): 初始化状态与绑定方法
  2. static getDerivedStateFromProps(): 根据 props 更新 state (React 16.3+)
  3. render(): 计算并返回 JSX
  4. DOM 更新
  5. componentDidMount(): DOM 挂载完成后执行,适合进行网络请求、订阅和DOM操作
更新阶段执行顺序
  1. static getDerivedStateFromProps(): 同挂载阶段
  2. shouldComponentUpdate(): 决定是否继续更新流程
  3. render(): 重新计算 JSX
  4. getSnapshotBeforeUpdate(): 在DOM更新前捕获信息
  5. DOM 更新
  6. componentDidUpdate(): DOM更新完成后执行
卸载阶段
  1. componentWillUnmount(): 清理订阅、定时器、取消网络请求等

Class 组件常见陷阱

class UserProfile extends React.Component {state = { userData: null };componentDidMount() {this.fetchUserData();}componentDidUpdate(prevProps) {// 常见错误:没有条件判断导致无限循环if (prevProps.userId !== this.props.userId) {this.fetchUserData();}}fetchUserData() {fetch(`/api/users/${this.props.userId}`).then(response => response.json()).then(data => this.setState({ userData: data }));}render() {// ...}
}
  1. 未在条件更新中比较props变化:导致无限循环
  2. this绑定问题:事件处理函数中this指向丢失
  3. 生命周期中的副作用管理混乱:副作用散布在多个生命周期方法中
  4. 忘记清理副作用:componentWillUnmount中未清理导致内存泄漏

函数组件与Hook系统剖析

Hook 彻底改变了React组件的编写方式,将分散在生命周期方法中的逻辑按照关注点聚合。

常用Hook与生命周期对应关系

function Clock() {const [date, setDate] = useState(new Date());useEffect(() => {console.log('组件挂载或更新');// 相当于 componentDidMount 和 componentDidUpdateconst timerID = setInterval(() => {setDate(new Date());}, 1000);// 相当于 componentWillUnmountreturn () => {console.log('清理副作用或组件卸载');clearInterval(timerID);};}, []); // 空依赖数组等同于仅在挂载时执行useEffect(() => {document.title = `当前时间: ${date.toLocaleTimeString()}`;}, [date]); // 仅在date变化时执行return <div>当前时间: {date.toLocaleTimeString()}</div>;
}
Class生命周期Hook对应方式
constructoruseState 初始化
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [依赖项])
componentWillUnmountuseEffect(() => { return () => {} }, [])
shouldComponentUpdateReact.memo + 自定义比较

useEffect 深度解析

useEffect 是React函数组件中管理副作用的核心机制,其工作原理与调度机制决定了React应用的性能与正确性。

useEffect 执行模型
function SearchResults({ query }) {const [results, setResults] = useState([]);const [isLoading, setIsLoading] = useState(false);useEffect(() => {// 1. 执行副作用前的准备工作setIsLoading(true);// 2. 异步副作用const controller = new AbortController();const signal = controller.signal;fetchResults(query, signal).then(data => {setResults(data);setIsLoading(false);}).catch(error => {if (error.name !== 'AbortError') {setIsLoading(false);console.error('搜索失败:', error);}});// 3. 清理函数 - 在下一次effect执行前或组件卸载时调用return () => {controller.abort();};}, [query]); // 依赖数组:仅当query变化时重新执行return (<div>{isLoading ? (<div>加载中...</div>) : (<ul>{results.map(item => (<li key={item.id}>{item.title}</li>))}</ul>)}</div>);
}
useEffect 内部执行机制
  1. 组件渲染后:React 记住需要执行的 effect 函数
  2. 浏览器绘制完成:React 异步执行 effect (与componentDidMount/Update不同,不会阻塞渲染)
  3. 依赖项检查:仅当依赖数组中的值变化时才重新执行
  4. 清理上一次effect:在执行新effect前先执行上一次effect返回的清理函数

常见的 useEffect 陷阱与解决方案

function ProfilePage({ userId }) {const [user, setUser] = useState(null);// 陷阱1: 依赖项缺失useEffect(() => {fetchUser(userId).then(data => setUser(data));// 应该添加 userId 到依赖数组}, []); // 错误:缺少 userId 依赖// 陷阱2: 过于频繁执行useEffect(() => {const handleResize = () => {console.log('窗口大小改变', window.innerWidth);};window.addEventListener('resize', handleResize);return () => window.removeEventListener('resize', handleResize);}); // 错误:缺少依赖数组,每次渲染都重新添加监听
}
解决方案:
function ProfilePage({ userId }) {const [user, setUser] = useState(null);// 解决方案1: 完整依赖项useEffect(() => {let isMounted = true;fetchUser(userId).then(data => {if (isMounted) setUser(data);});return () => { isMounted = false };}, [userId]); // 正确:添加 userId 到依赖数组// 解决方案2: 使用useCallback防止频繁创建函数const handleResize = useCallback(() => {console.log('窗口大小改变', window.innerWidth);}, []);useEffect(() => {window.addEventListener('resize', handleResize);return () => window.removeEventListener('resize', handleResize);}, [handleResize]); // 正确:添加handleResize到依赖数组
}

React Hook 规则与原理解析

Hook 工作原理:基于顺序的依赖系统

// React内部简化实现示意
let componentHooks = [];
let currentHookIndex = 0;// 模拟useState的实现
function useState(initialState) {const hookIndex = currentHookIndex;const hooks = componentHooks;// 首次渲染时初始化stateif (hooks[hookIndex] === undefined) {hooks[hookIndex] = initialState;}// 设置状态的函数const setState = newState => {if (typeof newState === 'function') {hooks[hookIndex] = newState(hooks[hookIndex]);} else {hooks[hookIndex] = newState;}// 触发重新渲染rerenderComponent(); };currentHookIndex++;return [hooks[hookIndex], setState];
}// 模拟函数组件执行
function RenderComponent(Component) {currentHookIndex = 0;const output = Component();return output;
}

Hook依赖固定的调用顺序,这就是为什么:

  1. 不能在条件语句中使用Hook:会打乱Hook的调用顺序
  2. 不能在循环中使用Hook:每次渲染时Hook数量必须一致
  3. 只能在React函数组件或自定义Hook中调用Hook:确保React能正确跟踪状态

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

// 自定义Hook: 封装数据获取逻辑
function useDataFetching(url) {const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {let isMounted = true;setLoading(true);const controller = new AbortController();fetch(url, { signal: controller.signal }).then(response => {if (!response.ok) throw new Error('网络请求失败');return response.json();}).then(data => {if (isMounted) {setData(data);setLoading(false);}}).catch(error => {if (isMounted && error.name !== 'AbortError') {setError(error);setLoading(false);}});return () => {isMounted = false;controller.abort();};}, [url]);return { data, loading, error };
}// 使用自定义Hook
function UserProfile({ userId }) {const { data: user, loading, error } = useDataFetching(`/api/users/${userId}`);if (loading) return <div>加载中...</div>;if (error) return <div>出错了: {error.message}</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p></div>);
}

自定义Hook优势:

  1. 关注点分离:将逻辑与UI完全解耦
  2. 代码复用:在多个组件间共享逻辑而不是组件本身
  3. 测试友好:逻辑集中,易于单元测试
  4. 清晰的依赖管理:显式声明数据流向

高级性能优化技巧

依赖数组优化

function SearchComponent({ defaultQuery }) {// 1. 基本状态const [query, setQuery] = useState(defaultQuery);// 2. 衍生状态/计算 - 优化前const [debouncedQuery, setDebouncedQuery] = useState(query);useEffect(() => {const handler = setTimeout(() => {setDebouncedQuery(query);}, 500);return () => clearTimeout(handler);}, [query]); // 每次query变化都会创建新定时器// 3. 网络请求 - 优化前useEffect(() => {// 这个函数每次渲染都会重新创建const fetchResults = async () => {const response = await fetch(`/api/search?q=${debouncedQuery}`);const data = await response.json();// 处理结果...};fetchResults();}, [debouncedQuery]); // 问题:fetchResults每次都是新函数引用
}

优化后:

function SearchComponent({ defaultQuery }) {// 1. 基本状态const [query, setQuery] = useState(defaultQuery);// 2. 使用useMemo缓存计算结果const debouncedQuery = useDebouncedValue(query, 500);// 3. 使用useCallback缓存函数引用const fetchResults = useCallback(async (searchQuery) => {const response = await fetch(`/api/search?q=${searchQuery}`);return response.json();}, []); // 空依赖数组,函数引用稳定// 4. 使用稳定函数引用useEffect(() => {let isMounted = true;const getResults = async () => {try {const data = await fetchResults(debouncedQuery);if (isMounted) {// 处理结果...}} catch (error) {if (isMounted) {// 处理错误...}}};getResults();return () => { isMounted = false };}, [debouncedQuery, fetchResults]); // fetchResults现在是稳定引用
}// 自定义Hook: 处理防抖
function useDebouncedValue(value, delay) {const [debouncedValue, setDebouncedValue] = useState(value);useEffect(() => {const handler = setTimeout(() => {setDebouncedValue(value);}, delay);return () => clearTimeout(handler);}, [value, delay]);return debouncedValue;
}

React.memo、useMemo 与 useCallback

// 阻止不必要的重渲染
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data, onItemClick }) {console.log('ExpensiveComponent渲染');return (<div>{data.map(item => (<div key={item.id} onClick={() => onItemClick(item.id)}>{item.name}</div>))}</div>);
});function ParentComponent() {const [count, setCount] = useState(0);const [items, setItems] = useState([{ id: 1, name: '项目1' },{ id: 2, name: '项目2' }]);// 问题:每次渲染都创建新函数引用,导致ExpensiveComponent重渲染const handleItemClick = (id) => {console.log('点击项目:', id);};return (<div><button onClick={() => setCount(count + 1)}>计数: {count}</button>{/* 即使count变化,items没变,ExpensiveComponent也会重渲染 */}<ExpensiveComponent data={items} onItemClick={handleItemClick} /></div>);
}

优化后:

function ParentComponent() {const [count, setCount] = useState(0);const [items, setItems] = useState([{ id: 1, name: '项目1' },{ id: 2, name: '项目2' }]);// 使用useCallback固定函数引用const handleItemClick = useCallback((id) => {console.log('点击项目:', id);}, []); // 空依赖数组表示函数引用永不变化// 使用useMemo缓存复杂计算结果const processedItems = useMemo(() => {console.log('处理items数据');return items.map(item => ({...item,processed: true}));}, [items]); // 仅当items变化时重新计算return (<div><button onClick={() => setCount(count + 1)}>计数: {count}</button>{/* 现在count变化不会导致ExpensiveComponent重渲染 */}<ExpensiveComponent data={processedItems} onItemClick={handleItemClick} /></div>);
}

从生命周期到Hook的迁移策略

渐进式迁移Class组件

// 步骤1: 从Class组件提取逻辑到独立函数
class UserManager extends React.Component {state = {user: null,loading: true,error: null};componentDidMount() {this.fetchUser();}componentDidUpdate(prevProps) {if (prevProps.userId !== this.props.userId) {this.fetchUser();}}fetchUser() {this.setState({ loading: true });fetchUserAPI(this.props.userId).then(data => this.setState({ user: data, loading: false })).catch(error => this.setState({ error, loading: false }));}render() {// 渲染逻辑...}
}// 步骤2: 创建等效的自定义Hook
function useUser(userId) {const [state, setState] = useState({user: null,loading: true,error: null});useEffect(() => {let isMounted = true;setState(s => ({ ...s, loading: true }));fetchUserAPI(userId).then(data => {if (isMounted) {setState({ user: data, loading: false, error: null });}}).catch(error => {if (isMounted) {setState({ user: null, loading: false, error });}});return () => { isMounted = false };}, [userId]);return state;
}// 步骤3: 创建函数组件版本
function UserManager({ userId }) {const { user, loading, error } = useUser(userId);// 渲染逻辑...
}

优雅处理复杂状态

// Class组件中复杂状态管理
class FormManager extends React.Component {state = {values: { name: '', email: '', address: '' },errors: {},touched: {},isSubmitting: false,submitError: null,submitSuccess: false};// 大量状态更新逻辑...
}// 使用useReducer优化复杂状态管理
function FormManager() {const initialState = {values: { name: '', email: '', address: '' },errors: {},touched: {},isSubmitting: false,submitError: null,submitSuccess: false};const [state, dispatch] = useReducer((state, action) => {switch (action.type) {case 'FIELD_CHANGE':return {...state,values: { ...state.values, [action.field]: action.value },touched: { ...state.touched, [action.field]: true }};case 'VALIDATE':return { ...state, errors: action.errors };case 'SUBMIT_START':return { ...state, isSubmitting: true, submitError: null };case 'SUBMIT_SUCCESS':return { ...state, isSubmitting: false, submitSuccess: true };case 'SUBMIT_ERROR':return { ...state, isSubmitting: false, submitError: action.error };case 'RESET':return initialState;default:return state;}}, initialState);// 使用dispatch来更新状态const handleFieldChange = (field, value) => {dispatch({ type: 'FIELD_CHANGE', field, value });};// 表单提交逻辑const handleSubmit = async (e) => {e.preventDefault();dispatch({ type: 'SUBMIT_START' });try {await submitForm(state.values);dispatch({ type: 'SUBMIT_SUCCESS' });} catch (error) {dispatch({ type: 'SUBMIT_ERROR', error });}};// 渲染表单...
}

未来:React 18+ 与 Concurrent 模式

随着 React 18 的发布,并发渲染模式将改变副作用的执行模型。Hook 系统设计与并发渲染天然契合,为未来的 React 应用提供更优雅的状态与副作用管理。

// React 18 中的新Hook: useTransition
function SearchResults() {const [query, setQuery] = useState('');const [isPending, startTransition] = useTransition();const handleChange = (e) => {// 立即更新输入框setQuery(e.target.value);// 标记低优先级更新,可被中断startTransition(() => {// 复杂搜索逻辑,在空闲时执行performSearch(e.target.value);});};return (<div><input value={query} onChange={handleChange} />{isPending ? <div>搜索中...</div> : <ResultsList />}</div>);
}

最后的话

从 Class 组件生命周期到函数组件 Hook 的演进,体现了 React 设计思想的核心变化:从基于时间的生命周期转向基于状态的声明式副作用。这种转变使组件逻辑更加内聚、可测试和可复用。

理解 React 组件的工作原理和 Hook 系统的设计哲学,是掌握 React 高级开发的关键。

在实际开发中,我们应该遵循 Hook 的核心规则,合理管理依赖数组,并善用 useMemo、useCallback 进行性能优化。

参考资源

  • React 官方文档 - useEffect 指南
  • React 生命周期图解
  • Dan Abramov - A Complete Guide to useEffect
  • Kent C. Dodds - React Hooks: What’s going to happen to my tests?
  • React Hooks FAQ
  • Amelia Wattenberger - Thinking in React Hooks
  • Rudi Yardley - Why Do React Hooks Rely on Call Order?

如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻

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

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

相关文章

数字孪生技术赋能西门子安贝格工厂:全球智能制造标杆的数字化重构实践

在工业4.0浪潮席卷全球制造业的当下&#xff0c;西门子安贝格电子制造工厂&#xff08;Electronic Works Amberg, EWA&#xff09;凭借数字孪生技术的深度应用&#xff0c;构建起全球制造业数字化转型的典范。这座位于德国巴伐利亚州的“未来工厂”&#xff0c;通过虚实融合的数…

从Homebrew找到openssl.cnf文件并拷贝到Go项目下使用

安装OpenSSL 在 macOS 上下载和安装 OpenSSL 最常见和推荐的方式是使用 Homebrew&#xff0c;这是一个 macOS 缺失的包管理器。 如果您还没有安装 Homebrew&#xff0c;请先安装它。安装 Homebrew 后&#xff0c;安装 OpenSSL 只需要一条命令。 步骤 1&#xff1a;安装 Home…

Qt 的简单示例 -- 地址簿

这个工程里有两个窗口&#xff0c;都是QWidget派生的窗口 主窗口&#xff1a; 1. 运用了布局&#xff0c;按钮控件&#xff0c;单行编辑框&#xff0c;富文本编辑框等窗口部件&#xff1b; 2. 运用了 QMap 类&#xff1b; 3. 实现了点击按钮弹出子窗口的功能&#xff0c;这里子…

kubernate解决 “cni0“ already has an IP address different from 10.244.0.1/24问题

问题 NetworkPlugin cni failed to set up pod “coredns-5d4b4db-jkmnl_kube-system” network: failed to set bridge addr: “cni0” already has an IP address different from 10.244.0.1/24 解决方案 这个问题通常是由于Flannel网络插件残留配置导致的IP地址冲突。以下…

QT+opecv如何更改图片的拍摄路径

如何更改相机拍摄图片的路径 前言&#xff1a;基础夯实&#xff1a;效果展示&#xff1a;实现功能&#xff1a;遇到问题&#xff1a;未解决&#xff1a; 核心代码&#xff1a; 前言&#xff1a; 最近在项目开发中遇到需要让用户更改相机拍摄路径的问题&#xff0c;用户可自己选…

66常用控件_QTableWidget的使用

目录 代码示例:使用QTableWidget Table Widget 使⽤ QTableWidget 表⽰⼀个表格控件. ⼀个表格中包含若⼲⾏, 每⼀⾏⼜包含若⼲列. 表格中的每个单元格, 是⼀个 QTableWidgetItem 对象. QTableWidget 核⼼⽅法 方法说明item(int row, int column)根据行数数列获取指定的…

记一次edu未授权访问漏洞

首先进入该网址是一个登录界面&#xff0c;查看源代码&#xff0c;找到js文件&#xff0c;发现存在js.map前端信息泄露&#xff0c;于是我们进行js还原。 得到前端的一些源代码&#xff0c;以及路由API等&#xff0c;我们就可以通过这个源代码&#xff0c;进行目录遍历&#xf…

MySQL 索引和事务

目录 前言 一、MySQL 索引介绍 1. 索引概述 2. 索引作用 3. 索引的分类 3.1 普通索引 3.2 唯一索引 3.3 主键索引 3.4 组合索引 (最左前缀) 3.5 全文索引 (FULLTEXT) 3.6 创建索引的原则依据 3.7 查看索引 3.8 删除索引 二、MySQL 事务 1. 事务的 ACID 原则 MYS…

HTML实战项目:高考加油和中考加油

设计思路 页面加载后会自动显示高考内容&#xff0c;点击顶部按钮可以切换到中考内容。倒计时会每秒更新&#xff0c;为考生提供实时的备考时间参考。 使用代表希望的蓝色和金色渐变作为主色调 顶部导航栏可切换高考/中考内容 添加动态倒计时功能 设计励志名言卡片和备考小贴…

What is Predictive Maintenance (PdM)? Learn How Industrial IoT Enables PdM

文章大纲 从预防性维护到预测性维护服务的转变传统预防性维护的局限性预测性维护的定义工业物联网(IIoT)如何助力预测性维护预测性维护带来的成本效益实施预测性维护面临的挑战企业转向预测性维护的原因参考文献大家好!今天,让我们一起深入了解一下预测性维护技术。它正在彻…

CCPC shandong 2025 G

题目链接&#xff1a;https://codeforces.com/gym/105930/problem/G 题目背景&#xff1a; n 名工人加工 m 个工件&#xff0c;第 i 个工件在第 ti 分钟的开头加入 工人 wi 的收件箱。 每分钟&#xff0c;工人从收件箱里拿出一个工件&#xff0c;完成加工后放入下 一个工人的收…

UE路径追踪Path Tracing和Lumen的区别

在Unreal Engine&#xff08;UE&#xff0c;虚幻引擎&#xff09;中&#xff0c;Path Tracing 和 Lumen 是两种不同的全局光照&#xff08;Global Illumination, GI&#xff09;和反射技术&#xff0c;各自适用于不同的使用场景。以下是它们的主要区别&#xff1a; &#x1f31…

JaCoCo 是什么

JaCoCo&#xff08;Java Code Coverage&#xff09;是一款广泛使用的 Java 代码覆盖率工具&#xff0c;用于分析测试用例对项目代码的覆盖程度&#xff0c;帮助开发者识别未被测试的代码区域&#xff0c;从而提升软件质量。它通常与 JUnit、TestNG 等测试框架及 Maven、Gradle …

火山引擎扣子系列

您提到的“火山引擎扣子系列”指的应该是 **火山引擎推出的智能AI对话开发与应用平台——Coze&#xff08;中文名&#xff1a;扣子&#xff09;**。这是一个由字节跳动旗下火山引擎开发的、面向开发者和非技术用户的**低代码/无代码AI Bot开发平台**&#xff0c;旨在帮助用户快…

OpenLayers 加载ArcGIS瓦片数据

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 随着GIS应用的不断发展&#xff0c;Web地图也越来越丰富&#xff0c;除了像ESRI、超图、中地数码这样GIS厂商有各自的数据源格式&#xff0c;也有Google…

大模型是什么?

大模型&#xff0c;英文名叫Large Model&#xff0c;也被称为基础模型&#xff08;Foundation Model&#xff09;。我们通常说的大模型&#xff0c;主要指的是其中最常用的一类——大语言模型&#xff08;Large Language Model&#xff0c;简称LLM&#xff09;。除此之外&#…

LLaMaFactory 微调QwenCoder模型

步骤一&#xff1a;准备LLamaFactory环境 首先,让我们尝试使用github的方式克隆仓库: git config --global http.sslVerify false && git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git # 创建新环境&#xff0c;指定 Python 版本&#xff08;以 3.…

【位运算】判断字符是否唯⼀(easy)

33. 判断字符是否唯⼀&#xff08;easy&#xff09; 题⽬描述&#xff1a;解法&#xff08;位图的思想&#xff09;&#xff1a;C 算法代码&#xff1a;Java 算法代码&#xff1a; 题⽬链接&#xff1a;⾯试题 01.01. 判定字符是否唯⼀ 题⽬描述&#xff1a; 实现⼀个算法&…

满天星之canvas实现【canvas】

展示 文章目录 展示Canvas 介绍【基础】简介兼容性关键特性注意事项应用场景&#xff1a;基本示例 满天星代码实现【重点】代码解释 全量代码【来吧&#xff0c;尽情复制吧少年】html引入JS代码 参考资源 Canvas 介绍【基础】 简介 Canvas是一个基于HTML5的绘图技术&#xff0…

可视化提示词(Prompt)在训练过程中的优化过程:visualize_prompt_evolution

可视化提示词(Prompt)在训练过程中的优化过程:visualize_prompt_evolution 这个函数 visualize_prompt_evolution 的作用是可视化提示词(Prompt)在训练过程中的优化过程,通过对比每个训练轮次(Epoch)的提示词与初始提示词的差异,直观展示哪些Token被保留、哪些被修改…