JS项目优化问题(项目面试题)

总结了最近的一些面试题和之前的知识点


前端性能优化建议

  1. 减少 HTTP 请求
  2. 使用 HTTP2
  3. 静态资源使用 CDN
  4. 将 CSS 放在文件头部,JavaScript 文件放在底部(CSS 执行会阻塞渲染,阻止 JS 执行;
    JS 加载和执行会阻塞 HTML 解析,阻止 CSSOM 构建)
  5. 图片优化
  6. 减少重绘重排
  7. CSS 选择器优先级
    博客

防止表单重复提交的解决方案

用户在操作表单Post数据时往往会出现表单数据重复提交的问题,尤其在Web开发中此类问题比较常见。刷新页面,后退操作以前的页面,单机多次按钮都会导致数据重复提交。此类问题是因为浏览器重复提交HTTP请求导致。

1、 在数据库添加唯一字段

在数据库建表的时候在ID字段添加主键约束,账号,名称的信息添加唯一性约束。确保数据库只可以添加一条数据。
此方法从根本上的防止了数据重复提交。

2、 用js为添加按钮禁用

当用户提交表单之后,可以使用js将提交按钮隐藏(disable属性),防止用户多次点击按钮提交数据。
注意:如果客户端禁用了js,则此方法无效。

3、 使用Post/Redirect/Get

Post/Redirect/Get简称PRG,是一种可以防止表单数据重复提交的一种Web设计模式,像用户刷新提交响应页面等比较典型的重复提交表单数据的问题可以使用PRG模式来避免。例如:当用户提交成功之后,执行客户端重定向,跳转到提交成功页面。
注意:PRG设计模式并不适用所有的重复提交情况,比如:

1)由于服务器响应缓慢,用户刷新提交POST请求造成的重复提交。

2)用户点击后退按钮,返回到数据提交界面,导致的数据重复提交。

3)用户多次点击提交按钮,导致的数据重复提交。

4)用户恶意避开客户端预防多次提交手段,进行重复数据提交。

4、 使用Session设置令牌

客户端请求页面时,服务器为每次产生的Form表单分配唯一的随机标识号,并且在Form的一个隐藏字段中设置这个标识号,同时在当前用户的Session中保存这个标识号。当提交表单时,服务器比较hidden和session中的标识号是否相同,相同则继续,处理完后清空Session,否则服务器忽略请求。

注意:恶意用户可利用这一性质,不断重复访问页面,以致Session中保存的标识号不断增多,最终严重消耗服务器内存。可以采用在Session中记录用户发帖的时间,然后通过一个时间间隔来限制用户连续发帖的数量来解决这一问题。

参考链接

防止表单重复提交的解决方案整理

渲染十万条数据解决方案

虚拟列表是最主流的解决方案,不渲染所有的数据,只渲染可视区域中的数据。当用户滑(滚)动时,通过监听 scroll 来判断是上滑还是下拉,从而更新数据。同理 IntersectionObserver 和 getBoundingClientRect 都能实现

延迟渲染,也叫懒加载。顾名思义,最开始不渲染所有数据,只渲染可视区域中的数据(同虚拟列表一致)。当滚动到页面底部时,添加数据(concat),视图渲染新增DOM

时间分片主要是分批渲染DOM,使用 requestAnimationFrame 来让动画更加流畅

虚拟列表(也叫按需渲染或可视区域渲染)

什么是虚拟列表

虚拟列表是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,是对长列表渲染的优化手段
说的明白一点,就是展示可视区域中的内容,当你向上向下滚动时,通过 DOM API 替换可视区域中的数据,做到动态加载十万条数据

两种解决思路

关于无限滚动,早期通过监听 scroll 事件,这是最常见的解决方案。可去 图片懒加载 中查看,简单来说,就是通过子项的 offsetTop(偏移高度)与 innerHeight(视窗高度)+ scrollTop(滚动高度)做对比来实现,当偏移高度 < 视窗高度+滚动高度时,说明已经滚到下方,就可展示图片
在 图片懒加载 中我们也提及 IntersectionObserver(交叉观察者)API,以此来解决 scroll 所不具备的效果,即  IntersectionObserver API 是异步的,不随目标元素的滚动同步触发,性能消耗小。当然还可以通过 getBoundingClientRect 来实现,getBoundingClientRect 方法返回元素的大小机器相对于视窗的位置

scroll 解决方案

