一、前端加载优化相关
首屏时间
首次可交互时间
首次有意义内容渲染时间
只请求当前需要的资源
- 异步加载
- 懒加载
- polyfill 比如Array.flat方法的往前面的语法版本转义
(polyfill url-builder)
缩减资源体积
- 打包压缩 webpack 4
- gzip on打开 1.2M-300k
- 图片格式的优化,分辨率和设备适配(根据屏幕分辨率展示不同分辨率的图片),压缩(tinypng),webp的图片
- 尽量控制cookie大小 request header, cookie
时序优化
- js promise.all 并行执行promise的方法(没有相互关联没有依赖关系的请求)
- ssr服务端渲染,某些用户看到的是有相同之处的,通过服务端渲染的时候根据不同的特征渲染出不同的页面做缓存,第二批用户进来的时候就拿缓存了。
- prefetch,prerender,preload
<link rel="dns-prefetch" href="xxxx.com>
DNS与解析
<link rel="preconnect" href="xxxx.com>
预连接
<link rel="preload" as="image" href="xxxx.com/p.png>
更重要的图片预加载
合理利用缓存
- CDN:非常大的流量下,cdn预热(预先分发),cdn刷新(强制回源),douyin.com,cdn-douyin.com(减少消耗)
如果一段js执行时间很长,怎么分析
- 装饰器
- console.time记录开始时间
- 执行原有函数
- 记录函数执行完成的时间
场景题
阿里云oss支持通过链接后面拼接参数来做图片的格式转换,尝试写一下,把任意图片格式转换为webp,需要注意什么?
判断浏览器支不支持格式
- 创建一个canvas元素,能很简单转换成baseurl64
- 转成baseurl64,开头能够带上图片的格式
如果有巨量的图片要展示,除了懒加载的方式,有没有什么其他方法限制一下同时加载图片数量
代码题
实现promise的并发控制
二、前端的内存处理
1、内存的生命周期
- 内存分配:声明变量、函数、对象的时候,js会自动分配内存
- 内存使用:调用的时候,使用的时候
- 内存回收:垃圾回收机制
2、js中的垃圾回收机制
垃圾回收就是当你的内存使用完成了之后,如果使用垃圾回收机制判断内存没有使用了,就会把这块内存权限收回来,不会给原来的变量了
引用计数垃圾回收
a对象对b对象有访问权限,那么成为a引用b对象。
如果没有任何对象引用b对象,就会回收。
- 缺陷:循环引用,a引用了b,b引用了a,但都没有被其他对象引用,就会内存泄漏
标记清除算法
无法拿到的对象,从根对象进行扫描,如果无法达到了,就说明需要进行回收了
- 在运行的时候给存储在内存的所有变量加上标记
- 从根部触发,能触及的对象,把标记清除
- 哪些有标记的就被视为即将要删除的变量
- 内存回收操作
js中,常见的内存泄漏
全局变量
window.a === ‘www’
window.a === null
未被清除的定时器和回调
要清除
闭包
内部函数有权访问包含它的函数的变量
DOM引用
const elements = {
image: document.getElementById('image')
}
//即使移除了 之前elements的引用还是存在
document.body.removeChild(document.getElementById('image'))
elements.image = null
4、避免内存泄漏
- 减少不必要的全局变量
- 使用完数据后,及时解除引用
实现sizeOf函数,传入一个参数object,计算这个object占用了多少bytes
number: 64位存储,8字节 byte
string: 每个长度2字节
boolean:4字节
三、前端HTTP请求相关
1、平时解决跨域问题的
- jsonp
- cors 服务端或者nginx设置的头,允许访问的可行域和方法
- node 正向代理, /api -> 同域的node服务 -> /api -> 前端
- nginx 反向代理,proxy_pass /api -> /same/api
- img标签
2、有做过全局的请求处理吗?比如统一处理登陆态?统一处理全局错误?
- axios 适配器 拦截器
3、代码题,给xhr添加hook,实现在各个阶段打日志?
new XMLHTTPRequest()
open onreadystatechange onload onerror 打日志
110 32
大体实现思路:
保留原生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
-
on 监听
-
emit 传入参数、触发属性
-
off 移除监听的回调
-
once 只触发一次
除了emit以外的参数都是event和callback
on 监听
- 如果没有事件的话,处理成数组类型
- 给event push一个callback
- return this 为了链式调用
off 移除监听
- 如果没callback 就移除—null
- 如果有,就把传进来的event给去掉,filter
- return this 为了链式调用
once
- 执行一次的方法然后取消掉
- 移除掉事件监听 然后再执行callback
emit
- 默认是一个数组
- 如果没有
- 如果有 就遍历调用
要设置最大监听数呢???
直接在on里面拦截最大监听
- 如果不等于初始值,并且大于等于最大监听数,就。。。。return掉this
class EventEmitter {
constructor(){
this.events = {}; // new Map()
//可以设置最大监听数
this.maxListeners = maxListeners || Infinity;
}
emit(event, ...args) {
const cbs = this.events[event];
if(!cbs){
console.log("没有这个事件");
return this;
}
cbs.forEach(cb => cb.apply(this, args));
return this;
}
on(event, cb) {
if (!this.events[event]) {
this.events[event] = [];
}
//TODO:拦截最大监听
if (this.maxListeners !== Infinity && this.events[event].length >= this.maxListeners){
console.warn(`当前事件${event}超过最大监听数`);
return this;
}
this.events[event].push(cb);
return this;
}
once(event, cb) {
const func = (...args) => {
this.off(event, func);
cb.apply(this, args);
}
this.on(event, func);
return this;
}
off(event, cb) {
if (!cb) {
this.events[event] = null; // 移除监听
} else {
this.events[event] = this.events[event].filter(item => item!==cb);
}
return this; // 如果要链式调用 比如 event.on('add', add).emit()
}
}
const add = (a, b) => console.log(a + b);
const log = (...args) => console.log(...args);
const event = new EventEmitter();
event.on('add', add);
event.on('log', log);
event.emit('add', 1, 2); // 3
event.emit('log', "hi~"); // hi~
event.off('add');
event.emit('add', 1, 2); // Error add event is not registered
event.once('once', add);
event.emit('once', 1, 2); // 3
event.emit('once', 1, 2);
event.emit('once', 1, 2);
五、01背包
01背包问题
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性,其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
举个简单的例子,输入如下:
N = 3(3个物品) , W= 4(背包用量4)
wt = [2, 1, 3]
val = [4, 2, 3]
算法返回6,选择前两件物品装进背包,总重量3小于W,可以获得最大价值6
动态规划解题思路
一、状态和选择
可选物品
背包的容量限制
状态有2个
1、可选择的物品
2、背包的容量
所以选择有两个
1、装进背包
2、不装进背包
for 状态1 in 状态1的所有取值
for 状态2 in 状态2的所有取值
dp[状态1][状态2] = 选择(选择1,选择2)
二、明确dp数组的定义
状态有两个,二维dp数组
dp[i][w] = 对于前i个物品 当前背包的容量为w 这种情况下可以装的最大价值是dp[i][w]
dp[0][w] = 0
dp[i][0] = 0
dp[3][5] = 6,对于所有物品,如果只对前三个进行选择,当背包容量为5的时候,最多能装下的价值是6
let dp[N+1][W+1]
dp[0][xxxx] = 0;
dp[xxx][0] = 0;
for 状态1 in 状态1的所有取值
for 状态2 in 状态2的所有取值
dp[状态1][状态2] = 选择(把物品i装进背包, 不装进背包)
return dp[N][W]
三、状态转移方程怎么写?
dp[i][w] 对于前i个物品,当前背包的容量为w,这种情况下可以装下的最大价值是dp[i][w]
1、如果没有把第i个物品装进背包
dp[i][w] = dp[i-1][w]
2、如果把第i个物品装进背包了
第i个物品的价值是val[i-1]
第i个物品的重量是wt[i-1]
dp[i][w] = dp[i-1][w - wt[i-1]] + val[i-1];
let dp[N+1][W+1]
dp[0][xxxx] = 0;
dp[xxx][0] = 0;
for i in [i...N]
for w in [1...W]
dp[i][w] = Math.max(dp[i-1][w],dp[i-1][w-wt[i-1] + val[i-1]);
return dp[N][W]