前端知识-面试题(滴滴)
ps:以下面试题均收集于网络,不代表本人亲身经历,答案本人整理。大家可以略过
一面
1.闭包是什么? 闭包的用途?
闭包是能读取其他函数内部变量的函数,是外部函数与内部函数之间通信的桥梁;闭包常见用途有:定时器、节流、防抖
2.简述事件循环原理
主进程中同步任务顺序执行,异步任务会放入任务队列中。任务队列中当微任务执行完之后此时微任务队列为空进入下一个事件循环,检查宏任务队列。
3.虚拟dom是什么? 原理? 优缺点?
虚拟DOM(Virtual DOM)是指通过JavaScript对象来描述真实DOM结构的一种技术。
其原理是通过创建一个虚拟的DOM树来描述实际的DOM结构,然后通过比较前后两次虚拟DOM的差异,仅更新发生变化的部分,从而实现DOM的高效更新。
虚拟DOM的优点是可以减少DOM操作的次数,提高DOM操作的效率,从而提高整个应用的性能。另外,虚拟DOM也可以实现跨平台渲染,比如可以在浏览器端和服务器端都可以进行渲染。
虚拟DOM的缺点是需要额外的内存开销来存储虚拟DOM对象,同时也需要进行额外的计算来比较前后两次的虚拟DOM差异。
4.vue 和 react 在虚拟dom的diff上,做了哪些改进使得速度很快?
- 基于key的优化:Vue和React都通过给每个虚拟DOM节点添加唯一的key属性来优化diff算法。这样可以更准确地比较新旧节点之间的关系,从而减少不必要的DOM操作。
- 使用双端比较算法:双端比较算法是指在比较新旧节点之间的差异时,同时从前往后和从后往前进行比较,从而可以更快地找到最长的共同子序列。
- 异步批量更新:Vue和React都支持异步批量更新机制,即在一次事件循环中,将所有的DOM操作都放到一个队列中,等到下一次事件循环时一次性执行,从而减少DOM操作的次数,提高性能。
5.vue 和 react 里的key的作用是什么? 为什么不能用Index?用了会怎样? 如果不加key会怎样?
key属性用于唯一标识一个虚拟DOM节点,可以帮助Vue和React更快地确定哪些节点需要被更新,哪些节点需要被删除或插入。如果不加key,那么Vue和Reac会将每个节点都当做是全新的节点,从而可能导致性能问题。
不能使用index作为key的原因是,如果列表中的项发生了变化,那么相同位置的节点就会使用不同的key,从而可能导致性能问题。比如,如果在列表的中间位置插入一项,那么后面的所有项的key都会发生变化。``
如果不加key,那么Vue和React会将每个节点都当做是全新的节点,从而会影响到性能。
6.vue 双向绑定的原理是什么?
Vue的双向绑定是通过使用数据劫持结合发布/订阅模式来实现的。当数据模型发生变化时,Vue会自动触发一个更新视图的操作,从而实现视图和数据的双向绑定。具体实现是通过使用Object.defineProperty方法来劫持数据模型的属性访问器,在属性被读取或设置时触发getter和setter方法,从而实现对数据的监控和更新。
7.vue 的keep-alive的作用是什么?怎么实现的?如何刷新的?
Vue的keep-alive是用于缓存组件的一个抽象组件,可以将需要缓存的组件包裹在keep-alive组件中,从而实现缓存功能。当被缓存的组件被切换时,可以避免重复创建和销毁组件,从而提高应用的性能。
keep-alive实现缓存功能的原理是,当一个被包裹在keep-alive组件中的组件被激活时,它会被挂载到DOM中,同时会被缓存起来。当这个组件被离开时,它不会被销毁,而是被缓存起来。当这个组件再次被激活时,它会被重新挂载到DOM中,从而避免了重复创建和销毁组件的开销。
keep-alive提供了两个生命周期钩子函数activated和deactivated,用于在缓存组件被激活和离开时执行相应的操作。在activated中可以实现组件的激活操作,而在deactivated中可以实现组件的离开操作。
keep-alive提供了一个include属性和一个exclude属性,用于指定哪些组件需要被缓存,哪些组件不需要被缓存。同时,还提供了一个max属性,用于指定最大缓存的组件数量。
keep-alive的刷新机制是,在某个被缓存的组件被激活时,会根据组件的key值从缓存中查找相应的组件实例,如果找到了就直接使用缓存的组件实例,否则就创建一个新的组件实例。这样可以保证组件在缓存中的状态和在DOM中的状态是一致的
8.vue 是怎么解析template的? template会变成什么?
模版编译器将模版解析成渲染函数,渲染函数将模版转换为VNode节点树,从而实现动态渲染。
模版编译器将模版解析成抽象语法树(AST),渲染函数的作用是根据数据和VNode节点树生成真正的DOM树。在渲染函数中,Vue使用VNode节点树描述了每个节点的类型、属性、子节点等信息。
9.如何解析指令? 模板变量? html标签
指令、模板变量和HTML标签都是模板中的语法结构,Vue会通过模板编译器将其解析成相应的VNode节点。
指令是以v-开头的特殊属性,例如v-bind、v-model、v-for等。Vue会将指令解析成相应的VNode节点,并将其添加到VNode节点树中。在渲染函数中,Vue会根据指令生成相应的DOM操作,从而实现指令的功能。
模板变量是一种特殊的语法结构,用于在模板中引用数据。例如{{ message }}就是一种模板变量。Vue会将模板变量解析成相应的VNode节点,并将其添加到VNode节点树中。在渲染函数中,Vue会将模板变量替换成相应的数据,从而实现模板变量的功能。
HTML标签是模板中用于描述DOM结构的语法结构。Vue会将HTML标签解析成相应的VNode节点,并将其添加到VNode节点树中。在渲染函数中,Vue会根据VNode节点树生成相应的DOM树。
10.用过vue 的render吗? render和template有什么关系
Vue中的render函数是用于生成VNode节点树的函数,可以手动编写render函数来代替使用模板。使用render函数可以更精细地控制组件的渲染行为,也可以实现一些复杂的组件功能。
render函数和template都用于生成VNode节点树,它们的最终目的都是生成真正的DOM树。template是一种更易于理解和编写的方式,而render函数则更加灵活和强大。
在使用template时,Vue会自动将其编译成渲染函数,而在使用render函数时,需要手动编写渲染函数。无论是使用template还是render函数,最终都会生成VNode节点树,从而实现动态渲染。
因此,render函数和template的关系是,它们都是用于生成VNode节点树的方式,但render函数更加灵活和强大,template模版更易理解和编写。
11、实现一个节流函数? 如果想要最后一次必须执行的话怎么实现?
(1) 使用时间戳实现
/**
* 函数节流
* @param {Function} func 要节流的函数
* @param {Number} wait 节流时间间隔
*/
function throttle(func, wait) {
let previous = 0; // 记录上一次执行的时间戳
return function(...args) {
const now = Date.now(); // 当前时间戳
if (now - previous > wait) { // 判断当前时间是否超过时间间隔
func.apply(this, args); // 执行函数
previous = now; // 更新上一次执行的时间戳
}
}
}
(2) 使用定时器实现
/**
* 函数节流
* @param {Function} func 要节流的函数
* @param {Number} wait 节流时间间隔
*/
function throttle(func, wait) {
let timer;
return function(...args) {
if (!timer) { // 判断定时器是否存在
timer = setTimeout(() => {
func.apply(this, args); // 执行函数
timer = null; // 清空定时器
}, wait);
}
}
}
(3)最后一次必须执行的实现方式:
/**
* 函数节流
* @param {Function} func 要节流的函数
* @param {Number} wait 节流时间间隔
*/
function throttle(func, wait) {
let timer;
return function(...args) {
if (timer) { // 如果定时器存在,清空定时器
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args); // 执行函数
timer = null; // 清空定时器
}, wait);
}
}
12、实现一个批量请求函数, 能够限制并发量
class Quene {
constructor() {
this._quene = [];
}
push(value) {
return this._quene.push(value);
}
shift() {
return this._quene.shift();
}
empty() {
return this._quene.length; // 队列的长度
}
}
// DelayTask类代表需要延迟的任务
class DelayTask {
constructor(resolve, fn, args) {
this.resolve = resolve; // Promise的resolve方法
this.fn = fn; // 要执行的函数
this.args = args; // 执行函数的参数
}
}
class TaskPool {
constructor(size) {
this.size = size; // 最大可并行的任务数量
this.quene = new Quene(); // 使用Quene类来实现任务队列
}
addTask(fn, args) {
return new Promise(resolve => {
// 创建一个DelayTask实例并将其添加到队列中
this.quene.push(new DelayTask(resolve, fn, args))
if (this.size) { // 如果当前可并行任务数量不为0,则直接执行任务
this.size--;
const { resolve, fn, args } = this.quene.shift(); // 取出队列中的第一个任务实例
resolve(this.runTask(fn, args)); // 执行该任务
}
})
}
pullTask() {
if (!this.quene.empty()) { // 如果任务队列不为空,则可以取出任务
return;
}
if (!this.size) { // 如果当前任务数量已经满载,则不能再继续添加新任务
return;
}
this.size--;
const { resolve, fn, args } = this.quene.shift(); // 取出队列中的第一个任务实例
resolve(this.runTask(fn, args)); // 执行该任务
}
runTask(fn, args) {
const result = Promise.resolve(fn(...args)); // 执行任务
result.then(() => {
this.size++; // 当任务执行完成后,可以继续添加新任务
this.pullTask(); // 尝试取出队列中的新任务并执行
}).catch((err) => {
this.size++; // 当任务执行出错时,可以继续添加新任务
this.pullTask(); // 尝试取出队列中的新任务并执行
})
return result; // 返回Promise实例
}
}
const task = timeout => new Promise((resolve) => setTimeout(() => {
console.log('timeout----', timeout);
resolve(timeout);
}, timeout));
const taskList = [4000, 3000, 2000, 1300, 800, 2000];
const cc = new TaskPool(2);
async function startConcurrentControl() {
console.time('CONCURRENT_CONTROL_LOG');
await Promise.all(taskList.map(item => cc.addTask(task, [item])));
console.timeEnd('CONCURRENT_CONTROL_LOG');
}
startConcurrentControl(); // 执行代码
二面
1、数组转树结构
const arr = [{
id: 2,
name: '部门B',
parentId: 0
},
{
id: 3,
name: '部门C',
parentId: 1
},
{
id: 1,
name: '部门A',
parentId: 2
},
{
id: 4,
name: '部门D',
parentId: 1
},
{
id: 5,
name: '部门E',
parentId: 2
},
{
id: 6,
name: '部门F',
parentId: 3
},
{
id: 7,
name: '部门G',
parentId: 2
},
{
id: 8,
name: '部门H',
parentId: 4
}
]
转换代码如下:
const builderTree = (arr,parentId) => {
const tree=[]
for (let node of arr){
if(node.parentId === parentId){
const children = builderTree(arr,node.id)
if(children.length >0 ){
node.children = children
}
tree.push(node)
}
}
return tree
}
终面
1、【代码题】 去除字符串中出现次数最少的字符,不改变原字符串的顺序。
const reduceMin = (str) => {
let map = new Map()
for(let i=0;i<str.length;i++){
if (map.has(str[i])){
let num = map.get(str[i])
num+=1
map.set(str[i],num)
}else {
map.set(str[i],1)
}
}
let min = Math.min(...map.values())
let keyName
for (let [key,value] of map.entries()){
if (value === min){
keyName = key
}
}
const newArr = str.split("").filter((value,index)=>value!==keyName)
return newArr.join('')
}