Skip to main content

前端知识之原理和应用

JavaScript 工作原理

为什么 JavaScript 是单线程

这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom; 这决定了它只能是单线程,否则会带来很复杂的同步问题。
举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变

扩展
  1. 什么是进程?
    进程:是cpu分配资源的最小单位;(是能拥有资源和独立运行的最小单位)

  2. 什么是线程?
    线程:是cpu调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

  3. 浏览器是多进程的?
    放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。
    为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。 所以,这个新标准并没有改变JavaScript单线程的本质。

宏队列、微队列

JS 中用来存储待执行回调函数的队列包含 2 个不同特定的列队

  • 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调、DOM事件回调、ajax回调
  • 微列队:用来保存待执行的微任务(回调),比如:promise的回调、MutationObserver的回调

JS 执行时会区别这 2 个队列

  • JS 引擎首先必须先执行所有的初始化同步任务代码
  • 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行

产生原因:

  • 竞争资源引起进程死锁
  • 可剥夺和非剥夺资源
  • 竞争非剥夺资源
  • 竞争临时性资源
  • 进程推进顺序不当

产生条件:

  1. 互斥条件:涉及的资源是非共享的
    • 涉及的资源是非共享的,一段时间内某资源只由一个进程占用, 如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
  2. 不剥夺条件:不能强行剥夺进程拥有的资源
    • 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
  3. 请求和保持条件:进程在等待一新资源时继续占有已分配的资源
    • 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链

解决办法:只要打破四个必要条件之一就能有效预防死锁的发生

暂时性死区

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

扩展

var不具有暂时性死区:

  • 当进入 var 变量的作用域(包围它的函数),立即为它创建(绑定)存储空间。变量会立即被初始化并赋值为 undefined
  • 当执行到变量声明的时候,如果变量定义了值则会被赋值。

通过 let 声明的变量拥有暂时性死区,生命周期如下:

  • 当进入 let 变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间。此时变量仍是未初始化的。
  • 获取或设置未初始化的变量将抛出异常 ReferenceError
  • 当执行到变量声明的时候,如果变量定义了值则会被赋值。如果没有定义值,则赋值为 undefined
if (true) { // 进入新的作用域,TDZ开始
// 已创建 tmp, 未初始化
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束, tmp初始化为 undefined
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}

const 工作方式与 let 类似,但是定义的时候必须赋值并且不能改变。在 TDZ 内部,如果获取或设置变量将抛出异常:

死区(dead zone)是真正短暂的(基于时间)和不受空间条件限制(基于位置):

if (true) { // 进入新的作用域,TDZ开始
const func = function () {
console.log(myVar); // OK!
};
// 这里在TDZ中,访问 myVar 将导致 ReferenceError
let myVar = 3 ; // TDZ 结束
func(); // 在 TDZ 外调用
}

注:变量在暂时性死区无法被访问,所以无法对它使用typeof

文件异步上传

普通表单上传

使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表明表单需要上传二进制数据。

<form action="/index.php" method="POST" enctype="multipart/form-data">
<input type="file" name="myfile">
<input type="submit">
</form>

然后编写index.php上传文件接收代码,使用内置方法move_uploaded_file方法即可

$imgName = 'IMG'.time().'.'.str_replace('image/','',$_FILES["myfile"]['type']);
$fileName = 'upload/'.$imgName;
// 移动上传文件至指定upload文件夹下,并根据返回值判断操作是否成功
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $fileName)){
echo $fileName;
} else {
echo "nonn";
}

form表单上传大文件时,很容易遇见服务器超时的问题。通过xhr,前端也可以进行异步上传文件的操作,一般有两种方法。

文件编码上传

第一个思路是将文件进行编码,然后在服务端进行解码。

在服务端需要做的事情也比较简单,首先解码base64,然后保存图片即可。

base64编码的缺点在于其体积比原图片更大(因为Base64将三个字节转化成四个字节,因此编码后的文本,会比原文本大出三分之一左右),对于体积很大的文件来说,上传和解析的时间会明显增加。

