react 事件机制

一 前言 我们来一起探讨一下React事件原理,这篇文章,我尽量用通俗简洁的方式,把React事件系统讲的明明白白。 我们讲的react版本是16.13.1 , v17之后react对于事件系统会有相关的改版,文章后半部分会提及。 老规矩,在正式讲解react之前,我们先想想这几个问题(如果我是面试官,你会怎么回答?): 我们写的事件是绑定在dom上么,如果不是绑定在哪里? 为什么我们的事件不能绑定给组件? 为什么我们的事件手动绑定this(不是箭头函数的情况) 为什么不能用 return false 来阻止事件的默认行为? react怎么通过dom元素,找到与之对应的 fiber对象的? onClick是在冒泡阶段绑定的? 那么onClickCapture就是在事件捕获阶段绑定的吗? 必要的知识概念 在弄清楚react事件之前,有几个概念我们必须弄清楚,因为只有弄明白这几个概念,在事件触发阶段,我们才能更好的理解react处理事件本质。 我们写在JSX事件终将变成什么? 我们先写一段含有点击事件的react JSX语法,看一下它最终会变成什么样子? class Index extends React.Component{ handerClick= (value) => console.log(value) render(){ return <div> <button onClick={ this.handerClick } > 按钮点击 </button> </div> } } 经过babel转换成React.createElement形式,如下: 最终转成fiber对象形式如下: fiber对象上的memoizedProps 和 pendingProps保存了我们的事件。 什么是合成事件? 通过上一步我们看到了,我们声明事件保存的位置。但是事件有没有被真正的注册呢?我们接下来看一下: 我们看一下当前这个元素<button>上有没有绑定这个事件监听器呢? button上绑定的事件 我们可以看到 ,button上绑定了两个事件,一个是document上的事件监听器,另外一个是button,但是事件处理函数handle,并不是我们的handerClick事件,而是noop。 noop是什么呢?我们接着来看。 原来noop就指向一个空函数。 然后我们看document绑定的事件 可以看到click事件被绑定在document上了。 接下来我们再搞搞事情😂😂😂,在demo项目中加上一个input输入框,并绑定一个onChange事件。睁大眼睛看看接下来会发生什么? class Index extends React.Component{ componentDidMount(){ console.log(this) } handerClick= (value) => console.log(value) handerChange=(value) => console.log(value) render(){ return <div style={{ marginTop:'50px' }} > <button onClick={ this.handerClick } > 按钮点击 </button> <input placeholder="请输入内容" onChange={ this.handerChange } /> </div> } } 我们先看一下input dom元素上绑定的事件 ...

December 17, 2024

列表和key

当我们在 React 要渲染一个列表时,如果没有在每一个被渲染的元件加上 key 这个 prop,就会跳出 Warning: Each child in a list should have a unique “key” prop. 这个错误讯息。直到开发者把 key 加上后,这个警告讯息才会消失。为什么要加上 key? 以及 key 有什么原则需要遵守? 这是在开发 React 时需要有的重要概念,也是面试经常会问的基础题。 为什么需要 key? key 就像一个独特身份,让 React 可以去分辨哪些子元件被新增、 移除,或是修改。 (编按:若要进一步说明此概念,推荐在面试时画下 Dan Abramov 的这个系列推文例子) 从上面的例子可以看到,当今天红黄蓝三个圈,变成红蓝黄;这会有两个可能性。可能性一第二个圈跟第三个圈的位置互换;可能性二是位置没互换,但第二个圈变蓝色、第三个圈变黄色。如果没有一个独特辨识的机制,我们会没办法知道,究竟是哪一个可能性。 如果没办法有效辨识,将可能出现 bug。举例来说,假如今天的圈圈是有状态的,例如里面有勾选方块,然后第个二圈有被勾选。假如今天换颜色是因为第二个圈被交换到第三个位置,这时表示新一次的渲染时,第三个圈要是被勾选的。不过假如我们没有一个辨识机制,要是 React 误以为换颜色不是因为换位置,而是单纯的第二个圈换颜色,那么将会渲染出仍是第二个圈是被勾选的;那么这就会是一个 bug。 然而,有了 key 这个辨识机制,React 就会知道在新的一次渲染时,原本的状态应该被保留在列表中的哪一个元件中。因此,React 之所以需要 key,正是因为 key 可以让 React 知道,哪些子元件被新增、 移除,或是修改。 除此之外,看到下面这段 React 官方文件的例子 (编按:面试时也推荐直接举这例子)。原本有个列表,我们在最上方新增一个 <li>Connecticut</li> ,如果没有 key,对 React 来说将会是,Duke 变成 Connecticut、Villanova 变成 Duke,而最后新增一个 Villanova。换句话说,整个列表都改变了,所以 React 会打掉旧的,重建一个新的列表。当列表变大时,就会很消耗效能。 ...

