-
JS任务执行循序
1,同步
2,异步
3,微任务
常见微任务:nextTick,Promise,observe
4,宏任务
常见宏任务:setTimeout,setInterval -
浏览器缓存
cookie | sessionStorage(本地存储) | localStorage(本地存储) | |
---|---|---|---|
生命周期 | 可以自己设置 | 页面关闭就结束 | 除非自己删除,否者一直纯在 |
储存类型 | 只能存储字符串 | 只能存储字符串 | 只能存储字符串 |
大小 | <4KB | 4MB | 4MB |
保存地址 | 客户端 | 客户端 | 客户端 |
安全性 | 弱 | 强 | 强 |
是否跨域 | 是 | 否 | 是 |
- 强缓存与协议缓存
强缓存 | 协议缓存 | |
---|---|---|
概念 | 强制缓存是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力。 | 协商缓存是浏览器与服务器之间进行通信以确认缓存资源是否仍然有效的过程。 |
原理 | 强制缓存的工作原理是通过HTTP响应头中的特定字段来控制的。这些字段通常包括Expires和Cache-Control,它们指示了资源的缓存有效时间。当浏览器在有效时间内再次请求同一资源时,它会直接从本地缓存中获取该资源,而不会向服务器发送请求。 | 协商缓存主要涉及两组HTTP头字段:ETag和If-None-Match,以及Last-Modified和If-Modified-Since。 |
参数一 | Expires:在HTTP 1.0版本中,通过Expires响应头来实现强制缓存。Expires表示未来资源会过期的时间点。如果当前时间超过了Expires设定的时间,资源缓存时间到期,浏览器会重新向服务器请求资源。 | ETag/If-None-Match:当浏览器第一次请求某个资源时,服务器会返回一个ETag(实体标签),它是一个资源版本的唯一标识符。浏览器在后续请求该资源时,会在请求头中携带If-None-Match字段,其值为先前接收到的ETag。服务器会根据这个值来判断资源是否有更新。如果有更新,服务器会返回新的资源和新的ETag;如果没有更新,服务器会返回304 Not Modified状态码,告诉浏览器可以使用缓存中的资源。 |
参数二 | Cache-Control:在HTTP 1.1版本中,引入了Cache-Control响应头,它提供了更灵活的缓存控制机制。例如,可以通过max-age参数设置缓存的最大生存时间(以秒为单位),如Cache-Control: max-age=1200表示缓存有效时间为1200秒。 | Last-Modified/If-Modified-Since:类似于ETag机制,但Last-Modified记录的是资源最后修改的时间。浏览器在后续请求时,会在请求头中携带If-Modified-Since字段,其值为先前接收到的Last-Modified时间。服务器会检查资源的最后修改时间是否在这个时间之后。如果是,说明资源有更新,服务器会返回新资源和新的Last-Modified时间;如果不是,服务器同样会返回304 Not Modified状态码。 |
- 深拷贝&浅拷贝
实现方法:JSON.parse(),JSON.parse(),Object.assign(),Array.prototype.slice(),展开运算符 …,递归等等。
深拷贝 | 浅拷贝 | |
---|---|---|
区别 | 不会影响到彼此,是独立的个体 | 双方不是独立的,还会互相影响 |
- Promise的概念
1,Promise是一个对象或者是构造函数,用来封装异步操作并获得成功或失败的结果。
2,Promise三种状态
pending:等待状态(初始状态)
resolved:成功状态
rejected:失败状态
new Promise((resolve, reject) => {
console.log('new Promise同步代码');
if () {
resolve('改为修改状态')
} else {
reject('改为失败状态')
}
})
3,Promise的静态方法
Promise.all():处理异步任务的并发场景,全部成功才能成功,有一个失败则返回失败结果
function fn1(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn1异步的结果:' + num)
}, 50)
})
return p
}
function fn2(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn2异步的结果:' + num)
// reject('fn2异步的结果:' + num) //有一个失败
}, 50)
})
return p
}
function fn3(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn3异步的结果:' + num)
}, 50)
})
return p
}
Promise.all([fn1(1), fn2(2), fn3(3)]).then(res => {
console.log('res:' + res);//全部成功返回res:fn1异步的结果:1,fn2异步的结果:2,fn3异步的结果:3
}).catch(err=>{
console.log('err:'+err);//一个失败则返回err:fn2异步的结果:2
})
Promise.race():并发处理多个异步任务,只要有一个任务完成就能获得到结果(all方法的补充)
function fn1(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn1异步的结果:' + num)
}, 2000)
})
return p
}
function fn2(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn2异步的结果:' + num)
}, 1000)
})
return p
}
Promise.race([fn1(1), fn2(2)]).then(res => {
console.log('res:' + res); //res:fn2异步的结果:2
})
Promise.any():并发处理多个异步任务,只要有一个任务执行成功就得到结果(all方法的补充)
function fn1(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn1异步的结果:' + num)
}, 2000)
})
return p
}
function fn2(num) {
var p = new Promise((resolved, reject) => {
setTimeout(() => {
resolved('fn2异步的结果:' + num)
}, 1000)
})
return p
}
Promise.any([fn1(1), fn2(2)]).then(res => {
console.log('res:' + res); //res:fn2异步的结果:2
})
- 原型和原型链的理解
原型 | 原型链 | |
---|---|---|
概念 | 原型是一个对象,是函数的一个属性prototype,通过该函数实例化出来的对象都可以继承得到原型上的所有属性和方法,原型对象默认有一个属性constructor ,值为对应的构造函数;另外,有一个属性__proto__,值为Object.prototype | 在JavaScript中万物都是对象,对象和对象之间并不是独立存在的,对象和对象之间有一定关系,通过对象__proto__属性指向函数的原型对象(函数.prototype)一层一层往上找,直到找到Object的原型对象(Object.prototype)为止,层层继承的链接结构叫做原型链(通过proto属性形成原型的链式结构,专业术语叫做原型链) |
- 切片上传
<script>
const SIZE = 10 * 1024 * 1024; // 切片大小
export default {
data: () => ({
// 存放文件信息
container: {
file: null
hash: null
},
data: [] // 用于存放加工好的文件切片列表
hashPercentage: 0 // 存放hash生成进度
}),
methods: {
// 获取上传文件
handleFileChange(e) {
const [file] = e.target.files;
if (!file) {
this.container.file = null;
return;
}
this.container.file = file;
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 生成文件hash
calculateHash(fileChunkList) {
return new Promise(resolve => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
// 可以用来显示进度条
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// hash生成
this.container.hash = await this.calculateHash(fileChunkList);
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks();
}
// 上传切片
async uploadChunks() {
const requestList = this.data
// 构造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 发送请求 上传切片
.map(async ({ formData }) =>
request(formData)
);
await Promise.all(requestList); // 等待全部切片上传完毕
await merge(this.container.file.name) // 发送请求合并文件
},
}
};
</script>
生成hash
无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则
这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互
由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts`函数用于导入外部脚本,通过它导入 spark-md5
// /public/hash.js
self.importScripts("/spark-md5.min.js"); // 导入脚本
// 生成文件 hash
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
// 新建读取器
const reader = new FileReader();
// 设定读取数据格式并开始读取
reader.readAsArrayBuffer(fileChunkList[index].file);
// 监听读取完成
reader.onload = e => {
count++;
// 获取读取结果并交给spark计算hash
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
// 获取最终hash
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
// 递归计算下一个切片
loadNext(count);
}
};
};
loadNext(0);
};
文件秒传
实际是障眼法,用来欺骗用户的。
原理:在文件上传之前先计算出文件的hash,然后发送给后端进行验证,看后端是否存在这个hash,如果存在,则证明这个文件上传过,则直接提示用户秒传成功
// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// hash生成
this.container.hash = await this.calculateHash(fileChunkList);
// hash验证 (verify为后端验证接口请求)
const { haveExisetd } = await verify(this.container.hash)
// 判断
if(haveExisetd) {
this.$message.success("秒传:上传成功")
return
}
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks();
}
暂停上传
原理:将所有的切片存在一个数组中,每当一个切片上传完毕,从数组中移除,这样就可以实现用一个数组只保存上传中的文件。此外,因为要暂停上传,所以需要中断请求 axios中断请求可以利用AbortController
const controller = new AbortController()
axios({
signal: controller.signal
}).then(() => {});
// 取消请求
controller.abort()
添加暂停上传功能
// 上传切片
async uploadChunks() {
// 需要把requestList放到全局,因为要通过操控requestList来实现中断
this.requestList = this.data
// 构造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 发送请求 上传切片
.map(async ({ formData }, index) =>
request(formData).then(() => {
// 将请求成功的请求剥离出requestList
this.requestList.splice(index, 1)
})
);
await Promise.all(this.requestList); // 等待全部切片上传完毕
await merge(this.container.file.name) // 发送请求合并文件
},
// 暂停上传
handlePause() {
this.requestList.forEach((req) => {
// 为每个请求新建一个AbortController实例
const controller = new AbortController();
req.signal = controller.signal
controller.abort()
})
}
恢复上传
原理:上传切片之前,向后台发送请求,接口将已上传的切片列表返回,通过切片hash将后台已存在的切片过滤,只上传未存在的切片
// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// 文件hash生成
this.container.hash = await this.calculateHash(fileChunkList);
// hash验证 (verify为后端验证接口请求)
const { haveExisetd, uploadedList } = await verify(this.container.hash)
// 判断
if(haveExisetd) {
this.$message.success("秒传:上传成功")
return
}
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 注:这个是切片hash 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks(uploadedList);
}
// 上传切片
async uploadChunks(uploadedList = []) {
// 需要把requestList放到全局,因为要通过操控requestList来实现中断
this.requestList = this.data
// 过滤出来未上传的切片
.filter(({ hash }) => !uploadedList.includes(hash))
// 构造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 发送请求 上传切片
.map(async ({ formData }, index) =>
request(formData).then(() => {
// 将请求成功的请求剥离出requestList
this.requestList.splice(index, 1)
})
);
await Promise.all(this.requestList); // 等待全部切片上传完毕
// 合并之前添加一层验证 验证全部切片传送完毕
if(uploadedList.length + this.requestList.length == this.data.length){
await merge(this.container.file.name) // 发送请求合并文件
}
},
// 暂停上传
handlePause() {
this.requestList.forEach((req) => {
// 为每个请求新建一个AbortController实例
const controller = new AbortController();
req.signal = controller.signal
controller.abort()
})
}
// 恢复上传
async handleRecovery() {
//获取已上传切片列表 (verify为后端验证接口请求)
const { uploadedList } = await verify(this.container.hash)
await uploadChunks(uploadedList)
}
- JS中检测数据类型的有哪些
typeof | instanceof | constructor | object.prototype.toString.call( ): |
---|---|---|---|
常用于判断基本数据类型,除了 null 检测为 object。对于引用数据类型除了 function 返回 function,其余全部返回 object | 主要用于检测引用数据类型,不适合用来检测基本数据类型。如果检测的类型在当前实例的原型链上,则返回 true,说明这个实例属于这个类型,否则返回 false。例如: A instanceof B,**判断 B 在不在 A 的原型链上,如果在就返回 true,如果找到原型链的尽头 null 都没找到,就返回 false。(由于原型链的指向可以随意改动,导致检测不准确) | 获取实例的构造函数判断和某个类型是否相同,如果相同就说明该数据是符合那个数据类型的。使用方法是:“实例.constructor”。constructor 可以检测出除了 undefined 和 null 以外的其他类型,因为 undefined 和 null 没有原生构造函数。(不可靠,容易被修改) | 适用于所有类型的判断检测,检测方法是: Object.prototype.toString.call(数据) ,返回的是该数据类型的字符串 |
- JS中的栈和堆是什么
栈 | 堆 | |
---|---|---|
栈是一种先进后出的数据解构,由操作系统自动分配内存空间,自动释放,占固定的大小空间。栈存储的是基本数据类型的值以及引用数据类型的引用地址。栈中存储的数据的生命周期随着当前环境的执行完成而结束。 | 堆由操作系统动态分配内存空间,大小不定也不会自动释放,一般由程序员分配释放也可由垃圾回收机制回收。栈存储的是对象和复杂数据结构,存储的是对象的实际数据,而不是对象的引用。引用数据类型只有在引用的它的变量不在时,被垃圾回收机制回收。 | |
优缺点 | 栈相对于堆存取速度更快,且栈内存中数据是可以共享的,但内存空间有限。栈内存可以及时得到回收,相对来说更容易管理内存空间,但存储在栈中的数据大小和生存期必须是确定的,缺乏灵活性。 | 堆存取效率相对较低,但内存空间大。堆的内存是操作系统动态分配的,方便存储和开辟内存空间。有垃圾回收机制,生存周期比较灵活。 |
栈和堆的区别
一、堆栈空间分配区别:
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
堆(操作系统): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
二、堆栈缓存方式区别:
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
三、堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
- JS 实现SSE通讯
一,什么是SSE
Server-Sent Events(SSE)是一种用于实现服务器向客户端实时推送数据的Web技术。与传统的轮询和长轮询相比,SSE提供了更高效和实时的数据推送机制
SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。
二,SSE 和 WebSocket 的差异
SSE 适用于服务器向客户端单向发送实时更新的数据,适合实时事件推送场景。SSE 使用的是标准的 HTTP 协议,对于浏览器的兼容性较好,但只支持客户端接收数据。
WebSocket 适用于客户端和服务器之间的双向实时通信,适合聊天应用、实时游戏等场景。WebSocket 需要独立的 TCP 连接,因此相比 SSE,会增加一定的网络开销,但能够实现双向通信。
(核心代码)客户端请求:
const source = new EventSource("http://localhost:9001/sse")
source.onmessage = function(event) {
console.log(event.data)
document.getElementById("content").innerHTML += event.data
}
source.onerror = function(event) {
console.log("EventSource failed:", event)
}
source.onopen = function(event) {
console.log("EventSource connected.")
}
// 关闭sse连接
// source.close()
服务器端:
const express = require('express')
const app = express()
const fs = require('fs')
//创建get请求
app.get('/sse', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Connection', 'close')
res.setHeader('Access-Control-Allow-Origin', '*')
const data = fs.readFileSync('sse.txt', 'utf-8')
const length = data.length
console.log(length)
let i = 0;
var interval = setInterval(() => {
console.log(i, length)
if(i >= length) {
console.log('end')
clearInterval(interval);
return;
}
res.write(`data:${data.split('')[i]}\n\n`)
i++;
}, 500);
})
//端口号9001
app.listen(9001)
console.log('server is running at http://localhost:9001')