前端面试题手册
在 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是一个不错的选择,因为它们可以推动用户重新参与使用应用。
阅读 28·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 架构使得开发者可以构建出更加响应快速、用户体验更好的应用程序。
阅读 23·2024年6月24日 16:43
结合 Vue 项目实现事件总线 Event Bug
事件总线是一种模式,可以通过一个中央通道分发事件,让不同的系统部分实现解耦。在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来在组件间共享状态。
阅读 27·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攻击的概述和防御策略。
阅读 26·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攻击的简要说明以及如何防御这类攻击的方法。
阅读 26·2024年6月24日 16:43
什么是柯里化函数?JavaScript 中有哪些使用场景?
柯里化(Currying)是一种在函数式编程中常见的技巧。它是指将一个多参数的函数转换成多个单参数(或较少参数)函数的序列。这样做的主要目的是参数复用、提前确认和延迟执行等。定义以 JavaScript 为例,柯里化的一个基本定义可以是这样的:function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } };}这个 curry 函数接收一个函数 fn 作为参数,并返回一个新的函数 curried。新函数检查接收的参数数量,如果足够执行原函数,则直接执行;如果不够,则返回一个新的函数,等待接收更多的参数。使用场景参数复用:当你有一个函数需要多次调用,但是某些参数在这些调用中是不变的时候,可以通过柯里化创建特定的函数,只传入剩下的参数。例如,如果你有一个 add 函数,可以创建一个 addFive 函数,它将一个数与5相加。 function add(a, b) { return a + b; } const addFive = curry(add)(5); addFive(10); // 返回 15延迟计算/执行:柯里化允许你将多个参数的获取分散到多个步骤中。只有当所有需要的参数都被提供后,函数才会被执行。这在需要等待某些数据才能执行操作的情况下非常有用。例如,如果一个函数需要从几个不同的数据源获取数据,可以将每个数据源的结果逐一传入柯里化函数中。动态生成函数:可以动态地生成需要的函数。在处理事件监听或者回调函数的时候,如果某些参数是已知的,可以通过柯里化生成一个新的函数,而无需重新定义函数体。 const on = curry(function(eventType, element, callback) { element.addEventListener(eventType, callback); }); const onClick = on('click'); onClick(document.getElementById('myButton'), () => console.log('Button clicked!'));函数组合:在函数式编程中,柯里化可以帮助实现函数组合。通过柯里化,可以轻松地将一个函数的输出作为另一个函数的输入。这在创建数据流和中间件等链式操作时非常有用。 const compose = (f, g) => (a) => f(g(a)); const multiplyBy2 = (n) => n * 2; const addTen = (n) => n + 10; const multiplyBy2AndAddTen = compose(addTen, multiplyBy2); multiplyBy2AndAddTen(5); // 返回 20总之,柯里化是一种强大的函数式编程技术,它在 JavaScript 中可以用于创建更灵活和可重用的代码。通过柯里化,可以更容易地实现参数复用、延迟执行和高阶函数构建等高级编程模式。
阅读 34·2024年6月24日 16:43
如何实现 javascript 的 bind 方法
JavaScript 中的 bind 方法用于创建一个新函数,该函数在被调用时会将其 this 关键字设置为提供的值,同时还可以接受一系列的参数。要实现一个自定义的 bind 函数,我们需要了解几个关键点:返回一个函数。确定 this 的值。参数的传递。下面是一个简单的实现例子:Function.prototype.myBind = function(context, ...args) { // this 指向调用 myBind 的函数 var fn = this; // 返回一个新的函数 return function(...newArgs) { // 使用 apply 方法调用函数,设定 this 的值,并传递参数 // 这里将预置的参数和新传入的参数拼接在一起 return fn.apply(context, args.concat(newArgs)); };};在这个例子中,myBind 函数接受了两个参数:context 指定了 this 的上下文,args 是一个由预置参数组成的数组。返回的函数在被调用时,会通过 apply 方法将 this 绑定到 context 对象,并将预置参数与新参数合并传递给原函数。让我们通过一个具体的例子来演示这个 myBind 方法的使用:function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation);}var person = { name: 'John'};// 使用原生的 bind 方法var greetJohn = greet.bind(person, 'Hello');greetJohn('!'); // 输出: Hello, John!// 使用我们自定义的 myBind 方法var greetJohnCustom = greet.myBind(person, 'Hi');greetJohnCustom('?'); // 输出: Hi, John?这里,我们定义了一个 greet 函数,它接受两个参数 greeting 和 punctuation,然后打印出问候语。我们使用 bind 方法(和我们的 myBind 方法)创建了一个新的函数 greetJohn(和 greetJohnCustom),它的 this 被绑定到 person 对象上,并且预置了“Hello”(和 "Hi")作为 greeting 参数。通过上面的例子,我们演示了如何实现和使用一个自定义的 bind 函数,它模仿了原生 bind 方法的行为。
阅读 18·2024年6月24日 16:43
JS内存泄露如何检测?场景有哪些?
JavaScript内存泄露是指在应用程序中不再需要使用的内存由于某些原因没有被释放或回收,导致可用内存逐渐减少,最终可能会导致应用程序或系统性能下降,甚至崩溃。 如何检测JS内存泄露?检测JavaScript内存泄露通常可以通过以下途径进行:浏览器开发者工具:大多数现代浏览器都提供了内置的开发者工具,可以用来监视内存使用情况。例如,Google Chrome的开发者工具中有"Performance"和"Memory"面板,允许开发者记录和分析网站的运行时性能和内存使用情况。堆快照(Heap Snapshots):通过浏览器的开发者工具,可以捕获堆快照,它会展示内存分配的静态视图。通过比较连续的堆快照,可以观察到哪些对象被分配内存后没有被释放。时间线记录(Timeline Profiling):这个工具可以帮助我们理解内存是如何随着时间的推移而增加的。我们可以使用浏览器工具的时间线功能记录一个时间段内的内存使用情况,寻找内存使用上升的趋势。代码审查(Code Review):定期进行代码审查以查找常见的内存泄露模式,如未取消的事件监听器、闭包的滥用、未清除的定时器等。内存泄露场景内存泄露可能出现在多种不同的场景中,以下是一些常见的场景:全局变量:意外地创建全局变量会导致这些变量不被回收,例如,忘记使用 var、let或 const关键字。事件监听器未移除:如果在DOM元素上添加了事件监听器,但在不需要时没有正确移除,它们会持续占用内存。闭包:不当使用闭包可能会导致父作用域中的变量无法被释放。DOM引用:JavaScript中的变量如果引用了已经从DOM中移除的元素,如果引用一直保持,那么这部分内存也不会被回收。定时器:设置了定时器(如 setInterval)而没有清除(clearInterval),可能会导致内部回调函数和相关变量长期占用内存。第三方库:使用的第三方库如果存在内存泄露,同样也会影响到使用它的应用程序。举个具体的例子:在开发一个单页应用时,我注意到随着页面的使用时间增加,页面的响应速度逐渐变慢。我使用Chrome开发者工具中的Performance面板进行了记录,发现内存使用量呈现持续上升的趋势。通过分析和比较不同时间点的堆快照,我发现存在一个大量DOM元素对应的监听器没有在元素被移除时一并清理。修复这个问题后,应用的性能得到了显著的提升。
阅读 21·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渲染的性能。但是,以上所述的基本步骤仍然适用。
阅读 23·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 方法的回调函数。
阅读 33·2024年6月24日 16:43