December 17, 2024

条件渲染的常见方法和注意事项

条件渲染方式 1. if 语句 先从 React 最基本的条件类型来看。如果有数据就显示组件,如果没有数据就不显示任何内容。posts 为需要渲染的列表: export default function App() { const { posts } = usePosts(); if (!posts) return null; return ( <div> <PostList posts={posts} /> </div> ); } 这种形式会生效的原因就是我们会提前返回,如果满足条件(posts 值不存在),就通过return null 在组件中不显示任何内容。 如果有多个要检查的条件时,也可以使用 if 语句。例如,在显示数据之前检查加载和错误状态: export default function App() { const { isLoading, isError, posts } = usePosts(); if (isLoading) return <div>Loading...</div>; if (isError) return <div>Error!</div>; return ( <div> <PostList posts={posts} /> </div> ); } 这里我们可以多次使用 if 语句,不需要再使用 else 或者 if-eles 语句,这样就减少了需要编写的代码,并且可读性更强。 2. 三元运算符 当我们想提前退出或者什么都不显示时,if 语句会很有用。但是,如果我们不想写一个与返回的 JSX 分开的条件,而是直接在其中写呢?那就可以使用三元表达式来编写条件。 在 React 中,我们必须在 JSX 中包含表达式,而不是语句。这就是为什么我们在 JSX 中只能使用三元表达式,而不是 if 语句来编写条件。 ...

December 17, 2024

Context API

先看看React官网对于Context的介绍 Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。 个人理解转成大白话:Context提供了一个局部的全局作用域,使用Context则无需再手动的逐层传递props。 本文主要介绍3种Context的使用方式: React.createContext提供的Provider和Consumer 函数组件:React.createContext提供的Provider和useContext钩子 Class组件:React.createContext提供的Provider和class的contextType属性 第一种:React.createContext提供的Provider和Consumer 先写好使用Context的基础环境条件,后续的代码都是基于此环境 //创建一个文件,暂且命名为context.js,导出createContext()的返回值 import { createContext } from "react"; export default createContext(); 在根组件App.jsx中,导入上面写的context,并使用context提供的Provider组件进行包裹,圈定局部的全局作用域,传值后可以提供给子组件进行消费 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。 import React, { createContext } from "react"; import MyContext from "./context"; import GeneralC from "./GeneralC"; import FnC from "./FnC"; import ClassC from "./ClassC"; export default function App() { return ( //Provider组件接收一个value属性,此处传入一个带有name属性的对象 <MyContext.Provider value={{ name: `context's value is string!` }}> {/*这里写后面要进行包裹的子组件,此处先行导入后续需要消费context的3个组件*/} <GeneralC/> <hr/> <FnC/> <hr/> <ClassC/> </MyContext.Provider> ); } 在GeneralC组件中,导入context,使用其提供的Consumer组件来订阅Context的变更,需要一个函数作为子元素,函数的第一个形参便是Provider组件提供的value值 import React, { useReducer } from "react"; import MyContext from "./context"; const GeneralC = () => { return ( // <MyContext.Consumer> {(value) => { return ( <div> 第一种使用Context方式获取的值:{JSON.stringify(value)} </div> ); }} </MyContext.Consumer> ); }; export default GeneralC; 此时页面中应该出现json格式的value值 ...

