# HTML
Doctype作用? 标准模式与兼容模式如何区分?
<!DOCTYPE> 声明位于文档中的最前面,处于 html 标签之前。告知浏览器以何种模式来渲染文档。
标准模式的排版和 JS 运作模式是 以该浏览器支持的最高标准运行。在兼容模式中,页面以宽松的向后兼容的方式显示。模拟老式浏览器的行为以防止站点无法工作。DOCTYPE 不存在或格式不正确会导致文档以兼容模式呈现。
语义化标签
- 利于 SEO
- 提高代码可读性
defer 和 async的区别
说一下 HTML5 drag api
darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发,。
dragend:事件主体是被拖放元素,在整个拖放操作结束时触发
dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
attribute和property区别
前者是 HTML 文档属性,后者是 JS 获取 DOM 属性
src和href的区别
src 是 source 的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求 src 资源时会将其指向的资源下载并应用到文档内,例如 js 脚本,img 图片和 frame 等元素。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等元素也如此,类似于将所指向资源嵌入当前标签内。这也是为什么将 js 脚本放在底部而不是头部。src 用于 img、input、style、script、iframe;
href 是 Hypertext Reference 的缩写,指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接,如果在文档中添加。那么浏览器会识别该文档为 css 文件,就会并行下载资源并且不会停止对当前文档的处理。 这也是为什么建议使用 link 方式来加载 css,而不是使用 @import 方式。href 用于 link a 标签
img标签的srcset属性作用 picture标签作用
可定义一组额外的图片集合,让浏览器根据不同的屏幕状况选取合适的图片来显示。
同样作为响应式匹配窗口
1 | <picture> |
iframe 有那些优点和缺点?
优点:
- 用来加载速度较慢的内容(如广告)
- 可以使脚本可以并行下载
- 可以实现跨子域通信
缺点:
- iframe 会阻塞主页面的 onload 事件
- 无法被一些搜索引擎索识别
- 会产生很多页面,不容易管理
# 浏览器
# 组成
浏览器的主要组成部分
- ⽤户界⾯ - 包括地址栏、前进 / 后退按钮、书签菜单等。除了浏览器主窗⼝显示的您请求的⻚⾯外,其他显示的各个部分都属于⽤户界⾯。
- 浏览器引擎 - 在⽤户界⾯和呈现引擎之间传送指令。
- 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- ⽹络 - ⽤于⽹络调⽤,⽐如 HTTP 请求。其接⼝与平台⽆关,并为所有平台提供底层实现。
- ⽤户界⾯后端 - ⽤于绘制基本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
- JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。
- 数据存储 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了 “⽹络数据库”,这是⼀个完整(但是轻便)的浏览器内数据库。
常见浏览器的内核
- IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
- Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核;
- Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
- Safari 浏览器内核:Webkit 内核;
- Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
- 360 浏览器、猎豹浏览器内核:IE + Chrome 双内核;
- 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
- 百度浏览器、世界之窗内核:IE 内核;
- 2345 浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
- UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。
浏览器的进程有哪些
- 1 个浏览器主进程
- 1 个 GPU 进程
- 1 个网络进程
- 多个渲染进程
- 多个插件进程
这些进程的功能:
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
- 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
- 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
渲染进程的线程有哪些
- GUI 渲染线程
负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
- JS 引擎线程
JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序;
- 时间触发线程
时间触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS 引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;
- 定时器触发进程
定时器触发进程即 setInterval 与 setTimeout 所在线程;浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;
- 异步 http 请求线程
XMLHttpRequest 连接后通过浏览器新开一个线程请求;
检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行;
# 机制
渲染机制、回流与重绘、如何避免
浏览器渲染大致分为四个阶段,其中在解析 HTML 后,会依次进入 Layout 和 Paint 阶段。样式或节点的更改,以及对布局信息的访问等,都有可能导致重排和重绘。而重排和重绘的过程在主线程中进行,这意味着不合理的重排重绘会导致渲染卡顿,用户交互滞后等性能问题。
- Parse HTML:相关引擎分别解析文档和样式表,生成 DOM 和 CSSOM ,最终合成为 Render 树。 浏览器采用流式布局模型(Flow Based Layout)
- Layout:浏览器通过 Render 树中的信息,以递归的形式计算出每个节点的尺寸大小和在页面中的具体位置。
- Paint:浏览器将 Render 树中的节点转换成在屏幕上绘制实际像素的指令,这个过程发生在多个图层上。
- 浏览器将所有层按照一定顺序合并为一个图层并绘制在屏幕上。
若 DOM 或 CSSOM 被修改会导致浏览器重复执行 Layout 和 Paint 过程
回流
布局或者几何属性改变时触发回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致其素有子元素以及 DOM 中紧随其后的节点、祖先节点元素的随后的回流。大部分的回流将导致页面的重新渲染。回流必定会发生重绘,重绘不一定会引发回流。重绘
由于节点的集合属性发生改变或者由于样式改变而不会影响布局的,成为重绘,例如 outline、visibility、color、background-color 等
减少回流和重绘
CSS
- 使用 transform 代替 top,开启硬件加速
- 使用 visibility 替换 display: none,前者引起重绘,后者引发回流
- 避免使用 table 布局
- 尽可能在 DOM 树的最末端改变 class
- 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多
- 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上,避免影响其他元素的布局
- 避免使用 CSS 表达式,可能会引发回流
Javascript
- 避免频繁操作样式,修改 class 最好
- 避免频繁操作 DOM,合并多次修改为一次 (虚拟 DOM、DocumentFragment)
- 避免频繁读取会引发回流 / 重绘的属性,将结果缓存(clientTop)
- 对用户改变窗口的操作进行防抖处理
什么是文档的预解析?
Webkit 和 Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
如何优化关键渲染路径?
为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:
- 关键资源的数量。
- 关键路径长度。
- 关键字节的数量。
关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化关键渲染路径的常规步骤如下:
- 对关键路径进行分析和特性描述:资源数、字节数、长度。
- 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。
- 优化关键字节数以缩短下载时间(往返次数)。
- 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度
说下进程、线程和协程
进程是应用程序运行的载体,是操作系统资源分配的最小单位。
线程是程序执行中一个单一的顺序控制流程,是 CPU 调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间 (也就是所在进程的内存空间)。一个标准的线程由线程 ID、当前指令指针 (PC)、寄存器和堆栈组成。而进程由内存空间 (代码、数据、进程空间、打开的文件) 和一个或多个线程组成。
协程是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。
进程和线程的区别与联系
【区别】:
调度:进程切换比线程切换的开销要大。线程是 CPU 调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
【联系】:
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
资源分配给进程,同一进程的所有线程共享该进程的所有资源;
处理机分给线程,即真正在处理机上运行的是线程;
线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
进程之前的通信方式
管道通信
管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程 1 可以将需要交互的数据拷贝到这段缓冲区,进程 2 就可以读取了。
管道的特点:
(1) 只能单向通信
(2) 只能血缘关系的进程进行通信
(3) 依赖于文件系统
(4) 生命周期随进程
(5) 面向字节流的服务
(6) 管道内部提供了同步机制消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存 1 的时候,我们就把信号量的值设为 0,然后进程 b 也要来访问内存 1 的时候,看到信号量的值为 0 就知道已经有进程在访问内存 1 了,这个时候进程 b 就会访问不了内存 1。所以说,信号量也是进程之间的一种通信方式。信号通信
信号(Signals )是 Unix 系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。套接字通信
上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。
父进程或子进程的死亡是否会影响对方? 什么是孤儿进程?僵尸进程?
子进程死亡不会影响父进程,不过子进程死亡时,会向它的父进程发送死亡信号。反之父进程死亡,一般情况下子进程也会随之死亡,但如果此时子进程处于可运行状态、僵死状态等等的话,子进程将被 init 进程收养,从而成为孤儿进程。
子进程死亡的时候(处于 “终止状态”),父进程没有及时调用 wait () 或 waitpid () 来返回死亡进程的相关信息,此时子进程还有一个 PCB 残留在进程表中,被称为僵尸进程。
死锁产生的原因? 如果解决死锁的问题?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
系统中的资源可以分为两类:
● 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU 和主存均属于可剥夺性资源;
● 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁的原因:
(1)竞争资源
● 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程 P1 使用,假定 P1 已占用了打印机,若 P2 继续要求打印机打印将阻塞)
● 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
(2)进程间推进顺序非法
若 P1 保持了资源 R1,P2 保持了资源 R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当 P1 运行到 P1:Request(R2)时,将因 R2 已被 P2 占用而阻塞;当 P2 运行到 P2:Request(R1)时,也将因 R1 已被 P1 占用而阻塞,于是发生进程死锁
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程 —— 资源的环形链。
预防死锁的方法:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
# 网络相关
一个页面从输入URL到页面加载显示完成,这个过程中都发生了什么?
请求:
- 浏览器会开启一个线程来处理这个请求,如果是关键字或不合法 url 则启动对应搜索引擎,如果输入是正确 url 则首先对本地缓存进行判断看是否命中强缓存,是则直接用缓存资源,否则往下走
- DNS 解析:浏览器按照
本地缓存->本地DNS服务器->根域名服务器->顶级域名服务器->权威域名服务器
顺序获取 IP 地址 (用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求)。 - 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
- 浏览器与远程 Web 服务器通过 TCP 三次握手协商来建立一个 TCP/IP 连接。该握手包括一个同步报文,一个同步 - 应答报文和一个应答报文,这三个报文在浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,而后服务器应答并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。
- 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把会话秘钥,以后双方通信前,就使用这个会话秘钥对数据进行加密后再传输。
- 此时, Web 服务器提供资源服务,客户端开始下载资源。
渲染:
- 解析
解析收到的文档,根据文档构建一颗 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
然后对 CSS 进行解析,生成 CSSOM 规则树
根据 DOM 树和 CSSOM 规则树构建 Render Tree。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 对象相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。
下载 JS 脚本、图片等资源 - 布局
接着就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情就是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为 “自动重排”。 - 绘制
布局阶段结束后是绘制阶段,比那里渲染树并调用对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。为了更好的用户体验,渲染引擎会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 解析完成之后再去构建和布局 render tree。它是解析完一部分内容就显示一部分内容,同时可能还在网络下载其余内容。
JS引擎解析:
- 创建 window 对象:window 对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于 window 的属性和方法,而 DOM Tree 也会映射在 window 的 doucment 对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。
- 加载文件:完成 js 引擎分析它的语法与词法是否合法,如果合法进入预编译
- 预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为 window 的属性加入到 window 对象中,并给变量赋值为 'undefined';寻找全局函数声明,把它作为 window 的方法加入到 window 对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在 ES6 中已经解决了,函数提升还存在。
- 解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在 ES5 非严格模式下这个变量会成为 window 的一个属性,也就是成为全局变量。string、int 这样的值就是直接把值放在变量的存储空间里,object 对象就是把指针指向变量的存储空间。函数执行,就将函数的环境推入一个环境的栈中,执行完成后再弹出,控制权交还给之前的环境。JS 作用域其实就是这样的执行流机制实现的。
TCP四次挥手:
最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
前端如何上传大文件
前端
断点续传,利用 Blob.prototype.slice 方法返回原文件的某个切片,并将切片通过 http 并发上传,同时要注意记录切片顺序和最大数量信息后端
在切片接收达到最大数量时即开始合并切片,可以使用 nodejs 的 api fs.appendFileSync, 先创建一个最终文件再逐步合并到此文件中
同源策略以及如何跨域
同源策略:协议域名端口相同才可以成功发送请求 (跨域请求可以发出和响应,但不成功), 跨域是浏览器的安全特性,与其他无关
跨域:
- CORS:服务端设置 Access-Control-Allow-Origin
不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:
请求方法:GET、HEAD、POST。
请求头:Accept、Accept-Language、Content-Language、Content-Type。
Content-Type 仅支持:application/x-www-form-urlencoded、multipart/form-data、text/plain。
当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个 OPTIONS 请求,通过服务端返回的 Access-Control-Allow- 判定请求是否被允许。
CORS 引入了以下几个以 Access-Control-Allow- 开头:
Access-Control-Allow-Origin 表示允许的来源
Access-Control-Allow-Methods 表示允许的请求方法
Access-Control-Allow-Headers 表示允许的请求头
Access-Control-Allow-Credentials 表示允许携带认证信息
当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求。 - 反向代理:依赖同源的服务端对请求做一个转发处理,将请求从跨域请求转换成同源请求。反向代理的实现方式为在页面同域下配置一套反向代理服务,页面请求同域的服务端,服务端请求上游的实际的服务端,之后将结果返回给前端。
- JSONP:通过 script 标签 get 请求某个 api,不同于上面两个方法,该方法需要前后端配合实现
非常用:
- postMessage:即在两个 origin 下分别部署一套页面 A 与 B,A 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息。
- window.name:主要是利用 window.name 页面跳转不改变的特性实现跨域,即 iframe 加载一个跨域页面,设置 window.name,跳转到同域页面,可以通过 $('iframe').contentWindow.name 拿到跨域页面的数据。
- document.domain: 可将相同一级域名下的子域名页面的 document.domain 设置为一级域名实现跨域。可将同域不同端口的 document.domain 设置为同域名实现跨域(端口被置为 null)。
另外:
- LocalStorage / SessionStorage 跨域
LocalStorage 和 SessionStorage 同样受到同源策略的限制。而跨域读写的方式也可以使用前文提到的 postMessage。 - 跨域与监控
前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在<script>
标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈。 - 跨域与图片
前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决。
如何实现浏览器标签页之间的通信
中介者模式
- 使用 WebSocket 将消息发送给服务器再在推给另一个客户端
- 调用本地存储如 localStorage 传递数据
发布订阅者
- 调用 webWorker 的 postMessage
正向代理和反向代理
正向代理:
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
反向代理:
服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡) 等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。
一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
讲讲CDN负载均衡
CDN 是一个内容分发网络,通过对源网站资源的缓存,利用本身多台位于不同地域、不同运营商的服务器,向用户提供资源就近访问的功能。用户的请求并不是直接发送给源网站,而是发送给 CDN 服务器,由 CDN 服务器将请求定位到最近的含有该资源的服务器上去请求。这样有利于提高网站的访问速度,同时通过这种方式也减轻了源服务器的访问压力。
CDN 访问过程:
- 用户输入访问的域名,操作系统向 LocalDns 查询域名的 ip 地址
- LocalDns 向 ROOT DNS 查询域名的授权服务器 (这里假设 LocalDns 缓存过期),ROOT DNS 将域名授权 dns 记录回应给 LocalDns
- LocalDns 得到域名的授权 dns 记录后,继续向域名授权 dns 查询域名的 ip 地址域名授权 dns 查询域名记录后 (一般是 CNAME),回应给 LocalDns
- LocalDns 得到域名记录后,向智能调度 DNS 查询域名的 ip 地址,智能调度 DNS 根据一定的算法和策略 (比如静态拓扑,容量等), 将最适合的 CDN 节点 ip 地址回应给 LocalDns
- LocalDns 将得到的域名 ip 地址,回应给 用户端,用户得到域名 ip 地址后,访问站点服务器
- CDN 节点服务器应答请求,将内容返回给客户端.(缓存服务器一方面在本地进行保存,以备以后使用,二方面把获取的数据返回给客户端,完成数据服务过程)
localhost:3000 与 localhost:5000 的 cookie 信息是否共享
根据同源策略,cookie 是区分端口的,但是浏览器实现来说,“cookie 区分域,而不区分端口,也就是说,同一个 ip 下的多个端口下的 cookie 是共享的!不同协议 http 和 https,也可以共享 但是带有 Secure 属性的不能被 http 共享 带有 HttpOnly 属性的 cookie 无法被 document.cookie 访问
为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片
- 能够完成整个 HTTP 请求 + 响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF 的最低合法体积最小(最小的 BMP 文件需要 74 个字节,PNG 需要 67 个字节,而合法的 GIF,只需要 43 个字节)
localhost:3000 与 localhost:5000 的 cookie 信息是否共享
根据同源策略,cookie 是区分端口的,但是浏览器实现来说,“cookie 区分域,而不区分端口,也就是说,同一个 ip 下的多个端口下的 cookie 是共享的!不同协议 http 和 https,也可以共享 但是带有 Secure 属性的不能被 http 共享 带有 HttpOnly 属性的 cookie 无法被 document.cookie 访问
# 安全
接口如何防刷
- 网关控制流量洪峰,对一个事件段内出现流量异常,可以拒绝请求
- 源 ip 请求个数限制,对请求来源的 ip 请求个数做限制
- http 请求头信息校验,如 host,User-Agent,Referer
- 对用户唯一身份 uid 进行限制和校验。id 的格式、时效性
- 前后端协议采用二进制的方式进行交互或者协议采用签名机制
- 人机验证码、短信、滑块验证之类的
cookie 和 token 都存放在 header 中,为什么不会劫持 token?
- token 不是防止 XSS 的,而是为了防止 CSRF 的
- CSRF 攻击的原因是浏览器会自动带上 cookie,而浏览器不会自动带上 token
介绍token的实现
- 需要一个 secret(随机数)
- 后端利用 secret 和加密算法 (如:HMAC-SHA256) 对 payload (如账号密码) 生成一个字符串 (token) 返回前端
- 前端每次 request 在 header 中带上 token
- 4. 后端用同样的算法解密
常见的网络攻击
参考站内贴子 网络攻击
(https://evilmood.github.io/mublog/core/ 网路攻击 /)
# 存储
web存储方式与特性
存储 | 大小 | 特性 |
---|---|---|
cookie | 4KB | 设置时间内有效,发送请求时携带 |
localStorage | 5MB | 持久存储 |
sessionStorage | 5MB | 关闭页面自动删除 |
indexDB | 无限 | 数据库 |
谈谈cookie的缺点
缺点:
- 大小不能超过 4KB
- cookie 被人拦截后可以直接使用
强缓存和协商缓存
浏览器请求时会执行强缓存,若本地没有缓存则会发送请求进行协商缓存强缓存:
通过 expire 记录过期日期,http1.1 中新增 cache-control 克服请求头限制,且优先级高于 expire
cache-control:no-cache/private/max-age=3600...(具体可到 MDN 查看)
(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
协商缓存
ETag+if-none-match 和 last-modified+if-modified-since, 前者优先级大于后者
服务器返回页面 A,并在给 A 加上一个 ETag。 客户端展现该页面,并将页面连同 ETag 一起缓存。 客户再次请求页面 A,并将上次请求时服务器返回的 ETag 一起传递给服务器。 服务器检查该 ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应 304(未修改 ——Not Modified)和一个空的响应体。
浏览器缓存位置
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由缓存哪些文件、如何匹配缓存、如何读取缓存,而缓存是可持续性的。Service Worker 也是 PWA 的核心技术。
Memory Cache
Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据很高效,但是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面基本上是最大的。它会根据 HTTPHeader 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。
Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被时候用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂(大约 5 分钟)。
(https://juejin.cn/post/6844903747357769742?utm_source=gold_browser_extension)
优先级 Service Worker > memory cache > disk cache > Push Cache
最佳实践:资源尽可能命中强缓存,且在资源文件更新时保证用户使用到最新的资源文件
强缓存只会命中相同命名的资源文件。
在资源文件上加 hash 标识(webpack 可在打包时在文件名上带上)。
通过更新资源文件名来强制更新命中强缓存的资源。
no-cache和no-store 的区别。
前者可以在客户端存储资源,但每次都必须去服务端做新鲜度校验,决定从服务端获取新资源(200)还是作缓存(304)处理
后者为不做缓存处理
HTTP状态码304是多好还是少好
服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制,当客户端在此对这些页面进行请求,服务器会根据缓存内容判断页面与之前是否相同,若相同便直接返回 304,此时客户端调用缓存内容,不必进行二次下载。
状态码 304 不应该认为是一种错误,而是对客户端有缓存情况下服务端的一种响应。
搜索引擎蜘蛛会更加青睐内容源更新频繁的网站。通过特定时间内对网站抓取返回的状态码来调节对该网站的抓取频次。若网站在一定时间内一直处于 304 的状态,那么蜘蛛可能会降低对网站的抓取次数。相反,若网站变化的频率非常之快,每次抓取都能获取新内容,那么日积月累,的回访率也会提高。
产生较多 304 状态码的原因:
- 页面更新周期长或不更新
- 纯静态页面或强制生成静态 html
304 状态码出现过多会造成以下问题:
- 网站快照停止;
- 收录减少;
- 权重下降。
# 性能优化
如何渲染几万条数据并不卡住界面
这道题考察了如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次。
图片懒加载
img.offsetTop < window.innerHeight + document.body.scrollTop;