先说 scroll 解决方案,简单来说,就是对其传来的数据进行分割展示,用到 slice 方法,它会返回一个新的数组
我们假设单个列表高度为 30px,一页展示的列表数量为 const count = Math.ceil(列表高度 / 30),展示的数据就是 visibleData = data.slice(start, start + count)(start 一开始为0)
当滚动时,动态修改 start 和 visibleData,虚拟列表scroll无virtual-list-phantom

这种方法的精髓在于设置开始渲染的点和展示的数据,当他滚动时动态修改,但是因为scroll 会频繁触发,当渲染的数据变多后会有性能问题

IntersectionObserver 解决方案

通过 IntersectionObserver 的特性,当目标对象中的 entry.isIntersecting 为 true 或者 intersectionRatio > 0 (元素与祖先元素交叉、可见)时,说明本来不可见的元素浮现在视图中,表示它向上或向下滑动,我们动态设置视图中的顶部和底部 id 即可对其判断。当下滑时 entry.traget.id === ‘bottom’,我们修改 start 和 end;同理,当上滑时entry.traget.id === ‘top 时,我们也一样修改 start 和 end

延迟渲染(即懒渲染)

懒加载

不多介绍,一句话解释:最开始不渲染所有数据,只展示视图上可见的数据,当滚动到页面底部时,加载更多数据
实现原理:通过监听父级元素的 scroll 事件,当然也可以通过 IntersectionObserver 或 getBoundingClientRect 等 API 实现
但 scroll 事件会频繁触发,所以需要手写节流;滚动元素内有大量 DOM ,容易造成卡顿,建议使用 IntersectionObserver

时间分片

对于大量数据渲染时,JS 运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段。也就是说 JS 执行是很快的,页面卡顿是因为同时渲染大量 DOM 所引起的,可采用分批渲染的方式来解决

我的理解是,通过递归来渲染DOM,刚开始可以是20个,20个渲染完后再渲染剩下的,循环如此,将其全部渲染完。又因为浏览器的渲染机制是“宏任务—微任务—GUI渲染—宏任务…”。遂第一个 loop 执行后,先等页面渲染完,再执行下一轮的setTimeout(宏任务)

总结

渲染十万条数据有三种解决方案,为虚拟列表、懒加载、时间分片。最优选是虚拟列表,DOM 树上只挂载有限的DOM;懒加载和时间分片的缺点在于插入大量的DOM,占内存运行时会造成卡顿
无论是虚拟列表还是懒加载,传统的做法是 scroll + 节流,这种做法的优势是老 API,兼容性刚刚的,缺点是,滑多了还是会引起性能问题,当然 IntersectionObserver 也是一样的,无非是换了个 API 做“元素是否出现在视图”判断,最好的方案是用 IntersectionObserver(交叉观察器),异步加载、性能消耗小

面试题:渲染十万条数据解决方案

实现图片懒加载(Lazyload)

懒加载的意义(为什么要使用懒加载)
对页面加载速度影响最大的就是图片,一张普通的图片可以达到几M的大小,而代码也许就只有几十KB。当页面图片很多时,页面的加载速度缓慢,几S钟内页面没有加载完成,也许会失去很多的用户。
所以,对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样子对于页面加载性能上会有很大的提升,也提高了用户体验。

原理

将页面中的img标签src指向一张小图片或者字符串,然后定义data-src(这个属性可以自定义命名,我才用data-src)属性指向真实的图片。src指向一张默认的图片,否则当src为空时也会向服务器发送一次请求。可以指向loading的地址。

注:图片要指定宽高

1
<img src="default.jpg" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" />复制代码当载入页面时,先把可视区域内的img标签的

data-src属性值负给src,然后监听滚动事件,把用户即将看到的图片加载。这样便实现了懒加载。
JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
var num = document.getElementsByTagName('img').length;
var img = document.getElementsByTagName("img");
var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历

lazyload(); //页面载入完毕加载可是区域内的图片

window.onscroll = lazyload;

