前端知识体系总结

网络基础

1. TCP 连接的建立和释放

TCP 建立连接(三次握手)

服务器保持监听状态,客户端主动发起连接,收到服务器响应后建立连接,服务器收到客户端连接请求后进一步确认客户端可连接,收到客户端响应后建立连接,此时客户端服务端双方都已确认各自可收发。(最后一次握手的作用:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误)

image-20210312113504239

TCP连接释放(四次挥手)

客户端主动发起连接释放请求,并且停止发送数据,服务器收到后返回确认信息,同时开始传输最后的数据,此时客户端还要等待服务器传回的数据。传输完成后服务器也发起连接释放请求,等待客户端回应后直接关闭连接,而客户端还需要等待2MSL(Maximum Segment Lifetime)后才关闭连接。(等待2MSL的作用:保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,同时防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中)

image-20210312114150079

2. HTTP 1.X

HTTP(Hypertext Transfer Protocol),超文本传输协议(应用层),是互联网上应用最为广泛的一种网络协议。HTTP 是一个客户端(用户)和服务端(网站)之间请求和响应的标准,用于从服务器传输超媒体文档到本地浏览器(用户代理)。HTTP 的连接很简单,是无连接(限制每次连接只处理一个请求)无状态(不保存请求和响应之间的通信状态)的,底层使用 TCP协议进行连接,数据都是明文传输的。

HTTP常用方法

  • GET:获取资源,用来请求已被 URI 识别的资源;
  • POST:传输实体的主体;
  • PUT:传输文件;
  • DELETE:删除文件;
  • HEAD:获取报文首部;
  • OPTIONS:查询支持的方法。

HTTP常见状态码

  • 200 OK:请求成功。一般用于 GET 与 POST 请求;
  • 301 Moved Permanently:永久性重定向。表示请求的资源被分配了新的 URI,以后应使用资源现在所指的 URI 进行请求;
  • 302 Found:临时性重定向。与 301 类似,但资源只是临时被移动,仅限本次使用新的 URI,之后客户端应继续使用原有 URI;
  • 304 Not Modified:服务器资源未修改。此时可直接使用客户端未过期的缓存;
  • 400 Bad Request:请求报文存在语法错误;
  • 403 Forbidden:服务器理解请求客户端的请求,但是拒绝执行此请求;
  • 404 Not Found:服务器无法根据客户端的请求找到资源;
  • 500 Internal Server Error:服务器内部错误,无法完成请求;
  • 502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应;
  • 504 Gateway Time-out:充当网关或代理的服务器,未及时从远端服务器获取请求。

HTTP 常见首部字段

HTTP 的头部字段根据不同上下文可分为通用首部字段请求首部字段响应首部字段实体首部字段;根据代理对其的处理方式又可分为端到端首部逐跳首部,逐跳首部只对单次转发有效,经过缓存或代理后不再转发,HTTP/1.1和之后的版本中,要使用逐跳首部时需提供 Connection 首部字段。端到端首部则会一直发送给最终接收目标。

通用首部字段

缓存相关:

  • Cache-Control:控制缓存的行为(客户端-(请求)->缓存服务器<-(响应)-源服务器)。
    • 请求指令
      • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证,即客户端不接收缓存过的响应而不代表不缓存;
      • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存;
      • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期,时间是相对于请求的时间;
      • max-stale[=<seconds>]:表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间;
      • min-fresh=<seconds>:表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应;
      • no-transform:不得对资源进行转换或转变。Content-EncodingContent-RangeContent-Type等HTTP头不能由代理修改;
      • only-if-cached:表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。
    • 响应指令
      • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存;
      • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容(对应用户的本地浏览器);
      • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证;
      • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存;
      • no-transform:不得对资源进行转换或转变。Content-EncodingContent-RangeContent-Type等HTTP头不能由代理修改;
      • must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求;
      • proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略;
      • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期,时间是相对于请求的时间;
      • s-maxage=<seconds>:与 max-age 功能相同,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
  • Pragma:HTTP/1.1 以前的遗留字段Pargma: no-cacheCache-Control: no-cache功能一致,只用在客户端发送请求时;

其他:

  • Connection:决定当前的事务完成后,是否会关闭网络连接。如果该值是 keep-alive,网络连接就是持久的,不会关闭,使得对同一个服务器的请求可以继续在该连接上完成。除此之外还用来控制不再转发给代理的首部字段。
    • close:表明客户端或服务器想要关闭该网络连接,这是HTTP/1.0请求的默认值;
    • keep-alive:表明客户端想要保持该网络连接打开,HTTP/1.1的请求默认使用一个持久连接;
    • 以逗号分隔的 HTTP 头:这个请求头列表由头部名组成,这些头将被第一个非透明的代理或者代理间的缓存所移除:这些头定义了发出者和第一个实体之间的连接,而不是和目的地节点间的连接。
  • Date:创建 HTTP 报文的时间和日期;
  • Keep-Alive:在 Connection 设置为 keep-alive 时有效,允许发送方设置连接超时和最大请求数;
  • Trailer:说明在报文主体后记录了哪些首部字段;
  • Transfer-Encoding:传输报文主体时采用的编码方式;
  • Upgrade:可用于将已经建立的客户端/服务器连接升级到其他协议(通过相同的传输协议);
  • Via:追踪客户端与服务器之前的请求和响应报文的传输路径;
  • Warning:可能出现的问题的常规警告信息。

