1、输入url到显示页面发生了什么
- 浏览器的地址栏输入URL并按下回车;
- 浏览器查找当前URL是否存在缓存,并比较缓存是否过期;
- DNS解析URL对应的IP。先在本地hosts文件查找,如果没找到,再到DNS服务器(由中国电信,中国移动等服务商提供)查找;
- 根据IP建立TCP连接(三次握手);
- 发送 HTTP 请求报文,包括(1)请求方法URI协议/版本(2)请求头(Request Header)(3)请求正文;
- 服务器处理请求,返回响应报文,浏览器接收HTTP响应,包含(1)状态行(2)响应头(Response Header)(3)响应正文;
- 如果服务器发现这个url需要重定向,则会返回重定向的响应,这是为了seo,
301
永久重定向,302
临时重定向,如果有重定向,浏览器会重新请求重定向之后的地址; - 关闭TCP连接(四次挥手);
- 浏览器解析html并渲染到页面;
ip和域名的对应关系:
- 任意多个域名可以解析到同一个IP,服务器根据header判断请求的是哪一个域名。
- 一个域名也可以对应多个IP,DNS服务商根据你的位置和运营商返回不同的解析结果。
2、html解析
解析顺序
- 先把html下载下来;
- 分析文档结构,具体的是一个树型结构,是各个节点的层级关系,如果文档中有资源不符合安全策略(如有的页面不允许加载跨域的资源),则会给出警告。并对所有的资源进行优先级排序;
- 开启下载线程,按照排好的优先级进行资源下载,一般css和font这类文件会优先加载。浏览器对同一域名下的下载并发不超过 6 个,不同域名的话,在浏览器设置的最大并发上限以内(默认是10个);
- 同时开启文档结构解析的线程。(4和3是同时进行的,算两个线程)自上而下构建dom,生成DOM Tree,按照 深度优先 的原则,将一个节点的全部子节点生成完成之后才会开始生成当前节点的兄弟节点;
- 浏览器会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。根据 Rendering Tree计算每个元素的位置、大小,这个过程叫reflow(回流)。当元素的位置、大小确定之后再去计算每个元素的字体大小、颜色等,这个过程叫repaint(重绘)。
阻塞型资源:
- 内联css
- 内联javascript
- 外联普通javascript
- 外联defer javascript
- javascript标签之前的外联css
非阻塞型资源:
- javascript标签之后的外联css
- image
- iframe
外联async javascript如果下载完后,dom还没解析完毕,会直接执行,阻塞。如果下载完后dom已经解析完毕,则不算阻塞。下载不会阻塞,运行可能会阻塞
解析dom的规则
- 遇到 DOM 标签时,执行 DOM 构建,将该 DOM 元素添加到
dom tree
中。 - 遇到内联的css,会生成cssom,和前边的
cssom
合并成css rule tree
,浏览器解析CSS选择器是按照从右往左的顺序解析的 - 遇到内联的javascript会直接执行阻塞后续内容的解析,直接执行js;
- 遇到
link
标签时,不会阻塞后续内容的解析(比如 DOM 构建),检查 link 资源是否已下载,如果已下载,则构建 cssom,前边的cssom合并成css rule tree,未下载则开启线程下载。 - 遇到
script
标签时,首先阻塞后续内容的解析,同时检查该script是否已经下载下来,如果已下载,便执行代码.如果还未下载完毕,则开启线程下载,现在完成后立即执行。 - 遇到
script defer
标签时,先检查是否已经下载,如果已经下载,则继续往下走构建dom,如果未下载,则开启新线程下载,下载完成后,如果dom树还未构造完毕,则等待dom树构建完毕后执行。如故dom树已经构建完毕,则立即执行。在派发DOMContentLoaded
事件之前 - 遇到
script async
标签时, 先检查是否已经下载,如果已经下载,则阻塞后续内容解析,直接执行。如果还未下载,则开启线程下载,下载完成后立即执行。可能在dom树构建完成(DOMContentLoaded)之前执行,也可能在之后执行。但一定在onload
之前 - 遇到
<img>
或者<video>
标签,不会阻塞页面,如果没有下载完毕,则进入下载线程的队列。img和video不影响DOMContentLoaded
事件的派发。 - html中每遇到< script >标签,页面就会重新渲染(浏览器会将dom tree和css rule tree合并成render tree)一次,因为要保证标签中的JS代码拿到的都是最新的样式。
DOMContentLoaded和onload
DOMContentLoaded为页面dom构建完成,同步js的代码执行完毕。图片资源,js里引入的异步资源等不一定加载完毕了;
onload在DOMContentLoaded之后,图片资源,异步的js资源都下载完毕才会触发。
3、前端缓存
- 后端缓存(数据库缓存,CDN缓存,代理服务器缓存等)
- 前端缓存,按照优先级
Service Worker
、Memory Cache
、Disk Cache
(也叫http chache
,分为强缓存
和协商缓存
)、Push Cache
;
Service Worker
- 是运行在浏览器背后的独立线程。主要用来实现离线缓存、消息推送和网络代理等功能,
PWA
(Progressive Web App渐进式的web App)应用较多。
memory cache
- 内存中的缓存,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效;
- 两种资源:(1)浏览器preloader发现的资源,比如css.html.font这些会优先下载;(2)显式指定的预加载资源
<link rel="preload">
,也会被放入 memory cache 中 - memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的
<img>
,两个 href 相同的<link>
) 都实际只会被请求最多一次,避免浪费。不过在匹配缓存时,除了匹配完全相同的 URL 之外,还会比对他们的类型,CORS 中的域名规则等。因此一个作为脚本 (script) 类型被缓存的资源是不能用在图片 (image) 类型的请求中的,即便他们 src 相等。 - 使用
no-store
即便是 memory cache 也不会存储
Disk Cache(http chache)缓存策略
强缓存
Cache-Control
的几个取值含义:
- private: 仅浏览器可以缓存
- public: 浏览器和代理服务器都可以缓存,用的多一些
- max-age=xxx 过期时间(重要)
- no-cache 不进行强缓存(重要)
- no-store 不强缓存,也不协商缓存,真正意义上的不缓
判断该资源是否命中强缓存,就看 response 中 Cache-Control 的值,如果有max-age=xxx
秒,则命中强缓存。如果Cache-Control的值是no-cache
,或者max-age=0
(强缓存,但已经过期了)说明没命中强缓存,走协商缓存。
Cache-Control优先级高于Expires(http旧的版本会用到)
Expires 指缓存过期的时间,会有一定弊端,比如用户修改了本地时间
简单粗暴,如果资源没过期,就取缓存,如果过期了,则请求服务器,看是走协商缓存(服务端返回304),还是请求新的数据
协商缓存
- response header中的
ETag
和Last-Modified
,request header中对应If-None-Match
和If-Modified-Since
- ETag优先级高于Last-Modified,会先判断ETag,再判断Last-Modified
ETag是为了解决几个Last-Modified比较难解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
- 某些服务器不能精确的得到文件的最后修改时间。
一些操作触发的缓存策略
- 地址栏回车,页面链接跳转,新开窗口,前进后退这些操作会走强缓存
- F5 会 跳过强缓存规则,直接走协商缓存;
- Ctrl+F5 ,跳过所有缓存规则,和第一次请求一样,重新获取资源
vue项目较为合理的缓存方案
- HTML:使用协商缓存。
- CSS&JS&图片:使用强缓存,文件命名带上hash值。
Push Cache
- Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右。
4、浏览器存储
Cookie
发送的cookie格式:
Cookie: name1=value1 [; name2=value2]
name
:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。value
:存储在cookie中的字符串值。最好为cookie的name和value进行url编码domain
:cookie对于哪个域名下是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:m.baidu.com),也可以不包含它(如:.baidu.com,则对于baidu.com的所有子域都有效).path
: 表示这个cookie影响到的路径,浏览器跟会根据这项配置,向指定域中匹配的路径发送cookie。expires
:过期时间,表示cookie自删除的时间戳。如果不设置这个时间戳,cookie就会变成会话Session类型的cookie,浏览器会在页面关闭时即将删除所有cookie,这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。max-age
: 与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires。HttpOnly
: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。secure
: 安全标志,指定后只有在使用SSL(https)链接时候才会发送到服务器,如果是http链接则不会传递该值。但是也有其他方法能在本地查看到cookie
Cookie的优点
- cookie键值对形式,结构简单
- 可以配置过期时间,不需要任何服务器资源存在于客户端上,
- 可以弥补HTTP协议无状态的部分不足
- 无兼容性问题。
Cookie的缺点
- 大小数量受到限制,每个domain最多只能有20条cookie,每个cookie长度不能超过4096或8192 字节,否则会被截掉。
- 用户配置可能为禁用 有些用户禁用了浏览器或客户端设备接收 Cookie 的能力,因此限制了这一功能。
- 增加流量消耗,每次请求都需要带上cookie信息。
- 安全风险,黑客可以进行Cookie拦截、XSS跨站脚本攻击和Cookie欺骗,历史上因为Cookie被攻击的网站用户不在少数,虽然可以对Cookie进行加密解密,但会影响到性能。
localStorage/sessionStorage
- 存储数据量大,5MB。
- 不会随http请求一起发送,有效的减少了请求大小
- sessionStorage只作用于当前窗口,不能跨窗口读取其他窗口的SessionStorage数据库信息,浏览器每次新建、关闭都是直接导致当前窗口的数据库新建和销毁。
- localStorage除非手动清理掉,否则会一直存在,不论浏览器,页面标签是否关闭。
indexedDB
- 浏览器内置的数据库,和
NoSQL
很像,与service work
搭配,实现离线访问; - 数据储存量无限大(只要你硬盘够),Chrome规定了最多只占硬盘可用空间的1/3;
- 操作大量数据的时候,可能存在性能上的消耗。
- 兼容性问题,只有ie11以上支持。
5、http版本
HTTP/1.1
- 线头阻塞 (Head Of Line Blocking) 问题
- TCP 连接数限制.对于同一个域名,浏览器最多只能同时创建 6~8 个 TCP 连接 (不同浏览器不一样)。为了解决数量限制,出现了 域名分片 技术,其实就是资源分域,将资源放在不同域名
HTTP/2
- HTTP/2解决了HTTP的队头阻塞问题,但是并没有解决TCP队头阻塞问题
- 二进制分帧,多路复用
- 服务端推送
HTTP/3
- Google开发QUIC协议,基于UDP实现,减少了 RTT
- 解决了队头阻塞问题
6、XMLHttpRequest、fetch和WebSocket
XMLHttpRequest
- 低版本IE的实现为ActiveXObject
- JQuery的$ajax是对XHR的封装,是针对mvc的编程模式,不太适合目前mvvm的编程模式。
- axios也是对原生XHR的一种封装,不过是Promise实现版本
fetch
- 写法简单,是Promise的实现
- 无法取消请求,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行。
- fetch不支持同步请求
- fetch默认不会带cookie,需要添加配置项
- fetch没有办法原生监测请求的进度,而XHR可以
WebSocket
var s = new WebSocket('ws://www.a.com/s.php')
// 必须传入绝对URL,可以是任何网站
7、XSS和CSRF
XSS
Cross-Site Scripting,跨站脚本攻击,解决方法:
- cookie设置httpOnly
- url,搜索参数等进行转义
- 输入内容,危险字符过滤,长度限制
CSRF
Cross-site request forgery跨站请求伪造
8、跨域
同源策略
协议,域名(子域名和主域名),端口三个都相同被认为是同源。如果当前网址(http://news.a.com
),以下都不是同源
https://news.a.com
不同协议http://news.a.com:8080
不同端口http://home.a.com
不同域名
同源策略的约束
- Cookie、LocalStorage 和 IndexDB 无法读取。
- JavaScript 的 API 中的一些引用,无法获得。如获取iframe里的dom节点,进行操作是不被允许的
- AJAX 请求不能发送。(也就是无法使用XMLHttpRequest)
解决跨域的方法
跨域资源共享(CORS)
- 在发送请求时会有个Origin头表示请求页面的源信息, 如果服务器返回的Access-Control-Allow-Origin中有相同的源信息或是* 那么就可以跨域请求信息
- 请求和响应都不包含cookie
jsonp
- jsonp方法主要是创建script标签来获得数据,一般通过请求后面跟?callback=fn 回掉函数来获取数据。
9、js执行顺序,宏任务和微任务
一个进程的运行,当然需要很多个线程互相配合,
JS是单线程的,onclick回调、setTimeout、Ajax这些是因为浏览器或node(宿主环境)是多线程的,即浏览器搞了几个其他线程去辅助JS线程的运行。
浏览器线程
- GUI 渲染线程(DOM的渲染)
- JS 引擎线程
- 定时器触发线程 (setTimeout)
- 浏览器事件线程 (onclick)
- http 异步线程
- 轮询处理线程event loop
event loop执行顺序
- 一开始整个脚本作为一个宏任务执行;
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列;
- 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完;
- 执行浏览器UI线程的渲染工作;
- 检查是否有Web Worker任务,有则执行;
- 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空;
tips:
- 宏任务必然是在微任务之后才执行的(因为微任务实际上是宏任务的其中一个步骤)
- DOM Tree的修改是实时的,而修改的Render到DOM上才是异步的
- new Promise执行本身时是属于同步代码,只有.then才是微任务
- async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似
微任务
Promise.then()
和catch()
;- Promise为基础开发的其它技术,比如fetch API;
- Node独有的
process.nextTick
; - V8的垃圾回收过程;
MutationObserver
;
宏任务
setTimeout
、setInterval
、setImmediate
(node环境)、- script、
- UI rendering、
- I/O、
requestAnimationFrame
(特殊)- 网络请求等等
10、垃圾回收和内存泄漏
垃圾回收:
JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
垃圾回收的方式
- 标记清楚(用的最多)
- 引用计数
导致内存泄漏的情况
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
- 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。