December 17, 2024

react hooks

一 前言 React hooks是react16.8 以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。 本章节将介绍目前 React 提供的所有 hooks ,介绍其功能类型和基本使用方法。 1.1 技术背景 react hooks 解决了什么问题? 先设想一下,如果没有 Hooks,函数组件能够做的只是接受 Props、渲染 UI ,以及触发父组件传过来的事件。所有的处理逻辑都要在类组件中写,这样会使 class 类组件内部错综复杂,每一个类组件都有一套独特的状态,相互之间不能复用,即便是 React 之前出现过 mixin 等复用方式,但是伴随出 mixin 模式下隐式依赖,代码冲突覆盖等问题,也不能成为 React 的中流砥柱的逻辑复用方案。所以 React 放弃 mixin 这种方式。 类组件是一种面向对象思想的体现,类组件之间的状态会随着功能增强而变得越来越臃肿,代码维护成本也比较高,而且不利于后期 tree shaking。所以有必要做出一套函数组件代替类组件的方案,于是 Hooks 也就理所当然的诞生了。 所以 Hooks 出现本质上原因是: 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。 解决逻辑复用难的问题。 放弃面向对象编程,拥抱函数式编程。 为什么要使用自定义 Hooks ? 自定义 hooks 是在 React Hooks 基础上的一个拓展,可以根据业务需求制定满足业务需要的组合 hooks ,更注重的是逻辑单元。通过业务场景不同,到底需要React Hooks 做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义 hooks 产生的初衷。 自定义 hooks 也可以说是 React Hooks 聚合产物,其内部有一个或者多个 React Hooks 组成,用于解决一些复杂逻辑。 ...

December 17, 2024

浏览器的存储机制

浏览器的存储机制涵盖多种技术,目的是满足 Web 应用在不同场景下的数据持久化、缓存和离线访问需求。它们各自具有不同的存储容量、生命周期、安全策略和访问方式,理解这些机制对于构建高效、安全的前端应用至关重要。 一、浏览器存储机制分类 1. Cookie 最早期的存储机制,主要用于会话管理和身份认证。 容量限制:单个约 4KB,总数有限制; 生命周期:可设置过期时间或为会话级; 访问方式:由浏览器自动在 HTTP 请求头中携带,也能通过 JS 访问(除非设置 HttpOnly); 安全性:可设置 HttpOnly、Secure、SameSite 防范安全风险。 2. Web Storage(LocalStorage 和 SessionStorage) HTML5 新增的简单键值对存储方案。 LocalStorage:永久存储,关闭浏览器数据仍保留,同源可访问; SessionStorage:页面会话存储,关闭标签页即清空; 容量:一般为 5~10MB; 访问方式:同步 API,容易使用但可能阻塞主线程。 3. IndexedDB 浏览器内置的底层结构化数据库,支持事务和索引,适合复杂和大容量数据存储。 容量:远大于 LocalStorage,通常以设备剩余空间为限; 访问方式:异步 API,支持复杂操作; 用途:离线应用、大数据缓存、文件存储等。 4. Cache Storage 由 Service Worker 管理的请求和响应缓存,用于离线和加速访问。 容量:较大,依赖设备和浏览器策略; 访问方式:异步 Promise API; 用途:缓存静态资源、接口响应,实现离线支持。 5. Service Worker 虽然不是存储机制本身,但作为浏览器后台代理进程,管理 Cache Storage,实现网络请求拦截和缓存策略,是现代离线应用的核心。 生命周期独立于页面,可在后台运行; 控制页面的网络请求,提供离线能力和资源预缓存; 可结合 Cache Storage 和 IndexedDB 进行数据管理。 二、存储机制的生命周期与访问作用域 Cookie:基于域和路径,可设置 HttpOnly、Secure 限制访问; LocalStorage/SessionStorage:基于同源策略,SessionStorage 进一步限定于标签页会话; IndexedDB 和 Cache Storage:基于同源,支持版本控制和升级; Service Worker:独立于页面,能控制同源下的所有相关页面。 三、容量限制与性能影响 Cookie 容量最小,且会随每次请求自动发送,影响网络性能; LocalStorage/SessionStorage 适合轻量存储,容量适中; IndexedDB 和 Cache Storage 容量大,适合海量数据和文件缓存; Service Worker 通过异步调度,避免阻塞主线程。 四、安全与隐私考虑 Cookie 可被服务器访问,设置 HttpOnly 避免脚本窃取; Web Storage、IndexedDB 只能由同源脚本访问,防止跨站数据泄漏; Service Worker 需 HTTPS 环境,避免被恶意注入; 用户隐私模式下,存储行为可能受限,数据不保证持久。 五、应用场景及选择建议 需要与服务器频繁交互、会话维持时用 Cookie; 存储简单配置、少量数据用 LocalStorage; 页面会话临时数据用 SessionStorage; 大规模结构化数据和离线存储用 IndexedDB; 静态资源及请求缓存用 Cache Storage,配合 Service Worker 提升离线体验和性能; Service Worker 负责管理缓存和拦截网络请求,实现 PWA 功能。 常见考点 面试考察点通常围绕存储类型、特点、使用场景、容量限制、安全性和生命周期展开: ...

