[TOC]

零、目录

image.png

一、浏览器安全

1. 什么是 XSS 攻击?

xss是跨站脚本攻击;分三种类型,1.存储型(攻击者将恶意代码通过请求发送到服务器,服务器把东西存到数据库,请求的时候从数据库中取出来拼接到html上会执行),2.反射型(攻击者将恶意代码拼接在url上,浏览器将url的代码取下来拼接在html上也会立即执行),3.DOM型(攻击者将恶意代码放在url后面,浏览器将url上的恶意代码取下来,通过dom的操作去执行恶意代码);

举个例子:比如黑客在一个网站注入可执行的恶意代码,并且页面没有对这段代码进行处理,当其他人进入这个页面的时候,点一下这段代码,就会执行这段代码,这样就会把用户自己的cookie等信息发送请求发送过去,这样用户的信息就泄露了;

解决方案:1.所以我们可以把script标签尖括号进行转义成&符npmb等,这样就不可被执行了;2.还可以用CSP建立白名单,这个只需要配置,如何拦截是浏览器那边实现的;3.cookie设置http-only(让js不可获取cookie)

(1)概念

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
image.png
攻击者可以通过这种攻击方式可以进行以下操作

  • 获取页面的数据,如DOM、cookie、localStorage;
  • DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;
  • 破坏页面结构
  • 流量劫持(将链接指向某网站);

(2)攻击类型

XSS 可以分为存储型、反射型和 DOM 型

  • 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行,最终完成 XSS 攻击。
  • DOM 型指的通过修改页面的 DOM 节点形成的 XSS

1)存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到⽬标⽹站的数据库中。
  2. ⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出拼接在 HTML 中返回给浏览器。
  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
  4. 恶意代码窃取⽤户数据发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。

2)反射型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. ⽤户打开带有恶意代码的 URL 时,⽹站服务端将恶意代码从 URL 中取出拼接在 HTML 中返回给浏览器。
  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥

反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击

3)DOM 型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码
  2. ⽤户打开带有恶意代码的 URL
  3. ⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL 中的恶意代码并执⾏
  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。

2. 如何防御 XSS 攻击?

可以看到XSS危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:

  1. 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断
  2. 使用 CSP (下面有详细说到csp),CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
    1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现
    2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式

3.对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
image.png
image.png
npm中也有工具解决这个问题;自动转换sript的箭头了
image.png

3. 什么是 CSRF 攻击?

csrf是跨站请求伪造攻击,通过引诱客户点击第三方网站冒充用户执行一些操作

举例子:例如用户登录一个银行账户转钱,登录银行网站后用户登录信息放在cookie上了,当黑客给用户发邮件,用户打开了这个邮件,邮件里有个链接,用户点击链接打开了链接,其实这个链接是一个接口,这个接口直接会调用户银行付款的接口,把钱转出去,因为这个时候用户登录过了,所以用户信息都已经存在了cookie里,这时候再通过黑客调用了付款接口就会验证信息通过直接把钱转出去,

解决方法:1.校验该接口是用户正常请求还是通过黑客攻击的方式,可以通过origin去拿到请求连接是否是允许访问的站点发送的请求;2.还可以CSRF Token验证,双方约定token的值,前后端对token进行解析,对比token是否一致;3.然后还可以做验证,不能直接调接口,还需要验证码;4.严格的cookie策略

(1)概念

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者可以利用这个登录状态绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是**利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充**。
image.png

(2)攻击类型

常见的 CSRF 攻击有三种

  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交
  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时自动提交这个表单
  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

4. 如何防御 CSRF 攻击?

CSRF 攻击可以使用以下方法来防护

  • 进行同源检测,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
  • 使用 CSRF Token 进行验证服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
  • Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离
  • 设置 cookie 属性的时候设置 Samesite限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。

image.png

5. 什么是中间人攻击?如何防范中间人攻击?

中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者通讯两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容

攻击过程如下:

  1. 客户端发送请求到服务端,请求被中间⼈截获
  2. 服务器向客户端发送公钥
  3. 中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个伪造的公钥,发给客户端
  4. 客户端收到伪造的公钥后,⽣成加密hash值发给服务器
  5. 中间⼈获得加密hash值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器
  6. 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

6. 有哪些可能引起前端安全的问题?

  • 跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了与 CSS 区分所以被称作 XSS。早期常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不限于 JavaScript / CSS / Flash 等;
  • iframe的滥⽤: iframe中的内容是由第三⽅来提供的,默认情况下他们不受控制,他们可以在iframe中运⾏JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端⽤户体验
  • 跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻击
  • 恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题。

7. 网络劫持有哪几种,如何防范?

⽹络劫持分为两种:
(1)DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)

  • DNS强制解析: 通过修改运营商的本地DNS记录,来引导⽤户流量到缓存服务器
  • 302跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起302跳转的回复,引导⽤户获取内容

(2)HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)

DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。

8. 什么是 CSP?

CSP(Content-Security-Policy)内容安全策略,在http协议中为了让我们的网站变得更加的安全就有了CSP;详细内容可以搜索cmd内的csp;以下是简单说明

它的作用:

  • 限制资源获取,可以限制 从哪里获取又发送到哪里
  • 报告资源获取越权, 可以在网页当中获取了不该获取的资源的时候给服务进行报告,让我们知道我们的网页出现了一些情况并作出一些调整

它的限制方式:

  • default-src限制全局跟连接有关的东西
  • 限制资源类型

image.png
我们的请求发送的目标限制;图片可以从哪几个网址下面进行加载;样式和脚本的资源可以从哪个网址加载;
这些需要从外链加载资源的都可以进行一个资源类型的限制

使用:
(一)在返回头中做限制
场景:防止XSS攻击,想限制直接在html中写js,
image.pngimage.png

'Content-Security-Policy':'default-src \'self\''     //只允许使用本地资源脚本


Content-Security-Policy':'default-src \'self\' http://baidu.com'  
//只允许使用本地资源脚本,不允许使用外链  还有该域名下的内容可加载

'Content-Security-Policy':'default-src \'self\';form-action \'self\''
//只允许使用本地资源脚本,不允许使用外链  限制form表单提交的范围


'Content-Security-Policy':'script-src \'self\''
//只允许使用本地的src图片资源

'Content-Security-Policy':'default-src \'self\';report-uri /服务器路径'
'Content-Security-Policy':'default-src \'self\';report-uri /report'
// 如果出现了我们不希望的情况,有让浏览器主动向服务器发送请求进行汇报


'Content-Security-Policy-Report-Only':'default-src \'self\';report-uri /report'
// 只做report的工作,不强制它不能加载

如果设置了CSP汇报 会在请求列表看到被限制的report的请求
image.png

如果是收到CSP限制的资源的话 会看到blocked:CSP
image.png

(二)在meta标签中做限制
image.png

并且report-uri(汇报)的功能不能在标签中设置,只能在header中去设置

9.遇到了哪些安全问题么,怎么解决的?

https://juejin.cn/post/6898124066063024136
xss、csrf、爬虫、薅羊毛等安全问题
传输加密、接口加签、环境变量、token、输入校验等

10.你认为前端传输过程的参数需要加密吗?为什么?怎么加密更安全?

看情况而定,
常规的请求参数:搜索关键词,分页参数没有任何加密的必要。
特殊的字段:身份证校验肯定是需要加密的。
同时前端游戏数据也是必须要加密的,不仅仅需要加密还需要加签名。

11.中间人攻击是什么?

中间人攻击是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。
image.png

12.浏览器有哪些高优先级事件举例一下

事件的input,input事件监听处理,定时器,每一帧绘制画面进行layout计算,painter渲染;

二、进程与线程

1. 进程与线程的概念

(1)定义

从本质上说,进程和线程都是 CPU 工作时间片的一个描述

  • 进程是操作系统分配的基本单位;进程中包含线程;(进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。)
  • 线程是进程中的更小单位,线程是由进程所管理的;描述了执行一段指令所需的时间。

进程资源分配的最小单位线程CPU调度的最小单位

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的
从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间
如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存

(2)特点

