乐闻世界logo
搜索文章和话题

前端面试题手册

在 Web 应用中,从服务器主动推送Data到客户端有那些方式?

在Web应用中,服务器向客户端主动推送数据是一个常见需求,可以实现如实时通知、即时聊天等功能。以下是一些实现服务器到客户端推送的技术:轮询(Polling)轮询是最简单的一种方式,客户端通过定时发送HTTP请求到服务器查询是否有新数据。这种方式的缺点是效率较低,并且会产生很多无用的网络流量,因为即使没有数据更新,客户端也会定时发起请求。长轮询(Long Polling)长轮询是对传统轮询的改进。客户端发送请求给服务器后,服务器会持有这个请求,直到有新数据可以发送或者达到某个时间限制。这种方式比传统轮询效率更高,但仍有延迟,并且会占用服务器资源。服务器发送事件(Server-Sent Events, SSE)SSE允许服务器通过HTTP连接向客户端推送事件。与轮询不同,这里的连接是单向的:服务器到客户端。SSE支持自动重连,并且可以只发送更新的数据。但SSE只支持文本数据,并且不是所有浏览器都支持。WebsocketWebsocket提供了一个全双工的通信通道,允许服务器和客户端之间进行双向通信。连接一旦建立,服务器就可以在任何时候发送数据给客户端,同样客户端也可以随时发送数据给服务器。Websocket适合需要高频实时交互的应用,如在线游戏、交易平台等。例子:一个即时消息应用可能会使用Websocket来推送消息。当一个用户发送消息时,服务器接收这个消息并通过已经打开的Websocket连接将它推送给其他在线用户。由于Websocket允许低延迟的双向通信,用户体验接近即时通信。Web Push Notifications这种技术允许服务器向注册了推送服务的客户端发送通知,即使Web应用没有在前台运行,用户也能够收到通知。Web推送通知通常用于向用户发送即时信息,比如电子邮件到达、社交媒体的新动态等。Message Queues(如RabbitMQ)和Push Services(如Google Firebase)一些服务器并不直接发送数据给客户端,而是利用消息队列和第三方推送服务。消息队列可以缓存消息,然后基于某些条件(如客户端在线)进行分发。第三方推送服务则提供了一套完整的解决方案来管理消息的推送。在设计一个系统时,选择哪种技术取决于应用的具体需求,比如是否需要低延迟、高吞吐量,以及客户端支持的技术等。通常,实时性要求较高的应用会选择Websocket,而对实时性要求较低的应用可以选择SSE或长轮询。对于移动应用,Web Push Notifications是一个不错的选择,因为它们可以推动用户重新参与使用应用。
阅读 35·2024年6月24日 16:43

React Fiber 架构是什么?有什么优势?

React Fiber 是 React 框架的一个核心算法重写版本,它是 React 16 版本中引入的。Fiber 架构的主要目标是增强 React 在处理动画、布局、手势等方面的能力,并且让这些任务的执行变得更加平滑,不会引起应用程序的卡顿。这种架构的引入是为了优化渲染过程,使之能够利用浏览器的空闲时间执行,从而提高应用程序的性能并使用户界面更加流畅。React Fiber 架构的主要优势有:增量渲染:Fiber 架构的主要功能之一是能够将渲染工作拆分成多个小任务,并将这些任务分散到多个帧中。这个特点允许 React 暂停和恢复渲染任务,这种“可中断”的渲染过程意味着主线程可以更响应用户操作,从而提高了应用的性能。任务优先级:Fiber 架构可以为更新分配优先级。一些任务(如动画)比其他任务(如数据的后台同步)更为紧急。React Fiber 可以区分这些任务,并且先执行更高优先级的任务,再在空闲时处理低优先级的任务。更好的错误处理:Fiber 引入了新的错误边界概念,使得组件能够更好地捕获子组件的错误,并且定义备用 UI,从而提供更好的用户体验。更平滑的动画和过渡:由于 React Fiber 可以利用浏览器的空闲时间执行渲染任务,因此可以更平滑地执行动画和过渡,降低了卡顿的可能性。更好的适配未来的变化:Fiber 架构为将来 React 框架的可能更新和改变打下了基础,比如并发模式(Concurrent Mode)和 Suspense 等新特性。示例:优先级调度:想象一下一个用 React Fiber 构建的聊天应用。用户正在输入消息,同时应用正在后台同步接收新消息。使用 Fiber 架构,React 可以给用户输入的响应分配更高的优先级,从而保证输入的流畅,而消息同步的任务可以在浏览器空闲时进行,用户体验因此得到提升。通过这些改进,React Fiber 架构使得开发者可以构建出更加响应快速、用户体验更好的应用程序。
阅读 33·2024年6月24日 16:43

