从 URL 输入到页面展现到底发生什么?
从开发 &运维角度方面来看,总体来说分为以下几个过程:
DNS 解析:将域名解析成 IP 地址
TCP 连接:TCP 三次握手
发送 HTTP 请求
服务器处理请求并返回 HTTP 报文
浏览器解析渲染页面
断开连接:TCP 四次挥手
一、什么是 URL?
URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。
scheme: // host.domain:port / path / filename ? abc = 123 # 456789
二、DNS 域名解析
在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。
IP 地址
什么是域名解析
浏览器如何通过域名去查询 URL 对应的 IP 呢?
DNS 的优化与应用
DNS 缓存
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
DNS 负载均衡
(DNS重定向) DNS负载均衡技术的实现原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时, DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问 引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。
大家耳熟能详的 CDN(Content Delivery Network)就是利用 DNS 的重定向技术,DNS 服务器会返回一个跟用户最接近的点的 IP 地址给用户,CDN 节点的服务器负责响应用户的请求,提供所需的内容。
dns-prefetch
DNS Prefetch 是一种 DNS 预解析技术。当你浏览网页时,浏览器会在加载网页时对网页中的域名进行解析缓存,这样在你单击当前网页中的连接时就无需进行 DNS 的解析,减少用户等待时间,提高用户体验。
OSI 参考模型与 TCP/IP 四层模型
三、TCP 三次握手
客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口
(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息
(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”
(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)
四、发送 HTTP 请求
TCP 三次握手结束后,开始发送 HTTP 请求报文。
为避免篇幅过长,http协议、缓存等相关内容请参阅:
从 HTTP 到 WEB 缓存
五、服务器处理请求并返回 HTTP 报文
每台服务器上都会安装处理请求的应用——Web server。常见的 web server 产品有 apache、nginx、IIS、Lighttpd 等。
假装我是一个传统的MVC模型,RD同学请无视
六、浏览器解析渲染页面
浏览器的主要构成
1.多进程的浏览器
浏览器是多进程的,有一个主控进程,以及每一个 tab 页面都会新开一个进程(某些情况下多个 tab 会合并进程),参考:前端进阶面试题详细解答
进程可能包括主控进程,插件进程,GPU,tab 页(浏览器内核)等等
Browser 进程:浏览器的主进程(负责协调、主控),只有一个
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
GPU 进程:最多一个,用于 3D 绘制
浏览器渲染进程(内核):默认每个 Tab 页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 tab 会合并成一个进程)
2.多线程的浏览器内核
每一个 tab 页面可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:
GUI 线程
JS 引擎线程
事件触发线程
定时器线程
网络请求线程
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
以webkit内核为例
1. HTML 解析,构建 DOM
简单的理解,这一步的流程是这样的:浏览器解析 HTML,构建 DOM 树。解析 HTML 到构建出 DOM 当然过程可以简述如下:
其中比较关键的几个步骤
2. 解析 CSS,生成 CSS 规则树
同理,CSS 规则树的生成也是类似。
3. 合并 DOM 树和 CSS 规则,生成 render 树
当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了
一般来说,渲染树和 DOM 树相对应的,但不是严格意义上的一一对应,因为有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者 display: none 等
4. 布局 render 树(Layout/Reflow),负责各元素尺寸、位置的计算
布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸。
5. 绘制 render 树(Paint),绘制页面像素信息
绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。
Layout,也称为 Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了
七、断开连接
当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手。
发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。
(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。
(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。
(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)
发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。
(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
执行结果如下:
在这道题目中,我们需要知道,无论是 thne 还是 catch 中,只要 throw 抛出了错误,就会被 catch 捕获,如果没有 throw 出错误,就被继续执行后面的 then。
new 操作符的实现原理
new 操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
具体实现:
实现 call、apply 及 bind 函数
(1)call 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
处理传入的参数,截取第一个参数后的所有参数。
将函数作为上下文对象的一个属性。
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性。
返回结果。
(2)apply 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
将函数作为上下文对象的一个属性。
判断参数值是否传入
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性
返回结果
(3)bind 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
保存当前函数的引用,获取其余传入参数值。
创建一个函数返回
函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
对 rest 参数的理解
扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组:
这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:
这就是 … rest 运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。
JavaScript 有哪些内置对象
全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类:
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等
(10)控制抽象对象例如 Promise、Generator 等
(11)反射。例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他。例如 arguments
总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
箭头函数使用被称为 “胖箭头” 的操作
=>
定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。箭头函数常用于回调函数中,包括事件处理器或定时器
箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
没有原型、没有 this、没有 super,没有 arguments,没有 new.target
不能通过 new 关键字调用
一个函数内部有两个方法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上
当直接调用时,执行 [[Call]] 方法,直接执行函数体
箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。
isNaN 和 Number.isNaN 函数的区别?
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
懒加载的概念
懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。
如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
常见的 HTTP 请求方法
GET: 向服务器获取数据;
POST:将实体提交到指定的资源,通常会造成服务器资源的修改;
PUT:上传文件,更新数据;
DELETE:删除服务器上的对象;
HEAD:获取报文首部,与 GET 相比,不返回报文主体部分;
OPTIONS:询问支持的请求方法,用来跨域请求;
CONNECT:要求在与代理服务器通信时建立隧道,使用隧道进行 TCP 通信;
TRACE: 回显服务器收到的请求,主要⽤于测试或诊断。
参考:前端进阶面试题详细解答
作用域
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找
作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找
作用域链的创建过程跟执行上下文的建立有关....
作用域可以理解为变量的可访问性,总共分为三种类型,分别为:
全局作用域
函数作用域
块级作用域,ES6 中的
let
、const
就可以产生该作用域
其实看完前面的闭包、this
这部分内部的话,应该基本能了解作用域的一些应用。
一旦我们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。
首先作用域链是在定义时就被确定下来的,和箭头函数里的 this 一样,后续不会改变,JS 会一层层往上寻找需要的内容。
其实作用域链这个东西我们在闭包小结中已经看到过它的实体了:
[[Scopes]]
图中的 [[Scopes]]
是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]
。
1. 全局作用域
全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量
从这段代码中我们可以看到,globalName 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的
当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。
2. 函数作用域
函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域
除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的
3. 块级作用域
ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。
在 JS 编码过程中 if 语句
及 for
语句后面 {...}
这里面所包括的,就是块级作用域
从这段代码可以看出,变量 a 是在
if 语句{...}
中由let 关键词
进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义
HTTP 1.0 和 HTTP 1.1 之间有哪些区别?
HTTP 1.0 和 HTTP 1.1 有以下区别:
连接方面,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。
资源请求方面,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
缓存方面,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。
http1.1 中新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。
http1.1 相对于 http1.0 还新增了很多请求方法,如 PUT、HEAD、OPTIONS 等。
什么是物理像素,逻辑像素和像素密度,为什么在移动端开发时需要用到 @3x, @2x 这种图片?
以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV 元素宽度为 414px,这个 DIV 就会填满手机的宽度;
而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3 个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。
对于图片来说,为了保证其不失真,1 个图片像素至少要对应一个物理像素,假如原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片才能保证 1 个物理像素至少对应一个图片像素,才能不失真。 当然,也可以针对所有屏幕,都只提供最高清图片。虽然低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽浪费和下载延迟,但从结果上说能保证图片在所有屏幕上都不会失真。
还可以使用 CSS 媒体查询来判断不同的像素密度,从而选择不同的图片:
CDN 的使用场景
**使用第三方的 CDN 服务:**如果想要开源一些项目,可以使用第三方的 CDN 服务
**使用 CDN 进行静态资源的缓存:**将自己网站的静态资源放在 CDN 上,比如 js、css、图片等。可以将整个项目放在 CDN 上,完成一键部署。
**直播传送:**直播本质上是使用流媒体进行传送,CDN 也是支持流媒体传送的,所以直播完全可以使用 CDN 来提高访问速度。CDN 在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。
代码输出结果
输出结果如下:
promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要 promise 内部的状态发生变化,因为这里内部没有发生变化,一直处于 pending 状态,所以不输出 3。
水平垂直居中的实现
利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:
利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况
使用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:
代码输出结果
输出结果如下:
代码执行过程如下:
首先遇到 Promise 构造函数,会先执行里面的内容,打印
1
;遇到定时器
steTimeout
,它是一个宏任务,放入宏任务队列;继续向下执行,打印出 2;
由于
Promise
的状态此时还是pending
,所以promise.then
先不执行;继续执行下面的同步任务,打印出 4;
此时微任务队列没有任务,继续执行下一轮宏任务,执行
steTimeout
;首先执行
timerStart
,然后遇到了resolve
,将promise
的状态改为resolved
且保存结果并将之前的promise.then
推入微任务队列,再执行timerEnd
;执行完这个宏任务,就去执行微任务
promise.then
,打印出resolve
的结果。
网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
(1)DNS 劫持: (输⼊京东被强制跳转到淘宝这就属于 dns 劫持)
DNS 强制解析: 通过修改运营商的本地 DNS 记录,来引导⽤户流量到缓存服务器
302 跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起 302 跳转的回复,引导⽤户获取内容
(2)HTTP 劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于 http 明⽂传输,运营商会修改你的 http 响应内容(即加⼴告)
DNS 劫持由于涉嫌违法,已经被监管起来,现在很少会有 DNS 劫持,⽽http 劫持依然⾮常盛⾏,最有效的办法就是全站 HTTPS,将 HTTP 加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
僵尸进程和孤儿进程是什么?
孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
如何优化动画?
对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作 DOM,就就会导致页面的性能问题,我们可以将动画的position
属性设置为absolute
或者fixed
,将动画脱离文档流,这样他的回流就不会影响到页面了。
HTTP 响应报文的是什么样的?
请求报⽂有 4 部分组成:
响应⾏
响应头
空⾏
响应体
响应⾏:由网络协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK 。
响应头:响应部⾸组成
响应体:服务器响应的数据
评论