进程和线程之间的关系有以下四个特点
(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。
(2)线程之间共享进程中的数据
(3)当一个进程关闭之后,操作系统会回收进程所占用的内存当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
(4)
进程之间的内容相互隔离。进程隔离就是为了使操作系统中的进程互不干扰**,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。

多进程的好处
提升了稳定性(某个网页挂了,另一个tab也还能用);具备安全性

(3)浏览器多进程模型

Chrome浏览器的架构图
五 【浏览器原理】 - 图15
从图中可以看出,最新的 Chrome 浏览器包括:

  • 1 个浏览器主进程
  • 1 个 GPU 进程
  • 1 个网络进程
  • 多个渲染进程
  • 多个插件进程

这些进程的功能

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程:主要负责页面的网络资源加载(html,css,js…..),之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响,更加安全靠谱

所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。

虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:

  • 高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。

2. 进程和线程的区别

  • 进程可以看做独立应用,线程不能
  • 资源:进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
  • 通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信
  • 调度:进程切换比线程切换的开销要大。线程是CPU调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O 等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小

3. 浏览器渲染进程(浏览器内核)的线程有哪些

什么是浏览器内核

浏览器是一个多进程的程序,浏览器的5个进程有渲染进程,网络进程,插件进程,GPU进程,主进程,其中渲染进程也叫做浏览器内核;浏览器内核有5个线程:GUI渲染线程,js引擎线程,事件触发线程,定时器线程,异步http请求线程;

setTimeOut/setInterval这些事件虽然是用js写的,但执行过程中是在js引擎中执行的吗?xmlhttp请求呢?GUI渲染线程呢?

不是,会被归并到定时器触发线程里去触发定时器的计时和完成,他等待js引擎空闲之后去执行。 xmlhttp请求这些连接也是通过浏览器内核的异步http请求线程去执行的;GUI渲染线程和JS引擎线程是互斥的

浏览器的渲染进程(浏览器内核)的线程总共有五种
image.png

(1)GUI渲染线程
负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
(2)JS引擎线程
JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;
注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞
(3)事件触发线程
事件触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);
(4)定时器触发线程
定时器触发进程即setInterval与setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;
注意:W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms
(5)异步http请求线程

  • XMLHttpRequest连接后通过浏览器新开一个线程请求;
  • 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行;

4. 进程之前的通信方式—-了解即可

(1)管道通信
管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
管道的特点:

  • 只能单向通信
  • 只能血缘关系的进程进行通信
  • 依赖于文件系统
  • 生命周期随进程
  • 面向字节流的服务
  • 管道内部提供了同步机制

(2)消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间
(3)信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
(4)信号通信
信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
(5)共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
(6)套接字通信
上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了

5. 僵尸进程和孤儿进程是什么?—-了解即可

  • 孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作
  • 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述仍然保存在系统中,这种进程称之为僵尸进程。

6. 死锁产生的原因? 如何解决死锁的问题?—-了解即可

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进
系统中的资源可以分为两类

  • 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
  • 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

产生死锁的原因
(1)竞争资源

  • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

(2)进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

产生死锁的必要条件

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链

预防死锁的方法:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源破坏不可剥夺条件
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

7. 如何实现浏览器内多个标签页之间的通信?

实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:

  • 使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。
  • 使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换
  • 使用 localStorage 的方式,我们可以在一个标签页对 localStorage 的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。
  • 使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用postMessage 方法,进行通信。

8. 对Service Worker的理解

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:

//1. index.js,先注册serviceWorker
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}


// sw.js
// 2. 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})


// 3. 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了:
五 【浏览器原理】 - 图17
在 Cache 中也可以发现所需的文件已被缓存
五 【浏览器原理】 - 图18

9.请为什么说js是单线程,而不是多线程呢,说说你的理解

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

三、浏览器缓存

1. 对浏览器的缓存机制的理解

浏览器缓存的全过程

  • 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;
  • 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用 expires 头判断是否过期;
  • 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求
  • 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;
  • 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;

五 【浏览器原理】 - 图19
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新

2. 浏览器资源缓存的位置有哪些?

资源缓存位置一共有 3 种,按优先级从高到低分别是:

  1. Service Worker:Service Worker 运行在 JavaScript 主线程之外,虽然由于脱离了浏览器窗体无法直接访问 DOM,但是它可以完成离线缓存、消息推送、网络代理等功能。它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。当 Service Worker 没有命中缓存的时候,需要去调用 fetch 函数获取 数据。也就是说,如果没有在 Service Worker 命中缓存,会根据缓存查找优先级去查找数据。但是不管是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示是从 Service Worker 中获取的内容。
  2. **Memory CacheMemory Cache 就是内存缓存,它的效率最快但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放一旦我们关闭 Tab 页面,内存中的缓存也就被释放**了。
  3. **Disk CacheDisk Cache 也就是存储在硬盘中的缓存,读取_速度慢_点,但是什么都能存储到磁盘中,比之 Memory Cache **胜在容量和存储时效性上在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
  1. **Push CachePush Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。**其具有以下特点:
    • 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
    • 可以推送 no-cache 和 no-store 的资源
    • 一旦连接被关闭,Push Cache 就被释放
    • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
    • Push Cache 中的缓存只能被使用一次
    • 浏览器可以拒绝接受已经存在的资源推送
    • 可以给其他域名推送资源

3. 协商缓存和强缓存的区别

(1)强缓存

使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。
强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。
(1)服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
(2)Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,
Cache-Control可设置的字段:

  • public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用max-age=来精确控制;
  • private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;
  • no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源
  • no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
  • max-age=:设置缓存的最大有效期,单位为秒
  • s-maxage=:优先级高于max-age=,仅适用于共享缓存(CDN),优先级高于max-age或者Expires头;
  • max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制

一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires

no-cache和no-store很容易混淆:

  • no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,但是会有协商缓存
  • no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。

(2)协商缓存

如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。

上面已经说到了,命中协商缓存的条件有两个:

  • max-age=xxx 过期
  • Cache-Control值为no-store

协商缓存如何命中?
不是第一次请求数据且cache-control不是no-store又不命中强缓存的时候,请求头会携带if-modified-since/if-none-match和接口返回的last-modified/etag作比较看是否过期或者是否最新文件有改变,如果最后修改时间或者唯一标识一样则代表最新内容和浏览器中缓存的内容一样,可以使用缓存命中协商缓存,这个时候接口返回304

last-modified和etag的优先级谁比较高?
etag 优先级更高,因为last-modified 1s内的变化不会被监听到,etag是只要文件内容变了,则生成的hash就变了,etag由hash生成,

什么场景下才需要命中协商缓存
当强缓存失效的时候或者说cache-control设为no-cache的时候

no-cache和no-store区别
no-cache不强缓存只进行协商缓存,no-store不缓存直接走接口获取数据

如果命中强缓存,状态码是多少
200,最后会写一个 from memory chache/from dist