结合 Vue.js 实现事件总线 Event Bus

事件总线是一种模式,可以通过一个中央通道分发事件,让不同的系统部分实现解耦。在Vue.js中,事件总线通常是通过一个空的Vue实例来实现的。 以下是我如何在Vue项目中实现一个事件总线,以及我可能会用到它的一个场景:实现事件总线创建事件总线:// event-bus.jsimport Vue from 'vue';export const EventBus = new Vue();在组件中使用事件总线:发射事件:// ComponentA.vue<template> <!-- 组件模板 --></template><script>import { EventBus } from './event-bus.js';export default { methods: { someMethod() { EventBus.$emit('my-event', { someData: 'Some data to send' }); } }}</script>监听事件:// ComponentB.vue<template> <!-- 组件模板 --></template><script>import { EventBus } from './event-bus.js';export default { mounted() { EventBus.$on('my-event', this.handleMyEvent); }, beforeDestroy() { EventBus.$off('my-event', this.handleMyEvent); }, methods: { handleMyEvent(payload) { console.log('Event received', payload); // 处理事件 } }}</script>在这个例子中,ComponentA 发射了一个事件 my-event,并传递了一些数据。ComponentB 监听这个事件,并定义了一个方法 handleMyEvent 来处理接收到的事件。例子:事件总线的使用场景假设我们有一个应用,其中有一个组件负责用户的认证(例如登录状态的显示),而另一个组件是一个模态框,用于登录。这两个组件位于不同的层级,也可能不直接相关。我们不希望在每个需要知道登录状态的组件中都直接与模态框组件通信,因为这会导致高耦合和难以维护的代码。在这种情况下,事件总线就派上了用场:当用户在模态框中登录成功后,模态框组件可以发射一个事件,比如 login-success。认证组件可以监听 login-success 事件,并据此更新用户的显示状态。这样,我们就可以保持组件间的解耦,同时使它们能够有效地沟通。注意事项Vue 2.x中支持使用 $on, $emit, 和 $off 这样的实例方法来实现事件总线。然而,在Vue 3.x中,这种模式已经不再推荐,因为它违背了Vue 3推崇的Composition API的设计原则。在Vue 3中,推荐使用 provide/inject、Vuex或者Vue Composition API中的 reactive、ref以及 watchEffect来在组件间共享状态。
阅读 71·2024年6月24日 16:43

什么是XSS攻击?

XSS攻击,全称是跨站脚本攻击(Cross-Site Scripting),它是一种网站应用程序的安全漏洞攻击,攻击者通过这种方式可以在用户浏览器端执行恶意脚本。这些恶意脚本一旦在用户浏览器上运行,就可以窃取用户信息、篡改网站内容、无意间欺骗用户执行某些操作等。XSS攻击通常分为三种类型:存储型XSS(Persistent XSS):恶意脚本被永久存储在目标服务器,如数据库、消息论坛、访客留言等地方。当用户浏览相关页面时,恶意脚本就会被执行。例如,攻击者在社交媒体网站上发表带有恶意JavaScript代码的评论,当其他用户查看该评论时,此脚本便会在他们的浏览器上执行。反射型XSS(Reflected XSS):恶意脚本不会被存储在服务器上,它是通过诸如URL、电子邮件、即时消息等传达给用户的,用户点击链接后,由服务器动态生成带有攻击代码的页面返回给用户,恶意脚本随即在用户浏览器上执行。比如一个搜索引擎的搜索结果页面包括了用户输入的搜索关键词,如果这个关键词没有被恰当地处理,攻击者可以构造一个特殊的URL,当用户点击这个链接时,搜索关键词处的恶意脚本就会执行。DOM型XSS(DOM-based XSS):这种类型的攻击中,恶意代码并没有直接在服务器的响应中反映出来,而是在页面已经加载到用户浏览器后,由于DOM环境中的数据流动不安全,攻击脚本得以在客户端运行。例如,一个网页根据URL的参数来决定内容展示,如果没有对参数进行合适的处理,攻击者可以修改URL参数,使页面执行恶意脚本。防御XSS攻击的常用方法包括:对用户输入进行验证和过滤,避免直接输出未经处理的用户输入。使用HTTP-only Cookie,防止JavaScript访问敏感Cookie。实施内容安全策略(CSP),限制页面可以加载和执行的资源类型和来源。对重要的操作使用CSRF令牌,确保请求是由用户自愿发起的。以上就是XSS攻击的概述和防御策略。
阅读 37·2024年6月24日 16:43

