浏览器的同源策略

概述 本文从以下三个问题,循序渐进,一步步拆解,何为浏览器的同源策略和跨域问题: 什么是同源策略和跨域问题? 怎样才算同源? 同源策略下会有什么限制? 什么是同源策略和跨域问题? 同源政策是 1995 年由 Netscape 公司(就是创造了 JS 的那家网景公司)引入浏览器的一种策略。 想要理解同源策略的话,可以先假设如果没有同源策略会发生什么事情。接下来我们通过一则恐怖故事代入理解一下没有同源策略的世界: 在某个深夜,小明独自一人在家浏览网页,无意间打开了一些特殊网站,而恰巧的是,小明之前刚好打开过某些重要支付平台的网站且还没关闭,这时特殊网站里面可能有一些特殊 JS 脚本,在偷偷获取小明支付平台上的信息,并且偷偷利用了这些信息再结合某些手段,成功把小明支付平台上的钱转走了,小明上完厕所回来后,突然收到支付平台转账成功的短信,然后然后然后瘫坐在地上,泣不成声,仰天长啸,大喊一声:“还我攒了半年的 9.99~” 恐怖故事听完了,我们来先上个概念,什么是同源策略? 借用 MDN 上的描述: 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 怎么理解这个概念呢?举个栗子,随着汽车数量的上升,现在很多城市都有各种限行政策,就拿广州来说,如果不是 粤A 的车牌进入城区的话会有开四停四的限行限制。 同源策略跟汽车限行政策有一点点像,同源策略规定在“同源”下的脚本、文档等资源访问不受限(有点像 粤A 牌的汽车在广州内行驶不会被限行),如果不是“同源”则无法互相访问脚本、文档等资源(非 粤A 牌的汽车在广州市区内受开四停四政策限制) 理解了同源策略后,对于跨域就很好理解了,同源策略是好,我们也需要同源策略,但有些时候我们确实需要不同源的两个网页互相读取信息,这是就产生了跨域问题。 额外补充,同源策略是浏览器的行为,是浏览器的行为,是浏览器的行为(对于这点的解析可以参考:同源策略下,服务器会收到浏览器的请求吗?) 怎样才算同源? 所谓“同源”,就是以下三者必须都相同: 协议相同 协议相同的两个域名:(都是 https 协议) https://juejin.cn https://juejin.cn 协议不同的两个域名:(一个是 http 协议,一个是 https 协议) http://juejin.cn https://juejin.cn 域名相同 域名相同的两个域名: https://juejin.cn https://juejin.cn 域名不同的两个域名:(一个是二级域名,一个是三级域名)(可能这个是最容易记错的) https://juejin.cn https://www.juejin.cn 端口相同 端口相同的两个域名: https://juejin.cn https://juejin.cn 端口不同的两个域名: https://juejin.cn https://juejin.cn:8081 再重复一遍, 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同源策略下会有什么限制? 在同源策略下,如果非同源,会有以下三个方面的行为受到限制: DOM 如果非同源,其 JS 脚本,不能互相对 DOM 进行读写操作 数据 如果非同源,不能互相读写 Cookies、LocalStorage、SessionStorage 和 IndexedDB 网络请求 如果非同源,不能通过 AJAX 的方式互相发送、接收数据 为了方便理解,接下来我们展开说说这三条规则 ↓↓↓ ...

December 17, 2024

受控组件和非受控组件

前端开发经常会涉及表单的处理,或者其他一些用于输入的组件,比如日历组件。 涉及到输入,就绕不开受控模式和非受控模式的概念。 什么是受控,什么是非受控呢? 想一下,改变表单值只有两种情况: 用户去改变 value 或者代码去改变 value。 如果不能通过代码改表单值 value,那就是非受控,也就是不受我们控制。 但是代码可以给表单设置初始值 defaultValue。 代码设置表单的初始 value,但是能改变 value 的只有用户,代码通过监听 onChange 来拿到最新的值,或者通过 ref 拿到 dom 之后读取 value。 这种就是非受控模式。 反过来,代码可以改变表单的 value,就是受控模式。 注意,value 和 defaultValue 不一样: defaultValue 会作为 value 的初始值,后面用户改变的是 value。 而一旦你给 input 设置了 value,那用户就不能修改它了,可以输入触发 onChange 事件,但是表单的值不会变。 用户输入之后在 onChange 事件里拿到输入,然后通过代码去设置 value。 这就是受控模式。 其实绝大多数情况下,非受控就可以了,因为我们只是要拿到用户的输入,不需要手动去修改表单值。 但有的时候,你需要根据用户的输入做一些处理,然后设置为表单的值,这种就需要受控模式。 或者你想同步表单的值到另一个地方的时候,类似 Form 组件,也可以用受控模式。 value 由用户控制就是非受控模式,由代码控制就是受控模式。 我们写代码试一下: npx create-vite 创建 vite + react 的项目。 去掉 main.tsx 的 index.css 和 StrictMode: ...

December 17, 2024

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