除了进行base64编码,还可以在前端直接读取文件内容后以二进制格式上传

// 读取二进制文件
function readBinary(text){
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++) {
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
console.log(ui8a)
}
var reader = new FileReader();
reader.onload = function(){
readBinary(this.result) // 读取result或直接上传
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);

formData异步上传

FormData对象主要用来组装一组用 XMLHttpRequest发送请求的键/值对,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。

let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);

iframe无刷新页面

在低版本的浏览器(如IE)上,xhr是不支持直接上传formdata的,因此只能用form来上传文件,而form提交本身会进行页面跳转,这是因为form表单的target属性导致的,其取值有

  • _self,默认值,在相同的窗口中打开响应页面
  • _blank,在新窗口打开
  • _parent,在父窗口打开
  • _top,在最顶层的窗口打开
  • framename,在指定名字的iframe中打开

如果需要让用户体验异步上传文件的感觉,可以通过framename指定iframe来实现。把formtarget属性设置为一个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,至于返回结果,也可以通过解析这个iframe内的文本来获取。

大文件上传

上面提到的几种上传方式中实现大文件上传会遇见的超时问题:

  • 表单上传和iframe无刷新页面上传,实际上都是通过form标签进行上传文件,这种方式将整个请求完全交给浏览器处理,当上传大文件时,可能会遇见请求超时的情形
  • 通过fromData,其实际也是在xhr中封装一组请求参数,用来模拟表单请求,无法避免大文件上传超时的问题
  • 编码上传,我们可以比较灵活地控制上传的内容

大文件上传最主要的问题就在于:在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。 试想,如果我们将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样是否可以解决大文件上传的问题呢?

综合上面的问题,看来大文件上传需要实现下面几个需求:

  • 支持拆分上传请求(即切片)
  • 支持断点续传
  • 支持显示上传进度和暂停上传

文件切片

编码方式上传中,在前端我们只要先获取文件的二进制内容,然后对其内容进行拆分,最后将每个切片上传到服务端即可。
JavaScript中,文件File对象是Blob对象的子类,Blob对象包含一个重要的方法slice,通过这个方法,我们就可以对二进制文件进行拆分。

拆分文件示例:

/**
* @param file 文件
* @param piece 每一段大小
* @returns {*[]}
*/
function slice(file, piece = 1024 * 1024 * 5) {
let totalSize = file.size; // 文件总大小
let start = 0;
let end = start + piece;
let chunks = []
while (start < totalSize) {
// 根据长度截取每次需要上传的数据
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks;
}

将文件拆分成piece大小的分块,然后每次请求只需要上传这一个部分的分块即可:

let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH); // 首先拆分切片

chunks.forEach( chunk => {
let fd = new FormData();
fd.append("file", chunk);
post('/mkblk.php', fd)
})

服务器接收到这些切片后,再将他们拼接起来就可以了。

测试时记得修改nginxserver配置,否则大文件可能会提示413 Request Entity Too Large的错误。

server {
client_max_body_size 50m;
}

上面这种方式来存在一些问题

  • 无法识别一个切片是属于哪一个切片的,当同时发生多个请求时,追加的文件内容会出错
  • 切片上传接口是异步的,无法保证服务器接收到的切片是按照请求顺序拼接的

因此接下来我们来看看应该如何在服务端还原切片:

还原切片

在后端需要将多个相同文件的切片还原成一个文件,上面这种处理切片的做法存在下面几个问题:

  • 如何识别多个切片是来自于同一个文件的,这个可以在每个切片请求上传递一个相同文件的context参数
  • 如何将多个切片还原成一个文件
  • 确认所有切片都已上传,这个可以通过客户端在切片全部上传后调用mkfile接口来通知服务端进行拼接
  • 找到同一个context下的所有切片,确认每个切片的顺序,这个可以在每个切片上标记一个位置索引值
  • 按顺序拼接切片,还原成文件

