douc1998 / interview_notes Goto Github PK
View Code? Open in Web Editor NEWInterview_Notes
Interview_Notes
当我们使用 JS 对 DOM 的修改引发了 DOM 的增删、几何尺寸变化、位置变化或浏览器窗口改变等情况,浏览器就需要重新计算元素的几何属性(因为一个元素改变很可能也引起其他元素的改变,因此也要计算其他元素),然后将计算的结果绘制出来。这个过程就叫回流,也叫重排。
当我们修改了 DOM 元素的样式(比如修改了背景颜色),但是并没有改变到它的几何属性、增删、位置变化等等,只是改变了样式。浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式。这个过程叫重绘。
回流一定会引起重绘,重绘不一定会引发回流。回流的操作成本比重绘高得多,因此我们除了在不得以的情况下,尽量避免回流。可以参考以下建议:
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
浏览器解析遇到 CSS 样式资源时,CSS 会异步下载,不会阻塞浏览器构建 DOM 树,但是会阻塞渲染。因为,在构建渲染树之前,会先等 CSS 下载并解析完毕才进行。(因为渲染会涉及到 CSS 样式,如果没有解析完 CSS 资源就渲染,会出问题)。
浏览器解析遇到 JS 脚本资源时,需要等待 JS 脚本资源下载并执行完毕之后,才会继续解析 HTML,因为 JS 会修改 DOM 元素。。(defer
和 async
标识的脚本除外)
CSS 加载会阻塞后面的 JS 语句的执行。H5 标准中规定,浏览器在执行 Script
脚本前,必须保证当前的外联 CSS 已经解析完成。因为 JS 可能会去获取或变更 DOM 得 CSS 样式。如果 CSS 没有解析好,那加载的结果就是有问题的。
解析遇到 Img 图片,直接异步下载,不会影响其他解析。
外联 CSS 无论放在哪里都不会阻塞 HTML 的解析,但是会影响 HTML 渲染。如果 CSS 放在头部,那么就可以和 HTML 并行解析,当两者都解析完毕时,构建渲染树进行渲染。
然而,如果 CSS 放在尾部,就会导致一系列的阻塞问题:
因为当浏览器解析 Script 时,就会立即下载并执行,中断 HTML 的解析过程。如果下载解析外部脚本时间太长,就会导致页面长时间未响应。
HTTP 应答状态码:
状态码 | 类别 | 描述 |
---|---|---|
1xx | Informational(信息性状态码) | 请求正在被处理 |
2xx | Success(成功状态码) | 请求处理成功 |
3xx | Redirection(重定向状态码) | 需要进行重定向 |
4xx | Client Error(客户端状态码) | 服务器无法处理请求 |
5xx | Server Error(服务端状态码) | 服务器处理请求时出错 |
HTTP 常见应答状态码:
状态码 | 描述 |
---|---|
200 | 客户端请求成功 |
204 | 请求处理成功,但响应体为空,即没有资源返回。一般用于只需从客户端往服务器发送信息。 |
206 | 表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求,实现断点续传或同时分片下载。响应报文中包含由 Content-Range 指定范围的实体内容。 |
301 | (永久重定向)请求的资源已被永久分配了新的 URI,以后应该永久使用资源现在所指的 URI 进行访问。 |
302 | (临时重定向)请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。 |
303 | 303 状态码和 302 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源。 |
304 | 客户端发送附带条件的请求时,服务器端允许请求访问资源,但请求资源未修改,可以使用缓存的资源,不用在服务器取,则返回 304。该相应不包含响应的主体部分(即可直接使用缓存) |
400 | 请求报文中存在语法错误 |
401 | 发送的请求需要 HTTP 认证 |
403 | 请求的资源禁止被访问。 |
404 | 服务器上无法找到请求的资源。 |
405 | 客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。 |
500 | 服务器内部错误。 |
503 | 服务器正忙,处于超负荷或维护状态,无法请求。 |
参考:
具有代表性的 HTTP 状态码
/**
* 米哈游笔试;连通块
* 一个 n x m 的矩阵中有 R G B 字母,每个格子里的值可能是三者之一。一个 + 号形状可以视为四连通。
* 也就是一个格子上/下/左/右边的格子和它里面字母一样,认为是一个连通块。
* 但是米小游是一个色盲,G 和 B 分不清,所以可以把含有 G 和 B 的格子都忽略掉,去寻找矩阵里 R 字母组成的连通块个数。
* 返回的结果是:矩阵中正确的连通块个数 - 米小游看到的连通块个数。
* 举例:
* R R G G B B
* R G B G R R
* 正确的连通块个数是:6(RR\RR\GG\GG\BB\RR)
* 米小游看到的连通块个数是: 3(RR\RR\RR)
* 返回的答案是: 6 - 3 = 3
*/
function getNum(matrix, n, m) {
// 记录每个位置的状态
const state = Array(n).fill().map(_ => Array(m).fill({
top: false,
right: false,
bottom: false,
left: false
}))
// 记录连通块个数
let num = 0;
// 遍历二维矩阵
for (let r = 0; r < n; r++) {
for (let c = 0; c < m; c++) {
// 上,判断有没有算过
if (r - 1 >= 0 && matrix[r][c] === matrix[r - 1][c] && !state[r - 1][c].bottom) {
num++;
}
// 右,不需要判断有没有算过,但是要设置 right 值,防止右边的数再算一次
if (c + 1 < m && matrix[r][c] === matrix[r][c + 1]) {
num++;
state[r][c].right = true;
}
// 下,不需要判断有没有算过,但是要设置 bottom 值,防止下边的数再算一次
if (r + 1 < n && matrix[r][c] === matrix[r + 1][c]) {
num++;
state[r][c].bottom = true;
}
// 左,判断有没有算过
if (c - 1 >= 0 && matrix[r][c] === matrix[r][c - 1] && !state[r][c - 1].right) {
num++;
}
}
}
return num;
}
const matrix = [['R', 'G', 'G', 'G', 'B', 'B'],
['R', 'G', 'B', 'G', 'R', 'R']]
let rightMatrix = Array(matrix.length).fill().map(_ => Array(matrix[0].length));
let wrongMatrix = Array(matrix.length).fill().map(_ => Array(matrix[0].length));
for(let i = 0; i < matrix.length; i++){
for(let j = 0; j < matrix[0].length; j++){
rightMatrix[i][j] = matrix[i][j];
// 把 G B 设置为随机数,大概率是不相等的,也可以设置为其他不等的符号。
wrongMatrix[i][j] = matrix[i][j] === 'R' ? matrix[i][j] : Math.random()
}
}
let rightNum = getNum(rightMatrix, matrix.length, matrix[0].length);
let wrongNum = getNum(wrongMatrix, matrix.length, matrix[0].length);
console.log(`正确的连通块个数为:${rightNum}\n米小游看到的连通块个数为:${wrongNum}\n答案为:${rightNum - wrongNum}`);
// 手撕 LRU (最久未使用)缓存替换策略
// capcacity 表示缓存容量,使用 map 来存储缓存的 key - value
var LRUCache = function(capacity) {
this.map = new Map();
this.capacity = capacity;
};
// 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
// 思路:每 get 一次就重新插入到 map 尾部,表示是最近使用过的
LRUCache.prototype.get = function(key) {
if(this.map.has(key)){
let value = this.map.get(key);
this.map.delete(key); // 删除后,再 set ,相当于更新到 map 最后一位
this.map.set(key, value);
return value;
} else {
return -1
}
};
// 如果关键字 key 已经存在,则变更其数据值 value ;
// 如果不存在,则向缓存中插入该组 key-value 。
// 如果插入操作导致关键字数量超过 capacity ,则应逐出 最久未使用 的 key。
LRUCache.prototype.put = function(key, value) {
// 如果已有,那就要更新,即要先删了再进行后面的 set
if(this.map.has(key)){
this.map.delete(key);
}
this.map.set(key, value);
// put 后判断是否超载,如果超载,就要删除最久未使用的 key-value, 就是第一个
if(this.map.size > this.capacity){
this.map.delete(Array.from(this.map.keys())[0]);
// this.map.delete(this.map.keys().next().value); // 或者直接用 next 来调用迭代器 iterator
}
};
1、使用前向纠错。前向纠错技术可以在发送方添加一些冗余数据,使接收方能够在接收到部分数据包时也能恢复丢失的数据。
2、重传机制。当出现丢包时,重新传输对应的包数据。
3、流量控制。根据网络情况限制发送端的发送速率,可以减少网络拥塞导致的丢包问题。
4、使用确认机制。尽管UDP本身没有确认机制,但你可以在应用层添加确认机制。例如,在发送数据时,要求接收方发送确认消息,以确保数据的完整性和正确性。
5、增加接收缓冲区大小。通过增加接收缓冲区的大小,可以减少丢包的可能性。
6、数据包重排列。给数据包添加 index 属性,接收端将接收到的数据包按照 index 排序,保证数据包按照正确顺序提交给应用层处理。
const data = [
{'name': 'jack', 'age': 24, 'home': '杭州'},
{'name': 'mike', 'age': 17, 'home': '上海'},
{'name': 'lucy', 'age': 22, 'home': '杭州'},
{'name': 'john', 'age': 25, 'home': '上海'}
]
query(data).select(item => item.age >= 18).orderBy('age').groupBy('home').execute();
// 实现当传入 data 之后,根据一些筛选、排序、组合的方法,输出以下结果
// [
// [
// {'name': 'lucy', 'age': 22, 'home': '杭州'},
// {'name': 'jack', 'age': 24, 'home': '杭州'}
// ],
// [
// {'name': 'john', 'age': 25, 'home': '上海'}
// ]
// ]
// 因为没有使用到 new,因此不能使用 类 或者 构造函数+原型 的方法,只能使用 闭包+返回对象
// 因为是链式调用,因此每次函数执行返回都应该是 this,指向 query返回的对象
function query(data){
let thisData = data; // 存数据
return {
select: function(fn){ // 通过 filter 筛选数据
thisData = thisData.filter(fn);
return this;
},
orderBy: function(key){ // sort 排序数据
thisData.sort((a, b) => a[key] - b[key]);
return this;
},
groupBy: function(key){ // 组织数据
let newData = [];
let myObj = {};
for(const item of thisData){
if(item[key] in myObj){
myObj[item[key]].push(item);
}else{
myObj[item[key]] = [item];
}
}
for(const key in myObj){
newData.push(myObj[key]);
}
thisData = newData;
return this;
},
execute: function(){ // 输出数据
console.log(thisData);
}
}
}
请求方法 | 描述 |
---|---|
GET | 请求资源 |
POST | 浏览器向服务器提交数据,一般会造成服务器的资源修改 |
HEAD | 类似 GET,但仅要求服务器返回头部信息 |
PUT | 上传资源用于更新 |
PATCH | 对 PUT 补充,对已知资源部分更新 |
DELETE | 删除某个资源 |
TRACE | 追踪请求/响应路径,用于测试或诊断 |
CONNECT | 将连接改为管道方式,用于代理服务器 |
OPTION | 查询服务器支持的请求方法,常用来跨域请求 |
/**
* 百度面试算法题
* 实现字符串的全排列:
* 'abcd' -> [abcd, abdc, acbd, acdb ...]
*/
function getResult(str){
// 先切分字符串
const arr = str.split('');
// 回溯,其实也可以添加一个参数,用来存储没被加入的数字。我这里是用时间复杂度换空间复杂度
function trackBack(res, path){
// 判断结束条件
if(path.length === arr.length){
res.push([...path].join(''));
return;
}
// 单层逻辑
for(let i = 0; i < arr.length; i++){
if(path.includes(arr[i])) continue; // 全排列不允许有重复
path.push(arr[i]);
trackBack(res, path); // 递归
path.pop(); // 回溯
}
}
let res = [], path = [];
trackBack(res, path);
return res;
}
// Test
console.log(getResult('abcd'));
// [
// 'abcd', 'abdc', 'acbd',
// 'acdb', 'adbc', 'adcb',
// 'bacd', 'badc', 'bcad',
// 'bcda', 'bdac', 'bdca',
// 'cabd', 'cadb', 'cbad',
// 'cbda', 'cdab', 'cdba',
// 'dabc', 'dacb', 'dbac',
// 'dbca', 'dcab', 'dcba'
// ]
编译链接过程不仅存在于我们前端了解的 webpack
中,在任何需要编译打包程序的过程中,都需要使用到。这里简单介绍两种链接方式:静态链接和动态链接。
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件。然而多个源文件之间不是独立的,它们之间会存在多种依赖关系,如:在源文件 A 中需要调用源文件 B 中定义的函数,但是在编译过程中每个源文件都是独立编译的,因此为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。
简而言之,静态链接就是在形成可执行程序之前对具有依赖关系的源文件进行链接,静态库也可以被认为是一系列存在关系的文件的集合。
优点:
缺点:
浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf() 函数,则这多个程序中都含有该函数所在的源文件副本。
更新比较困难。因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
动态链接就是为了解决静态链接浪费时间、更新困难等问题而存在的。动态链接的基本**是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
举个例子,假设程序 A 依赖某个源文件 C,程序 B 也依赖源文件 C。系统首先加载程序 A,当系统发现程序 A 中用到了文件 C 的函数,那么系统接着加载文件 C,如果程序 A 和 文件 C 还依赖于其他目标文件,则依次全部加载到内存中。当程序 B 运行时,系统发现它依赖于文件 C,但是此时 C 已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的 C 映射到 B 的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。
优点:
节省存储空间。即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多个副本,而是这多个程序在执行时共享同一份副本。
更新便捷。更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
缺点:
线程的实现可以分两类:用户级线程、内核级线程(或混合式线程)。
用户级线程(也称为用户态线程)是指:不需要内核支持而在用户程序中实现的线程,它的内核切换是由用户态程序自己控制内核的切换,不需要内核的干涉。但是它不能像内核级线程一样更好的运用多核CPU。
优点:
线程的调度不需要内核直接参与,控制简单。
可以在不支持线程的操作系统中实现。
同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起,可以节约更多的系统资源。
缺点:
一个用户级线程的阻塞将会引起整个进程的阻塞。
用户级线程不能利用系统的多重处理,仅有一个用户级线程可以被执行。
内核级线程(又称内核态线程)切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态。可以很好的运用多核CPU,例如电脑的四核八线程,双核四线程等。
优点:
当有多个处理机时,一个进程的多个线程可以同时执行。
由于内核级线程只有很小的数据结构和堆栈,切换速度快,当然它本身也可以用多线程技术实现,提高系统的运行速率。
缺点:
相同点:
不同点:
内核线程是由操作系统内核创建、管理、切换、调度的,而用户线程是由应用程序创建、管理、切换、调度的。
内核线程运行在内核态,可以访问操作系统的所有资源,而用户线程运行在用户态,只能访问应用程序的资源。
内核线程可以执行任何操作系统提供的服务,如文件系统、网络等,而用户线程只能执行应用程序提供的服务。
用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行,用户线程无法并发,同一进程中只能同时有一个线程在运行。而内核线程的调度则以线程为单位,由 OS 的线程调度程序负责线程的调度。
浏览器网页维护安全的主要方法就是:同源策略限制。
同源策略指的是我们访问站点的:协议、域名、端口号 一致才叫同源,有一个不一样,都会被认为是跨源(跨域)。
浏览器默认同源站点之间是可以互相访问资源和操作 DOM 的,而不同元源之间想要互相访问资源或者操作 DOM 的话,就需要添加一些安全策略的限制。如下:
当然,同源策略也不是绝对隔离不同源的站点,比如 link
、img
、script
标签都没有跨域限制,这也导致了一些安全问题。如 XSS 攻击和 CSRF 攻击
XSS 叫跨站脚本攻击,原本应该叫 CSS,但是为了和层叠样式区别开,叫 XSS。XSS 攻击是一种代码注入攻击,通过恶意注入 JS 脚本在浏览器运行,然后调取用户信息。
造成 XSS 攻击本质上还是因为网站没有过滤恶意代码。当恶意代码混入正常代码中一起执行时,浏览器没有办法分辨哪些是可信的,然后导致恶意代码也被执行。引起的危害有:
Cookie
、LocalStorage
、DOM
等。addEventListener
来监听键盘事件,获取用户密码。XSS 攻击有三种类型:存储型、反射型、DOM 型。
存储型
存储型 XSS 攻击主要是将恶意脚本存储到服务端,当读取到该恶意脚本时,浏览器就会识别为一段 JS 代码来执行。比如在评论区,有人写了一段恶意脚本并提交,恶意脚本就会被服务器存储到数据库。当别人访问时,加载这段评论,浏览器就把它识别为 JS 代码来执行。
反射型
通过 URL 参数注入恶意脚本,经服务器解析并响应后,凭借在 HTML 中传回浏览器,然后浏览器解析时就会执行恶意脚本。比如打开包含恶意脚本的链接,打开后会向服务器发送请求,服务器会获取 URL 中的数据然后凭借在 HTML 上返回,然后执行。它和存储型的区别在于不会存储在服务器中。
基于 DOM 型
通过一定的手段在网页向服务端请求资源时,劫持并修改页面的数据,插入恶意代码。
http-only
。当 Cookie 设置 http-only 后,会禁止 JavaScript 来访问 Cookie。CSRF 是跨站请求伪造攻击,顾名思义,就是第三方利用用户的登录信息伪造成用户发起跨域请求。比如邮箱里的乱七八糟链接,打开链接的时候邮箱处于登陆状态,第三方就可以利用这个登陆状态,伪造带有正确 Cookie 的 http 请求,绕过后台验证,冒充用户进行一些操作。
发起 CSRF 攻击有三个必要条件:
CSRF 本质上是利用进行 HTTP 同源请求时会携带 Cookie 信息这一特点,实现冒充用户。
Referer
是否从第三方网站发出来的,阻止第三方网站请求接口。但是这两者可以通过 ajax
自定义请求头的方式被伪造。为了提高用户的体验和浏览器请求数据的效率,降低服务器的开销,浏览器会对请求资源进行缓存。
对于缓存,浏览器主要有两种缓存策略:强缓存 和 协商缓存。
强缓存通过设置两种 HTTP Header
实现,分别是:Expires
和 Cache-Control
。强缓存表示在缓存期间不需要发送请求,直接使用即可,返回的状态码为 200。
Expires
是 HTTP 1.0
的产物。值表示的是服务端的时间,并且 Expires
受限于本地时间,如果修改了本地时间导致本地时间和服务器时间不一致,可能会造成缓存失效。Cache-Control
是 HTTP 1.1
的产物,优先级高于 Expires
。该属性具有 max-age
值,表示资源会在请求后多少秒过期。Cache-Control
的值也可以设置为 no-store
或 no-cache
,两个指令在请求和响应中都可以使用。
no-store
表示不进行任何缓存,告知服务器或缓存服务器,我请求或响应的内容里有机密信息。no-cache
如果在请求头中使用,表示强制使用协商缓存,;如果在响应头中被返回时,表示缓存服务器不能对该资源进行缓存,客户端可以缓存资源,但是每次使用之前都必须向服务器确认其有效性。如果缓存过期了或者 Cache-Control
设置为了 no-cache
,客户端就需要向服务端发起请求验证资源是否有更新。在服务器发送请求时,服务器会根据这个请求头的 If-Modified-Since
和 If-None-Match
来判断是否命中协商缓存。如果命中,则返回 304
状态码并更新浏览器缓存有效期。
协商缓存的标识为:Last-Modified
和 ETag
。
Last-Modified
标识本地文件最后修改时间。发送请求时,会将当前的 Last-Modified
值作为 If-Modified-Since
字段的内容,放在请求头中发送给服务器,去询问服务器在这个时间后资源是否有更新。有更新的话服务端就返回新的资源,没有的话返回 304
状态码。
ETag
类似于文件指纹。客户端请求时会将当前的 ETag
作为 If-None-Match
字段的内容,并放在请求头中发送给服务器。服务器接收到 If-None-Match
后会跟服务器上该资源的 ETag
进行对比。如果有变动的话,就把新的资源返回,没有的话返回 304
状态码。
字段 | Header 类型 | HTTP 版本 | 缓存类型 |
---|---|---|---|
Last-Modified | Response 头 | 1.0 | 协商缓存 |
If-Modified-Since | Request 头 | 1.0 | 协商缓存 |
ETag | Response 头 | 1.1 | 协商缓存 |
If-None-Match | Resquest 头 | 1.1 | 协商缓存 |
ETag
的出现是为了弥补 Last-Modified
的缺点:
Last-Modified
记录的时间单位为秒,如果文件在 1s 内被修改多次,就无法记录。response header
及该请求的返回时间一起缓存下来。Cache-Control
的 max-age
,则缓存没有过期,命中强缓存,不发请求直接读取本地缓存。如果浏览器不支持 HTTP 1.1
,则用 expires
判断是否过期。如果时间过期,浏览器向服务器发送 header
带有 If-None-Match
和 If-Modified-Since
的请求。ETag
的值判断被请求的文件有没有修改,没有修改则命中协商缓存,返回 304。反之,直接返回新资源,带上新的 ETag
值并返回 200。ETag
值,则将 If-Modified-Since
和被请求文件的最后修改时间做比对,相同则命中协商缓存;反之返回新的 Last-Modified
和文件,并返回 200。/**
* 阿里 3.26 笔试第二题
* n 个学生围成一圈报数,编号为 1 到 n,学生们从 1 开始依次报数,报到素数的学生出列,剩下的学生继续报。
* 一直报数报到只剩一个学生时,停止报数,求解这个学生的编号。
* ======================
* 解题思路:模拟队列
*/
// 判读素数
function isPrime(num){
if(num <= 1){
return false;
}
for(let i = 2; i <= Math.sqrt(num); i++){
if(num % i === 0){
return false
}
}
return true;
}
function getStudent(n){
let students = new Array(n).fill(true);
let num = 1; // 报数号码
let loc = 0; // 当前报数学生的编号
while(true){
if(isPrime(num)){ // 判断素数
students[loc] = false;
}
// 判断是否还剩一个学生
if(students.filter(item => !!item).length === 1){
return students.findIndex(item => !!item) + 1
}
// 寻找为 true 的下一个学生
loc = (loc + 1) % n;
while(!students[loc]){
loc = (loc + 1) % n;
}
num++;
}
}
console.log(getStudent(4)); // 4
console.log(getStudent(6)); // 4
console.log(getStudent(9)); // 1
/**
* 题目:支持同时加载 N 张图片,且支持配置整体资源加载超时时间
* 要求-1: x 秒内资源没有全部响应完成,控制台输出 resource load cost over xxx ms
* 要求-2: x 秒内资源全部响应完成,控制台输出加载失败的资源的错误信息,成功的不输出。
*
* @param {*} imgUrls
* @param {*} timeout
*/
// preloadImage
async function preloadImage(imgUrls, timeout){
// 使用 race 限制超时,使用 all 并行请求
let res = await Promise.race([Promise.all(imgUrls.map(item =>
fetch(item).then(response => {
// fetch 除了网络请求问题,一般都会得到 resolve 状态的期约,主要根据 response.ok 决定是否请求成功
if(response.ok) return;
else return `${item} load fail !`
})
)), new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`resource load cost over ${timeout} ms`)
}, timeout)
})]);
// 如果返回的不是数组,说明是 timeout,反之输出数组内容。
if(Array.isArray(res)){
res.forEach(item => {
if(item) console.log(item);
})
}else{
console.log(res);
}
}
// Test:user1.json 不存在,user2.json 存在,user3.json 不存在
const imgUrls = ['./myData/users1.json', './myData/users2.json', './myData/users.json'];
preloadImage(imgUrls, 1000);
/**
* ./myData/users1.json load fail !
* ./myData/users2.json load fail !
*/
Session 和 Cookie 主要用来识别登录者身份的,默认通过 SessionID 唯一编号进行验证。Session 是在服务器端保存的一个数据结构,用来跟踪用户的状态,也可以保存用户相关的一些数据,可以保存在内存、缓存、数据库等存储结构中。Cookie 是客户端保存用户信息的一种机制。
当客户端第一次发出请求,请求服务器时,如果服务器需要记录该用户状态,就会使用 response 向客户端浏览器响应回一个Cookie。客户端会把 Cookie 保存起来,保存在浏览器的内存中。
Cookie 是服务器发送到浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一用户,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
Cookie 主要用于以下三个方面:
每个用户访问服务器都会建立一个 Session,当用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId,用于辨别用户。
Session 代表着服务器和客户端一次会话的过程。Session 对象(key-value形式)存储特定用户会话的状态(实际上和 Cookie 类似,都是为了存储用户相关的信息)。这样,当用户多次请求服务器时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。
Session 和 Cookie 是相互合作实现的,而它们两个之间的桥梁便是 SessionId。
用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。
当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
在先前的一篇 issue
中提到了 cookie 和 session 的区别。这一次又出现了 token
。不过无论是 cookie
还是 token
,也都是一些熟悉字眼儿了,包括在平时生活中也有所了解。
我们知道 cookie
和 session
是相辅相成的,所以实际上我认为谈论它俩的区别有点牵强。但是讨论 token
和 cookie-session
的区别还是很有意义的。
这里就不介绍什么是 cookie
和 session
了,可以跳转到上面的链接去了解一下。下面主要讲解 token
。
token
的全名叫 JSON Web Token
,缩写为 JWT
。
首先我们已经知道 cookie
和 session
一个存储在客户端,一个存储在服务端,并且通过验证内部的 sessionId
来实现用户的身份验证。之所以需要这种验证,主要还是因为 HTTP
的无状态特点,以至于用户每个页面切换时,都需要进行登陆,就很麻烦了。但是 cookie
和 session
可以帮助我们实现这种身份验证。
那么既然有了 cookie
和 session
,为什么还需要 token
呢?
这里我们先不谈 session
,因为它是存储在服务端的。cookie
和 token
是存储在客户端的,我们就先谈一谈 cookie
的不足之处。
cookie
是小文本文件,大小只有 4KB,很显然它存储的内容是十分有限的,一般也就是 key-value 形式保存一下我们的身份信息。cookie
是无法发送的。(除非我们通过设置客户端和服务端的 header
实现,包括 withCredentials
、Access-Control-Allow-Credntials
和 Access-Control-Allow-Origin
)cookie
不能存储敏感信息,因为 cookie
能够被客户端篡改。现在我们再谈一谈 session
的不足之处。
session
会存储在服务端,每一个用户都会对应一个自己的用户信息,如果用户量非常大,这会增加服务器的负载。既然上面提出了一些 cookie
和 session
的不足之处,下面我们就谈一谈 token
。
token
实际上就类似于 HTTPS
的签名方式帮助服务器实现校验。token
由三部分构成:头部、负载和签名。
token
的类型和签名算法(如类型是 JWT,加密算法是 HS256)最后把这三部分连接起来就可以得到一个 token
了。
而使用 token
认证的流程如下:
token
,发送给客户端。token
后会把它存储起来,比如放在 cookie
中获 localStorage
中。token
(把 token
放在 HTTP
的 header
中)。token
(通过解密算法解密),验证成功则返回对应的数据。从上面对 token
的介绍和认证流程,我们知道 token
是一种服务端无状态的认证方式,服务端不需要存储 token
数据,而是存在每个客户端中。服务端用解析 token
的计算时间换取 session
的存储空间,从而减轻服务器的存储压力和频繁查询数据库的性能压力。
token
具有加密签名,而 cookie
和 session
是没有的。token
能存储的数据较多,而 cookie
仅有 4 KB。此外,token
还支持跨域认证的,而 cookie
是不允许的。
因此,token
和 cookie
、session
的区别总结如下:
token
大小相较于 cookie
更大,能够存储更多信息。token
支持跨域认证,而 cookie
不可以,需要客户端和服务端一起设置一些 header
才行。token
具有加密签名,相比 cookie
更加安全。token
是无状态的,存储在客户端本地,无需像 session
一样存储在服务器中增加内存压力。token
利用解密的计算时间换取 session
在服务端的存储空间,并且不需要频繁的查询数据库。HTML的标签大多数都是行内元素和块级元素,那么行内元素和块级元素和有哪些区别呢?哪些是行内元素,哪些是块级元素呢?还有什么是行内块元素
行内元素不可以设置宽高,其宽高会根据内容自适应(表现在高度根据字体大小决定,宽度根据内容长度决定)。不过,行内元素可以与其他行内元素位于同一行。但是,行内元素内不可以包含块级元素。
行内元素有以下特点:
行内元素有:
<a> // 标签可定义锚
<abbr> // 表示一个缩写形式
<acronym> // 定义只取首字母缩写
<b> // 字体加粗
<bdo> // 可覆盖默认的文本方向
<big> // 大号字体加粗
<br> // 换行
<cite> // 引用进行定义
<code> // 定义计算机代码文本
<dfn> // 定义一个定义项目
<em> // 定义为强调的内容
<i> // 斜体文本效果
<kbd> // 定义键盘文本
<label> // 标签为 input 元素定义标注(标记)
<q> // 定义短的引用
<samp> // 定义样本文本
<select> // 创建单选或多选菜单
<small> // 呈现小号字体效果
<span> // 组合文档中的行内元素
<strong> // 加粗
<sub> // 定义下标文本
<sup> // 定义上标文本
<tt> // 打字机或者等宽的文本效果
<var> // 定义变量
块级元素可以自己设置宽高,并且每一个 “块” 都会独占一行。块级元素可以用作容器,其内部可以具有行内元素。
块级元素有以下特性:
块级元素包括:
<address> // 定义地址
<caption> // 定义表格标题
<dd> // 定义列表中定义条目
<div> // 定义文档中的分区或节
<dl> // 定义列表
<dt> // 定义列表中的项目
<fieldset> // 定义一个框架集
<form> // 创建 HTML 表单
<footer> // 页脚
<h1> // 定义最大的标题
<h2> // 定义副标题
<h3> // 定义标题
<h4> // 定义标题
<h5> // 定义标题
<h6> // 定义最小的标题
<hr> // 创建一条水平线
<header> // 页头
<legend> // 元素为 fieldset 元素定义标题
<li> // 标签定义列表项目
<noframes> // 为那些不支持框架的浏览器显示文本,于 frameset 元素内部
<noscript> // 定义在脚本未被执行时的替代内容
<ol> // 定义有序列表
<ul> // 定义无序列表
<p> // 标签定义段落
<pre> // 定义预格式化的文本
<section> // 段落
<table> // 标签定义 HTML 表格
<tbody> // 标签表格主体(正文)
<td> // 表格中的标准单元格
<tfoot> // 定义表格的页脚(脚注或表注)
<th> // 定义表头单元格
<thead> // 标签定义表格的表头
<tr> // 定义表格中的行
行内块级元素,它既具有块级元素的特点,也有行内元素的特点,它可以自由设置元素宽度和高度,也可以在一行中放置多个行内块级元素。
行内块元素有以下特性:
font-size
为 0,才会消除间隙;行内块元素包括:
<button> // 按钮
<input> // 输入框
<textarea> // 多行纯文本编辑控件
<select> // 选项菜单
<img> // 图片
HTTP 1.1 的新特性有:
默认持久连接
只要客户端和服务端任意一端没有明确提出断开 TCP 连接,就会一直保持连接,并且在这个过程中可以发送多次 HTTP 请求。HTTP 1.0 默认使用短连接,而 HTTP 1.1 默认使用长连接。
管线化
客户端可以同时发送多个 HTTP 请求,不用等待响应。
断点续传
利用 HTTP 消息头(Range 和 Content-Range)使用分块传输编码,将实体主体进行分块传输。
缓存策略
在HTTP1.0中主要使用header里的 Expires
, Last-Modified / If-Modified-Since
来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如:cache-control
, ETag / If-None-Match
等更多可供选择的缓存头来控制缓存策略。
HTTP 2.0 的新特性有:
二进制格式传输
HTTP 1.x 的解析都是基于文本,而 HTTP 2.0 采用二进制格式,实现了效率更高的传输。
并发传输
HTTP 1.1 基于请求-响应模型,同一个连接中, HTTP 完成一个事务(请求与响应),才能处理下一个事务。在发出请求等待响应的过程中是没办法做其他事情的,这会造成队头阻塞问题 。
HTTP2 通过 Stream(流)设计,多个 Stream 复用一条 TCP 连接,达到了并发的效果。(实际上就是一个请求对应了一个 ID,一个 TCP 连接可以有多个请求,接收方根据请求的 ID 将请求归属到不同的服务端请求中)
压缩头部
HTTP 1.1 报文的 Header 部分含有很多固定字段,且很多字段值是重复的。因此 HTTP 2.0 采用 HPACK 算法(包括静态字典、动态字典、哈夫曼编码)对头部进行压缩,降低头部占用空间大小,提高传输效率。
服务器主动推送资源
HTTP 1.1 不支持服务器主动推送资源给客户端,都是客户端发起请求之后,才能获取到服务器响应的资源。在 HTTP 2.0 中,客户端访问 HTML 时,服务器可以主动推送 CSS 文件,减少了消息传递的次数。
永久性
HTTP 2.0 中只要客户端和某个服务器完成连接,将具有永久性,之后如果再次需要和该服务器进行连接时,可以直接复用,不需要再次建立连接。
HTTP
即超文本传输协议,主要用于 Web 上传输超媒体文本的底层协议,经常在浏览器和服务器之间传递数据。通信就是以纯文本的形式进行。
无连接、无状态、灵活易于扩展、简单快速。
无连接:每一次请求都要连接一次,请求结束就会断掉,不会保持连接
无状态:每一次请求都是独立的,服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,从而减少了网络开销。这既是优点也是缺点。
灵活易于扩展:HTTP 协议中的各种请求方法、URI / URL、状态码、头字段等每个组成都是可以由开发人员自定义和扩充的。
简单快速:基本报文的格式为 header + body,头部信息也是键值对的文本形式,易于理解。当发送请求访问某个资源时,只需传送请求方法和 URL 就可以了,使用简单。正由于http协议简单,使得http服务器的程序规模小,因而通信速度很快
无状态、明文传输、不安全
无状态:服务器不会记忆 HTTP 的状态,所以不需要申请额外的资源,能够减轻服务器的负担。但是也就无法区分多个请求发起者身份是不是同一个客户端的,意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。
明文传输:传输的报文 (header部分) 是肉眼可见的,直接将信息暴露给了外界。
不安全:明文传输可能被窃听不安全,缺少身份认证可能遭遇伪装,缺少报文完整性验证可能遭到篡改。
HTTPS 在 HTTP 的基础上添加了 SSL / TLS
安全传输协议,使得报文能够加密传输。
HTTPS 在 TCP 三次握手之后,还需要进行 SSL /TLS 的握手过程,才可进入加密报文传输。
信息加密、校验机制、身份证书
信息加密:浏览器和服务器之间交互的信息无法被窃取。
校验机制:无法篡改通信的内容,因为一旦被篡改就不能正常显示。
身份证书:提供当前报文完整性的证明。
在数据传输过程中,使用密钥加密,安全性更高。
能够认证用户和服务器,确保数据发送到正确的用户和服务器。
HTTPS 是 HTTP 协议的安全版本,HTTP 协议的数据传输是明文的,是不安全的,HTTPS 使用了 SSL/TLS 协议进行了加密处理,相对更安全。
HTTP 和 HTTPS 使用连接方式不同,默认端口也不一样,HTTP是 80,HTTPS 是443。
HTTPS 由于需要设计加密以及多次握手,性能方面不如 HTTP
计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。
TCP/IP 网络模型一系列网络协议的总称,不同协议各司其职。该网络模型可以划分为四层:链路层、网络层、传输层和应用层。(也可以根据 OSI 模型,划分为七层,主要是对链路层和应用层进行了功能细分)
TCP 和 UDP 都是传输层协议。
TCP 协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。#3 在这篇 issue 中,讲解了 TCP 的三次握手和四次挥手过程,涉及了 TCP 建立连接和结束连接的详细过程。
TCP 协议的特点如下:
面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。
一对一传输
每条 TCP 传输连接只能有两个端点,进行一对一的数据传输,而不支持一对多、多对一、多对多等方式。
面向字节流
TCP 传输是在不保留报文边界的情况下以字节流方式进行传输,而不是一个一个报文的形式进行传输。
可靠传输
在三次握手的过程中,TCP 头部包含了客户端和服务端的随机序列号以及确认应答码信息,每次连接这些信息都是不一样的。因此,两端可以通过这些信息互相进行确认,确保传递的可靠性。
提供拥塞控制
TCP 具有慢开始和避免拥塞的特点。当刚建立 TCP 连接时,TCP 连接会一点点提速,试探网络的承受能力,以免打乱网络通信的秩序,慢慢地翻倍提速。如果遇到网络拥塞,TCP 会降低传输速度,缓解拥塞。
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
它有以下几个特点:
具体来说就是:在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层。在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作。
有单播,多播,广播的功能
UDP 不仅支持一对一的传输方式,还支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
面向报文
发送方的 UDP 对应用程序传下来的报文,在添加首部(UDP 头标识)后就向下交付 IP 层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文。
不可靠性
无连接的特性允许数据想发就发,而不需要进行验证,也不会关心对方是否已经收到数据了。再者,UDP 协议一直以恒定的速度发送数据,如果遇到网络拥塞,可能会出现丢包现象,但是它也不会去确认数据的完整性。
头部开销小
UDP 的头部开销小,只需要 8 字节,比 TCP 至少 20 字节小得多,因此传输更高效。对于一些实时性要求高的场景(如视频、电话会议),UDP 协议更适合。
对比 | UDP | TCP |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅 8 字节 | 首部最小 20 字节,最大 60 字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
HTTP 3.0 是基于 QUIC 协议的新版本 HTTP 协议,相比之前的 HTTP 1.1 和 HTTP 2.0 有以下几个变动:
传输协议改变:HTTP 3.0 基于 QUIC 协议,而 HTTP 1.1和 HTTP 2.0 是基于 TCP 协议。QUIC 协议是一个基于 UDP 协议的安全传输协议。HTTP3.0 使用 QUIC 协议,可以更快地建立连接和更好地管理网络拥塞。QUIC 协议通过减少握手次数、降低延迟和网络拥塞控制等机制来提高网络性能。
多路复用改进:HTTP 3.0 通过 QUIC 协议实现多路复用,该方法能够将数据流动态地分割成多个数据包,这些数据包可以并行地发送和接收,避免了头阻塞的问题。此外,不需要像 HTTP 2.0 那样预先将请求和响应打包成帧,而是将它们作为数据流动态地切分为多个数据包,这使得它更加灵活,可以适应不同的网络条件。减少了延迟和头阻塞等问题。
加密改进:HTTP3.0 使用 TLS 1.3 来加密数据传输,而 TLS 1.3 相比 TLS 1.2 有更好的性能和安全性。
服务器推送改进:HTTP 3.0 将服务器推送机制优化,基于流的传输方式可以在一个连接上同时进行多个流,从而使得服务器推送更加高效和灵活。此外,还支持取消服务器推送,避免浪费网络资源。
首部压缩改进:HTTP 3.0 将采用 QPACK 首部压缩算法,相比于 HTTP 2.0 的 HPACK 算法,更加高效。
/**
* 腾讯音乐笔试题第二题
* 题目:一个由字母组成的字符串的权值定义为:字母种类个数 * 字符串长度,如 value(abac) = 3 * 4 = 12
* 输入一个字符串 str,和正整数 k ,请把该字符串按顺序分为 k 个子串,找出 k 个子串的权值最大值 maxValue。
* 一共有很多种切分的方法,求解所有切分方法里的 min(maxValue1, maxValue2, ... ),即最大值的最小值。
*
* 思路:递归遍历 + 回溯
* 剪枝:下面方法没有剪枝。思路是:可以给递归加一个curMaxValue 参数,实时计算当前 path 数组里的最大权值,如果已经大于 minMaxValue 了,就没必要继续了
*/
// 计算字符串的权值
const getValue = (str) => {
let words = new Set(str);
return words.size * str.length;
}
// 计算数组里 k 个子串的权值最大值
const getMaxValue = (arr) => {
let maxValue = -1;
for (const item of arr) {
let curValue = getValue(item);
// console.log(item, curValue);
if (maxValue < curValue) maxValue = curValue;
}
return maxValue;
}
// 计算最终结果
const getMinMaxValue = (str, k) => {
let minMaxValue = Infinity;
// 递归 + 回溯
const trackBack = (str, path, startIndex) => {
// 判断结束条件
if (path.length === k) {
let temp = [...path];
let maxValue = getMaxValue(temp);
if (minMaxValue > maxValue) minMaxValue = maxValue;
return;
}
// 单层逻辑:
for (let i = startIndex; i < str.length + path.length - k + 1; i++) {
path.push(str.slice(startIndex, i + 1));
if (path.length === k - 1) { // 如果已经分割了 k - 1 个了,最后一个就是剩余子串
path.push(str.slice(i + 1));
trackBack(str, path, str.length);
// 回溯,要连续取两个出来
path.pop();
path.pop();
} else {
trackBack(str, path, i + 1); // 下一个子串从第 i + 1个 位置开始切割
path.pop();
}
}
}
trackBack(str, [], 0);
return minMaxValue;
}
console.log(getMinMaxValue('ababbbb', 2)); // 6
console.log(getMinMaxValue('ababbbb', 3)); // 4
console.log(getMinMaxValue('abcbbdef', 3)); // 9
// sum(1, 2, 3, 4)(5)(6).sumOf() 输出 21
// sum 方法可以传入任意多数值,执行 sumOf 方法将输出结果。
function sum(...params){
// 闭包存一个计数器
let res = 0;
params.forEach(item => {
res += item;
})
// 因为 sum 可以多次调用,因此需要返回一个函数,这个函数能够继续把传入的参数加到 res 上。
const add = (...args) => {
args.forEach(item => {
res += item;
})
return add;
}
// 返回的函数有一个 sumOf 的属性,输出对应的结果(函数也是一个对象,因此也可以有属性)
add.sumOf = () => {
console.log(res);
}
return add;
}
sum(1, 2, 3, 4)(5)(6).sumOf(); // 21
/**
* 题目:
* 1、输入一个 num
* 2、用 r、e、d 三个字母(不限个数)组合成一个字符串
* 3、要求该字符串能够组成的回文子串数量等于 num
* eg:num = 3,该字符串可以是:red -> [r, e, d] 或 rr -> [r, r, rr]
*/
// 判断是不是回文串
function isPalindrome(s, l, r) {
for (let i = l, j = r; i < j; i++, j--) {
if (s[i] !== s[j]) return false;
}
return true;
}
// 查找回文子串
function partition(s) {
const res = [], path = [], len = s.length;
function backtracking(startIndex){
// 终止条件:startIndex 前面的字符串都是分好了的回文串
if(startIndex >= len){
// 为了统计数量,把 path 元素全都提出来放进 res
res.push(...path);
return;
}
for(let i = startIndex; i < len; i++){
// 如果这一段是回文串,就插入 path 结果中,从下一个字母继续开始判断
if(isPalindrome(s, startIndex, i)){
path.push(s.slice(startIndex, i + 1));
backtracking(i + 1);
path.pop();
}else{
// 不是回文串,i 就再往后移进行判断
continue;
}
}
}
backtracking(0);
return res;
}
// 获取结果
function getStr(num){
let res = ''; // 保存结果
let str = 'r'; // 初始字符串
let otherWords = ['r', 'e', 'd'];
function backtracking(s){
// 得到 s 字符串的所有回文子串
let arr = partition(s);
// 递归终止条件 1: 如果已经找到了一个符合要求的结果就不需要继续递归了。
if(res) return;
// 递归终止条件 2: 如果当前字符串 s 生成的回文子串的个数达到了 num,说明找到了一个符合要求的解。
if(arr.length === num){
res = s;
return;
}
// 剪枝: 如果数量超过了 num,之后只会越来越多,没必要迭代
if(arr.length > num){
return;
}
for(let i = 0; i < otherWords.length; i++){
str += otherWords[i];
// 递归查找
backtracking(str);
// 回溯
str = str.slice(0, str.length - 1);
}
}
backtracking(str);
return res;
}
// Test
console.log(getStr(50)); // rrerrderde
console.log(getStr(100)); // rrrredredredre
console.log(getStr(500)); // rrrrrederderderd
浏览器渲染:Step 8 - Step 9 详细步骤:
----- 至此,渲染主线程任务都完成了 -----
为什么 transform 效率高?
因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个 draw 阶段。
由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。
更新 2023 - 08 - 15
被京东的面试官问到了这个问题,但是他当时拓展问到该如何更对整个过程进行优化?
answer:
但是,上面答的似乎都没答到面试官想要的。他说了一点:
/**
* 阿里 3.15 笔试第一题
* 求解满二叉子树的个数
*/
// 树结点
function treeNode(val) {
this.val = val;
this.left = null;
this.right = null;
}
// 记录满二叉子树的个数
let count = 0;
// 深度遍历
function deepTraversal(root) {
// 叶子结点
if (!root.left && !root.right) {
root.val = true;
return;
}
// 当前逻辑:如果
let left = false, right = false; // left right 初始化为 false,这样如果没有左结点和右结点的话就一定不是满二叉树
root.left && (function () { deepTraversal(root.left); left = root.left.val })();
root.right && (function () { deepTraversal(root.right); right = root.right.val })();
root.val = left && right;
}
// 寻找满二叉子树的个数
function getAllFullBinaryTrees(root) {
// 递归结束条件
if (!root) return;
if (root.val) {
count++;
}
getAllFullBinaryTrees(root.left);
getAllFullBinaryTrees(root.right);
}
// Test
let root = new treeNode('root');
let l = new treeNode('l');
let r = new treeNode('r');
let ll = new treeNode('ll');
let lr = new treeNode('lr');
let rl = new treeNode('rl');
let rr = new treeNode('rr');
root.left = l;
root.right = r;
l.left = ll;
l.right = lr;
r.left = rl;
r.right = rr;
deepTraversal(root);
getAllFullBinaryTrees(root);
console.log(count); // 7
TCP是一种面向连接的、可靠安全的、基于字节流的传输层协议。TCP中文名称是传输控制协议,属于传输层协议。HTTP 是应用层协议,它是建立在 TCP / IP 协议基础上进行的。
第一次:浏览器随机初始化序列号 x,放进 TCP 首部序列号段,即 seq = x,并把 SYN 设置为 1。然后把 SYN 码发送给服务器,请求和服务器建立连接,浏览器进入 SYN-SENT 状态。
第二次:服务器接收到 SYN 码后,把自己的序列号 y 放进 TCP 首部序列号段,即 seq = y,并把确认应答号 ack 设置为 x + 1。把 SYN 和 ACK 设置为 1,发送给浏览器,告诉浏览器已建立连接。服务器进入 SYN-RECV 状态。
第三次:浏览器接收 ACK 码,需确认 ACK 码是否正确。如果正确,浏览器会再向服务器发送一个数据包,数据包中将 ACK 置为 1,并将确认应答号 ack 设置为 y+1,表示收到了来自服务器的 SYN。
此后,浏览器和服务器都进入 ESTABLISHED 建立连接状态。
以浏览器先发出结束请求为例。
第一次:浏览器发送 FIN 码给服务器,告诉服务器,我要传给你的数据已经完成。浏览器进入 FIN_WAIT_1 状态。
第二次:服务器接收到 FIN 码,然后发送 ACK 码给浏览器,告诉浏览器已经收到消息。服务器发送完之后,就进入 CLOSE_WAIT 状态。浏览器进入 FIN_WAIT_2 状态。
第三次:虽然浏览器给服务器传输的数据完成了,但是服务器给浏览器的数据可能尚未完成。因此,当服务器数据传输成功后,发送FIN 码告诉浏览器自己的数据传输完毕。此时服务器进入 LAST_ACK 状态。
第四次:浏览器接收到 FIN 码之后,同样会发送 ACK 码给服务器,告诉服务器,我已接收到,你可以断开连接。此时,浏览器进入 TIME_WAIT 状态。
服务器收到 ACK 码后,进入 CLOSE 状态。在 2 MSL 后(MSL 是报文最大生存时间,但是消息传递是一来一回的,因此需要 2 倍),浏览器也会自动进入 CLOSE 状态。
四次挥手实际上就是浏览器和服务器之间的两次消息发送与应答。当浏览器第一次发送 FIN 报文之后,只是代表着浏览器不再发送数据给服务端,但此时浏览器还是有接收数据的能力的。而服务器虽然收到 FIN 报文的时候,但可能还有数据要传输给客户端,所以只能先回复 ACK给客户端,等自己传输完毕之后再告诉浏览器传输结束。
/**
* 阿里 3.26 笔试第一题
* 环形数组,切两刀且成两个数组,让两个数组的元素和相等则算一种方案,求问一共多少种方案
* ======================
* 解题思路:前缀和 + Map 存储
*/
// 不使用 Map 存储,双 for 循环会超时
function getAllResult(arr){
let len = arr.length;
let count = 0;
let preSum = [arr[0]];
// 计算前缀和并存储
for(let i = 1; i < len; i++){
preSum.push(preSum[i - 1] + arr[i]);
}
// 不论是 i 还是 j,在位置 index 切,都是切在 index 对应数的后面,也就是会算上 index 这个数
// i 不用切在第 0 个数前面,因为 j 切到最后一个数后面和这种情况是等效的。
for(let i = 0; i < len - 1; i++){
for(let j = i; j < len; j++){
let sum1 = preSum[j] - preSum[i];
let sum2 = preSum[len - 1] - preSum[j] + preSum[i];
if(sum1 === sum2){
console.log(i, j)
count++;
}
}
}
return count;
}
let arr = [1, 2, -1, 2];
console.log(getAllResult(arr)); // 2
// 使用 Map 存储:前缀和 - 对应前缀和元素所在位置的集合
// 降低时间复杂度
function getAllResult(arr){
let len = arr.length;
let count = 0;
let preSum = [arr[0]];
// 计算前缀和并存储
for(let i = 1; i < len; i++){
preSum[i] = preSum[i - 1] + arr[i];
}
let sum = preSum[len - 1];
if(sum % 2 === 1) return count; // 如果数组所有元素总和不是偶数,那肯定不存在分两个数组元素和相等
// 用于记录前缀和有几种,对应的元素编号是多少
let preSumMap = {};
for(let i = 0; i < len; i++){
if(preSum[i] in preSumMap){
preSumMap[preSum[i]].push(i);
}else{
preSumMap[preSum[i]] = [i];
}
}
// 遍历前缀和数组
for(let i = 0; i < len; i++){
let pre = preSum[i] - sum / 2; // 找出当前切割位置需要的另一个切割位置的前缀和值
if(pre in preSumMap){
// 如果另一个切割位置在当前位置的前面,就符合,因为我们是用 preSum[i] - sum / 2
for(let j = 0; j < preSumMap[pre].length; j++){
if(j < i){
count++
}
}
}
}
return count;
}
console.log(getAllResult(arr)); // 2
当我们需要存储大量数据时,我们通常需要将数据分成多个部分,然后将这些部分存储在磁盘或内存中。其中,基本分页存储和请求分页存储是两种常见的数据分割和存储方式。
基本分页存储是将数据划分为固定大小的页,并按顺序存储在磁盘或内存中。例如,一个文件可以被分割为多个固定大小的页,每一页包含一定数量的字节。当需要访问特定的数据时,系统会根据数据所在的页号和页内偏移量来定位数据。这种存储方式通常用于传统的文件系统和关系型数据库管理系统(RDBMS)中。
优点:
基本分页存储的优点是:简单、高效、易于管理和维护,
缺点:
浪费内存。作业装入内存后,便一直驻留在内存中,直至作业运行结束。尽管运行中的进程会因 I/O 而长期等待,或有的程序模块在运行过一次后就不再需要了,但它们都仍将继续占用宝贵的内存资源。
无法进行分布式存储和处理。
请求分页存储中的数据仍然按页分割,但不一定按顺序存储。每个页都有一个唯一的标识符,并且存储在分布式系统的多个节点上。在进程开始运行之前,仅装入当前要执行的部分页面即可运行。在执行过程中,当需要某一个页面时,再请求从外部调入。如需要请求访问但又不在内存的页面,可以通过缺页中断机构,请求OS将所缺之页调入内存。当内存空间已满,而又需要装入新的页面时,者根据置换功能适当调出某个页面,以便腾出空间而装入新的页面。
为实现请求分页,需要一定的硬件支持,包括:页表机制、缺页中断机制、地址变换机制。
1、页表机制包括几个属性:
2、 缺页中断机制:
缺页中断是指当一个程序访问虚拟内存中的某一页时,该页并未在物理内存中,需要将该页从磁盘或其他存储介质中读入到物理内存中。此时操作系统就会触发缺页中断机制,将控制权交给操作系统内核,内核根据页表信息将缺失的页面读入物理内存,然后将控制权返回给程序,使程序能够继续执行。
3、 地址变换机制:
地址变换机制指将虚拟地址转换成物理地址的过程。在现代操作系统中,为了保护程序的内存空间和实现虚拟内存的功能,程序访问的地址一般都是虚拟地址,而不是直接访问物理内存。
当一个程序发起内存访问请求时,其请求的地址是虚拟地址,操作系统会通过地址变换机制将其转换成物理地址,然后才能真正的访问内存。地址变换机制一般包括以下几个步骤:
地址解析:将虚拟地址中的页号和页内偏移量解析出来。
查找页表:通过页表查找到对应的页表项,获取该页表项中的物理页号。
地址重定位:将物理页号和页内偏移量组合成物理地址,完成地址重定位。
访问物理内存:使用物理地址访问物理内存中的数据。
在操作系统中,地址变换机制是由硬件和软件共同实现的。硬件主要负责地址解析和地址重定位的部分,而软件则负责页表的管理和维护。通过地址变换机制,操作系统可以实现多道程序之间的内存隔离和虚拟内存的实现,从而提高了系统的稳定性和可用性。
优点:
缺点:
总的来说,基本分页存储适用于单机环境,适合小规模的数据存储和处理;而请求分页存储适用于分布式环境,适合大规模数据存储和处理。在实际应用中,我们需要根据具体的需求和场景选择合适的存储方式。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.