记一次某大厂面经及复盘

本文最后更新于:2023年3月19日 晚上

自我介绍及项目介绍、项目难点

介绍一下响应式布局、移动端的自适应方案

讲了 flex,rem,rm,vw、vh、微信小程序的 rpx、viewport、阿里的 flexible.js。
还学习到还有 CSS3 新增标签

从零开始写网页,如何写一个脚本实现自适应

没有答上来,我猜想问的是阿里 flexible.js 的原理,根据设备 windows.devicePixelRatio 获得一个 dpr 值,如 ios 是 2 或 3。
然后设置 html 根节点的 font-size 如doc.body.style.fontSize = 12 * dpr + ``'px'``;
然后再页面中使用 rem,这里可以用插件自动将 px 转为 rem。如设计稿 750px,一个 button 宽 100px,rem 应该为 100 / 75 rem

事实上 flexible.js 做了下面三件事:

  • 动态改写标签
  • 给元素添加 data-dpr 属性,并且动态改写 data-dpr 的值
  • 给元素添加 font-size 属性,并且动态改写 font-size 的值

介绍一下 ViewPort

讲了一下用于移动端响应式,讲了一下基本属性,初始放大,最大放大,最小放大,是否允许用户放大缩小等属性。

前端性能优化的指标,如何知道优化不是正向优化

不知道这些指标,说到了一个白屏时间,然后试图改变问题,讲到了 JS 放在头部会造成阻塞渲染,放一个小的 css 到顶部实现 loading 和骨架屏等,被面试官拉回来了。

  • 白屏时间
  • HTML 加载完成时间
  • 首屏图片加载完成时间
  • 首屏接口完成加载完成时间
  • 各资源耗时(主要统计 css/js 资源耗时)
  • FP(首次绘制时间)
  • FCP(首次内容渲染时间)
  • onload 时间

转自:https://blog.csdn.net/c_kite/article/details/104237256

★FP (First Paint) 首次绘制

FP (First Paint) 首次绘制: 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点.

★FCP (First Contentful Paint) 首次内容绘制

FCP (First Contentful Paint) 首次内容绘制 标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 元素.

LCP (Largest Contentful Paint) 最大内容渲染

LCP (Largest Contentful Paint) 最大内容渲染: 代表在 viewport 中最大的页面元素加载的时间. LCP 的数据会通过 PerformanceEntry 对象记录, 每次出现更大的内容渲染, 则会产生一个新的 PerformanceEntry 对象.(2019 年 11 月新增)

★DCL (DomContentloaded)

DCL (DomContentloaded): 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载.

★FMP(First Meaningful Paint) 首次有效绘制

FMP(First Meaningful Paint) 首次有效绘制: 例如,在 YouTube 观看页面上,主视频就是主角元素. 看这个 csdn 的网站不是很明显, 这几个都成一个时间线了, 截个 weibo 的看下. 下面的示例图可以看到, 微博的博文是主要元素.

L (onLoad)

L (onLoad), 当依赖的资源, 全部加载完毕之后才会触发.


再来说几个, 这个 performance panel 上没画的名词, 但用的上的:

★TTI (Time to Interactive) 可交互时间

TTI (Time to Interactive) 可交互时间: 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点.

★TBT (Total Blocking Time) 页面阻塞总时长

TBT (Total Blocking Time) 页面阻塞总时长: TBT 汇总所有加载过程中阻塞用户操作的时长,在 FCP 和 TTI 之间任何 long task 中阻塞部分都会被汇总.
来个例子说明一下:


在主线程上运行任务所花费的总时间为 560 毫秒,但只有 345(200 + 40 + 105)毫秒的时间被视为阻塞时间(超过 50ms 的 Task 都会被记录).

FID (First Input Delay) 首次输入延迟

FID (First Input Delay) 首次输入延迟: 指标衡量的是从用户首次与您的网站进行交互(即当他们单击链接,点击按钮等)到浏览器实际能够访问之间的时间, 下面来张图来解释 FID 和 TTI 的区别:

CLS (Cumulative Layout Shift) 累积布局偏移

CLS (Cumulative Layout Shift) 累积布局偏移: 总结起来就是一个元素初始时和其 hidden 之间的任何时间如果元素偏移了, 则会被计算进去, 具体的计算方法可看这篇文章 https://web.dev/cls/

SI (Speed Index)

SI (Speed Index): 指标用于显示页面可见部分的显示速度, 单位是时间,

前端优化相关

https://segmentfault.com/a/1190000020690092

浏览器渲染页面的过程

只知道 CSS 树和 DOM 树的加载和渲染过程。看了一下以下视频,了解了许多。https://www.bilibili.com/video/BV1dt411W7Y4?t=147

浏览器如何解析 HTML

整理,参考于:https://juejin.im/post/6844903745730396174

  1. 整个dom的解析过程是顺序,并且渐进式的。顺序指的是从第一行开始,一行一行依次解析;
  2. 阻塞型的资源为阻塞dom解析,包括以下:
    • 内联 css
    • 内联 javascript
    • 外联普通 javascript
    • 外联 defer javascript