上面有一个重要的参数,即context,我们需要获取为一个文件的唯一标识,可以通过下面两种方式获取:

  • 根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
  • 根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大

断点续传

即使将大文件拆分成切片上传,我们仍需等待所有切片上传完毕,在等待过程中,可能发生一系列导致部分切片上传失败的情形,如网络故障、页面关闭等。由于切片未全部上传,因此无法通知服务端合成文件。这种情况下可以通过断点续传来进行处理。

断点续传指的是:可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。

由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:

  • 在切片上传成功后,保存已上传的切片信息
  • 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
  • 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并

因此问题就落在了如何保存已上传切片的信息了,保存一般有两种策略

  • 可以通过localStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
  • 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录

上传进度和暂停

通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。

使用setInterval请求实时数据,返回顺序不一致怎么解决

  1. 使用setTimeout代替setInterval 程序首先设置10秒后发起请求,当数据返回后再隔10秒发起第二次请求,以此类推。这样的话虽然无法保证两次请求之间的时间间隔为固定值,但是可以保证到达数据的顺序。
  2. 使用 WebSocket
补充

WebSocket 协议本质上是一个基于 TCP 的协议。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的HTTP 请求不同,包含了一些附加头信息,其中附加头信息Upgrade: WebSocket表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了 双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

获取当前页面url

  • window.location.href: 设置或获取整个 URL 为字符串
  • window.location.protocol: 设置或获取 URL 的协议部分 https:
  • window.location.host 设置或获取 URL 的主机部分
  • window.location.port 设置或获取与 URL 关联的端口号码
  • window.location.pathname 设置或获取与 URL 的路径部分
  • window.location.search 设置或获取 href 属性中跟在问号后面的部分
  • window.location.hash 设置或获取 href 属性中在 # 后面的分段

例子:

// url: https://mail.163.com/js6/main.jsp?sid=xxx&df=xxx#module=xxx
window.location.href // https://mail.163.com/js6/main.jsp?sid=xxx&df=xxx#module=xxx
window.location.protocol // https:
window.location.host // mail.163.com
window.location.port // ''
window.location.pathname // /js6/main.jsp
window.location.search // ?sid=xxx&df=xxx
window.location.hash // module=xxx

写一个判断是否是空对象的函数

function objIsEmpty(obj) {
return (
obj == null || (obj instanceof 'object' && Object.keys(obj).length === 0)
)
}

颜色值16进制转10进制 rgb

function colorhex2rgb(hex) {
const hexRegExp = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
let match = hex.match(hexRegExp);
// console.log(match)
if(match != null) {
return 'rgb(' + parseInt(match[1], 16) +',' + parseInt(match[2], 16) + ',' +parseInt(match[3], 16) + ')';
} else {
// error
return hex;
}
}

// ===
colorhex2rgb('#232332');

比较

console.log([]==[]); // false
console.log([]== 0); // true

对象的比较并非值的比较, 而是引用的比较, 所以[]!=[]
[]==0,是数组进行了隐式转换,空数组会转换成数字0,所以相等

三数之和

LeetCode - 三数之和

/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
const ans = [];
nums.sort((a, b) => a-b); // 排序,从小到大
for(let i = 0;i < nums.length-2;i++) {
if(i>0 && nums[i]===nums[i-1]) {
// 过滤重复值
continue;
}
if(nums[i] > 0) {
// 最小的都大于0,那就没戏了
break;
}
// 双指针
let left = i+1, right = nums.length-1;
while(left<right) {
if(nums[right] < 0) {
// 最大小的都小于0,那就没戏了
return ans;
}
const sum = nums[i] + nums[left] + nums[right];
if(sum === 0) {
ans.push([nums[i], nums[left], nums[right]]);
while(left<right && nums[left] === nums[left+1]) {
left++;
}
while(right > left && nums[right] === nums[right-1]){
right--;
}
left++;
right--;
} else if(sum < 0){
left++;
} else {
right--;
}
}
}
return ans;
};