面试的节奏不能停,今天我们就来感受一下百度的面试题,不仅涵盖八股,更多还是实际开发中的场景题和代码题。
Baidu
手风琴
如何去实现一个类似于手风琴效果的页面;
手风琴效果本质上是通过块级元素来实现的,元素是从上到下排列的。
布局结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手风琴</title>
<link rel="stylesheet" href="common.css">
</head>
<body>
<ul class="accordion">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
</body>
</html>
Stylus 使用
在 CSS 这块,我们可以用 stylus 去实现;Stylus 是 CSS 的预处理器,可以通过简化语法来减少代码的冗余,特别是在编写手风琴效果的时候,Stylus 的缩进和模块化特性可以极大简化编写过程。
common.styl
*
margin 0
padding 0
box-sizing border-box
ul,li
list-style none
.accordion
display flex
width 600px
height 200px
li
flex 1
cursor pointer
line-height 200px
text-align center
color white
transition flex 500ms
&:nth-child(1)
background-color #f66
&:nth-child(2)
background-color #66f
&:nth-child(3)
background-color #f90
&:nth-child(4)
background-color #09f
&:nth-child(5)
background-color #9c3
&:nth-child(6)
background-color #3c9
&:hover
flex 2
编译命令:
stylus common.styl -o common.css
-
-o参数用于指定输出文件。 -
使用
-w选项可以持续监听.styl文件的变化:
stylus -w common.styl -o common.css
不仅方便编写,stylus 还给 css 引入了编程特性。
编译后的 CSS 文件:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
ul,
li {
list-style: none;
}
.accordion {
display: flex;
width: 600px;
height: 200px;
}
.accordion li {
flex: 1;
cursor: pointer;
line-height: 200px;
text-align: center;
color: #fff;
transition: flex 500ms;/*为子元素的弹性伸缩加一个0.5s的过度动画 更加平滑*/
}
.accordion li:nth-child(1) {
background-color: #f66;
}
.accordion li:nth-child(2) {
background-color: #66f;
}
.accordion li:nth-child(3) {
background-color: #f90;
}
.accordion li:nth-child(4) {
background-color: #09f;
}
.accordion li:nth-child(5) {
background-color: #9c3;
}
.accordion li:nth-child(6) {
background-color: #3c9;
}
.accordion li:hover {
flex: 2;
}
数组的扁平化
假设有一个数组 arr 为 [1, [2, [3, 4] ] ],要怎么去扁平化得到 [ 1, 2, 3, 4 ]呢?
递归
第一眼望去,用递归解决是一个不错的解决方案;
// 展平 递归 (大问题拆分成多个类似的子问题)
const arr = [1,[2,[3,4]]]
function flatten(arr) {
let result = []
// len 缓存了值,arr是对象, .length 耗时
for (let i = 0 , len = arr.length ; i < len ; i++){
// 数组,递归
if (Array.isArray(arr[i])){
result = result.concat(flatten(arr[i])); // concat() 方法用于连接两个或更多的数组,并返回一个新的数组,原数组不会被改变
}else{
result.push(arr[i])
}
// 否则 加入数组
}
return result;
}
console.log(flatten(arr));
每次递归都去判断数组中的每个值是否为一个新数组,递归直至数组中再无嵌套数组。
toString
除了递归,我们还可以用数组身上的 toString 方法去把数组转化为字符串,完成扁平化处理。
// 数组对象 toString 一下,1,2,3,4 扁平化的希望了
// console.log([1,[2,[3,[4,[5,6]]]]].toString(),typeof [1,[2,[3,4]]].toString());// 类型转换,变成了字符串
var arr = [1,[2,[3,[4,[5,6]]]]]
// 扁平化
function flatten(arr) {
var strArr = arr.toString();// ,隔开的字符串
console.log(strArr);
var numArr = strArr.split(',');
var result = []
for (var i = 0,len = numArr.length; i < len; i++){
// result.push(parseInt(numArr[i]))
// numArr[i]字符串,js 可以强制类型转换 (Number() 显示类型转换)
// result.push(Number(numArr[i]))
// + 和 Number() 是一样的,+,一是拼接字符串,二是数值计算
// 三是类型转换 隐式类型转换
result.push(+numArr[i])
}
return result;
console.log(result);
}
console.log(flatten(arr));
Array.prototype.toString() 方法会将数组中的每个元素调用其各自的 toString() 方法,如果该元素是一个数组,它就会调用那个数组的 toString() 方法,这样就隐式地处理了嵌套结构。
map 优化
对于上面的代码,我们还可以用 map 方法进一步去优化;
var arr = [1,[2,[3,[4,[5,6]]]]]
function flatten(arr) {
// map;对集合(如数组)中的每个元素应用一个指定的函数,并返回一个新的集合,新集合中的元素是原集合元素经过该函数处理后的结果
return arr.toString().split(',').map(function(item){
// console.log(item);
return +item
})
}
console.log(flatten(arr));
Event Loop
JavaScript 是一种单线程语言,意味着它一次只能执行一个任务。为了确保即使在复杂的交互和任务处理下页面也不会卡顿,JavaScript 引擎采用了 事件循环(Event Loop) 机制,来协调同步和异步代码的执行。
1. JavaScript 引擎的单线程架构
JavaScript 在单线程环境中运行,这意味着它不能同时执行多个任务。所有的代码都会在同一个线程上顺序执行。然而,实际开发中需要处理异步操作(如 I/O 操作、定时器、HTTP 请求等),为了在执行异步任务时不阻塞页面的渲染和响应,JavaScript 引入了事件循环机制。
2. 同步代码与异步代码
- 同步代码:在调用栈中依次执行的代码,任务完成之前不会进行其他操作。比如常见的变量赋值、函数调用等。
- 异步代码:不会立即执行,而是放入相应的任务队列,等待某些条件满足后再执行。典型的例子包括
setTimeout、Promise、I/O 操作等。
3. 执行栈与事件循环
当 JavaScript 程序开始执行时,所有的代码会被放入 调用栈(Execution Stack) 中,调用栈中的代码会逐行执行。当遇到异步任务时,如setTimeout、Promise,它们会被分配到不同的任务队列中,等待执行。
JavaScript 的任务类型分为两大类:宏任务(macro task) 和 微任务(micro task) 。
4. 宏任务与微任务
-
宏任务(macro task) :
- 常见的宏任务包括:
script(整体代码)、setTimeout、setInterval、setImmediate、I/O 操作、UI 渲染等。 - 宏任务会被存放在一个队列中,等待事件循环依次处理。
- 常见的宏任务包括:
-
微任务(micro task) :
- 微任务通常优先级高于宏任务,根据事件循环的机制,当一个宏任务执行完毕后,JavaScript 引擎会立即检查并执行微任务队列中的所有任务,然后才开始执行下一个宏任务。
- 常见的微任务包括:
Promise回调、process.nextTick、MutationObserver等。 - 微任务存放在独立的微任务队列中,且会在每个宏任务执行完成后立刻执行所有微任务。
5. 事件循环的执行过程
事件循环的核心任务是确保调用栈中的同步代码执行完毕后,再依次处理宏任务和微任务。具体的过程如下:
- 初始状态:调用栈为空,栈底为全局作用域。宏任务队列中只有一个
script,即整个脚本的同步代码,微任务队列为空。 - 执行同步代码:全局上下文被推入调用栈,开始执行同步代码。如果在执行过程中产生了异步任务(如定时器、Promise 等),这些任务会被分别加入宏任务队列或微任务队列。
- script 移出宏任务队列:当所有同步代码执行完毕后,
script任务从宏任务队列中移除。 - 执行微任务:如果在执行宏任务期间产生了微任务,那么事件循环会在当前宏任务完成后立即执行所有微任务,直到微任务队列被清空。
- 事件循环继续:事件循环会继续执行下一个宏任务,完成之后再次检查微任务队列,依次反复,直到所有任务都完成。
- 渲染操作:在每次事件循环的结尾,JavaScript 引擎会执行页面的渲染操作,更新 UI 界面。
小结
在 JavaScript 的事件循环(Event Loop)中,页面的重新渲染(UI 更新)通常发生在 宏任务和微任务执行完之后,但在下一个宏任务开始之前。
- 执行宏任务:浏览器首先执行当前的宏任务(例如脚本、定时器、I/O 操作等)。
- 执行微任务:当宏任务完成后,浏览器会检查并执行微任务队列中的任务(如
Promise)。 - 页面渲染:在所有宏任务和微任务执行完成后,如果有 DOM 变化或需要更新的部分,浏览器会在此时进行页面的渲染。
- 进入下一个宏任务:渲染完毕后,浏览器开始执行下一个宏任务。
因此,页面的渲染操作通常发生在宏任务和微任务全部执行完成之后,并且在下一个宏任务开始之前。如果微任务持续添加,渲染可能会被延迟,从而导致页面卡顿或更新不及时。