defer外联 defer js 与普通 js 不同的是 defer js 的下载不阻塞 dom 解析,但是它的执行会阻塞dom,虽然说是 html 已经解析完了,但dom的解析完成取决于html和阻塞js的完成,defer js,内联 js,普通外联 js 这三种就属于阻塞 js。所以说 defer js 是会阻塞 dom 解析的。

  • javascript 标签之前的外联 css

非阻塞的资源包括以下:

  • javascript 标签之后的外联 css
  • image
  • iframe
  • 外联 async javascript

浏览器发现 img,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码
服务器返回图片文件,由于图片占用了一定面积,影响了页面排布,浏览器需要回过头来重新渲染这部分代码;

  1. 这些阻塞型的资源请求并执行完之后dom树的解析便完成了,这时document对象就会派发DOMContentLoaded事件,表示dom树构建完成。
  2. 外联javascript加载过程

html页面中可以引入内联javascript,也可以引入外联javascript外联javascript又分为:

  • 外联普通 javascript
  • 外联 defer javascript
  • 外联 async javascript

其中第一种就是外联普通javascript,会阻塞html的解析,html解析过程中每遇到这种<script>标签就会请求并执行,如下图所示,绿色表示html解析;灰色表示html解析暂停;蓝色表示外联javascript加载;粉色表示javascript执行

外联普通javascript的加载执行过程如下:
第二种外联defer javascript稍有不同,html解析过程中遇到此类<script>标签不阻塞解析,而是会暂存到一个队列中,等整个html解析完成后再按队列的顺序请求并执行javascript,但是这种外联defer javascript全部加载并执行完成后才会派发DOMContentLoaded事件,外联defer javascript的加载执行过程如下:
第三种外联async javascript则不阻塞html的解析过程,注意这里是说的脚本的下载过程不阻塞html解析,如果下载完成后html还没解析完成,则会暂停html解析,先执行完成下载后的javascript代码再继续解析html,过程如下:但是如果html已经解析完毕,外联async javascript还未下载完成,则不阻塞DOMContentLoaded事件的派发。因此外联async javascript很有可能来不及监听DOMContentLoaded事件,比如stackoverflow上的这个问题
说明下,这几个图引用自这里
补充

  • 解析 HTML 构建 DOM 时,遇到 JavaScript 会被阻塞
  • JavaScript执行会被CSSOM构建阻塞,也就是说,JavaScript必须等到CSSOM构建完成后才会执行
  • 如果使用异步脚本,脚本的网络请求优先级降低,且网络请求期间不阻塞 DOM 构建,直到请求完成才开始执行脚本

浏览器是如何渲染页面的

参考于:https://juejin.im/post/6844904131346300942

Critical Rendering Path

_Critical Rendering Path_,中文翻译过来,叫做关键渲染路径。指的是浏览器从请求 HTML,CSS,JavaScript 文件开始,到将它们最终以像素输出到屏幕上这一过程。包括以下几个部分:

  1. 构建DOM
    • 将 HTML 解析成许多 Tokens
    • 将 Tokens 解析成 object
    • 将 object 组合成为一个 DOM 树
  2. 构建CSSOM
    • 解析 CSS 文件,并构建出一个 CSSOM 树(过程类似于 DOM 构建)
  3. 构建Render Tree
    • 结合 DOM 和 CSSOM 构建出一颗 Render 树
  4. Layout
    • 计算出元素相对于 viewport 的相对位置
  5. Paint
    • 将 render tree 转换成像素,显示在屏幕上

值得注意的是,上面的过程并不是依次进行的,而是存在一定交叉

为什么使用虚拟 DOM,直接操作 DOM 不是更快吗?

没答到点上,只说了双向绑定的核心是虚拟 DOM。应该想问的是 diff 算法。
但是说了数据量非常大的情况下,使用虚拟 DOM 也不一定更好。

为啥真实 DOM 操作不推荐

真实 DOM 的操作,一般都会对某块元素的整体重新渲染

为啥虚拟 DOM 能提升性能

采用虚拟 DOM 的话,当数据变化的时候,只需要局部刷新变化的位置就好了。

实现固定长宽比(4:3)的块

忘记了加 top,问题不大。

1
<div class="element"></div>

考点:padding 是根据父元素的 width 计算的。

1
2
3
4
.element {
padding-top: 30%;
width: 40%;
}

npm 包的版本语义化规范说一下

没了解过,学习了一下。
转自:https://segmentfault.com/a/1190000018714929
为了在软件版本号中包含更多意义,反映代码所做的修改,产生了语义化版本,软件的使用者能从版本号中推测软件做的修改。npm 包使用语义化版控制,我们可安装一定版本范围的 npm 包,npm 会选择和你指定的版本相匹配(latest)最新版本安装。
npm 的版本号由三部分组成:
主版本号次版本号补丁版本号。变更不同的版本号,代表不同的意义:

  • 主版本号(major):软件做了不兼容的变更(breaking change 重大变更);
  • 次版本号(minor):添加功能或者废弃功能,向下兼容;
  • 补丁版本号(patch):bug 修复,向下兼容。