什么是CRSF攻击?

CRSF攻击,全称是跨站请求伪造(Cross-Site Request Forgery),是一种网络攻击方式,它允许攻击者在用户不知情的情况下,以该用户的身份执行非授权的命令或更改用户账户信息。CRSF攻击通常利用用户已经认证的身份,例如在网站的认证机制中,用户通常通过输入密码等方式登录之后,会获得一个认证标记(如Cookie),后续的操作就不需要重复进行身份认证。CRSF攻击的典型场景如下:用户登录到银行网站,并在浏览器中保存了登录凭证(如Cookie)。在不退出银行网站的情况下,用户在另一个标签页中访问了一个恶意网站。恶意网站包含了一个指向银行网站的请求(例如,一个图片的链接或一个自动提交的表单),这个请求中包含了转账操作的命令。当用户的浏览器加载恶意网站时,此请求被发出,并且由于用户在银行网站上已经登录,请求中携带了用户的认证凭证。银行网站接收到请求后,误认为是用户自愿发起的操作,从而执行了转账。这种攻击的危险之处在于,用户完全不知情,而攻击者却可以执行诸如转账、密码更改、购买商品等操作。如果网站没有适当的防御机制,CSRF攻击可以对用户造成重大的财务或数据损失。防御CSRF攻击的常见措施包括:使用Anti-CSRF Token:服务器生成一个随机的、不可预测的token,并在每次敏感操作请求时要求客户端提交这个token,服务器验证此token后才执行操作。双重验证:对于重要操作,要求用户重新输入密码或进行其他形式的认证。设置Cookie的SameSite属性:通过设置Cookie的SameSite属性为Strict或Lax,可以限制Cookie不随跨站请求发送,从而减少CSRF攻击的风险。检查Referer和Origin头:服务器可以通过检查HTTP请求头部的Referer或Origin来验证请求的合法性。以上就是对CSRF攻击的简要说明以及如何防御这类攻击的方法。
阅读 39·2024年6月24日 16:43

什么是defineProperty方法?什么时候需要用到defineProperty?

Object.defineProperty 方法是 JavaScript 中的一个非常重要的方法,它允许精确地添加或修改对象的属性。通过这个方法,我们可以控制属性的各种特性,如它们是否可写、可枚举或可配置,甚至定义存取器属性(getter 和 setter)。Object.defineProperty 方法接受三个参数:要在其上定义属性的对象。将被定义或修改的属性的名称。描述符对象,它定义了属性的具体行为和特征。属性描述符对象可以包含以下键:value:属性的值。writable:如果为 true,则属性的值可以被重写。configurable:如果为 true,则该属性的描述符可以被改变,也可以从对象上删除该属性。enumerable:如果为 true,则该属性会出现在对象的枚举属性中。get:作为属性的 getter 函数,当访问属性时触发。set:作为属性的 setter 函数,当属性值被修改时触发。以下是一个使用 Object.defineProperty 的例子:const person = {};Object.defineProperty(person, 'name', { value: '张三', writable: false, enumerable: true, configurable: true});console.log(person.name); // 输出 '张三'person.name = '李四'; // 由于 writable 是 false,这里的赋值操作不会成功console.log(person.name); // 仍然输出 '张三'在上面的例子中,我们定义了一个对象 person 并使用 Object.defineProperty 给它添加了一个名为 name 的属性。这个属性被设置为不可写,所以尝试更改它的值将不会有任何效果。需要用到 Object.defineProperty 的情况包括:控制属性特性:当你需要精细地控制一个对象属性的可枚举性、可配置性、可写性等。创建只读属性:当你希望对象的某些属性是只读的,不允许修改。定义存取器属性:如果你想通过 getter 和 setter 控制属性的访问和赋值。保护对象结构:在对象创建后保护其结构不被意外改变,比如防止属性被删除。这些特性使得 Object.defineProperty 成为在创建具有特定行为的对象属性时非常有用的工具,特别是在模块和库的开发中,我们经常需要确保对象的接口行为符合预期,防止意外的修改。
阅读 22·2024年6月24日 16:43

javascript 定时器为什么是不精确的?

