阿里云前端面试
09/14 一面
- 自我介绍,前端经历
1. 长列表渲染优化,异步加载节点信息。元素不定高场景下怎么做虚拟列表。
在长列表渲染优化和虚拟列表中,如果元素的高度不固定,可以采用以下方法实现虚拟列表:
-
基于可视区域的渲染:只渲染可视区域内的元素,而不是将所有列表项都渲染出来。这可以通过监听滚动事件,计算可视区域的位置和大小,然后动态地确定需要渲染的列表项范围。
-
动态调整列表项高度:如果列表项的高度不固定,可以动态地获取列表项的高度,并根据实际高度来进行渲染。可以通过DOM操作获取列表项的高度,或者使用测量组件(如react-virtualized中的CellMeasurer)来获取动态高度。
-
缓存已渲染的列表项:为了提高性能,可以将已经渲染的列表项缓存起来,避免重复渲染。可以使用一个缓存池来存储已渲染的列表项,当需要重新渲染时,先从缓存中查找是否已经存在对应的列表项,如果存在则直接使用缓存中的内容。
-
懒加载节点信息:针对异步加载节点信息的场景,可以在列表项进入可视区域时再触发异步加载操作,而不是一次性加载所有节点信息。可以通过监听列表项的进入可视区域事件(如Intersection Observer)来触发异步加载操作,并将加载的节点信息动态地插入到虚拟列表中。
-
使用虚拟滚动组件库:可以使用现有的虚拟滚动组件库(如react-virtualized、react-window、vue-virtual-scroller等)来实现虚拟列表,这些库通常提供了丰富的功能和配置选项,可以简化虚拟列表的实现过程,并提高性能和可维护性。
综上所述,对于元素不定高的场景下的虚拟列表,可以通过动态渲染可视区域内的列表项、动态获取列表项高度、缓存已渲染的列表项、懒加载节点信息等方法来实现性能优化和流畅的滚动体验。
3. 性能优化的指标。
性能优化的指标通常包括以下几个方面:
-
加载速度(Load Time):网页或应用程序的加载速度是用户体验的重要指标之一。优化加载速度可以减少用户等待时间,提高用户满意度。常见的加载速度指标包括首次加载时间、页面完全加载时间、资源加载时间等。
-
渲染性能(Rendering Performance):渲染性能指标反映了页面或应用程序在浏览器中的渲染速度和流畅度。优化渲染性能可以提高页面的交互体验,降低页面卡顿和闪烁现象。常见的渲染性能指标包括帧率(FPS)、页面重绘次数、页面重排次数等。
-
响应时间(Response Time):响应时间是指服务器在收到请求后,向客户端返回响应的时间。优化响应时间可以提高用户体验,减少用户等待时间。常见的响应时间指标包括服务器响应时间、网络传输时间、客户端渲染时间等。
-
资源利用率(Resource Utilization):资源利用率指标反映了系统资源的使用情况,包括CPU利用率、内存利用率、网络带宽利用率等。优化资源利用率可以提高系统的稳定性和性能,并降低系统的运行成本。
-
错误率(Error Rate):错误率指标反映了系统的稳定性和可靠性,包括页面错误率、服务器错误率、API调用错误率等。降低错误率可以提高用户满意度,并减少系统维护和修复的成本。
综合考虑以上指标,可以制定合理的性能优化策略,并通过监控和分析工具对系统性能进行持续跟踪和优化,以确保系统始终保持良好的性能表现。
3. XSS注入和SQL注入排查修复。
XSS(Cross-Site Scripting)注入和SQL(Structured Query Language)注入是常见的Web安全漏洞,需要及时排查和修复以保护应用程序的安全性。
XSS注入排查与修复:
-
输入过滤与转义:对于用户输入的内容,进行严格的输入过滤和字符转义,特别是针对HTML、JavaScript和CSS等敏感字符进行转义,以防止恶意脚本的注入。
-
CSP(Content Security Policy)设置:通过CSP策略限制页面中可加载的资源和脚本,禁止外部脚本的执行,防止XSS攻击。
-
HTTPOnly Cookie:将敏感信息存储在HTTPOnly Cookie中,防止JavaScript脚本获取敏感信息,减少XSS攻击的风险。
-
XSS过滤器:使用现成的XSS过滤器库(如DOMPurify)来检测和过滤潜在的XSS攻击代码。
SQL注入排查与修复:
-
使用参数化查询:使用参数化查询或预编译语句,将用户输入的数据作为参数传递给SQL查询,而不是将其直接拼接到SQL语句中,避免SQL注入攻击。
-
输入验证与过滤:对用户输入的数据进行严格的验证和过滤,确保输入数据符合预期格式和范围,防止恶意SQL语句的注入。
-
最小权限原则:数据库用户应具有最小的权限,只能执行必要的数据库操作,减少攻击者利用SQL注入漏洞的可能性。
-
错误信息处理:对于发生的SQL错误,不要将详细的错误信息返回给用户,以防止攻击者利用错误信息获取敏感信息。
-
ORM框架:使用ORM(Object-Relational Mapping)框架来处理数据库操作,ORM框架通常会自动进行参数化查询,降低SQL注入的风险。
-
安全审计:定期对应用程序进行安全审计和漏洞扫描,发现和修复潜在的SQL注入漏洞。
通过以上方法,可以有效排查和修复XSS注入和SQL注入漏洞,提高应用程序的安全性。同时,开发人员还应不断学习最新的安全技术和漏洞攻击方式,及时更新和完善防御措施。
3. 微前端技术,qiankun框架,spa和mpa,js隔离、样式隔离。
微前端技术是一种将前端应用拆分成更小、更独立的部分,并将其独立部署、运行和维护的架构模式。它使得不同团队可以独立开发、测试和部署自己的前端应用,最终集成到一个统一的整体中。微前端技术的出现旨在解决单体应用随着功能不断增加而变得庞大和难以维护的问题,同时提升团队的开发效率和灵活性。
Qiankun是一个基于微前端架构的解决方案,由蚂蚁金服开源。它提供了一套完整的微前端解决方案,包括应用拆分、独立部署、路由管理、状态共享等功能。Qiankun框架允许将不同的前端应用集成到同一个页面中,实现统一的用户体验。它还支持跨域通信、样式隔离、JS隔离等特性,保证了各个子应用之间的独立性和安全性。
SPA(Single Page Application)和MPA(Multi-Page Application)是两种常见的前端架构模式:
- SPA是指单页面应用,整个应用由一个页面组成,通过JavaScript动态加载数据和更新页面内容,实现了无刷新的用户体验。典型的SPA框架有Vue.js、React等。
- MPA是指多页面应用,每个页面对应一个完整的HTML文件,页面之间通过超链接进行跳转。每个页面都有自己的独立路由和功能,开发和维护相对独立。传统的后端渲染技术(如JSP、ASP.NET等)通常采用MPA模式。
在微前端架构中,可以将不同的子应用划分为SPA或MPA,然后集成到同一个页面中。Qiankun框架提供了一套完整的解决方案来管理各个子应用之间的通信、样式隔离和JS隔离等问题,确保各个子应用之间的独立性和安全性。
样式隔离和JS隔离是微前端架构中的两个重要特性:
-
样式隔离:不同的子应用可能会使用不同的UI库或样式框架,为了避免样式冲突和污染,需要对各个子应用的样式进行隔离。Qiankun框架提供了样式隔离的解决方案,通过CSS命名空间或CSS模块化等技术,确保各个子应用的样式不会相互影响。
-
JS隔离:不同的子应用可能会使用不同的JavaScript框架或版本,为了避免全局变量污染和版本冲突,需要对各个子应用的JavaScript代码进行隔离。Qiankun框架提供了JS隔离的解决方案,通过沙箱技术或JavaScript模块化等技术,确保各个子应用的JavaScript环境相互隔离。
3. 通用上传组件,中间件机制洋葱模型。
洋葱模型(Onion Model)是一种常用于描述中间件处理流程的模型,通常用于Web开发中。在这个模型中,请求或数据在经过一系列的中间件处理时,会像剥洋葱一样,依次经过每个中间件,然后再返回结果。
上传组件通常涉及到文件的上传、处理和存储等功能,可以使用洋葱模型来描述其处理流程。下面是一个简单的上传组件处理流程示例:
-
收到上传请求:首先,上传组件收到来自客户端的上传请求。
-
前置中间件处理:请求经过一系列的前置中间件处理,例如身份验证、请求解析等。这些中间件可以对请求进行预处理和验证,确保请求的合法性和完整性。
-
文件上传处理:接下来,请求进入文件上传中间件,该中间件负责处理文件的上传和存储。上传中间件通常会将文件保存到服务器上的指定位置,并生成对应的文件路径或文件信息。
-
后置中间件处理:文件上传完成后,请求进入后置中间件处理阶段,例如文件处理、压缩、转换等。这些后置中间件可以对上传的文件进行进一步处理,以满足业务需求。
-
返回响应:最后,经过所有中间件处理后,上传组件生成相应的结果或数据,并将其返回给客户端。
使用洋葱模型可以使上传组件的处理流程更加清晰和灵活,每个中间件只负责自己的特定功能,各个中间件之间相互独立、松耦合,方便维护和扩展。
- 低代码平台优势劣势。(问了低代码协议相关,没看过) 低代码平台是一种通过图形化界面和可视化工具,帮助用户快速开发应用程序的平台。使用低代码平台,开发人员可以通过拖拽组件、配置属性等简单操作,快速构建应用程序,而无需编写大量的代码。下面是低代码平台的一些优势和劣势:
优势:
- 快速开发:低代码平台提供了可视化的开发环境,使得开发过程更加快速和高效。开发人员无需从头开始编写代码,只需拖拽组件、配置属性即可快速构建应用程序。
- 降低技术门槛:低代码平台降低了对开发人员技术水平的要求,使得非专业的开发人员也能参与应用程序的开发。这样可以扩大开发人员的范围,加快应用程序的开发速度。
- 减少重复性工作:低代码平台提供了丰富的组件库和模板,开发人员可以直接使用这些组件和模板,避免重复编写相似的代码,提高了开发效率。
- 易于维护和更新:由于使用了图形化界面和可视化工具进行开发,因此低代码平台开发的应用程序更易于维护和更新。开发人员可以通过简单的操作进行修改和更新,而无需深入理解代码逻辑。
- 适应快速变化的需求:低代码平台的灵活性和易用性使得应用程序更加容易适应快速变化的需求和业务场景。开发人员可以快速响应需求变化,进行迭代和优化。
劣势:
- 灵活性不足:由于低代码平台提供的是预定义的组件和模板,因此在某些复杂的场景下可能会缺乏灵活性,无法满足特定的定制需求。
- 性能问题:低代码平台生成的代码可能存在性能问题,特别是在处理大量数据或复杂业务逻辑时,可能会导致应用程序性能下降。
- 定制化难度高:一些特定的定制化需求可能无法通过低代码平台实现,需要编写自定义代码进行扩展和定制,这会增加开发成本和复杂度。
- 学习曲线:虽然低代码平台降低了开发门槛,但是对于新手来说,仍然需要一定的学习曲线。熟练掌握低代码平台的各种功能和工具也需要一定的时间和经验积累。
- 依赖厂商支持:使用低代码平台开发的应用程序可能会受到平台厂商的限制和约束,依赖于平台厂商的技术支持和服务,一旦平台厂商出现问题或停止维护,可能会影响应用程序的正常运行和维护。
7. 进程线程协程概念及区别。线程共享进程中哪些资源。
进程、线程和协程是计算机中用于实现并发的三种不同的概念,它们之间有一些区别:
-
进程(Process):
- 进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。
- 每个进程拥有独立的内存空间,进程之间不能直接共享数据,通信需要通过进程间通信(IPC)的方式。
- 进程之间的切换开销比较大,因为需要切换内存空间。
-
线程(Thread):
- 线程是进程中的一个执行单元,一个进程可以包含多个线程。
- 线程共享进程的内存空间,可以直接访问进程的所有资源,包括代码段、全局变量、堆内存等。
- 线程之间的切换开销较小,因为它们共享相同的地址空间。
-
协程(Coroutine):
- 协程是一种轻量级的线程,由程序员自己控制调度。
- 协程拥有自己的执行栈,但它们可以共享数据。
- 协程的切换不需要操作系统的参与,因此切换开销非常小,通常比线程更高效。
区别总结如下:
- 进程是系统中资源分配和调度的基本单位,线程是进程中的执行单元,而协程是程序员自己控制的执行单元。
- 进程拥有独立的内存空间,线程共享进程的内存空间,而协程可以共享数据但拥有自己的执行栈。
- 进程之间通信需要通过IPC,线程之间直接共享数据,而协程可以通过通道等方式进行通信。
- 进程之间的切换开销较大,线程之间的切换开销较小,而协程的切换开销非常小。
线程共享进程中的一些资源,包括:
- 内存空间(包括代码段、全局变量、堆内存等)
- 文件描述符(如打开的文件)
- 网络连接
- 其他进程间通信的相关资源,如信号量、消息队列等。
3. 如何写一个死锁。
死锁(Deadlock)是指多个进程或线程因为互相持有对方所需资源而相互等待,导致都无法继续执行的状态。在编程中,可以通过一些方式模拟出死锁的情况,下面是一个简单的JavaScript示例:
// 创建两个资源对象
const resource1 = {};
const resource2 = {};
// 函数A尝试获取资源1,然后等待一段时间后再尝试获取资源2
function functionA() {
console.log('Function A trying to acquire resource 1...');
setTimeout(() => {
console.log('Function A trying to acquire resource 2...');
resource1.acquire();
setTimeout(() => {
console.log('Function A acquired resource 1 and resource 2');
}, 1000);
}, 1000);
}
// 函数B尝试获取资源2,然后等待一段时间后再尝试获取资源1
function functionB() {
console.log('Function B trying to acquire resource 2...');
setTimeout(() => {
console.log('Function B trying to acquire resource 1...');
resource2.acquire();
setTimeout(() => {
console.log('Function B acquired resource 1 and resource 2');
}, 1000);
}, 1000);
}
// 同时执行函数A和函数B
functionA();
functionB();
在这个例子中,函数A尝试先获取资源1,然后等待一段时间后再尝试获取资源2;而函数B尝试先获取资源2,然后等待一段时间后再尝试获取资源1。如果两个函数同时执行,就会发生死锁:函数A持有资源1并等待资源2,而函数B持有资源2并等待资源1,导致彼此互相等待,无法继续执行下去。
3. ES6新增特性,js作用域和块级作用域。
ES6(ECMAScript 2015)引入了许多新的特性,其中一些主要的新增特性包括:
-
let 和 const 关键字:引入了块级作用域的概念,用于声明变量。
let声明的变量可被重新赋值,而const声明的变量是常量,不可重新赋值。 -
箭头函数:简化了函数的声明语法,省略了
function关键字和return语句。 -
模板字符串:使用反引号(`)来创建多行字符串和嵌入表达式,使字符串拼接更加方便。
-
解构赋值:可以将数组或对象中的值解构到单独的变量中,提供了一种简洁的方式来访问数组和对象中的元素。
-
扩展运算符(Spread Operator):用于将数组或对象展开为单独的元素,方便了数组的合并和对象的拷贝。
-
默认参数:在函数声明时可以指定参数的默认值,简化了函数的调用。
-
类和继承:引入了类的概念,使得JavaScript的面向对象编程更加直观和易用。
-
模块化:引入了
import和export关键字,提供了一种更好的方式来组织和管理代码。
至于JavaScript的作用域和块级作用域:
-
作用域(Scope):作用域是指变量的可访问范围,JavaScript中的作用域分为全局作用域和局部作用域。全局作用域中定义的变量在整个程序中均可访问,而局部作用域中定义的变量只能在其所在的代码块(函数、循环、条件语句等)中访问。
-
块级作用域(Block Scope):块级作用域是指变量的作用范围被限制在最近的一对花括号({})之间,例如if语句、for循环、函数等。在ES6之前,JavaScript只有函数作用域和全局作用域,没有块级作用域,导致在使用var声明变量时容易造成变量污染和意外的覆盖。而ES6引入了
let和const关键字,使得变量的作用域可以限制在块级作用域内,提高了代码的安全性和可读性。
3. 闭包和箭头函数特性。箭头函数编译后的es5产物(不会QAQ)。
闭包(Closure)是指函数可以访问其外部作用域中的变量,即使在函数声明之后执行,也能够记住并访问声明时的作用域。闭包通常用于创建私有变量、模块化开发、以及解决作用域链断裂等问题。
箭头函数(Arrow Function)是ES6新增的一种函数表达式,相比普通函数有以下特性:
- 语法简洁:箭头函数的语法更加简洁,省略了function关键字和return语句(当只有一个表达式时)。
- 绑定this:箭头函数的this始终指向其定义时的上下文,而不是调用时的上下文。这解决了普通函数中this指向的困扰。 箭头函数的this始终指向其定义的时候上下文你,若不是调用的时候上下文,
- 不绑定arguments对象:箭头函数没有自己的arguments对象,可以通过扩展操作符...args获取参数。
- 不能作为构造函数:箭头函数不能使用new关键字调用,因为它没有自己的this。
- 不能使用arguments对象:箭头函数没有arguments对象,可以使用rest参数或者结构参数代替。
下面是一个简单的箭头函数的ES6代码及其编译后的ES5产物的示例:
ES6箭头函数代码:
const add = (a, b) => a + b;
编译后的ES5产物:
"use strict";
var add = function add(a, b) {
return a + b;
};
可以看到,编译后的ES5代码与原始的ES6代码非常相似,只是将箭头函数的语法转换成了普通的函数表达式,但其特性和行为保持不变。
3. 原型与原型链,es6的class编译成es5的产物。
当使用ES6的class语法编写JavaScript代码时,这些代码在经过编译后会被转换成ES5的代码,以保证在不支持ES6语法的环境中能够正常运行。下面是一个简单的示例,展示了一个ES6的class如何被转换成ES5的产物:
ES6的class代码:
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
编译后的ES5产物:
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person(name) {
_classCallCheck(this, Person);
this.name = name;
};
Person.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name);
};
在编译后的ES5代码中,可以看到以下变化:
- 使用了严格模式("use strict")。
- 类被转换成了一个函数,构造函数(constructor)的代码被提取到了函数体内部。
- 原型链上的方法被转换成了对象的属性赋值形式。
5. CommonJS和ESModule区别
CommonJS和ES Module(ESM)是两种不同的模块化规范,用于在JavaScript中组织和管理代码。
CommonJS:
-
同步加载:CommonJS模块是同步加载的,即在模块加载时会立即执行模块中的代码,并且模块中的导出会被缓存起来,之后再次引用时可以直接获取缓存的导出对象。
-
Node.js环境:CommonJS最初是为Node.js环境设计的,用于在服务器端和命令行工具中加载模块。
-
导出:使用
module.exports导出模块,可以导出任意类型的值。 -
导入:使用
require()函数来导入模块。
ES Module (ESM):
-
异步加载:ES Module是异步加载的,即模块中的导入和导出是在模块加载完成后才执行的,不会阻塞后续代码的执行。
-
浏览器环境和Node.js环境:ES Module是ECMAScript标准的一部分,在现代浏览器中已经原生支持,也可以在Node.js环境中使用。
-
导出:使用
export关键字导出模块,可以导出变量、函数、类等,但是不能导出变量的引用。 -
导入:使用
import关键字来导入模块。 -
静态分析:ES Module的导入语句是静态分析的,即在编译阶段就可以确定模块的依赖关系。
-
顶层作用域:ES Module中的模块是在顶层作用域中执行的,因此模块中的变量在模块外部是不可见的。
-
严格模式:ES Module默认是严格模式的,不需要手动指定。
总的来说,CommonJS适用于Node.js环境和早期的前端开发,而ES Module适用于现代的浏览器环境和Node.js环境,具有更好的性能和可靠性,并且是JavaScript的标准规范之一。
3. Vue3响应式原理,vue2中怎么解决新增属性的响应式。
Vue 3的响应式原理与Vue 2有所不同,Vue 3引入了Proxy作为响应式系统的核心。下面简要介绍Vue 3的响应式原理,以及Vue 2中如何解决新增属性的响应式。
Vue 3的响应式原理:
-
Proxy代理:Vue 3使用ES6的Proxy对象来实现响应式。当一个对象被代理后,对这个对象的操作(读取、赋值等)都会被拦截,从而实现对对象的监听和触发更新。
-
Reactive函数:Vue 3中提供了
reactive函数用于创建响应式对象。当调用reactive函数时,会返回一个被Proxy代理的对象,从而实现对象的响应式。 -
Ref函数:Vue 3中提供了
ref函数用于创建响应式的基本数据类型,例如数字、字符串等。ref函数返回一个带有value属性的对象,该对象的value属性是一个可响应的Proxy对象。 -
Effect函数:Vue 3中引入了
effect函数和reactive对象的组合,用于创建响应式的副作用。当reactive对象发生变化时,相关的effect函数将会被自动执行。
Vue 2中解决新增属性的响应式:
在Vue 2中,由于采用了Object.defineProperty来实现响应式,导致新增属性时无法自动触发视图更新。为了解决这个问题,可以通过以下方式实现新增属性的响应式:
- Vue.set方法:Vue 2提供了
Vue.set方法用于为响应式对象添加新属性。当使用Vue.set方法添加新属性时,Vue会自动更新视图。
Vue.set(vm.someObject, 'newProperty', 'new value');
- **vm.set`方法来添加新属性。
this.$set(this.someObject, 'newProperty', 'new value');
- Object.assign或展开运算符:将响应式对象的旧对象和新属性合并生成新的对象,然后再赋值给响应式对象。
this.someObject = Object.assign({}, this.someObject, { newProperty: 'new value' });
// 或者
this.someObject = { ...this.someObject, newProperty: 'new value' };
这些方法可以保证新增属性的响应式,并且自动触发视图更新。