请求首部字段

内容协商:

  • Accept:用来告知(服务器)客户端可以处理的内容类型及其优先级,这种内容类型用 MIME 类型来表示(如:text/htmltext/plainapplication/jsonimage/png 等);
    • <MIME_type>/<MIME_subtype>:单一精确的 MIME 类型;
    • */*:任意类型的 MIME 类型;
    • ;q=<weight>:值代表优先顺序,用相对质量价值表示,又称作权重。
  • Accept-Charset:告知服务器用户代理支持的字符集及其优先级;
  • Accept-Encoding 告知服务器用户代理支持的内容编码及其优先级;
    • gzip:由文件压缩程序 gzip 生成的编码格式;
    • compress:由 UNIX 文件压缩程序 compress 生成的编码格式;
    • deflate:组合使用 zlib 格式及由 deflate 压缩算法生成的编码格式;
    • identify:不执行压缩或不会变化的默认编码格式。
  • Accept-Language:告知服务器用户代理支持的语言集及其优先级(中文、英文等);
  • TE:告知服务器用户代理能够处理的传输编码及其优先级,还可以指定伴随 trailer 字段的分块传输编码方式。

条件请求:

  • Expect:客户端通过该字段告知服务器只有在满足期望条件的情况下才能妥善地处理请求,如果服务器无法处理该期望条件,就应该返回 417 Expectation Failed。目前规范中只规定了 “100-continue” 这一个期望条件;
  • If-Match:服务器会比对该字段的值和资源的 ETag 值,仅当两者一致时,才会执行请求,否则,返回 412 Precondition Failed。该字段值为 * 时,会忽略 ETag 值;
  • If-Modified-Since:该字段值应该是一个日期,如果服务器上资源的更新时间较该字段值新则处理该请求,否则,返回 304 Not Modified
  • If-None-Match:与 If-Match 相反,该字段的值与请求资源的 ETag 不一致时,处理该请求;
  • If-Range:该字段的值(ETag 或时间)与资源的 ETag 或时间一致时,作为范围请求处理(使用首部字段 Range),否则,返回全体资源;
  • If-Unmodified-Since:与 If-Modified-Since 相反,服务器上资源的更新时间早于该字段值时处理请求,否则,返回412 Precondition Failed
  • Range:范围请求,只获取部分资源。如 Range: bytes=5001-10000,表示获取从第 5001 字节至 10000 字节的资源。成功处理范围请求时返回 206 Partial Content 响应,无法处理范围请求时返回 200 OK 响应及全部资源。

验证相关:

  • Authorization:向服务器回应自己的身份验证信息。客户端收到来自服务器的 401 Authentication Required 响应后,要在其请求中包含这个首部;

  • Proxy-Authorization:与Authorization类似,用于客户端与代理服务器之间的身份验证。

安全相关:

  • Cookie:包含与服务器相关联的已存储的 Cookie(即服务器先前使用 Set-Cookie 发送的 Cookie,或使用 Document.cookie 在 Javascript 中设置的 Cookie)。

其他:

  • From:请求来自何方,格式是客户端用户的有效电子邮件地址;
  • Host:服务器的主机名和端口号;
  • Referer:这次请求的 URL 是从哪里获得的;
  • User-Agent:客户端的浏览器或代理信息。

响应首部字段

条件响应:

  • Accept-Ranges:服务器是否能处理范围请求,bytes 表示能,none 表示不能;
  • ETag:用于表示服务器资源特定版本的标识符,服务器资源更新时,ETag 值也会更新,如果服务器资源未更改,则 ETag 值不变。服务器通过对比 If-None-Match 发送的 ETag 值,决定Web 服务器不需要重新发送完整的响应;
  • Vary:指定一些首部名称,客户端后续请求相同资源时,这些首部与缓存的那次请求完全一致时才会返回缓存的资源,同时服务器可以使用它来指示在内容协商中使用了哪些首部名称。

验证相关:

  • Proxy-Authorizate:与 WWW-Authenticate 类似,用于代理与客户端之间的认证,407 Proxy Authentication Required 响应必须包含该首部;
  • WWW-Authenticate:告诉客户端访问所请求资源的认证方案,401 Unauthorized 响应中肯定有该首部。

安全相关:

  • Set-Cookie:用于将 Cookie 从服务器发送到用户代理。

其他:

  • Age:表示对象在代理缓存中的停留时间(以秒为单位);
  • Location:客户端应重定向到指定 URI,基本配合 3** 响应出现;
  • Retry-After:告诉客户端多久之后再次发送请求。主要配合 503 Service Unavailable 使用,或与 3** 响应一起使用;
  • Server:HTTP 服务器的应用程序信息。

实体首部字段

实体主体:

  • Content-Encoding:告诉客户端实体的主体部分选用的内容编码方式;
  • Content-Language:告诉客户端实体主体使用的自然语言(中文、英文等);
  • Content-Length:表明实体主体部分的大小(单位:字节)。对实体主体进行内容编码传输时,不能再使用该首部字段;
  • Content-Location:报文主体部分相对应的 URI;
  • Content-MD5:一串由 MD5 算法生成的值。对于检查在传输过程中数据是否被无意的修改非常有用,但不能用于安全目的,因为报文如果被有意的修改,该字段的值也可以计算后作相应修改;
  • Content-Range:针对范围请求,提供了请求实体在原始实体内的位置(范围),还给出了整个实体的长度;
  • Content-Type:响应报文中对象的媒体类型。

缓存相关:

  • Expires:资源失效日期,当 Cache-Control 有指定 max-age 指令时,会优先处理 max-age
  • Last-Modified:资源最终修改时间。

其他:

  • Allow:通知客户端可以对特定资源使用那些 HTTP 方法。405 Method Not Allowed 响应中必须包含该首部。

HTTP 缓存

HTTP 缓存按缓存地点可分为浏览器缓存和代理缓存,而根据缓存方式可分为强缓存(也称本地缓存)和协商缓存(也称弱缓存)。

640029216-5bad97d9bb48c_fix732

  • 强缓存:强缓存是利用 HTTP 首部字段中的 ExpiresCache-Control 两个字段来控制的,用来表示资源的缓存时间。使用普通刷新时会忽略它,使用强制刷新时会带上 Cache-Control:no-cachePragma:no-cache 首部字段;
  • 协商缓存:协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。普通刷新会启用弱缓存,忽略强缓存。协商缓存主要涉及到两组首部字段:EtagIf-None-MatchLast-ModifiedIf-Modified-Since。两组字段同时使用的情况下,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304 Not Modified

HTTP 安全策略

CSP(Content-Security-Policy):实质就是白名单制度,常用于防止跨站点脚本攻击(XSS)。开发者明确告诉客户端,哪些外部资源可以加载和执行。它的实现和执行全部由浏览器完成,开发者只需提供配置。CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。

启用 CSP 的方式有两种:

  • 通过 Content-Security-Policy 首部字段;
  • 通过 HTML 的 <meta> 标签。

资源加载限制指令:

  • script-src:外部脚本;
  • style-src:样式表;
  • img-src:图像;
  • media-src:媒体文件(音频和视频);
  • font-src:字体文件;
  • object-src:插件(比如 Flash);
  • child-src:框架;
  • frame-ancestors:嵌入的外部资源(比如 <frame><iframe><embed><applet>) ;
  • connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等);
  • worker-srcworker脚本;
  • manifest-src:manifest 文件;
  • default-src:来设置上面各个选项的默认值,如果同时设置某个单项限制(比如 font-src),则该单项会覆盖默认值。

URL 限制指令:

  • frame-ancestors:限制嵌入框架的网页;
  • base-uri:限制 <base#href>
  • form-action:限制 <form#action>

其他限制指令:

  • block-all-mixed-content:HTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启);
  • upgrade-insecure-requests:自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
  • plugin-types:限制可以使用的插件格式;
  • sandbox:浏览器行为的限制,比如不能有弹出窗口等;
  • report-uri:仅记录不阻止 XSS 攻击,参数为一个 URL。该指令使用POST方法,发送一个 JSON 对象报告攻击行为给这个地址。

指令可用值:

  • 主机名:example.orghttps://example.com:443
  • 路径名:example.org/resources/js/
  • 通配符:*.example.org*://*.example.com:*(表示任意协议、任意子域名、任意端口)
  • 协议名:https:data:
  • 关键字'self':当前域名,需要加引号
  • 关键字'none':禁止加载任何外部资源,需要加引号。

除了常规值,script-src还可以设置一些特殊值:

  • 'unsafe-inline':允许执行页面内嵌的 <script> 标签和事件监听函数;
  • unsafe-eval:允许将字符串当作代码执行,比如使用 evalsetTimeoutsetIntervalFunction 等函数;
  • nonce 值:每次HTTP回应给出一个授权 token,页面内嵌脚本必须有这个 token,才会执行;
  • hash 值:列出允许执行的脚本代码的 Hash 值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。

HTTP 持久连接

HTTP1.0 和HTTP1.1 都不支持持久性的链接,不过为了解决这个问题,HTTP1.1 提出了 keep-alive(HTTP connection reuse)的方法,在一定时间内保持连接状态。

HTTP 状态管理

由于 HTTP 本身是无状态的,为了实现类似保存用户登录状态的功能,引入了 Cookie 技术,通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。 Cookie 会根据服务端发送的 Set-Cookie 首部字段信息,通知客户端保存 Cookie,下次客户端发送请求时,会自动在请求报文内加入 Cookie。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息);
  • 个性化设置(如用户自定义设置、主题等);
  • 浏览器行为跟踪(如跟踪分析用户行为等)。

2. HTTPS

超文本传输安全协议,HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包,主要作用是在不安全的网络上创建一个安全信道,并可在使用适当的加密包和服务器证书可被验证且可被信任时,对窃听和中间人攻击提供合理的防护。

HTTPS 协议的工作原理

16a45839ceacbb52

  1. Client 发起一个HTTPS 的请求,一般是通过 443 端口;
  2. Server 把事先配置好的公钥证书(public key certificate)返回给客户端;
  3. Client 验证公钥证书:比如是否在有效期内,证书的用途是不是匹配 Client 请求的站点,是不是在 CRL 吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的 Root 证书或者 Client 内置的 Root 证书)。如果验证通过则继续,不通过则显示警告信息;
  4. Client 使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给 Server;
  5. Server 使用自己的私钥(private key)解密这个消息,得到对称密钥。至此,Client 和 Server 双方都持有了相同的对称密钥;
  6. Server 使用对称密钥加密“明文内容 A”,发送给 Client;
  7. Client 使用对称密钥解密响应的密文,得到“明文内容 A”;
  8. Client 再次发起 HTTPS 的请求,使用对称密钥加密请求的“明文内容 B”,然后 Server 使用对称密钥解密密文,得到“明文内容B”。

HTTPS协议的缺点

  • HTTPS 握手阶段比较费时,会使页面加载时间延长 50%;
  • HTTPS 缓存不如 HTTP 高效,会增加数据开销;
  • 增加 10%~20% 的耗电;
  • SSL证书成本较高等。

3. HTTP 2.0

HTTP/2 没有改动 HTTP 的应用语义。 HTTP 方法、状态代码、URI 和标头字段等核心概念一如往常。 不过,HTTP/2 修改了数据格式化(分帧)以及在客户端与服务器间传输的方式。HTTP/2 的主要目标是通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP 标头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持。

二进制分帧(Binary Framing)

HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。

binary_framing_layer01

新的二进制分帧机制改变了客户端与服务器之间交换数据的方式。HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。为了说明这个过程,我们需要了解 HTTP/2 的三个概念:

  • 数据流:已建立的连接内的双向字节流,可以承载一条或多条消息;
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧;
  • :HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。

这些概念的关系总结如下:

  • 所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流;
  • 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息;
  • 每条消息都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧;
  • 帧是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载等等。 来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

streams_messages_frames01

多路复用 (Multiplexing)

在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接。 这是 HTTP/1.x 交付模型的直接结果,该模型可以保证每个连接每次只交付一个响应(响应排队)。 更糟糕的是,这种模型也会导致队首阻塞,从而造成底层 TCP 连接的效率低下。除此之外,浏览器客户端在同一时间,针对同一域名下的请求有一定数量的限制(跟具体浏览器相关),超过限制数目的请求会被阻塞,这也是为何一些站点会有多个静态资源 CDN 域名的原因之一(即域名分片)。

multiplexing01

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求/响应消息。HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。同时也解决了 HTTP/1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖,其结果就是应用速度更快、开发更简单、部署成本更低。

HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。

头部压缩(Header Compression)

HTTP/2 使用 Encoder 来减少需要传输的 Header 大小,通讯双方各自缓存一份头部字段表,既避免了重复 Header 的传输,又减小了需要传输的大小。HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用两种简单但是强大的技术:

  • 通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各个传输的大小;
  • 客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。

利用霍夫曼编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对。

header_compression01

作为一种进一步优化方式,HPACK 压缩上下文包含一个静态表和一个动态表: 静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段(例如,有效标头名称)的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。 因此,为之前未见过的值采用静态 Huffman 编码,并替换每一侧静态表或动态表中已存在值的索引,可以减小每个请求的大小。

请求优先级(Request Priorities)

HTTP/2 标准允许每个数据流都有一个关联的权重和依赖关系,同时允许客户端随时更新这些优先级:

  • 可以向每个数据流分配一个介于 1 至 256 之间的整数。
  • 每个数据流与其他数据流之间可以存在显式依赖关系。

stream_prioritization01

数据流依赖关系和权重的组合让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。 反过来,服务器可以使用此信息通过控制 CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,带宽分配可以确保将高优先级响应以最优方式传输至客户端。

HTTP/2 内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。 声明数据流依赖关系指出,应尽可能先向父数据流分配资源,然后再向其依赖项分配资源。共享相同父项的数据流(即,同级数据流)应按其权重比例分配资源。

服务端推送(Server Push)

push01

服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且,服务端推送能把客户端所需要的资源伴随着 index.html 一起发送到客户端,省去了客户端重复请求的步骤。推送资源可以进行以下处理:

  • 由客户端缓存
  • 在不同页面之间重用
  • 与其他资源一起复用
  • 由服务器设定优先级
  • 被客户端拒绝

所有服务器推送数据流都由 PUSH_PROMISE 帧发起,表明了服务器向客户端推送所述资源的意图,并且需要先于请求推送资源的响应数据传输。 这种传输顺序非常重要:客户端需要了解服务器打算推送哪些资源,以免为这些资源创建重复请求。 满足此要求的最简单策略是先于父响应(即,DATA 帧)发送所有 PUSH_PROMISE 帧,其中包含所承诺资源的 HTTP 标头。在客户端接收到 PUSH_PROMISE 帧后,它可以根据自身情况选择拒绝数据流(通过 RST_STREAM 帧)。 (例如,如果资源已经位于缓存中,便可能会发生这种情况。) 相比之下,使用资源内联(一种受欢迎的 HTTP/1.x“优化”)等同于“强制推送”: 客户端无法选择拒绝、取消或单独处理内联的资源。

浏览器基础

1. 浏览器工作原理

浏览器和服务器建立请求阶段的路由和响应暂时忽略,着重理解浏览器的解析和渲染部分。

14kb 规则

第一个响应包是14kb大小。这是慢开始的一部分,慢开始是一种均衡网络连接速度的算法。慢开始逐渐增加发送数据的数量直到达到网络的最大带宽。在 TCP 慢开始中,在收到初始包之后, 服务器会将下一个包的大小加倍到大约 28kb。 后续的包依次是前一个包大小的二倍直到达到预定的阈值,或者遇到拥塞。因此 web 性能优化需要将此初始 14Kb 响应作为优化重点。

解析

即使请求页面的 HTML 大于初始的 14kb 数据包,浏览器也将开始解析并尝试根据其拥有的数据进行渲染。这就是为什么在前 14kb 中包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板(第一次渲染所需的 CSS 和 HTML)对于 web 性能优化来说是重要的。

  1. 构建 DOM 树(第一步):HTML 解析涉及到 tokenization 和树的构造。当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个 CSS 文件时,解析也可以继续进行,但是对于 <script> 标签(特别是没有 async 或者 defer 属性)会阻塞渲染并停止 HTML 的解析;
    • 预加载扫描器:浏览器使用主线程构建 DOM 树,此时预加载扫描仪将解析可用的内容并请求高优先级资源,如 CSS、JavaScript 和 web 字体,不必等到解析器找到对外部资源的引用后才发起请求。它将在后台检索资源,以便在主 HTML 解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。预加载扫描仪提供的优化减少了阻塞;
  2. 构建 CSSOM 树(第二步):CSS 对象模型和 DOM 是相似的。DOM 和 CSSOM 是两棵树,它们是独立的数据结构。浏览器将 CSS 规则转换为可以理解和使用的样式映射。浏览器遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树;
  3. 其他过程:当 CSS 被解析并创建 CSSOM 时,其他资源,包括 JavaScript 文件正在下载(预加载扫描器)。JavaScript 被解释、编译、解析和执行,脚本被解析为抽象语法树并将其传递到解释器中;除此之外还包括构建辅助功能(accessibility )树。

渲染

渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的 CSSOM 树和 DOM 树组合成一个 Render 树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。

  1. 样式 Style(第三步):将 DOM 和 CSSOM 组合成一个 Render 树(渲染树),计算样式树或渲染树从 DOM 树的根开始构建,遍历每个可见节点。对于任何具有 display: none 样式的结点,将不会出现在 Render 树上,与之相对应的,具有 visibility: hidden 的节点则会出现在 Render 树上。Render 树保存所有具有内容和计算样式的可见节点——将所有相关样式匹配到 DOM 树中的每个可见节点,并根据 CSS 级联确定每个节点的计算样式;
  2. 布局 Layout(第四步):构建渲染树后,开始布局。第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定;
  3. 绘制 Paint(最终):最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为(FMP)。在绘制或光栅化阶段,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。绘制可以将布局树中的元素分解为多个层,将内容提升到 GPU上 的层(而不是 CPU 上的主线程)可以提高绘制和重新绘制性能。层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用;
  4. 合成 Compositing:为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层,当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。

补充:性能指标

  • FP(First Paint):首次绘制,代表浏览器第一次向屏幕传输像素的时间,也就是页面在屏幕上首次发生视觉变化的时间;
  • FCP(First Contentful Paint):首次内容绘制,代表浏览器第一次向屏幕绘制内容,只有首次绘制文本、图片(包含背景图)、非白色的canvas或 SVG 时才被算作FCP;
  • FMP(First Meaningful Paint):首次有效绘制,表示页面的“主要内容”开始出现在屏幕上的时间点。它是我们测量用户加载体验的主要指标;
  • LCP(Largest Contentful Paint):表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点;
  • TTI(Time to Interactive):可交互时间,表示网页第一次完全达到可交互状态的时间点,可交互状态指的是页面上的UI组件是可以交互的。TTI 可以让我们了解我们的产品需要多久可以真正达到“可用”的状态;
  • FCI(First CPU Idle): 第一次CPU空闲,是对 TTI 的一种补充,TTI 告诉我们页面什么时候完全达到可用,FCI 告诉我们浏览器第一次可以响应用户输入是什么时候。

2. 本地存储

Cookie、sessionStorage、localStorage 的异同:

  • 首先三者都是保存在浏览器端,并且是同源的 ;
  • Cookie:可以在浏览器和服务器端来回传递,存储容量小,只有大约 4K 左右;
  • sessionStorage:本身就是一个会话过程,关闭浏览器后消失,session 为一个会话,当页面不同即使是同一页面打开两次,也被视为同一次会话;
  • localStorage:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效。

3. XSS 和 CSRF

  • XSS(Cross-Site Scripting):跨站脚本攻击,攻击者通过向页面注入代码,达到窃取信息等目的,本质是数据被当作程序执行,常使用输入检查/转义或 CSP 内容安全策略的方法避免攻击;
  • CSRF(Cross-Site Request Forgery):跨站请求伪造,与 XSS 不同的是,XSS 是攻击者直接对我们的网站 A 进行注入攻击,CSRF 是通过网站 B 对我们的网站 A 进行伪造请求。常使用同源检测、Samesite Cookie、Token 验证等方式来避免攻击。

4. CORS(Cross-Origin Resource Sharing)

跨域资源共享,是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它源(域,协议和端口),这样浏览器就可以访问加载这些资源。跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

cors_principle

  • 简单请求:不会触发 CORS 预检请求的请求称为简单请求,若请求满足所有下述条件,则该请求可视为“简单请求”:
    • 使用下列方法之一:GETHEADPOST
    • 允许人为设置的字段:AcceptAccept-LanguageContent-LanguageContent-Type
    • Content-Type 仅允许 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
    • 请求中没有在任何 XMLHttpRequest.upload 对象上注册事件侦听器;
    • 请求中没有使用 ReadableStream 对象。
  • 预检请求:需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
  • JSONP:解决跨域问题的另一种方法,通过 <script> 标签的 src 属性请求服务端返回数据和回调函数,浏览器执行后即可拿到数据。

5. 性能优化

xxx

前端语言基础

1. HTML 基础

Doctype 的作用

Doctype 声明于文档最前面,告诉浏览器以何种方式来渲染页面,分为严格模式和混杂模式。

  • 严格模式:排版和 JS 的运作模式以该浏览器支持的最高标准运行;
  • 混杂模式:向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。

2. CSS 基础

重绘和重排

DOM 的变化影响到了元素的几何属性比如宽高,浏览器重新计算元素的几何属性,其他元素的几何属性也会受到影响,浏览器需要重新构造渲染树,这个过程称之为重排,浏览器将受到影响的部分重新绘制在屏幕上的过程称为重绘。重排一定导致重绘,重绘不一定导致重排,引起重排重绘的原因有:

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸位置的改变;
  • 浏览器页面初始化;
  • 浏览器窗口大小发生改变。

BFC(Block Fomatting Context)

BFC,块级格式化上下文,用于解决浮动元素溢出和 margin 重叠的问题,常见的创建 BFC 的方式有:

  • 浮动元素(float 不为 none);
  • 绝对定位元素;
  • 行内块元素;
  • 表格单元格、表格标题、匿名表格单元格元素;
  • overflow 计算值不为 visible 的块元素;
  • display 值为 flow-root 的元素等。

CSS 动画

  • transition 和 animation 的区别:animation 和 transition 大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是 transition 需要触发一个事件才能改变属性,而 animation 不需要触发任何事件的情况下才会随时间改变属性值,并且 transition 为 2 帧,从from … to,而 animation 可以逐帧。

3.JavaScript 基础

JS 代码执行过程

JS 作为一种解释型语言,代码执行总共包含两个阶段:解释阶段和执行阶段。

  • 解释阶段:

    • 词法分析
    • 语法分析
    • 作用域规则确定(需要注意的是,作用域链在创建时确定而非执行时)

    执行阶段:

    • 创建执行上下文
    • 执行函数代码
    • 垃圾回收

事件循环(Event Loop)

JavaScript 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,JavaScript 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环”的原因。

v2-da078fa3eadf3db4bf455904ae06f84b_720w

实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task):

  • 微任务:
    • new Promise()
    • new MutaionObserver()
    • process.nextTick() (Node,实际上不属于事件循环)。
  • 宏任务:
    • script(主程序代码);
    • setInterval();
    • setTimeout();
    • setImmediate()(Node);
    • I/O;
    • UI交互事件。

二者的优先顺序为:当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

Node 环境下的事件循环

在 node 中,事件循环表现出的状态与浏览器中大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现是依靠的 libuv 引擎。我们知道 node 选择 chrome v8 引擎作为 JS 解释器,v8 引擎将 JS 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。因此实际上 node 中的事件循环存在于 libuv 引擎中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<--connections--- │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

这些阶段大致的功能如下:

  • timers:这个阶段执行定时器队列中的回调如 setTimeout()setInterval()
  • I/O callbacks:这个阶段执行几乎所有的回调。但是不包括 close 事件,定时器和 setImmediate() 的回调;
  • idle, prepare:这个阶段仅在内部使用,可以不必理会;
  • poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里;
  • check:setImmediate() 的回调会在这个阶段执行;
  • close callbacks:例如 socket.on('close', ...) 这种close事件的回调。

事件流(Event Flow)

事件流描述的是从页面中接收事件的顺序,早期的 IE 和 Netscape 提出了完全相反的事件流概念,IE 事件流是事件冒泡,而 Netscape 的事件流就是事件捕获。事件冒泡(事件委托就是利用冒泡实现的),即从下至上,从目标触发的元素逐级向上传播,直到 window 对象;事件捕获,即从 document 逐级向下传播到目标元素。目前主要有 DOM0~3 总共4个级别。

事件流的三个阶段:

  • 事件捕获阶段

  • 处于目标阶段

  • 事件冒泡阶段

eventflow

  • DOM0:使用elem.onXXX的形式绑定事件;
  • DOM2:DOM2级事件规定的事件流包括三个阶段: (1)事件捕获阶段 (2)处于目标阶段 (3)事件冒泡阶段,通过2个方法来绑定事件:addEventListener()removeEventListener()

使用事件委托的时候需要注意,以下多个事件不支持冒泡:

focus、blur、mouseenter、mouseleave、load、unload、resize

垃圾回收

垃圾回收的方法:标记清除、计数引用。

  • 标记清除:这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
  • 计数引用:另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减1,当这个值的引用次数为0的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为0的这些值。

JavaScript 常见回收规则如下:

  1. 全局变量不会被回收;
  2. 局部变量会被回收;
  3. 只要被另外一个作用域所引用就不会被回收 (闭包)。

模块化

  • CommonJS:CommonJS是服务器模块的规范,Node.js采用了这个规范。根据 CommonJS 规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
  • AMD(Asynchronous Module Definition):AMD 规范加载模块是异步的,并允许函数回调,不必等到所有模块都加载完成,后续操作可以正常执行。AMD 中,使用 require 获取依赖模块,使用 exports 导出 API。
  • CMD(Common Module Definition):CMD规范和AMD类似,都主要运行于浏览器端,写法上看起来也很类似。主要是区别在于模块初始化时机:AMD中只要模块作为依赖时,就会加载并初始化,而CMD中,模块作为依赖且被引用时才会初始化,否则只会加载;CMD 推崇依赖就近,AMD 推崇依赖前置。
  • UMD(Universal Module Definition):UMD 是AMD 和 CommonJS 的糅合。UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。

继承

由于 JavaScript 是一门基于原型的语言,因此无法直接实现传统意义上的继承。

  • 原型链继承:将父类的实例作为子类的原型。
    • 缺点:父类的引用属性会被所有子类实例共享。
1
2
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
  • 构造函数继承:将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype的继承。
    • 优点:父类的引用属性不会被共享且子类构建实例时可以向父类传递参数。
    • 缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。
1
SuperType.call(SubType);
  • 组合继承:原型式继承和构造函数继承的组合,兼具了二者的优点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.name = 'parent';
this.arr = [1, 2, 3];
}

SuperType.prototype.say = function() {
console.log('this is parent')
}

function SubType() {
SuperType.call(this) // 第二次调用SuperType
}

SubType.prototype = new SuperType() // 第一次调用SuperType
  • 原型式继承:原型式继承使用一个空对象作为代理,本质上是对参数对象的一个浅复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function object(o){
function F(){}
F.prototype = o;
return new F();
}

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
  • 寄生式继承:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(original){ 
var clone=object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
  • 寄生组合继承:解决组合继承会两次调用父类的构造函数造成浪费的缺点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}

function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
alert(this.name);
};

function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}

异步

Promise

首先通过一个例子直观对比传统回调和使用 Promise 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
// 传统方式,给异步函数传入回调函数
someFunction(someParams, successCallback, failureCallback) {

// 使用 Promise,在 then 中执行回调
new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
}).then(successCallback, failureCallback);

promises

Promise 内部维护了两个状态变化过程:pending -> fulfilledpending -> rejected,代表了异步事件的结果。状态发生变化后便根据最终状态决定执行 successCallback 或者 failureCallback。可以用 promise.then()promise.catch()promise.finally() 这些方法将目标操作与一个已确定状态的 Promise 关联起来。这些方法还会返回一个新生成的 Promise 对象,因此可以实现链式调用,从而避免了传统回调函数方式产生的回调地狱问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});

// 需要及时处理错误的情况
myPromise
.then(handleResolvedA, handleRejectedA)
.then(handleResolvedB, handleRejectedB)
.then(handleResolvedC, handleRejectedC);

// 不需要及时处理的情况
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);

链式调用中的 Promise 像是一个栈,每个都必须从顶端被弹出。链式调用中的第一个 Promise 是嵌套最深的一个,也将是第一个被弹出的。

1
(promise D, (promise C, (promise B, (promise A))))

方法:

  • Promise.all(iterable):将多个 Promise 实例,包装成一个新的 Promise 实例,状态由所有实例决定:只要参数实例有一个变成rejected状态,包装实例就会变成rejected状态;如果所有参数实例都变成fulfilled状态,包装实例就会变成fulfilled状态;
  • Promise.any(iterable):将多个 Promise 实例,包装成一个新的 Promise 实例,只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态;
  • Promise.race(iterable):将多个 Promise 实例,包装成一个新的 Promise 实例,状态由任一实例决定;
  • Promise.allSettled(iterable):将多个 Promise 实例,包装成一个新的 Promise 实例,且只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束;
  • Promise.reject(reason):返回一个给定原因的状态为 rejected 的 Promise;
  • Promise.resolve(value):返回一个给定值的状态为 fulfilled 的 Promise。

Generator

JavaScript 的 Generator(尤其是与 Promises 结合使用时)是一种非常强大的异步编程工具,因为它们可以缓解回调问题,例如回调地狱和控制反转。

调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器( iterator )对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*,则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。

当在生成器函数中显式 return时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 donetrue。如果 return后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。

next() 方法返回一个对象,这个对象包含两个属性:valuedonevalue 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。

1
2
3
4
5
6
7
8
9
10
11
12
function* generator() {
yield 1;
yield 2;
}

const gen = generator();

console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // undefined
console.log(generator().next().value); // 1
console.log(generator().next().value); // 1

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法:

1
2
3
4
5
6
7
8
9
10
11
function* foo() {
yield 1;
yield 2;
yield 3;
return 4;
}

for (let v of foo()) {
console.log(v);
}
// 1 2 3

需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回值不包括在for...of循环之中。

可以利用这个特性给对象添加遍历器接口,使得对象可以通过 for...of 遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);

for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

// 直接调用函数
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

// 添加到对象的 Symbol.iterator
jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

async/await

// 终于写到这里了

判断变量类型

JavaScript 总共有 5 中基本的数据类型(primitive types):

  • number
  • string
  • boolean
  • undefined
  • null

除此之外的变量类型都是引用类型。其区别在于,基本类型在复制其变量时会创建这个值的一个副本,而引用类型变量本质是一个指针,指向内存中的对象,复制引用类型变量实际是复制该指针,即他们都指向同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Value               Class      Type
-------------------------------------
1.2 Number number
"foo" String string
true Boolean boolean
undefined Undefined undefined
null Null object
new Function("") Function function
new String("foo") String object
new Number(1.2) Number object
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object

typeof

typeof 只适用于判断简单的变量类型,返回上表中对应“Type”中的值。

instanceof

instanceof 操作符用来比较两个操作数的构造函数,一般用于比较自定义的对象。

Object.prototype.toString

Object.prototype.toString方法返回上表中对应“Class”中的值,也是最准确的判断方法。

常用功能实现

1. 深拷贝

1
2
3
4
5
6
7
function deepClone(obj) {
var newObj = obj instanceof Array ? [] : {};
for (var i in obj) {
newObj[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
}
return newObj;
}

2. 实现 bind 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function (obj) {
const args = Array.prototype.slice.call(arguments, 1);
const fn = this;

let F = function () {};
let bound = function () {
const params = Array.prototype.slice.call(arguments);
fn.apply(obj, args.concat(params));
};
F.prototype = fn.prototype;
bound.prototype = new F();
return bound;
};

3. 使用 Promise 封装 ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function ajax(method, url, params) {
return new Promise((resolve, reject) => {
// 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
// 状态改变时的回调
xhr.onreadystatechange = function () {
// readyState为4的时候已接收完毕
if (xhr.readyState === 4) {
// 状态码200表示成功
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
}
};

// get
if (method === 'get' || method === 'GET') {
if (typeof params === 'object') {
// params拆解成字符串
params = Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
url = params ? url + '?' + params : url;
xhr.open(method, url, true);
xhr.send();
}

//post
if (method === 'post' || method === 'POST') {
xhr.open(method, url, true);
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
xhr.send(JSON.stringify(params));
}
});
}

4. 尾递归优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 蹦床函数
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}

// 尾递归优化
function tco(f) {
let value;
let active = false;
let accumulated = [];

return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}

var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});

sum(1, 100000) // 100001

5. 实现一个简单的 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}

myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}

应用开发框架

1. Vue.js 的响应式原理实现

a6054cb4746ac78184b75cd07b35fb75

reactive

Vue.js 2.X 实现响应式对象的原理就是利用 Object.defineProperty 给数据添加了 getter 和 setter,getter 做的事情是依赖收集,setter 做的事情是派发更新。

实现这一点的底层函数是 defineReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

依赖收集

defineReactive 函数的 get 方法中,通过 dep.depend 做依赖收集,这里的 depDep 类的一个实例。该类的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

export function popTarget () {
Dep.target = targetStack.pop()
}

通过代码可知,Dep 实际上是实现对观察者 Watcher 的一种管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
let uid = 0

/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
// ...
}

其中,this.depsthis.newDeps 表示 Watcher 实例持有的 Dep 实例的数组;而 this.depIdsthis.newDepIds 分别代表 this.depsthis.newDeps 的 id,其作用主要是后续添加依赖项的过程中去重。newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。

派发更新

defineReactive 函数的 set 方法中,通过 dep.notify() 通知所有的订阅者。当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法。从上面 Dep 类的定义中可以发现,dep.notify() 实际上实现的是遍历 Watcher 的实例数组,然后调用对应的 update 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}

在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}

由此可见,Vue 在做派发更新的时候并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue触发回调。先看 flushSchedulerQueue 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
let flushing = false
let index = 0
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
flushing = true
let watcher, id

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)

// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()

// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}

这部分代码主要的逻辑包括:队列排序、队列遍历和状态恢复。

其中对队列进行排序目的是确保组件的更新由父到子以及用户的自定义 watcher 要优先于渲染 watcher 执行。

队列遍历就是执行对应 watcherrun 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Watcher {
// ...

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}

getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
}

run 函数实际上就是执行 this.getAndInvoke 方法,并传入 watcher 的回调函数。getAndInvoke 函数逻辑也很简单,先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。

对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同。

状态恢复就是把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Reset the scheduler's state.
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}

简单实现

xxx

2. Virtual DOM

行业前沿

1. 微前端

2. WebAssembly


前端知识体系总结
https://infiniture.cn/2021/03/10/前端知识体系总结/
作者
NickHopps
发布于
2021年3月10日
许可协议