JavaScript 中的定时器包括 setTimeout 和 setInterval,这两个函数通常用于延迟执行或定期执行某些代码。但是,它们的执行时间并不精确,原因主要有以下几点:单线程和事件循环:JavaScript 是单线程执行的,它依赖于事件循环来处理异步事件。当你设置一个定时器时,你实际上是告诉事件循环在未来的某个时间点上执行一段代码。但是,如果事件循环在处理其他任务时被阻塞,例如执行一个耗时的同步操作,那么定时器的代码执行将会被推迟直到主线程空闲为止。Web 浏览器的定时器分辨率:出于性能和节能的考虑,Web 浏览器的定时器分辨率不会非常高。这意味着定时器的回调函数可能不会在预期的精确毫秒数后立即执行。不同的浏览器和不同的环境(比如浏览器后台运行或系统睡眠)可能会对定时器的精度有不同的影响。最小延迟时间:在 HTML5 标准中,规定了 setTimeout 和 setInterval 的最小延迟时间通常不得低于4ms(若之前调用了5次以上的定时器,则最小延迟10ms)。这意味着即使你请求更短的时间间隔,执行也会被推迟到这个最小值。浏览器标签页的背景化:当用户将包含JavaScript定时器的浏览器标签页置于后台时,大多数现代浏览器会减少定时器的执行频率以节约资源和电池寿命。这可能导致后台标签页中的定时器变得非常不精确。垃圾回收:JavaScript的垃圾回收机制可能会暂时阻塞主线程,从而延迟定时器的执行。例如,假设你有一个使用 setTimeout 指定在10ms后执行的回调函数,但是此时主线程正在处理一个复杂的计算任务,这可能需要100ms才能完成。在这种情况下,即使定时器的时间到了,回调函数也必须等到计算任务完成后,事件循环才能调用它。所以实际上回调函数的执行时间将晚于预期的10ms。综上所述,由于单线程的特性、事件循环的机制、Web浏览器的实现细节和环境限制,JavaScript定时器的执行时间就不能保证绝对精确。开发者在使用定时器时,应该考虑到这些因素,并且不应该依赖定时器来进行精确的时间控制。
阅读 34·2024年6月24日 16:43

当添加原生事件不移除时,为什么会出现内存泄露?

内存泄露通常是指在程序运行过程中,分配的内存未能及时释放,导致随着时间的推移,系统可用内存逐渐减少。当我们在JavaScript中处理原生DOM事件时,如果不正确移除这些事件监听器,很容易导致内存泄露。在详细解释原因之前,先来了解一下事件监听器和内存泄露的基本概念。事件监听器是我们绑定到DOM元素上的函数,用来响应特定的事件,比如点击或者键盘按键。而内存泄露则是指已经不再需要的内存,由于各种原因,没有被及时回收,使得应用程序占用的内存越来越多。现在,让我说明为什么不移除原生事件监听器会导致内存泄露:事件监听器与DOM元素的引用关系:当你给一个DOM元素添加事件监听器时,浏览器会创建一个引用,指向该监听器的函数。即使你从DOM树中移除了这个元素,如果事件监听器没有被移除,那么浏览器的事件处理系统仍然会保留一个对该函数的引用,这导致了DOM元素和监听器函数无法被垃圾回收机制回收,因为从技术上讲,它们依旧是可达的。闭包:在JavaScript中,闭包是一个常见的功能,它允许函数访问并操纵函数外部的变量。如果事件监听器是一个闭包,它可能会引用其他外部变量或对象。只要这个监听器存在,所有它引用的对象也都无法被回收,即使这些对象已经没有其他用途。复杂的引用链:在大型的web应用程序中,DOM元素、事件监听器、以及其他相关对象可能会构成复杂的引用链。这些引用链使得垃圾回收变得复杂,如果链中的某一部分没有被正确管理和解除引用,就可能导致整个链条上的对象都不能被回收。让我举一个例子来说明这个问题:假设我们有一个简单的网页应用,它有一个按钮,当用户点击时会弹出一个警告框:function setup() { var button = document.getElementById('myButton'); button.addEventListener('click', function handleClick() { alert('Button clicked!'); });}setup();如果我们某个时刻决定移除这个按钮:var button = document.getElementById('myButton');button.remove();虽然DOM元素被移除了,但是我们没有移除与之绑定的handleClick事件监听器。这意味着handleClick依然保留着对button元素的引用,因此即使我们从DOM中移除了这个元素,它也不会被垃圾回收。如果我们的应用中有很多这样的操作,随着时间的推移,内存消耗将不断增加,这就是内存泄露。解决这个问题的一个方法是在移除DOM元素之前,显式地移除事件监听器:var button = document.getElementById('myButton');button.removeEventListener('click', handleClick);button.remove();这样,我们就手动断开了事件监听器与DOM元素之间的引用关系,使得这些对象可以被垃圾回收。这是为什么在处理原生DOM事件时,开发者需要注意正确地添加和移除事件监听器,以避免不必要的内存
阅读 26·2024年6月24日 16:43