function lazyload() { //监听页面滚动事件
var seeHeight = document.documentElement.clientHeight; //可见区域高度
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
for (var i = n; i < num; i++) {
if (img[i].offsetTop < seeHeight + scrollTop) {
if (img[i].getAttribute("src") == "default.jpg") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
</script>

使用节流函数进行性能优化

如果直接将函数绑定在scroll事件上,当页面滚动时,函数会被高频触发,这非常影响浏览器的性能。

我想实现限制触发频率,来优化性能。

节流函数:只允许一个函数在N秒内执行一次。
实现图片懒加载(Lazyload)

图片预加载

预加载

1.什么是预加载

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。

2.为什么要用预加载

在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。

3.实现预加载的几种办法(使用HTML标签:display:none,使用Image对象)

使用HTML标签
1
<img src="http://pic26.nipic.com/20121213/6168183 0044449030002.jpg" style="display:none"/>
使用Image对象
1
2
3
4
<script src="./myPreload.js"></script>
//myPreload.js文件
var image= new Image()
image.src="http://pic26.nipic.com/20121213/6168183 004444903000 2.jpg"

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

参考文章

懒加载和预加载

判断图片是否加载完成的六种方式

一、load事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>img - load event</title>
</head>
<body>
<img id="img1"src="http://pic1.win4000.com/wallpaper/f/51c3bb99a21ea.jpg">
<p id="p1">loading...</p>
<script type="text/javascript">
img1.onload =function() {
p1.innerHTML ='loaded'
}
</script>
</body>
</html>

测试,所有浏览器都显示出了“loaded”,说明所有浏览器都支持img的load事件
二、img的complete属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>img - complete attribute</title>
</head>
<body>
<img id="img1"src="http://pic1.win4000.com/wallpaper/f/51c3bb99a21ea.jpg">
<p id="p1">loading...</p>
<script type="text/javascript">
functionimgLoad(img, callback) {
vartimer = setInterval(function() {
if(img.complete) {
callback(img)
clearInterval(timer)
}
}, 50)
}
imgLoad(img1,function() {
p1.innerHTML('加载完毕')
})
</script>
</body>
</html>

轮询不断监测img的complete属性,如果为true则表明图片已经加载完毕,停止轮询。该属性所有浏览器都支持。
参考文章

原生JS实现轮播图 方法总结

  1. 方法一:
    利用绝对定位absolute偏移量的改变来实现
    具有往左往右滑动的效果

  2. 方法二:
    利用 display/opacity/visibility状态切换来实现
    没有往左往右滑动的效果

  3. 方法三
    旋转木马轮播图
    存储每个图片的位置信息(absolute位置信息+z-index属性+opacity透明度 等等)到一个数组。对数组进行pop push shift unshift等操作再引用到DOM元素上,产生轮播效果。
    参考

实现一个div元素的拖拽 (addEventListen/removeEventListen)

1.js 如何实现拖动滑块( mousedown,mousemove,mouseup)
实现拖动滑块,先分析,滑块可以拖动应该改变滑块在页面中的坐标,那就采用定位拿到元素的 top 和 left 对它们进行赋值,接下来就是准备事件,既然是鼠标拖动应该具备 mousedown,mousemove,mouseup 三种事件,通过 mousedown 鼠标按下事件选中滑块,mousemove 事件拖动滑块,在拖动滑块的时候获取鼠标在可视窗口的坐标赋值给滑块的 top 和 left
参考

2.实现思路:
鼠标按下开始拖拽
记录摁下鼠标时的鼠标位置以及元素位置
拖动鼠标记下当前鼠标的位置
鼠标当前位置-摁下时鼠标位置= 鼠标移动距离
元素位置= 鼠标移动距离+鼠标摁下时元素的位置
参考

白屏合理性优化 (骨架屏,loading)

4.路由懒加载
9.骨架屏
骨架屏就是在进入项目的FP阶段,给它来一个类似轮廓的东西,当我们的页面加载完成之后就消失,这个也很好做的,很多ui库都有这个东西,可以参考一下
10.loading
首页加一个loading或许是最原始的方法了,在index.html里加一个loadingcss效果,当页面加载完成消失

Vue首屏加载白屏问题及解决方案

按需引入组件库 element-ui

参考 element-ui 的导出方案,组件库导出的组件依赖,要提供每个组件单独打包的依赖文件。
全量导出 index.js 文件无需改动,在 index.js 同级目录增加新文件 base.js,用于导出基础组件。
将Vue组件库更换为按需加载

  1. 支持bable的自动引入

2. 支持tree-shaking

如何中断已经发出去的请求

  1. 调用 XMLHttpRequest 对象上的 abort 方法来取消请求
  2. CancelToken
    博客

多个Promise顺序执行的解决思路

1. 使用回调函数解决

2. 使用promise/then链式调用

1
2
3
4
5
6
7
Promise.resolve().then(() => a())
.then(res => console.log(res))
.then(() => b())
.then(res => console.log(res))
.then(() => c())
.then(res => console.log(res))
.then(() => console.log(4))

3. 使用async/await解决 (for of)

1
2
3
4
5
6
7
8
9
async function log(a, b, c) {
const args = [].slice.call(arguments, 0)
for (let item of args) {
let res = await item()
console.log(res)
}
console.log(4)
}
log(a, b, c)

大文件上传处理

  1. 对文件做切片,即将一个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始

步骤1-切片,合并切片
在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice通过这个方法

步骤2-并发控制
结合Promise.race和异步函数实现,多个请求同时并发的数量,防止浏览器内存溢出
步骤3-断点续传
在单个请求失败后,触发catch的方法的时候,讲当前请求放到失败列表中,在本轮请求完成后,重复对失败请求做处理
2. 通知服务器合并切片,在上传完切片后,前端通知服务器做合并切片操作
3. 控制多个请求的并发量,防止多个请求同时发送,造成浏览器内存溢出,导致页面卡死
4. 做断点续传,当多个请求中有请求发送失败,例如出现网络故障、页面关闭等,我们得对失败的请求做处理,让它们重复发送
博客

了解Ts,Ts的好处是?

静态类型

静态类型化是一种功能,可以在开发人员编写脚本是检测错误,有了这项功能,就会允许开发人员编写更健壮的代码并对其进行维护,以便使得代码质量更好、更清晰。
(从代码可知变量num是number类型,如果我们给num赋予其他类型的值就会报错。)

大型项目的优势

对于大型项目的开发,有时为了优化改进项目,对代码进行小小更改。这些小小的变化可能会产生严重的、意想不到的后果,因此有必要撤销这些变化。使用TypeScript工具来进行重构更变的容易、快捷。

更好的协作

对于大型项目的开发一般会有很多开发人员一起开发,此时乱码和错误的机也会增加。类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误。这为开发团队创建了一个更高效的编码和调试过程。

typescript 中 interface 和 type 的区别

相同点 (都可以描述一个对象或者函数&&都允许拓展(extends))

不同点 (
type可以interface不可以::

type 可以声明基本类型别名,联合类型,元组等类型,type 语句中还可以使用 typeof 获取实例的 类型进行赋值 ,

interface可以type不可以:

interface 能够声明合并)
参考
一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type

如何让localStorage支持过期时间设置?

聊到 localStorage 想必熟悉前端的朋友都不会陌生, 我们可以使用它提供的getItem, setItem, removeItem, clear 这几个 API 轻松的对存储在浏览器本地的数据进行「读,写, 删」操作, 但是相比于 cookie, localStorage 唯一美中不足的就是「不能设置每一个键的过期时间」

问题描述

在实际的应用场景中, 我们往往需要让 localStorage 设置的某个 「key」 能在指定时间内自动失效, 所以基于这种场景, 我们如何去解决呢?

  1. 初级写法 (维护成本极高, 且不利于工程化复用)
1
2
3
4
5
6
localStorage.setItem('dooring''1.0.0')
// 设置一小时的有效期
const expire = 1000 * 60 * 60;
setTimeout(() => {
  localStorage.setItem('dooring''')
}, expire)
  1. 中级解法
    前端工程师在有一定的工作经验之后, 往往会去考虑工程化和复用性的问题, 并对数据结构有了一定的了解, 所以可能会有接下来的解法:

基本思路:
(1)封装函数在保存数据时,指定有效时间,把数据和这个有效时间一起保存起来。
(2)封装函数在获取数据时,不着急直接给出结果,而是检查之前保存的有效时间是否过期,如果在就返回数据,否则就删除数据,并返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// key: 属性名
// value: 要保存的值
// availabletime: 有效时间,毫秒为单位
const setItem = (key, value,availabletime = 1000*60*60*24) => {
// 最晚这时间点
const t = new Date().getTime() + availabletime;
localStorage.setItem(key, JSON.stringify({ data: value, time: t }));
}

// 获取数据
const getItem = (key) => {
// 去初始值
const localData = localStorage.getItem(key);
const localDataObj = JSON.parse(localData);
if (Date.now() > localDataObj.time) {
// console.log("数据已过期");
//删除
localStorage.removeItem(key);
return false;
} else {
// 返回真正的数据
return JSON.parse(localDataObj.data);
}
};

重写 set(存入) 方法:
首先有三个参数 key、value、expired ,分别对应 键、值、过期时间,
过期时间的单位可以自由发挥,小时、分钟、天都可以,
注意点:存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify,
这个时间如何设置呢?在这个值存入的时候在键(key)的基础上扩展一个字段,如:key+’expires’,而它的值为当前 时间戳 + expired过期时间
掘金