December 17, 2024

React 生命周期

简介 React从v16.3的版本开始, 对生命周期的钩子进行了渐进式的调整,分别废弃和新增了一些生命周期的钩子函数,本文会从以下四点开始讲解: 新旧生命周期函数的对比 分析为什么要废弃旧的钩子函数 详解新的生命周期使用场景 实例代码演示并进行总结 新旧生命周期对比 一个完整的React组件生命周期会依次调用如下钩子: old lifecycle 挂载 constructor componentWillMount render componentDidMount 更新 componentWillReceiveProps shouldComponentUpdate componentWillUpdate render componentDidUpdate 卸载 componentWillUnmount new lifecycle 挂载 constructor getDerivedStateFromProps render componentDidMount 更新 getDerivedStateFromProps shouldComponentUpdate render getSnapshotBeforeUpdate componentDidUpdate 卸载 componentWillUnmount 从以上生命周期的对比,我们不难看出,React从v16.3开始废弃 componentWillMount componentWillReceiveProps componentWillUpdate 三个钩子函数 分析废弃原因 Facebook花了两年多的时间搞出了React Fiber, 因为在v15的版本,更新过程是同步的,往往一个主线程长时间被占用,会导致页面性能问题 而 React Fiber的机制: 利用浏览器 requestIdleCallback 将可中断的任务进行分片处理,每一个小片的运行时间很短,这样唯一的线程就不会被独占 需要详细了解可以点下面链接 Morgan大佬 - 知乎 那么React Fiber会对生命周期带来什么影响吗? 因为React Fiber Reconciliation 这个过程有可能暂停然后继续执行,所以挂载和更新之前的生命周期钩子就有可能不执行或者多次执行; 目前React为这几个生命周期钩子提供了别名,分别是: UNSAFE_componentWillMount UNSAFE_componentWillReceiveProps UNSAFE_componentWillUpdate React17将只提供别名,取个别名的目的就是恶心你,不让你使用。 ...

December 17, 2024

浏览器的垃圾回收机制