React 的调和阶段, setState内部做了哪些动作?

在React中,setState 函数用于更新组件的状态,并触发重新渲染流程。调和(Reconciliation)阶段是React用来对比新旧虚拟DOM树差异,并决定如何高效更新真实DOM的过程。当你调用 setState 时,内部会触发以下动作:排队状态更新(Enqueuing State Update):setState 调用并不会立即更新组件的状态,而是将状态更新排队。这意味着React可能会累积多个 setState 调用,然后批量更新状态以优化性能。标记组件需要更新(Marking Component for Update):一旦状态被置入队列,React会将当前组件标记为“脏”(dirty),意味着组件的状态与显示的输出不同步,需要进行更新。批处理和合并状态(Batching and Merging State):React会将所有排队的 setState 调用进行批处理。如果有多个状态更新,React会将它们合并以减少不必要的渲染和调和操作。调用生命周期方法(Lifecycle Methods Invoking):在实际更新之前,React会调用 componentWillUpdate(在旧版本的React中)或 getDerivedStateFromProps 和 shouldComponentUpdate(在新版本中),这些生命周期方法允许开发者在渲染发生前执行额外的操作。创建新的虚拟DOM树(Virtual DOM Tree Creation):有了新的状态,React会创建新的虚拟DOM树,这个树反映了状态更新后的组件结构。对比新旧虚拟DOM(Diffing Virtual DOM Trees):接下来,React会使用调和算法对比新旧虚拟DOM树,确定哪些部分需要更新。这个过程产生了所谓的“差异”(diffs)。生成更新操作(Generating Update Operations):根据差异,React会生成一系列更新操作,这些操作将被应用到真实的DOM上以实现UI的最终变化。执行更新操作(Executing Update Operations):React会按照效率最高的方式批量执行这些更新操作,这可能包括添加、移动、更新或删除DOM节点。调用生命周期方法(Lifecycle Methods Invoking):在更新操作完成之后,React会调用 componentDidUpdate 生命周期方法,使开发者有机会执行需要DOM更新后才能进行的操作。例如,假设我们有一个计数器组件,其中包含一个按钮,当点击按钮时,它会通过 setState 增加计数值。React将按照上述步骤进行操作,确保界面反映了最新的计数状态,并以最高效的方式更新DOM。请注意,从React 16开始,引入了Fiber架构,它改变了内部工作原理,特别是更新过程可以被中断和恢复,以便更好地管理UI渲染的性能。但是,以上所述的基本步骤仍然适用。
阅读 41·2024年6月24日 16:43

如何实现Promise的resolve?

在JavaScript中,Promise 对象是异步编程的一种解决方案。一个 Promise 在创建时处于 pending(等待)状态,可以通过其 resolve 方法转变为 fulfilled(成功)状态,或通过其 reject 方法转变为 rejected(失败)状态。要实现 Promise 的 resolve,通常是在异步操作成功完成时调用。下面是一个简单的例子说明如何使用 Promise 的 resolve 方法:function asyncOperation() { // 创建一个新的Promise对象 return new Promise((resolve, reject) => { // 执行异步操作 setTimeout(() => { const operationWasSuccessful = true; // 假设这是基于异步操作结果的条件 if (operationWasSuccessful) { resolve('Operation successful'); // 如果操作成功,调用resolve并传递结果 } else { reject('Operation failed'); // 如果操作失败,调用reject并传递错误信息 } }, 1000); // 假设这个异步操作需要1秒钟 });}asyncOperation() .then(result => { console.log(result); // 打印成功结果 }) .catch(error => { console.error(error); // 打印错误信息 });在上述代码中,asyncOperation 函数返回一个新的 Promise 对象。在这个 Promise 的构造函数中,有两个参数:resolve 和 reject。这两个参数也是函数,它们被用来分别处理异步操作的成功和失败情况。在异步操作(这里使用 setTimeout 模拟)完成后,根据操作的结果调用 resolve 或 reject。如果异步操作成功(在这个例子中,我们假设 operationWasSuccessful 为 true),则调用 resolve 函数并传递结果消息 'Operation successful'。这将使得 Promise 对象的状态变为 fulfilled,并将结果传递给随后的 .then 方法的回调函数。如果异步操作失败,就调用 reject 函数并传递错误消息 'Operation failed'。这将使得 Promise 对象状态变为 rejected,并将错误信息传递给随后的 .catch 方法的回调函数。
阅读 44·2024年6月24日 16:43