一. 前端加载优化相关工作?
Q1.做性能优化的目的是什么?
- 首屏时间
- 首次可交互时间
- 首次有意义内容渲染时间
页面性能检测:https://developers.google.com/speed/pagespeed/insights/
pollyfill:https://polyfill.io/v3/url-builder/
Q2.性能优化方式有哪些?
-
只请求当前需要的资源
异步加载, 懒加载,pollyfill
-
缩减资源体积
打包压缩 webpack
gzip 1.2M 300K
图片格式的优化,压缩,根据屏幕分辨率展示不同分辨率的图片,webp
尽量控制cookie大小 4k -
时序优化
js promise.all
ssr(对SEO友好)
prefetch、prerender、preload<link rel=“dns-prefetch” href=“xxxxxx” /> <link rel=“preconnect” href=“xxxxxxx” /> <link rel=“preload” as=“image” href=“xxxxxxxxx” />
-
合理利用缓存
cdn(cdn预热 cdn刷新)
http缓存
localStorage,sessionStorage
Q3.如果一段js执行时间非常长, 怎么去分析?
代码题, 装饰器器计算函数执⾏时间
decorator.ts
export function measure(target: any, name: string, descriptor: any) {
const oldValue = descriptor.value;
descriptor.value = async function () {
console.time(name);
const ret = await oldValue.apply(this, arguments);
console.timeEnd(name);
return ret;
}
return descriptor;
}
Q4.webp格式转换
问题描述:
阿⾥云oss⽀持通过链接后拼参数实现图⽚格式转换, 尝试写⼀下, 把图⽚转为webp格式? 需要注意什么?
代码实现,判断浏览器是否支持webp格式转换
/**
* 判断浏览器是否支持webp格式
*/
function checkWebp() {
try {
return document.createElement('canvas')
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0
} catch (e) {
return false
}
}
const supportWebp = checkWebp();
export function getWebpImageUrl(url) {
if (!url) {
throw Error('url 不能为空');
}
if (url.startWith('data:')) {
return url;
}
if (!supportWebp) {
return url
}
return url + '?x-oss-processxxxxxxxxx'
}
Q5.使⽤promise.race()
异步控制并发数量
问题描述
如果有巨量的图片需要展示在⻚面, 除了懒加载这种方式, 还有什么好的⽅法限制其同一时间加载的数量?
懒加载原理
一张图片就是一个标签,在图片没有进入到可视区域时,先不给的src属性赋值,这样,浏览器就不会发送请求。等到图片进入可视区域再给src赋值,替换占位图片。
Promise.race
实现思路
先并发请求limit个图片资源,这样可以得到limit个Promise
实例,组成一个数组promises,然后不断调用Promise.race来返回最快改变状态的Promise,然后从数组(promises)中删除掉这个Promise对象实例,再加入一个新的Promise实例,直到全部的url被取完。
var urls = [
'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',
'https://www.kkkk1000.com/images/getImgData/gray.gif',
'https://www.kkkk1000.com/images/getImgData/Particle.gif',
'https://www.kkkk1000.com/images/getImgData/arithmetic.png',
'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',
'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',
'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',
'https://www.kkkk1000.com/images/wxQrCode2.png'
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
console.log('一张图片加载完成');
resolve();
}
img.onerror = reject
img.src = url
})
};
function limitLoad(urls, handler, limit) {
// 对数组做一个拷贝
const sequence = [].concat(urls)
let promises = [];
//并发请求到最大数
promises = sequence.splice(0, limit).map((url, index) => {
// 这里返回的 index 是任务在 promises 的脚标,
//用于在 Promise.race 之后找到完成的任务脚标
return handler(url).then(() => {
return index
});
});
(async function loop() {
let p = Promise.race(promises);
for (let i = 0; i < sequence.length; i++) {
p = p.then((res) => {
promises[res] = handler(sequence[i]).then(() => {
return res
});
return Promise.race(promises)
})
}
})()
}
limitLoad(urls, loadImg, 3)
二. 平时有关注前端内存处理吗?
1. 内存的生命周期
- 内存分配:当我们申明变量、函数、对象的时候,js会自动分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
2. js中的垃圾回收机制
2.1 引用计数垃圾回收
a对象对b对象有访问权限,那么叫a引用b对象。
引用计数的含义的是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则该值得引用次数就是1;如果同一个值又被赋给另一个变量,则该值得引用次数加1;如果包含对该值引用的变量又取得了另外一个值,则该值的引用次数减1。
当该值得引用次数变为0时,则可以回收其占用的内存空间。当垃圾回收器下一次运行时,就会释放那些引用次数为0的值所占用的内存。
缺陷:循环引用。
function obj() {
var objA = new Object();
var objB = new Object();
objA.p = objB
objB.p = objA
}
如果两个对象相互引用,尽管它们已经不再使用,垃圾回收不会进行回收,从而导致内存泄漏。
2.2 标记清除法(老生代内存回收)
- 垃圾收集器在运行的时候会给内存中的所有变量都加上标记;
- 从根部出发将能触及到的对象的标记清除;
- 那些还存在标记的变量被视为准备删除的变量;
- 垃圾收集器在执行工作时,会销毁那些带标记的值并回收它们所占用的内存空间。#
3. 内存泄漏
3.1 常见的内存泄漏
- 全局变量
- 未被清除的定时器和回调函数
- 闭包
- DOM引用
3.2 如何避免内存泄漏
- 减少不必要的全局变量,使用严格模式避免意外创建全局变量;
- 在使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
Q1.实现sizeOf函数
既然对内存这么了了解,那么来一道代码题:实现sizeOf函数, 计算传入的对象所占的Bytes数值.
考察内容:
- 对于计算机基础,js内存基础的考察
- 递归
- 细心, 区分数组和对象(对象里的key也占内存空间)
const seen = new WeakSet();
function sizeOfObject(object) {
if (object === null) {
return 0;
}
let bytes = 0;
const properties = Object.keys(object);
for (let i = 0; i < properties.length; i++) {
const key = properties[i];
bytes += calculator(key);
// 如果同一个对象被引用多次,只需要计算一次
if (typeof object[key] === 'object' && object[key] !== null) {
if (seen.has[object[key]]) {
continue;
}
seen.add(object[key]);
}
bytes += calculator(object[key]);
}
return bytes
}
function calculator(object) {
const objectType = typeof object;
switch (objectType) {
case 'string':
return object.length * 2;
case 'boolean':
return 4;
case 'number':
return 8;
case 'object':
if (Array.isArray(object)) {
// 数组累加实现
object.map(calculator)
.reduce((res, current) => res + current, 0);
} else {
return sizeOfObject(object)
}
default:
return 0;
}
}
三. 来聊一下前端HTTP请求相关吧
Q1. 平时怎么解决跨域问题的?
- jsonp
- CORS,服务端设置允许通过的源
- node正向代理,利用服务端不跨域的特性
- Nginx反向代理 proxy_pass
Q2. 全局请求处理(Axios)
有做过全局的请求处理吗?⽐如统一请求并设置登录态, 比如报错统一弹toast?
Axios的request interceptor 和 response interceptor, 单例
Q3. 重写XHR
你能给xhr添加hook, 实现在各个阶段打印日志吗?
大体实现思路:
- 保留原生xhr对象
- 将XMLHttpRequest对象置为新的对象
- 对xhr对象的方法重写后放入新的对象中
- 对属性进行重写 然后放到新的对象中
class XhrHook {
constructor(beforeHooks = {}, afterHooks = {}) {
this.XHR = window.XMLHttpRequest; // 保存原有的xhr
this.beforeHooks = beforeHooks;
this.afterHooks = afterHooks;
this.init()
}
init() {
let _this = this
// 像这样,把XMLHttpRequest赋值为一个新的函数,用户全局访问到的就是我们自己写的新的函数。
window.XMLHttpRequest = function () {
this._xhr = new _this.XHR(); // 在实例上挂一个保留的原生的xhr实例
_this.overwrite(this)
}
}
/**重写方法和属性 */
overwrite(proxyXHR) {
for (let key in proxyXHR._xhr) {
if (typeof proxyXHR._xhr[key] === 'function') {
this.overwriteMethod(key, proxyXHR);
continue;
}
this.overwriteAttributes(key, proxyXHR);
}
}
/**重写方法 */
overwriteMethod(key, proxyXHR) {
let beforeHooks = this.beforeHooks;
let afterHooks = this.afterHooks;
proxyXHR[key] = (...args) => {
// 拦截
if (beforeHooks[key]) {
const res = beforeHooks[key].apply(proxyXHR, args);
if (res === false) {
return;
}
}
// 执行原生xhr实例中对应的方法
const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);
// 原生方法执行完后执行的钩子
afterHooks[key] && afterHooks[key].call(proxyXHR._xhr, res);
return res
}
}
/**重写属性 */
overwriteAttributes(key, proxyXHR) {
Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR));
}
setProperyDescriptor(key, proxyXHR) {
let obj = Object.create(null);
let _this = this
obj.set = function (val) {
// 如果属性不是on开头,直接挂载
if (!key.startsWith('on')) {
proxyXHR['__' + key] = val;
return
}
// 如果是on开头,查看是否有要执行的钩子
if (_this.beforeHooks[key]) {
// 包装一下,先执行钩子,再执行函数
this._xhr[key] = function (...args) {
_this.beforeHooks[key].call(proxyXHR);
val.apply(proxyXHR, args);
}
return;
}
// 如果没有,直接挂载
this._xhr[key] = val;
}
obj.get = function () {
// 返回重写的属性
return proxyXHR['__' + key] || this._xhr[key];
}
return obj;
}
}
// 代码测试
new XhrHook({
open: function () {
console.log('open')
},
onload: function () {
console.log('onload')
},
onreadystatechange: function () {
console.log('onreadystatechange')
},
onerror: function () {
console.log('onerror')
}
})
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.baidu.com', true);
xhr.send();
xhr.onreadystatechange = function (res) {
console.log(res)
}
xhr.onerror = function (e) {
console.log(e)
}
四. Event Bus
Q1. 平时用过发布订阅模式吗?
- ⽐如Vue的event bus, node的event emitter3,
- 这种模式, 事件的触发和回调之间是同步的还是异步的?
Q2.实现⼀个简单的EventEmitter?
代码题,实现eventEmitter, 包含on emit once, off四个⽅方法