浏览器的垃圾回收(Garbage Collection, GC)机制是前端性能优化和内存管理的重要基础。 一、垃圾回收的基本概念 目的:自动回收不再使用的内存,避免内存泄漏,保证浏览器性能稳定。 GC 触发:当浏览器检测到内存不足或特定条件时,启动垃圾回收过程。 二、主要垃圾回收算法 1. 标记清除(Mark-and-Sweep) 浏览器从**根对象(Global、执行上下文中的变量)**开始,标记所有可达对象。 没被标记的对象被认为不可达,即不再被使用,进行回收。 是现代 JS 引擎普遍采用的算法。 2. 引用计数(Reference Counting) 每个对象维护引用计数,引用增加时计数+1,引用消失时计数-1。 计数为 0 的对象立即回收。 缺陷:无法处理循环引用,现代引擎一般不单独使用。 三、垃圾回收的触发时机 内存分配达到一定阈值时自动触发。 主动调用相关接口(如 Chrome DevTools 手动触发)。 页面卸载时进行清理。 四、内存泄漏常见原因 全局变量未释放 全局变量一直被引用,无法回收。 闭包导致的变量无法释放 闭包作用域内变量被外部引用。 定时器未清除 setInterval、setTimeout 未正确清除,导致引用保留。 DOM 节点引用未释放 JS 中持有对已删除 DOM 的引用。 事件监听未移除 绑定事件后,未及时解绑,导致内存无法回收。 五、性能优化建议 避免不必要的全局变量。 使用完定时器及时清除。 解绑不再使用的事件监听。 谨慎使用闭包,避免无用变量持久存在。 小心操作 DOM,及时释放引用。 常见考点 面试中考察点主要包括 GC 的原理、算法、触发时机、内存泄漏原因及避免方法: 浏览器垃圾回收的原理和常见算法? 标记清除与引用计数的区别与优缺点? 什么是内存泄漏?常见的内存泄漏类型? 如何避免内存泄漏? JS 引擎如何判断对象是否可回收? 浏览器中 GC 触发的时机? 如何用 Chrome DevTools 监测内存泄漏? 事件监听和闭包如何导致内存泄漏?

December 17, 2024

state 与 props

state 基本定义 state是组件内部的状态(数据),不能够直接修改,必须要通过setState来改变值的状态,从而达到更新组件内部数据的作用。 props 基本定义 props是指组件间传递的一种方式,props自然也可以传递state。由于React的数据流是自上而下的,所以是从父组件向子组件进行传递;另外组件内部的this.props属性是只读的不可修改。 代码示例 // 父组件 import stateJj from './stateJj.js'; // 引入子组件 class stateJjFather extends React.Component { constructor(props) { super(props); // 设置state的默认值 且只能在constructor中设置 this.state = { mes: '给子组件的信息', obj: { }, arr: [] } } fun(e) { } render() { const {mes, obj, arr} = this.state; return ( <div> // 不写箭头函数 this指向会发生错误 也可以使用bind的方式绑定this <stateJj name={'给子组件的信息'} name1={mes} fun={(e) => this.fun(e)} obj={obj} arr={arr} /> </div> ); } } // 子组件 class stateJj extends React.Component { constructor(props) { super(props); this.props; // 这里面就有父组件传的值 // 设置 默认state this.state = { text: props.initialValue || 'placeholder' }; // ES6 类中函数必须手动绑定 this.handleChange = this.handleChange.bind(this); } handleChange(event) { this.setState({ text: event.target.value }); } render() { return ( <div> {mes} </div> ); } } // 设置props值的类型 stateJj.propTypes = { optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalSymbol: PropTypes.symbol, // 目前可以声明的所有变量类型 }; // 设置默认的props值 stateJj.defaultProps = { }; props的特性 只读性 props经常被用作渲染组件和初始化状态,当一个组件被实例化之后,它的props是只读的,不可改变的。如果props在渲染过程中可以被改变,会导致这个组件显示的形态变得不可预测。只有通过父组件重新渲染的方式才可以把新的props传入组件中。 ...

December 17, 2024

浏览器的缓存机制

一、前言 缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。 接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。 如需获取思维导图或想阅读更多优质文章请猛戳GitHub博客 二、缓存位置 从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。 Service Worker Memory Cache Disk Cache Push Cache 1.Service Worker Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。 Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。 当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。 2.Memory Cache Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。 那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢? 这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存 内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。 需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。 3.Disk Cache Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。 ...

December 17, 2024