有时候为了表达更加确切的版本,还会在版本号后面添加标签或者扩展,来说明是预发布版本或者测试版本等。比如 3.2.3-beta-3
常见的标签有 :

标签 意义 补充
demo demo 版本 可能用于验证问题的版本
dev 开发版 开发阶段用的,bug 多,体积较大等特点,功能不完善
alpha α 版本 用于内部交流或者测试人员测试,bug 较多
beta 测试版(β 版本) 较 α 版本,有较大的改进,但是还是有 bug
gamma (γ)伽马版本 较 α 和 β 版本有很大的改进,与稳定版相差无几,用户可使用
trial 试用版本 本软件通常都有时间限制,过期之后用户如果希望继续使用,一般得交纳一定的费用进行注册或购买。有些试用版软件还在功能上做了一定的限制。
stable 稳定版
csp 内容安全版本 js 库常用
latest 最新版本 不指定版本和标签,npm 默认安最新版

更多关于标签的内容
查看标签:

1
npm dist-tags ls <pkg>
1
npm dist-tags ls vue

得到:

1
2
3
beta: 2.6.0-beta.3
csp: 1.0.28-csp
latest: 2.6.10

安装带标签的版本

1
npm i <pkg>@<tag>
1
npm i vue@beta # 安装 2.6.0-beta.3

版本号变更规则

  1. 版本号只升不降,不得在数字前加 0,比如 2.01.2 不允许的;
  2. 0.y.z,处于开发阶段的版本;
  3. 第一个正式版版本往往命名为 1.0.0;
  4. 先行版本必须在补丁版本之后添加,比如 2.3.7-0,0表示先行版本,和补丁版本用-分隔;
  5. 版本的比较依次比较主版本次版本补丁版本先行版本,直到第一个能得出比较结果为止;
  6. 不小心把一个不兼容的改版当成了次版本号发行了该怎么办?一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。

了解 EventLoop 吗

做一道 EventLoop 题,并详细复述过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log("start");
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function () {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function () {
console.log("promise2");
});
}, 0);
Promise.resolve().then(function () {
console.log("promise3");
});
console.log("end");

场景题

改造  update  函数,根据事件循环模式,将多次更新合并成一次。进行了多次 update,执行耗时的函数,但是前面的不重要,如何合并所有的操作,只执行最后的操作。
提示说是 EventLoop,如何开启下一次 EventLoop,谈到了页面渲染的机制,栈空的时候再渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<html lang="en">
 
<head>
   
<meta charset="UTF-8" />
   
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
   
<title>test render</title>
 
</head>
 
<body>
   
<div id="result">this is result</div>
 
</body>
 
<script>
function render(num, id) {
// 非常耗时的计算
const array = Array.from({ length: 10000000 });
for (let index = 0; index < array.length; index++) {
array[index] = {};
} // 开始准备渲染
let product = num * num;
let ele = document.getElementById(id);
ele.textContent = product;
} // 改造 update 函数,根据事件循环模式,将多次更新合并成一次

function update(num, id) {
render(num, id);
} // 更新多次

update(2, "result");
update(3, "result");
update(4, "result");
update(5, "result");
update(6, "result");
update(7, "result");
update(8, "result");
</script>
</html>

我的做法,暂时想不到不设一个全局变量能实现的方法。用了防抖的思想。

1
2
3
4
5
6
7
let timer = null;
function update(num, id) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
render(num, id);
});
}

实现 Lodash Chunk 函数

1
_.chunk(["a", "b", "c", "d"], 2); //[['a','b'], ['c','d']]

这题比较简单,秒答了:

1
2
3
4
5
6
7
8
const chunk = (arr, length) => {
const res = [];
while (arr.length > length) {
res.push(arr.splice(0, length));
}
res.push(arr);
return res;
};

场景题

无法使用加减符号,但是可以有一个机器可以获得加减结果,实现 sum 获得求和结果。提示 promiseify
https://www.cnblogs.com/ryzz/p/13264798.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asyncAdd(3, 5, (err, result) => {
console.log(result); // 8
});
function asyncAdd(a, b, cb) {
setTimeout(() => {
cb(null, a + b);
}, Math.floor(Math.random() * 100));
}
// 实现 sum
function sum() {}

(async () => {
const result1 = await sum(1, 4, 6, 9, 10, 14);
const result2 = await sum(3, 4, 9, 20, 22, 30, 32, 100, 200);
const result3 = await sum(1, 6, 10, 15);
console.log([result1, result2, result3]); // [44, 420, 32]
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(...args) {
return new Promise(async (resolve, reject) => {
let sum = 0;
while (args.length) {
sum = await new Promise((res, rej) => {
asyncAdd(sum, args.shift(), (err, result) => {
res(result);
});
});
}
resolve(sum);
});
}

反问

公司技术栈:全 React
移动端的技术栈:公司自研框架,小部分 RN
对我的学习前端的方向有什么意见:还需要多操作,多使用


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!