使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。
协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 Etag 和 Last-Modified 属性。
(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性

总结:

强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

4. 为什么需要浏览器缓存?

对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。

所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
使用浏览器缓存,有以下优点:

  • 减少了服务器的负担提高了网站的性能
  • 加快了客户端网页的加载速度
  • 减少了多余网络数据传输

5. 点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容
  • 点击刷新按钮或者按 F5**:强缓存失效,协商缓存还有;浏览器直接当做本地缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
  • 用户按 Ctrl+F5(强制刷新):相当于第一次进入该页面;浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200**。

四、浏览器组成

二面-浏览器原理-未做笔记-278

1. 对浏览器的理解

浏览器的主要功能是将用户选择的 web 资源呈现出来,它向服务端发送http请求,从服务器请求资源,并将其显示在浏览器窗口中。
资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。

  • shell 是指浏览器的外壳:例如菜单,工具栏等。主要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种功能的。
  • 内核是浏览器的核心。内核是基于标记语言显示内容的程序或模块。

2. 对浏览器内核的理解

浏览器内核就是渲染进程,里面包含五部分
image.png
(二)3中有详解

3. 常见的浏览器内核

trident(ie),webkit(safari),blink(谷歌), Firefox(Firefox)

  • **Trident这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决**,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。
  • **Gecko/firefox这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存**。
  • **Presto**Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快3倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性
  • **WebkitWebkit 是 Safari** 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。
  • **Blink**谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。

4. 常见浏览器所用内核—-了解即可

(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink内核
(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
(4) Safari 浏览器内核:Webkit 内核;
(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
(6) 360浏览器、猎豹浏览器内核:IE + Chrome 双内核;
(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
(8) 百度浏览器、世界之窗内核:IE 内核
(9) 2345浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核

5. 浏览器的主要组成部分

  • ⽤户界⾯ - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗⼝显示的您请求的⻚⾯外,其他显示的各个部分都属于⽤户界⾯。
  • 浏览器引擎 - 在⽤户界⾯和呈现引擎之间传送指令。
  • 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  • ⽹络 - ⽤于⽹络调⽤,⽐如 HTTP 请求。其接⼝与平台⽆关,并为所有平台提供底层实现。
  • ⽤户界⾯后端 - ⽤于绘制基本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
  • JavaScript 解释器 - ⽤于解析和执⾏ JavaScript 代码。
  • 数据存储 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“⽹络数据库”,这是⼀个完整(但是轻便)的浏览器内数据库。

值得注意的是,和⼤多数浏览器不同,Chrome 浏览器的每个标签⻚都分别对应⼀个呈现引擎实例每个标签⻚都是⼀个独⽴的进程

五、浏览器渲染原理

0.运行环境

  • 运行环境通常是指浏览器(server端有nodejs)
  • 前端代码中浏览器中的加载:要先下载网页代码,渲染出页面,期间会执行若干js;每一步我们都可以去优化
  • 要保证代码在浏览器中;稳定且高效
  • 通过了解页面加载的过程,提高页面性能,提高用户体验,增强安全性

1. 浏览器的渲染过程

下载资源(各个资源类型,下载过程)和渲染页面(生成各种树然后整合布局渲染绘制上去)
页面加载的时候,默认先扫描link和js,先进行预加载。一般这两个是并行加载的
(一)加载的资源
html,js,css,媒体文件;如图片,视频等等, 加载其实就是把资源下载到本地

img的加载不会堵塞dom的渲染;但img的渲染会比较慢

(二)加载资源的过程

  1. DNS解析(Domain Name System): 域名-> IP地址 域名好记;ip不好记;不同的地址的ip地址还不一样;所以展示都是域名展示;网络中还是要转成ip解析的
  2. 浏览器根据ip地址向服务器发起http请求
  3. 服务器处理http请求,并返回给浏览器;请求什么就返回什么

(三)浏览器渲染过程

  1. 首先解析收到的文档(html),根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的网页结构框架。
  2. 然后对 CSS 进行解析,生成 CSSOM 规则树,它决定了网页的样式。
  3. DOM树 和 CSSOM规则树 构建成 渲染树(Render Tree)。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  4. 渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
  5. 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint方法 将它们的内容显示在屏幕上,绘制使用 UI 基础组件,遇到 ``` ### (7)location.hash + iframe跨域 实现原理:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的**location.hash**传值,相同域之间直接js访问来通信。
    具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
    1)a.html:(domain1.com/a.html) ``` ``` 2)b.html:(.domain2.com/b.html) ``` ``` 3)c.html:([http://www.domain1.com/c.html](https://link.zhihu.com/?target=http%3A//www.domain1.com/c.html)) ``` ``` ### (8)window.name + iframe跨域 window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
    a要访问不同源b的数据:
    1)a.html:(domain1.com/a.html) ``` var proxy = function(url, callback) { var state = 0; var iframe = document.createElement('iframe'); // 加载跨域页面 iframe.src = url; // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name iframe.onload = function() { if (state === 1) { // 第2次onload(同域proxy页)成功后,读取同域window.name中数据 callback(iframe.contentWindow.name); // 销毁这个iframe destoryFrame(); } else if (state === 0) { // 第1次onload(跨域页)成功后,切换到同域代理页面/修改iframe所指向的页面 iframe.contentWindow.location = 'http://www.domain1.com/proxy.html'; state = 1; } }; // 把iframe放入body document.body.appendChild(iframe); // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问) function destoryFrame() { iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } }; // 请求跨域b页面数据、代理b的地址 proxy('http://www.domain2.com/b.html', function(data){ alert(data); }); ``` 2)proxy.html:(domain1.com/proxy.html)
    中间代理页,与a.html同域,内容为空即可。
    3)b.html:(domain2.com/b.html) ```
    **通过iframe的src属性由外域转向本地域**,跨域数据即**由iframe的window.name从外域传递到本地域**。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是**安全操作**。
    <a name="kVvsq"></a>
    ### (9)WebSocket协议跨域
    WebSocket protocol是**HTML5一种新的协议**。它实现了浏览器与服务器**全双工通信**,同时**允许跨域**通讯,是server push技术的一种很好的实现。<br />原生WebSocket API使用起来不太方便,我们使用**Socket.io**,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。<br />1)前端代码:
    
    user input:
    2)Nodejs socket后台: var http = require(‘http’); var socket = require(‘socket.io’); // 启http服务 var server = http.createServer(function(req, res) { res.writeHead(200, { ‘Content-type’: ‘text/html’ }); res.end(); }); server.listen(‘8080’); console.log(‘Server is running at port 8080…’); // 监听socket连接 socket.listen(server).on(‘connection’, function(client) { // 接收信息 client.on(‘message’, function(msg) { client.send(‘hello:’ + msg); console.log(‘data from client: —-> ‘ + msg); }); // 断开处理 client.on(‘disconnect’, function() { console.log(‘Client socket has closed.’); }); }); <a name="PnefT"></a> ### (10)服务器端设置 http header response.setHeader(“Access-Control-Allow-Origin”, “http://m.juejin.com/“); // 第二个参数填写允许跨域的域名称,不建议直接写 ““ response.setHeader(“Access-Control-Allow-Headers”, “X-Requested-With”); response.setHeader(“Access-Control-Allow-Methods”, “PUT,POST,GET,DELETE,OPTIONS”); // 接收跨域的cookie response.setHeader(“Access-Control-Allow-Credentials”, “true”); <a name="ZsQRS"></a> ## 3. 正向代理和反向代理的区别---了解即可 - 正向代理: 客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是 客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了**对真实服务器隐藏真实客户端的目的**。实现**正向代理需要修改客户端,比如修改浏览器配置**。 - 反向代理: 服务器为了能够将工作负载分到多个服务器来**提高网站性能** (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。<br />一般使用反向代理后,需要**通过修改 DNS 让域名解析到代理服务器 IP**,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。 两者区别如图示:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1631239227421-803e8440-bc4d-4961-bcbf-7d09a68ef1f4.png#crop=0&crop=0&crop=1&crop=1&height=660&id=ua85be103&margin=%5Bobject%20Object%5D&name=image.png&originHeight=660&originWidth=522&originalType=binary&ratio=1&rotation=0&showTitle=false&size=92093&status=done&style=none&title=&width=522)<br />正向代理和反向代理的结构是一样的,**都是 client-proxy-server 的结构**,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。 <a name="FmPA1"></a> ## 4. Nginx的概念及其工作原理---了解即可 Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡(说白了就是好多台机器,里面都是一样的服务,要怎么均衡的去访问,防止一台机器压力太大,轮询每台机器就是负载均衡的一种方式)和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。 <a name="kAfBJ"></a> ### (一)nginx作为http服务器 传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。<br />Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。 <a name="xhPLY"></a> ### (二)我们可以使用nodejs java启web服务,那么为什么我们还要用nginx呢? 因为nginx是一个非常纯粹的做http请求的工具;而我们用的nodejs java Python启的web服务都是为了实现业务逻辑,但nginx是一个单纯的web服务器,他只做http服务,可以篡改http请求的内容<br />先在官网上下载下来, 下载下来后点击exe文件启动nginx服务<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633686800122-47d9a024-e5e1-4388-8f2b-0120b0c252cd.png#clientId=ua9c85561-6842-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=213&id=uad9661ba&margin=%5Bobject%20Object%5D&name=image.png&originHeight=426&originWidth=1484&originalType=binary&ratio=1&rotation=0&showTitle=false&size=393035&status=done&style=none&taskId=u3c7c5a3b-9517-48e8-ab05-240b0223193&title=&width=742)<br />点开nginx的config<br />其中里面的 include 是nginx用的指令,用于导入文件<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633696707822-d4722a9c-217c-4a70-876c-0b569de507d4.png#clientId=ua9c85561-6842-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=238&id=u63c784de&margin=%5Bobject%20Object%5D&name=image.png&originHeight=476&originWidth=1076&originalType=binary&ratio=1&rotation=0&showTitle=false&size=222907&status=done&style=none&taskId=u4ef3acf3-5fd9-45c2-91c2-cc7a3f115f4&title=&width=538) <br />浏览器发送的请求是发送到nginx;nginx这边进行转发,相当于nginx发起了一个http请求,他发送到我们实际的node服务,这个时候作为发起方,他认为的host就是proxy_pass指向的地址,这是符合我们http代理原则的;有时候服务端想拿到浏览器的host而不是代理的host,这个时候可以通过proxy_set_header Host $host,有时候我们发现拿不到真正host的话,我们就可以查一下是否被中间代理修改了,在http上中间代理可以做任何修改的事情,http是铭文传输<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633697997154-890f05d9-5e92-4245-b47a-6ad309ce2550.png#clientId=ua9c85561-6842-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=688&id=u06d171c5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1376&originWidth=1444&originalType=binary&ratio=1&rotation=0&showTitle=false&size=639000&status=done&style=none&taskId=uac393949-3fc3-4d22-930f-1e05bba5e91&title=&width=722)<br />但是对于https的话,整个传输过程都是加密的,中间代理不能修改。 <a name="wgByV"></a> ### (三)服务器为什么要使用host? host是用来区分名字的,nginx是通过host的名字来区分代理到哪个地方<br />所以作为被代理的那方,最终到我们这里拿到的host 并不是被代理方自己所能决定的,而是被客户端以及中间一层层的代理所决定的, 对于nginx来说host就是用来区分我们实际访问的是哪个服务;当我们用一台机器起了一个nginx,但是80端口只有一个,我们要通过不同的域名去访问不同机器上不同的服务,所以我们要用nginx代理配置,他根据实际的域名去请求访问转发到这台机器上的其他的服务,这样我们一台物理机器就可以跑几个不同的服务,就节省了很多资源,我们就可以更高效的利用计算机 所以 在http中,所有的内容都是不可靠的,都可能被修改 <a name="JNPca"></a> ### (四)使用nginx代理缓存 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633742513342-6105e81d-a9d5-4c8e-9725-2cc2c607b7f1.png#clientId=u63816b57-05a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=427&id=u4b36a51b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=853&originWidth=1243&originalType=binary&ratio=1&rotation=0&showTitle=false&size=445997&status=done&style=none&taskId=u40d8989e-bb4e-446c-828a-9bbf1350210&title=&width=621.5)<br />代理缓存的作用:代理缓存是在代理那层设置的,每个新的请求都会经过代理,如果代理缓存那边已经缓存过一次了,比如在我们浏览器这边,只要第一个用户请求之后代理服务器缓存了之后,其他的用户都可以直接使用代理缓存;代理缓存最大的意义:它更高效的对于一些通用的缓存内容能够更好的提供缓存功能 <a name="qxXh1"></a> ### (五)代理缓存设置的http头和缓存设置http头的关系 代理缓存只会使用s-maxage;浏览器缓存只会使用max-age <br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633749634875-d23039b4-8dc6-4e88-85ee-c8400a3eec4c.png#clientId=u63816b57-05a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=61&id=u879f4e69&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=733&originalType=binary&ratio=1&rotation=0&showTitle=false&size=94364&status=done&style=none&taskId=u1349f070-7702-4ea0-bcdf-a35ca96d1bf&title=&width=366.5)<br />设置private的话,代理服务就不能缓存这部分数据了<br /> ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633749780282-2382e8d3-43ec-4650-abc0-ef8ebcd54e99.png#clientId=u63816b57-05a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=262&id=u920c7d56&margin=%5Bobject%20Object%5D&name=image.png&originHeight=523&originWidth=766&originalType=binary&ratio=1&rotation=0&showTitle=false&size=163108&status=done&style=none&taskId=u0fbc18a9-202b-4365-82c5-b498efec22e&title=&width=383)<br />vary:不仅url一样,连头部信息也要一样的情况才能使用缓存<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633750318231-cca6b19d-d16c-4f0c-8a08-e1681f0fc463.png#clientId=u63816b57-05a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=152&id=u6fc2ad89&margin=%5Bobject%20Object%5D&name=image.png&originHeight=303&originWidth=839&originalType=binary&ratio=1&rotation=0&showTitle=false&size=176186&status=done&style=none&taskId=udb0a8b99-6bc7-49f6-992a-ecbb3dd2383&title=&width=419.5) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633750523111-f79fd421-6725-448d-b35f-17ce47d19ea7.png#clientId=u63816b57-05a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=428&id=ud999d1cd&margin=%5Bobject%20Object%5D&name=image.png&originHeight=855&originWidth=1326&originalType=binary&ratio=1&rotation=0&showTitle=false&size=373273&status=done&style=none&taskId=uf9370829-0d37-4ec1-a10a-2ce25b37b52&title=&width=663)<br />使用场景:<br />我们使用了服务器缓存,但是我们需要返回的数据会根据user-agent不一样(pc端和移动端要拿到的数据不一样),这个时候我们就可以在vary中去声明判断user-agent 一样的情况下才会使用同一份缓存,如果不一样他们用的就分别是两个缓存,这个就是这个头带给我们的作用;我们还可以拿去判断其他的,比如英语和中文的区别,他们也需要区分进行缓存,那个头是非常重要的,如果我们使用代理缓存,很多情况还是要使用vary这个头,因为很多情况下就算url一样也要根据不同的情况返回不同的数据;具体的代理缓存还有很多其他的功能进行提升的,我们可以写一些脚本让缓存使用我们的内存数据库,这样搜索性能就会更高,因为默认情况下,nginx的缓存是写在磁盘上面的,这样读写磁盘的效率就会降低,还可以设置cache-key这些东西,这些都是配置上的内容 <a name="dBieT"></a> ### (六)nginx部署https服务 首先要生成公钥和私钥,可以在网上先找证书生成的命令<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633770604848-9836d960-1835-40a8-bb24-843cdc8487e8.png#clientId=ua3cd06d4-7184-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=17&id=ub5bfa279&margin=%5Bobject%20Object%5D&name=image.png&originHeight=33&originWidth=762&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33981&status=done&style=none&taskId=u7b18df6e-7acd-4331-b27f-07be8194696&title=&width=381)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633770727502-b9d30119-824f-4f26-bb72-c1331a9ee255.png#clientId=ua3cd06d4-7184-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=286&id=ua162837a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=572&originWidth=1113&originalType=binary&ratio=1&rotation=0&showTitle=false&size=486733&status=done&style=none&taskId=u189ff809-a596-49ca-8ea0-5947cbb0e45&title=&width=556.5)<br />然后我们在config里写入<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633771977542-b16e0388-9252-409e-8c31-22ca137e5a4f.png#clientId=ua3cd06d4-7184-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=286&id=u32709ac8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=571&originWidth=1029&originalType=binary&ratio=1&rotation=0&showTitle=false&size=334660&status=done&style=none&taskId=ubafd9dcb-ed0c-4acd-9bba-3c7673861e8&title=&width=514.5)<br />接着启动nginx和node服务打开对应域名页面<br />提示不是私密连接的原因:因为像chrome这些浏览器认为的安全证书是要通过有权威的机构签发的,权威机构签发证书要先验证域名是你的而且保证这个服务就是你这边运行的,这样才会给你签发证书<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633772180348-d8568036-8af8-4d6e-80df-1b6c96f720cd.png#clientId=ua3cd06d4-7184-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=343&id=ud3111a40&margin=%5Bobject%20Object%5D&name=image.png&originHeight=686&originWidth=1211&originalType=binary&ratio=1&rotation=0&showTitle=false&size=207548&status=done&style=none&taskId=u044f029f-030d-4555-826d-c5cf9109fac&title=&width=605.5) <a name="llRWn"></a> ### (七)访问http直接跳转到https ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633772981795-438f5a23-344b-438f-aac3-28b30c92a879.png#clientId=ua3cd06d4-7184-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=299&id=ubba1291c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=598&originWidth=1203&originalType=binary&ratio=1&rotation=0&showTitle=false&size=366209&status=done&style=none&taskId=uf99764d7-338e-45ae-8dc4-b356b97f5a5&title=&width=601.5) <a name="ZoED9"></a> ## 5.表单可以跨域吗 <br />表单提交是可以进行跨域的,不受浏览器的同源策略限制,估计是历史遗留原因,也有可能是表单提交的结果js是拿不到的,所以不限制问题也不大。但是存在一个问题,就是csrf攻击,具体不展开了,因为可以自动带上cookie造成攻击成功,而cookie的新属性SameSite就能用来限制这种情况 <a name="UMaEJ"></a> # 八、浏览器事件机制 <a name="nSmQY"></a> ## 1. 事件是什么?事件模型? <a name="u822v"></a> #### 什么是事件模型 现在浏览器有三种事件模型:dom0级事件模型,用来监听的;ie事件模型,只有事件处理和事件冒泡两个阶段;dom2级事件模型在一次事件中有三个过程,事件捕获从外到内,目标事件处理,然后事件冒泡从内到外; 事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作(event 的方法)。<br />事件是用户操作网页时发生的交互动作或者网页本身的一些操作,**现代浏览器一共有三种事件模型**: - DOM0 级事件模型,这种模型**不会传播,所以没有事件流的概念**,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以**通过 js 属性来指定监听函数**。所有浏览器都兼容这种方式。直接在dom对象上注册事件名称,就是DOM0写法。 - IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。 - DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是**addEventListener**,其中第三个参数可以指定事件是否在捕获阶段执行。 //DOM2 级事件 var btn = document.getElementById(‘btn1’) btn.addEventListener(‘click’, function (event) { // event.preventDefault() // 阻止默认行为 // event.stopPropagation() // 阻止冒泡 console.log(‘clicked’) }) // 通用的事件绑定函数 function bindEvent(elem, type, fn) { elem.addEventListener(type, fn) } var a = document.getElementById(‘link1’) // 调用该方法 bindEvent(a, ‘click’, function(e) { console.log(e.target) // 拿到触发元素 e.preventDefault() // 阻止默认行为,比如a标签,link标签等的跳转行为 alert(‘clicked’) }) <a name="PzLk0"></a> ## 2. 事件冒泡 (一)如何阻止事件冒泡 - 普通浏览器使用:event.**stopPropagation**() - IE浏览器使用:event.**cancelBubble** = true; (二)冒泡的现象

    激活

    取消

    取消

    取消

    取消

    取消

    // 要求点击 p1 时候进入激活状态,点击其他任何

    都取消激活状态 var body = document.body // 调用用上面写好的监听事件的方法 bindEvent(body, ‘click’, function (e) { // 所有p的点击都会冒泡到body上,DOM 结构中body是p的上级节点,事件会沿着DOM 树向上冒泡 alert(‘取消’) }) var p1 = document.getElementById(‘p1’) bindEvent(p1, ‘click’, function (e) { e.stopPropagation() // 阻止冒泡,不阻止事件冒泡点击p1的话,会先弹出激活再弹出取消 alert(‘激活’) }) <a name="r27qV"></a> ## ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1633147861833-649c907a-b687-4295-a04f-263c69d5a52b.png#clientId=u91a77453-fa92-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=271&id=u34174699&margin=%5Bobject%20Object%5D&name=image.png&originHeight=542&originWidth=1500&originalType=binary&ratio=1&rotation=0&showTitle=false&size=691876&status=done&style=none&taskId=u9a65fbb0-698c-4f0f-b67b-e323e73d159&title=&width=750) 1. 我们的事件冒泡是基于DOM树形结构的; 1. 事件是会顺着触发元素往上冒泡的; 1. 事件代理就是依赖于我们的事件冒泡实现的 <a name="NE6Xa"></a> #### 什么是捕获什么是冒泡/事件冒泡捕获的原理和区别? 捕获:body到目标节点的过程,从外到内;冒泡:目标节点到body的过程,从内到外 <a name="XzxqK"></a> ## 3. 对事件委托的理解 <a name="JpZF4"></a> #### 事件代理是什么? 把事件绑定到父元素上去通过冒泡原理操作子元素就叫做事件代理;不用一个个子元素绑定事件减少了dom的操作,可以动态绑定,提高了性能 <a name="Q82LH"></a> #### 事件代理为什么可以提高性能? 给每个子元素都绑定事件的话,代码就多了,这样就会浪费内存,性能会较低;所以事件代理提高性能是因为节省了内存空间,事件太多页面就容易卡顿 <a name="gVfjq"></a> #### (1)事件委托的概念 事件委托的原理:不给每个子节点单独设置事件监听器,而是设置在其父节点上,然后利用冒泡原理设置监听每个子节点。 事件委托本质上是**利用了浏览器事件冒泡的机制**。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。 一般可以用在瀑布流中,不知道里面会有多少个子元素,还要把子元素和触发机制结合起来。使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。 **子节点对父节点事件委托,父节点对子节点事件代理** // 代理绑定(事件绑定在父元素上去操作子元素,且对子元素有特定的判断) const div3 = document.getElementById(‘div3’) bindEvent(div3,’click’,event => { event.preventDefault() // 组织默认行为,不让a标签跳转 const target = event.target if(target.nodeName === ‘A’) { // 如果是a标签的话 alert(target.innerHTML) } }) <a name="L2THC"></a> #### (2)事件委托的特点及实现 - **减少内存消耗** 如果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:

    • item 1
    • item 2
    • item 3
    • ……
    • item n
    如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后**在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率。** - **动态绑定事件** 给上述的例子中每个列表项都绑定事件,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。event.target获取子节点。 // 来实现把 #list 下的 li 元素的事件代理委托到它的父层元素也就是 #list 上: // 给父层元素绑定事件 document.getElementById(‘list’).addEventListener(‘click’, function (e) { // 兼容性处理 var event = e || window.event; var target = event.target || event.srcElement; // 判断是否匹配目标元素 if (target.nodeName.toLocaleLowerCase === ‘li’) { console.log(‘the content is: ‘, target.innerHTML); } }); ``` 在上述代码中, target 元素则是在 #list 元素之下具体被点击的元素,然后通过判断 target 的一些属性(比如:nodeName,id 等等)可以更精确地匹配到某一类 #list li 元素之上; *进阶版事件代理

    image.png
    优化以上两个bindEvent方法,写一个能兼容第一个和第二个bindEvent的方法 1. 事件绑定 addEventListener 1. 用e.target获取触发元素 1. 用matches来判断是否触发元素 <div id="div1"> <a href="#">a1</a> <a href="#">a2</a> <a href="#">a3</a> <a href="#">a4</a> </div> <button>点击增加一个 a 标签</button> 事件代理: function bindEvent(elem, type, selector, fn) { // 这样处理,可接收两种调用方式 bindEvent(div1, 'click', 'a', function () {...}) // 和 bindEvent(div1, 'click', function () {...}) 这两种 if (fn == null) {// 判断是否有传入目标元素 fn = selector selector = null } // 绑定事件 elem.addEventListener(type, function (e) { if (selector) { //根据有没有传入目标元素来判断是否需要事件代理,传入的话说明需要做事件代理 var target = e.target //获取触发元素 if (target.matches(selector)) { // 判断触发元素target是否符合selector选择器 fn.call(target, e) } } else { // 无 selector ,说明不需要事件代理,普通调用函数 fn(e) } }) } // 使用代理,bindEvent 多一个 'a' 参数 var div1 = document.getElementById('div1') bindEvent(div1, 'click', 'a', function (e) { e.preventDefault() console.log(this.innerHTML) }) 为什么事件代理内是fn要把this指向目标元素再调用?fn.call(target, e)还原真实的事件函数的this指向问题 ```

    我们在div下的所有a标签,我们要绑定一个事件,但我们不知道我们绑定的是哪个a标签, 这个时候我们需要把this值指向我们真正触发的那个a标签,并把所有属性作为参数传过去 我们才能拿到触发的a标签的href等属性去绑定事件 这是为了还原真实的diss触发的函数的this指向问题,比如this指向有四种情况: (1)构造函数的this永远指向他的实例对象 (2)对象的方法调用,this指向该对象 (3)es6的箭头函数的this指向上下文的this (4)最后是我们说的这个,如果作为事件函数被调用的话,那么this永远指向该元素 (所以我们为了还原真实的事件函数手动将this指向该元素)

    
    <a name="AKDpG"></a>
    #### (3)事件委托的局限性
    当然,事件委托也是有局限的。比如 focus、blur 之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。所以也不要滥用事件委托<br />当然事件委托不是只有优点,它也是有缺点的,事件委托会影响页面性能,主要影响因素有:
    
    - 元素中,绑定事件委托的次数;
    - 点击的最底层元素,到绑定事件元素之间的DOM层数;
    
    在必须使用事件委托的地方,可以进行如下的处理:
    
    - **只在必须的地方**,使用事件委托,比如:ajax的局部刷新区域
    - 尽量的**减少绑定的层级,不在body元素上,进行绑定**
    - **减少绑定的次数**,如果可以,那么把**多个事件的绑定,合并到一次事件委托中去**,由这个事件委托的回调,来进行分发。
    
    <a name="XLzxk"></a>
    #### (4)事件委托的优点
    
    - 减少内存消耗和 dom 操作,提高性能 ;在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的操作 dom,那么引起浏览器重绘和回流的可能也就越多,页面交互的事件也就变的越长,这也就是为什么要**减少 dom 操作**的原因。如果我们给每一个事件处理函数,都是一个对象,多一个事件处理函数,**内存**中就会被多占用一部分空间。如果要用事件委托,就会将所有的操作放到 js 程序里面,只对它的父级进行操作,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;
    - 动态绑定事件 因为事件绑定在父级元素 所以新增的元素也能触发同样的事件
    
    
    
    
    <a name="O3dZR"></a>
    ## 4. 事件委托的使用场景
    场景:给页面的所有的a标签添加click事件,代码如下:
    

    document.addEventListener(“click”, function(e) { if (e.target.nodeName == “A”) console.log(“a”); }, false);

    但是这些**a标签可能包含一些像span、img等元素**,如果**点击到了这些a标签中的元素,就不会触发click事件**,因为事件绑定上在a标签元素上,而触发这些内部的元素时,e.target指向的是触发click事件的元素(span、img等其他元素)。<br />这种情况下就可以使用事件委托来处理,**将事件绑定在a标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到a标签为止**,代码如下:<br />其实就是子节点a标签内部还有元素,这种情况可以给父节点和a标签上都绑定事件
    

    document.addEventListener(“click”, function(e) { var node = e.target; while (node.parentNode.nodeName != “BODY”) { if (node.nodeName == “A”) { console.log(“a”); break; } node = node.parentNode; } }, false);

    <a name="zLlDM"></a>
    ## 
    <a name="ck6qX"></a>
    ## 5. 同步和异步的区别
    
    - 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会**一直等待下去,直到消息返回**为止再继续向下执行。
    - 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,**不会阻塞**等待消息的返回,当消息返回时系统再通知进程进行处理。
    <a name="NB5jG"></a>
    ## 
    <a name="FRfCK"></a>
    ## 6. 事件循环/ event loop
    [【event loop】](https://www.yuque.com/wozhuangzuoyu/gamwp4/girymn?view=doc_embed)
    
    <a name="cEZno"></a>
    ### (一)事件循环的
    <a name="Y7ZeN"></a>
    #### 什么是事件循环
    js是单线程的,从前到后一行行执行,先执行同步代码,遇到异步的话把异步推入任务队列中,当执行完同步代码后,event loop查询是否有异步代码需要执行,有微任务就先执行微任务,清空微任务队列后,如果有必要的话会渲染页面,接着开始下一轮eventloop,执行宏任务中的异步代码。
    
    - js是单线程运行的
    - 异步要基于回调来实现
    - event loop就是异步回调的实现原理
    - 只要用了回调就基于event loop:DOM事件,异步(setTimeout,ajax等等)
    - Js是单线程的,而且和DOM渲染共用一个线程
    - JS执行的时候,得留一些时机供DOM渲染
    
    
    1. 因为 js 是**单线程**运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
    1. 在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
    1. 当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。
    1. 任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
    
    [![](https://camo.githubusercontent.com/58d2d8bf045e1a3f2168903797a438fb63c0f523c3db17e635cf6c1c620f34ca/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363530303231372d34373235363365312d646536372d343033662d626161372d3066643537346430653631382e706e673f782d6f73732d70726f636573733d696d616765253246726573697a65253243775f31353030#crop=0&crop=0&crop=1&crop=1&height=605&id=Mdt2i&margin=%5Bobject%20Object%5D&originHeight=605&originWidth=1500&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=1500)](https://camo.githubusercontent.com/58d2d8bf045e1a3f2168903797a438fb63c0f523c3db17e635cf6c1c620f34ca/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363530303231372d34373235363365312d646536372d343033662d626161372d3066643537346430653631382e706e673f782d6f73732d70726f636573733d696d616765253246726573697a65253243775f31353030)
    
    Event Loop** 执行顺序**如下所示:
    
    - 从前到后,**一行一行执行**
    - 如果某一行执行**报错,则停止**下面代码的执行
    - **首**先执行同步代码,这属于**宏任务,**遇到异步,会先把异步任务“记录”到任务队列(宏/微任务队列)中
    - 当执行完所有同步代码后,**执行栈为空**,event loop**查询是否有异步代码需要执行**
    - 执行所有**微任务**
    - 当执行完所有**微任务后,如有必要会渲染页面**
    - 然后开始下一轮 Event Loop,**执行宏任务中的异步代码**
    
    <a name="Jq6Nt"></a>
    ### (二)宏任务/微任务
    
    1. 宏任务:setTimeout,setInterval,Ajax,DOM
    1. 微任务:Promise async/await
    1. 微任务执行时机比宏任务要早
    
    宏任务和微任务的区别
    
    - 宏任务:DOM渲染后触发,如setTimeout
    - 微任务:DOM渲染前触发,如Promise
    
    每次执行栈清空,即同步任务执行完毕,都是Dom重新渲染的机会,DOM结构若有改变则重新渲染,然后再去触发下一次event loop
    
    js异步

    const $p1 = $(‘

    一段文字

    ‘) const $p2 = $(‘

    一段文字

    ‘) const $p3 = $(‘

    一段文字

    ‘) $(‘#container’) .append($p1) .append($p2) .append($p3)

    console.log(‘length’, $(‘#container’).children().length ) // 打印出来是 3 alert(‘本次 call stack 结束,DOM 结构已更新,但尚未触发渲染’) // 现象:页面中只有一个元素,点击确认后新增的三行字段才展示,说明dom渲染在同步代码之后执行 // alert 会阻断 js 执行,也会阻断DOM渲染,便于查看效果

    // 微任务:DOM渲染之前执行 (DOM 结构已更新) Promise.resolve().then(() => { console.log(‘length’,$(‘container’).children().length) alert(‘Promise then’) })

    // 执行情况:打印3 弹出alert 页面只展示 js异步 字段 // 说明,DOM没渲染

    // 宏任务:DOM渲染之后执行 (DOM 结构已更新) setTimeout(() => { console.log(‘length’,$(‘container’).children().length) alert(setTimeout) })

    // 执行情况:打印3 弹出alert 页面展示 js异步 字段 以及三段文字 // 说明,DOM渲染了

    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1632911473866-ddd8b66d-aa55-4aaf-b59a-8041dc2f6812.png#crop=0&crop=0&crop=1&crop=1&height=386&id=ub0605cda&margin=%5Bobject%20Object%5D&name=image.png&originHeight=386&originWidth=688&originalType=binary&ratio=1&rotation=0&showTitle=false&size=165060&status=done&style=none&title=&width=688)
    <a name="UOILh"></a>
    ### (三)为什么微任务比宏任务执行的早(从event loop角度解释)
    **根本原因:**
    
    - 微任务是ES6语法规定的(promise)JS 引擎来统一处理即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
    - 宏任务是浏览器也就是W3C规定的(ajax    dom操作  两个定时器)JS 引擎不处理,浏览器(或 nodejs)干预处理。
    
    <a name="YaOM8"></a>
    ### (四)Promise+event loop 题
    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1632912141298-d565eb6d-8e42-4500-bbfb-2801c49bbaf0.png#crop=0&crop=0&crop=1&crop=1&height=703&id=u367b8819&margin=%5Bobject%20Object%5D&name=image.png&originHeight=703&originWidth=1398&originalType=binary&ratio=1&rotation=0&showTitle=false&size=660655&status=done&style=none&title=&width=1398)
    

    1 3 1 2 3 1 2

    
    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1632912240375-a593f6d1-33a0-43a9-9794-3636dd4b8246.png#crop=0&crop=0&crop=1&crop=1&height=683&id=u43b222c2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=683&originWidth=1339&originalType=binary&ratio=1&rotation=0&showTitle=false&size=671808&status=done&style=none&title=&width=1339)
    

    一个promise对象 100

    start a 100 b 200 报错

    
    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1632912566958-65385806-4275-4113-93ab-d96764978141.png#crop=0&crop=0&crop=1&crop=1&height=305&id=u71e2a70b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=305&originWidth=603&originalType=binary&ratio=1&rotation=0&showTitle=false&size=116605&status=done&style=none&title=&width=603)
    

    100 400 300 200

    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1632912617344-4eff6581-5a84-407b-8ec4-480e1cee0ffd.png#crop=0&crop=0&crop=1&crop=1&height=699&id=u1f57b3a6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=699&originWidth=1199&originalType=binary&ratio=1&rotation=0&showTitle=false&size=794033&status=done&style=none&title=&width=1199)
    

    答案: ‘script start’ ‘async1 start’ ‘async2’ ‘promise1’ ‘script end’ (同步代码执行完毕,还有两段微任务1段宏任务) ‘async1 end’ ‘promise2’ (再渲染DOM 再event loop 再宏任务) ‘setTimeout’

    
    
    

    解析: // 拿到题目,看看是考什么,考异步Promise这些的话我们先看微任务再看宏任务 // 函数光是声明是不会执行的,所以前两个可以不用看 // 然后打印’script start’ // 然后一个定时器内部有函数,这是一个异步回调,但也不是立马执行,属于宏任务

    // 然后到async1() 执行这个方法打印’async1 start’ 然后又看到了await 后面是async2() // 这一句会同步执行async2(),打印’async2’。await 方法后面的都作为 回调内容 属于微任务

    // 再往下看到new Promise,初始化promise时,传入的函数会立刻被执行,所以打印’promise1’ // 但后面的.then 后面是一个微任务

    // 最后看到打印了’script end’,这下同步代码都被执行完了,调用栈被清空,接下来执行微任务 // 会发现有两段微任务,按顺序执行他们’async1 end’ ‘promise2’, //执行完微任务后尝试触发DOM渲染,再触发event loop机制,再执行宏任务’setTimeout’

    async function async1 () { console.log(‘async1 start’) await async2() // 这一句会同步执行,返回 Promise ,其中的 console.log('async2') 也会同步执行 console.log(‘async1 end’) // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务) }

    async function async2 () { console.log(‘async2’) }

    console.log(‘script start’)

    setTimeout(function () { // 异步,宏任务 console.log(‘setTimeout’) }, 0)

    async1()

    // 初始化promise时,传入的函数会立刻被执行 new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码 console.log(‘promise1’)
    resolve() }).then (function () { // 异步,微任务 console.log(‘promise2’) })

    console.log(‘script end’)

    async返回一个promise对象    await接一个promise对象?
    

    setTimeout(function () { console.log(“1”); }, 0); async function async1() { console.log(“2”); const data = await async2(); console.log(“3”); return data; } async function async2() { return new Promise((resolve) => { console.log(“4”); resolve(“async2的结果”); }).then((data) => { console.log(“5”); return data; }); } async1().then((data) => { console.log(“6”); console.log(data); }); new Promise(function (resolve) { console.log(“7”); // resolve() }).then(function () { console.log(“8”); });

    // 247536 async2 的结果 1 // 最后一个 Promise 埋了个坑 没有调用 resolve 方法;可以看看调用和调用的打印分别是什么

    <a name="fGSYU"></a>
    #### 事件循环代码题
    

    console.log(1);

    setTimeout(()=>{ console.log(2);
    setTimeout(()=>{ console.log(14);
    new Promise((resolve,reject)=>{ console.log(15); resolve() }).then(res=>{ console.log(16); }) }) new Promise((resolve,reject)=>{ console.log(3); resolve() }).then(res=>{ console.log(4); }) })

    new Promise((resolve,reject)=>{ resolve() }).then(res=>{ console.log(5); }).then(res=>{ console.log(6); })

    new Promise((resolve,reject)=>{ console.log(7); resolve() }).then(res=>{ console.log(8); }).then(res=>{ console.log(9);

    })

    setTimeout(()=>{ console.log(10);
    new Promise((resolve,reject)=>{ console.log(11); resolve() }).then(res=>{ console.log(12); }) })

    console.log(13);

    // 1 7 13 5 8 6 9 2 3 4 10 11 12 14 15 16

    
    <a name="rsfn2"></a>
    ### (五)同步和异步的区别是什么?
    **同步**,执行顺序需要一个一个来,要排队执行,前面的没执行完会堵塞后面的执行,只有接收到返回的值或消息后才往下执行其他的命令。<br />**异步**,**不会阻塞**后面程序的运行,可以和其他时间线同时进行
    
    
    
    <a name="gVRum"></a>
    ## 7. 宏任务和微任务分别有哪些
    
    - 微任务包括: **promise** 的回调、node 中的 **process.nextTick** 、对 **Dom 变化监听的 MutationObserver**。
    - 宏任务包括:** script 脚本的执行**、setTimeout ,setInterval ,setImmediate 一类的**定时事件**,还有如 **I/O 操作、UI 渲染**等。
    <a name="oW2Cx"></a>
    ## 
    <a name="q9baY"></a>
    ## 8. 什么是执行栈
    可以把执行栈认为是一个**存储函数调用的栈结构**,**遵循先进后出的原则**。执行栈是U型,函数调用顺序采用的栈结构,栈就是先进后出,用数组理解就是只能push和pop;<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1631265880577-007633d2-9f4c-4682-be81-8783473a6ed8.png#crop=0&crop=0&crop=1&crop=1&height=623&id=u03473c83&margin=%5Bobject%20Object%5D&name=image.png&originHeight=623&originWidth=1211&originalType=binary&ratio=1&rotation=0&showTitle=false&size=91942&status=done&style=none&title=&width=1211)
    
    当开始执行 JS 代码时,根据先进后出的原则,**后执行的函数会先弹出栈**,可以看到,foo 函数后执行,当执行完毕后就从栈中弹出了。<br />平时在开发中,可以在报错中找到执行栈的痕迹:
    

    function foo() { throw new Error(‘error’) } function bar() { foo() } bar()

    [![](https://camo.githubusercontent.com/d8cf793901256808263a0c85295506a5534ccbf5e300a0ae698efe55f3f507bc/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363337353436322d32363963366135342d613231642d343630632d616138322d6137313365663961323939392e706e67#crop=0&crop=0&crop=1&crop=1&height=121&id=ugzxY&margin=%5Bobject%20Object%5D&originHeight=121&originWidth=614&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=614)](https://camo.githubusercontent.com/d8cf793901256808263a0c85295506a5534ccbf5e300a0ae698efe55f3f507bc/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363337353436322d32363963366135342d613231642d343630632d616138322d6137313365663961323939392e706e67)<br />可以看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。当使用递归时,因为栈可存放的函数是有限制的,**一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题**
    

    function bar() { bar() } bar()

    ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1631265860242-85b5f796-cf6c-4ebe-a534-00ddb93b5e75.png#crop=0&crop=0&crop=1&crop=1&height=158&id=u8d3dcca8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=158&originWidth=511&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12058&status=done&style=none&title=&width=511)
    <a name="kLnlM"></a>
    ## 9. Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?(鲨鱼哥说别看了!)
    Node 中的 Event Loop 和浏览器中的是完全不相同的东西。以前两者是有区别的,后来浏览器解决了这种兼容性?<br />**Node 的 Event Loop 分为 6 个阶段**,它们会按照顺序反复运行。**每当进入某一个阶段的时候,**都会从对应的回调队列中取出函数去执行。**当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。**<br />[![](https://camo.githubusercontent.com/9507a0c838ee7bb32348ffcafe063582b7f5761fd5a82fdd53b79412e2325dd5/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363634313930342d61393736333033342d393131342d346137632d616135302d3162646236313266326635652e706e67#crop=0&crop=0&crop=1&crop=1&height=442&id=ldDoS&margin=%5Bobject%20Object%5D&originHeight=442&originWidth=745&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=745)](https://camo.githubusercontent.com/9507a0c838ee7bb32348ffcafe063582b7f5761fd5a82fdd53b79412e2325dd5/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363634313930342d61393736333033342d393131342d346137632d616135302d3162646236313266326635652e706e67)<br />(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会**判断是否存在过期的计时器**回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,**执行完微任务后再进入 Pending callbacks 阶段**。<br />(2)Pending callbacks:执行**推迟到下一个循环迭代的I / O回调(系统调用相关的回调**)。<br />(3)Idle/Prepare:仅供内部使用。<br />(4)Poll(轮询阶段):
    
    - 当回调队列**不为空**时:**会执行回调**,若回调中触发了相应的微任务,这里的**微任务**执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是**针对每一个回调执行完毕后,就执行相应微任务**。**执行完所有的回调后,变为下面的情况**。
    - 当回调队列**为空**时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,**会结束轮询阶段,进入 Check 阶段**。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。
    
    (5)Check(查询阶段):会检查**是否存在 setImmediate 相关的回调,如果存在则执行所有回调**,执行完毕后,如果回调中**触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段**。<br />(6)Close callbacks:执行一些**关闭回调**,比如socket.on('close', ...)等。
    
    
    
    下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机的
    

    setTimeout(() => { console.log(‘setTimeout’) }, 0) setImmediate(() => { console.log(‘setImmediate’) })

    对于以上代码来说,setTimeout 可能执行在前,也可能执行在后
    
    - 首先 **setTimeout(fn, 0) === setTimeout(fn, 1)**,这是由源码决定的
    - 进入事件循环也是需要成本的,如果在准备时候**花费了大于 1ms** 的时间,那么在 timer 阶段就会直接执行 **setTimeout** 回调
    - 那么如果准备时间**花费小于 1ms**,那么就是 **setImmediate** 回调先执行了
    
    当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
    

    const fs = require(‘fs’) fs.readFile(__filename, () => { setTimeout(() => { console.log(‘timeout’); }, 0) setImmediate(() => { console.log(‘immediate’) }) })

    在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 **poll 阶段**执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。<br />上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,下图中的 Tick 就代表了 microtask<br />[![](https://camo.githubusercontent.com/4387ee2bce6edd00b4262f3eb9ace40b197879c9bc455f93d232790a650509f1/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363634313932372d37353430396439312d333861312d343739372d616137352d6362303264643935643733322e706e67#crop=0&crop=0&crop=1&crop=1&height=1227&id=CzSdu&margin=%5Bobject%20Object%5D&originHeight=1227&originWidth=644&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=644)](https://camo.githubusercontent.com/4387ee2bce6edd00b4262f3eb9ace40b197879c9bc455f93d232790a650509f1/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f706e672f313530303630342f313631353437363634313932372d37353430396439312d333861312d343739372d616137352d6362303264643935643733322e706e67)
    

    setTimeout(() => { console.log(‘timer21’) }, 0) Promise.resolve().then(function() { console.log(‘promise1’) })

    对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。
    
    
    最后来看 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
    

    setTimeout(() => { console.log(‘timer1’) Promise.resolve().then(function() { console.log(‘promise1’) }) }, 0) process.nextTick(() => { console.log(‘nextTick’) process.nextTick(() => { console.log(‘nextTick’) process.nextTick(() => { console.log(‘nextTick’) process.nextTick(() => { console.log(‘nextTick’) }) }) }) })

    对于以上代码,永远都是先把 nextTick 全部打印出来。
    <a name="XCw3O"></a>
    ## 
    <a name="Hqm25"></a>
    ## 10. 事件触发的过程是怎样的
    事件触发有**三个阶段**:
    
    - **window** 到目标元素**事件触发处**传播,从外到内,遇到注册的**捕获事件会触发**
    - 传播到事件**触发处**时**触发注册的事件**
    - 从目标元素**到 window 传播**,从内到外,遇到注册的**冒泡事件会触发**
    
    事件触发一般来说会按照上面的顺序进行,但是也有特例,如果**给一个 **body** 中的子节点同时注册冒泡和捕获**事件,事件触发会**按照注册的顺序执行**。
    

    // 以下会先打印冒泡然后是捕获 node.addEventListener( ‘click’, event => { console.log(‘冒泡’) }, false ) node.addEventListener( ‘click’, event => { console.log(‘捕获 ‘) }, true )

    通常使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。
    
    addEventListener默认是冒泡,注意第三个参数默认为false代表执行冒泡事件,否则捕获事件,第三个参数默认值为false其实也是商业因素在里面,以前ie主推默认冒泡,网景浏览器(Netscape)主推默认捕获,他们两家公司在打商业战,最后ie打败了网景,所以默认冒泡<br />对于布尔值 **useCapture** 参数来说,该参数默认值为 false ,useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
    
    - capture:布尔值,和 useCapture 作用一样,默认false(使用事件冒泡),true(捕获)
    - once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    - passive:布尔值,表示永远不会调用 preventDefault
    
    一般来说,如果只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。<br />stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
    

    node.addEventListener( ‘click’, event => { event.stopImmediatePropagation() console.log(‘冒泡’) }, false ) // 点击 node 只会执行上面的函数,该函数不会执行 node.addEventListener( ‘click’, event => { console.log(‘捕获 ‘) }, true )

    
    <a name="sbmdh"></a>
    # 九、浏览器垃圾回收机制---了解即可????
    [硬核-垃圾回收机制](https://juejin.cn/post/6981588276356317214)
    <a name="tgDQa"></a>
    ## 1. V8的垃圾回收机制是怎样的
    V8 实现了**准确式 GC(垃圾回收)**,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。<br />(1)新生代算法<br />新生代中的**对象一般存活时间较短**,使用 Scavenge GC 算法。<br />在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。<br />(2)老生代算法<br />老生代中的**对象一般存活时间较长且数量也多**,使用了两个算法,分别是标记清除算法和标记压缩算法。
    
    先来说下什么情况下对象会出现在老生代空间中:
    
    - 新生代中的对象是否已经经**历过一次 Scavenge 算法**,如果经历过的话,**会将对象从新生代空间移到老生代空间中**。
    - To 空间的**对象占比大小超过 25 %**。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
    
    老生代中的空间很复杂,有如下几个空间
    

    enum AllocationSpace { // TODO(v8:7464): Actually map this space’s memory as read-only. RO_SPACE, // 不变的对象空间 NEW_SPACE, // 新生代用于 GC 复制算法的空间 OLD_SPACE, // 老生代常驻对象空间 CODE_SPACE, // 老生代代码对象空间 MAP_SPACE, // 老生代 map 对象 LO_SPACE, // 老生代大空间对象 NEW_LO_SPACE, // 新生代大空间对象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };

    ``` 在老生代中,以下情况会先启动标记清除算法:

    • 某一个空间没有分块的时候
    • 空间中被对象超过一定限制
    • 空间不能保证新生代中的对象移动到老生代中

    在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。

    清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

    2. 哪些操作会造成内存泄漏?

    • 第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
    • 第二种情况是设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
    • 第三种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
    • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。