diff --git a/404.html b/404.html index 4369dfa..4d43565 100644 --- a/404.html +++ b/404.html @@ -10,13 +10,13 @@ - +
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 9ab61c7..4d9be52 100644 --- a/about.html +++ b/about.html @@ -10,13 +10,13 @@ - +
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/e31563f4.3b2fb9de.js b/assets/js/e31563f4.d05d9934.js similarity index 94% rename from assets/js/e31563f4.3b2fb9de.js rename to assets/js/e31563f4.d05d9934.js index 63fd598..c9c9be3 100644 --- a/assets/js/e31563f4.3b2fb9de.js +++ b/assets/js/e31563f4.d05d9934.js @@ -1 +1 @@ -"use strict";(self.webpackChunkjjbook=self.webpackChunkjjbook||[]).push([[3280],{3905:(e,t,r)=>{r.d(t,{Zo:()=>s,kt:()=>f});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function l(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function p(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var o=n.createContext({}),u=function(e){var t=n.useContext(o),r=t;return e&&(r="function"==typeof e?e(t):p(p({},t),e)),r},s=function(e){var t=u(e.components);return n.createElement(o.Provider,{value:t},e.children)},m="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},k=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,l=e.originalType,o=e.parentName,s=i(e,["components","mdxType","originalType","parentName"]),m=u(r),k=a,f=m["".concat(o,".").concat(k)]||m[k]||c[k]||l;return r?n.createElement(f,p(p({ref:t},s),{},{components:r})):n.createElement(f,p({ref:t},s))}));function f(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var l=r.length,p=new Array(l);p[0]=k;var i={};for(var o in t)hasOwnProperty.call(t,o)&&(i[o]=t[o]);i.originalType=e,i[m]="string"==typeof e?e:a,p[1]=i;for(var u=2;u{r.r(t),r.d(t,{assets:()=>o,contentTitle:()=>p,default:()=>c,frontMatter:()=>l,metadata:()=>i,toc:()=>u});var n=r(87462),a=(r(67294),r(3905));const l={sidebar_label:"\u524d\u8a00",sidebar_position:.5,slug:"/"},p="\u524d\u8a00",i={unversionedId:"preface",id:"preface",title:"\u524d\u8a00",description:"\u5199\u4f5c\u80cc\u666f",source:"@site/docs/preface.md",sourceDirName:".",slug:"/",permalink:"/awesome-interview/",draft:!1,tags:[],version:"current",sidebarPosition:.5,frontMatter:{sidebar_label:"\u524d\u8a00",sidebar_position:.5,slug:"/"},sidebar:"tutorialSidebar",previous:{title:"\u5173\u4e8e\u6211\u4eec",permalink:"/awesome-interview/about"},next:{title:"\u6d4f\u89c8\u5668\uff1a\u6d4f\u89c8\u5668\u8de8\u57df",permalink:"/awesome-interview/book1/browser-cross-origin"}},o={},u=[{value:"\u5199\u4f5c\u80cc\u666f",id:"\u5199\u4f5c\u80cc\u666f",level:2},{value:"\u9762\u8bd5\u75db\u70b9",id:"\u9762\u8bd5\u75db\u70b9",level:2},{value:"\u5199\u4f5c\u7406\u5ff5",id:"\u5199\u4f5c\u7406\u5ff5",level:2},{value:"\u9002\u5408\u4eba\u7fa4",id:"\u9002\u5408\u4eba\u7fa4",level:2},{value:"\u4e92\u52a8\u4e0e\u52d8\u8bef",id:"\u4e92\u52a8\u4e0e\u52d8\u8bef",level:2}],s={toc:u},m="wrapper";function c(e){let{components:t,...r}=e;return(0,a.kt)(m,(0,n.Z)({},s,r,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"\u524d\u8a00"},"\u524d\u8a00"),(0,a.kt)("h2",{id:"\u5199\u4f5c\u80cc\u666f"},"\u5199\u4f5c\u80cc\u666f"),(0,a.kt)("p",null,"\u5728\u6211\u4eec\u56e2\u961f\u4e2d\uff0c\u6bcf\u5e74\u90fd\u4f1a\u6709\u90e8\u5206\u4eba\u9700\u8981\u66f4\u6362\u65b0\u7684\u5de5\u4f5c\u73af\u5883\uff0c\u56e0\u6b64\u6211\u4eec\u7684\u804a\u5929\u8bdd\u9898\u603b\u662f\u9636\u6bb5\u6027\u7684\u53d8\u6210\u9762\u8bd5\u9898\u63a2\u8ba8\u3002\u968f\u7740\u8fd9\u51e0\u5e74\u6211\u4eec\u5bf9\u524d\u7aef\u9762\u8bd5\u65b9\u9762\u7684\u7ecf\u9a8c\u79ef\u7d2f\u548c\u603b\u7ed3\uff0c\u6211\u4eec\u4e00\u81f4\u8ba4\u4e3a\u524d\u7aef\u9762\u8bd5\u7684\u590d\u4e60\u662f\u6709\u5173\u952e\u8def\u5f84\u7684\u3002\u57fa\u4e8e\u6211\u4eec\u5185\u90e8\u9700\u8981\uff0c\u4e5f\u66fe\u8fed\u4ee3\u8fc7\u51e0\u4e2a\u9762\u8bd5\u9898\u5e93\u7248\u672c\u3002"),(0,a.kt)("p",null,"\u4ece\u9762\u8bd5\u89d2\u5ea6\u5206\u6790\uff0c\u9762\u8bd5\u6700\u5178\u578b\u7684\u7279\u70b9\u662f\u65f6\u95f4\u6709\u9650\u3002\u8fd9\u610f\u5473\u7740\u9762\u8bd5\u5b98\u548c\u5019\u9009\u4eba\u9700\u8981\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u505a\u51fa\u6700\u5927\u7a0b\u5ea6\u7684\u6709\u6548\u6c9f\u901a\u3002"),(0,a.kt)("p",null,"\u9762\u8bd5\u5b98\u5982\u4f55\u6709\u6548\u4e14\u5168\u9762\u7684\u4e86\u89e3\u5019\u9009\u4eba\uff0c\u662f\u53e6\u5916\u4e00\u95e8\u5b66\u95ee\u3002\u800c\u5019\u9009\u4eba\u7684\u6311\u6218\u5728\u4e8e\u9762\u5bf9\u95ee\u9898\u65f6\uff0c\u5982\u4f55\u5728\u4e00\u4e24\u5206\u949f\u5185\u4f5c\u51fa\u6709\u6548\u56de\u7b54\u3002\u6709\u6548\u56de\u7b54\u662f\u6307\uff1a\u7528\u4e24\u4e09\u53e5\u8bdd\u5bf9\u95ee\u9898\u4f5c\u51fa\u6982\u62ec\u6027\u56de\u7b54\uff0c\u5e76\u5f15\u5bfc\u9762\u8bd5\u5b98\u5bf9\u56de\u7b54\u4e2d\u63d0\u5230\u7684\u5173\u952e\u8bcd\u8fdb\u4e00\u6b65\u6df1\u5165\u63d0\u95ee\u3002"),(0,a.kt)("p",null,"\u4e0d\u5c11\u540c\u884c\u7684\u5fc3\u6001\u662f\uff1a\u5982\u679c\u9762\u8bd5\u95ee\u5f97\u96be\uff0c\u90a3\u4fbf\u662f\u9762\u8bd5\u9020\u822a\u6bcd\uff0c\u5de5\u4f5c\u62e7\u87ba\u4e1d\uff1b\u5982\u679c\u9762\u8bd5\u95ee\u5f97\u7b80\u5355\uff0c\u90a3\u4fbf\u662f\u4e1c\u897f\u6211\u4f1a\u7528\uff0c\u4f46\u6211\u4e0d\u4f1a\u8bf4\uff0c\u6ca1\u53d1\u6325\u597d\u3002\u7531\u4e8e\u5404\u79cd\u539f\u56e0\uff0c\u5931\u53bb\u4e86\u66f4\u591a\u7684\u9009\u62e9\u3002\u672c\u4e66\u5e0c\u671b\u80fd\u591f\u5e2e\u52a9\u5927\u5bb6\u5c3d\u53ef\u80fd\u89e3\u51b3\u8fd9\u65b9\u9762\u7684\u95ee\u9898\uff0c\u8ba9\u524d\u7aef\u5f00\u53d1\u8005\u5728\u9762\u8bd5\u590d\u4e60\u9636\u6bb5\u4e8b\u534a\u529f\u500d\uff0c\u4ee5\u66f4\u597d\u7684\u72b6\u6001\u8fdb\u884c\u9762\u8bd5\u3002"),(0,a.kt)("p",null,"\u57fa\u4e8e\u4ee5\u4e0a\u5199\u4f5c\u80cc\u666f\uff0c\u6211\u4eec\u53ef\u4ee5\u8fbe\u6210\u4e24\u4e2a\u5171\u8bc6\uff1a"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u9762\u8bd5\u65f6\u95f4\u603b\u662f\u6709\u9650\u7684")),(0,a.kt)("p",{parentName:"li"},"\u56f4\u7ed5\u4e00\u9053\u666e\u901a\u6280\u672f\u9898\u76ee\u7684\u65f6\u957f\u4e00\u822c\u5728 1-3 \u5206\u949f\uff0c\u4e00\u8f6e\u6280\u672f\u9762\u8bd5\u7684\u65f6\u957f\u4e00\u822c\u63a7\u5236\u5728 30-60 \u5206\u949f\uff0c\u9762\u8bd5\u9898\u76ee\u901a\u5e38\u6d89\u53ca\u4e0d\u540c\u77e5\u8bc6\u9762\u3002")),(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u95ee\u9898\u7684\u56de\u7b54\u4e00\u822c\u662f\u81ea\u9876\u5411\u4e0b\u7684")),(0,a.kt)("p",{parentName:"li"},"\u4ee5\u4e00\u4e2a\u6982\u62ec\u6027\u8f83\u5f3a\u7684\u56de\u7b54\u8fdb\u884c\u53cd\u9988\uff0c\u9762\u8bd5\u5b98\u83b7\u5f97\u53cd\u9988\u540e\uff0c\u901a\u5e38\u4f1a\u57fa\u4e8e\u5019\u9009\u4eba\u56de\u7b54\u4e2d\u63d0\u5230\u7684\u5173\u952e\u70b9\u6216\u9762\u8bd5\u5b98\u8ba4\u4e3a\u7684\u5176\u4ed6\u5173\u952e\u70b9\u5c55\u5f00\u8fdb\u4e00\u6b65\u63d0\u95ee\u3002"))),(0,a.kt)("h2",{id:"\u9762\u8bd5\u75db\u70b9"},"\u9762\u8bd5\u75db\u70b9"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://user-images.githubusercontent.com/17002181/132717020-3fa8010a-ae1d-4921-88f9-5563b5b53c22.png",alt:"image"})),(0,a.kt)("p",null,"\u4e0a\u9762\u8fd9\u5f20\u7ed3\u6784\u56fe\uff0c\u6211\u4eec\u68b3\u7406\u4e86\u4ece\u51c6\u5907\u9762\u8bd5\u5230\u6700\u7ec8\u83b7\u5f97 Offer \u7684\u5927\u81f4\u8fc7\u7a0b\u3002\u8f83\u591a\u4eba\u7684\u590d\u4e60\u65b9\u6cd5\u4e3b\u8981\u6765\u6e90\u4e8e\u7cfb\u7edf\u5730\u590d\u4e60\u6240\u6709\u77e5\u8bc6\u70b9\uff0c\u6216\u8005\u6709\u76ee\u7684\u6027\u7684\u67e5\u627e\u9762\u7ecf\u5e76\u590d\u4e60\u5bf9\u5e94\u7684\u77e5\u8bc6\u70b9\u3002\u5728\u65f6\u95f4\u5145\u5206\u7684\u60c5\u51b5\u4e0b\uff0c\u7cfb\u7edf\u590d\u4e60\u5728\u590d\u4e60\u9636\u6bb5\u521d\u671f\u4f1a\u6709\u4e00\u5b9a\u6210\u6548\u3002\u4f46\u662f\u4ec5\u505c\u7559\u5728\u8fd9\u4e00\u6b65\uff0c\u4e5f\u8bb8\u5e76\u4e0d\u662f\u6700\u597d\u7684\u9009\u62e9\u3002"),(0,a.kt)("p",null,"\u6211\u4eec\u53ef\u4ee5\u4ece\u4ee5\u4e0b\u89d2\u5ea6\u5256\u6790\u9762\u8bd5\u590d\u4e60\u9636\u6bb5\u7684\u5173\u6ce8\u70b9\uff1a"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u590d\u4e60\u4ec0\u4e48")),(0,a.kt)("p",{parentName:"li"},"\u5019\u9009\u4eba\u901a\u5e38\u4f1a\u7cfb\u7edf\u5730\u67e5\u770b\u77e5\u8bc6\u70b9\u603b\u7ed3\uff0c\u800c\u5e02\u9762\u4e0a\u5927\u90e8\u5206\u7684\u77e5\u8bc6\u70b9\u603b\u7ed3\uff0c\u63d0\u7eb2\u6392\u5217\u987a\u5e8f\u4e3a\u7531\u6613\u5230\u96be\uff0c\u901a\u5e38\u4ece\u57fa\u672c\u6570\u636e\u7c7b\u578b\u7ae0\u8282\u5f00\u59cb\u8bb2\u89e3\uff0c\u5185\u5bb9\u6bd4\u8f83\u5197\u957f\u3002\u7cfb\u7edf\u590d\u4e60\u6709\u4e00\u5b9a\u7684\u5fc5\u8981\u6027\uff0c\u4f46\u662f\u5728\u4e3b\u6d41\u9762\u8bd5\u4e2d\u4e00\u822c\u6709\u660e\u786e\u9762\u8bd5\u8303\u56f4\u548c\u8003\u5bdf\u9891\u7387\u3002\u56e0\u6b64\u590d\u4e60\u9636\u6bb5\u5e94\u8be5\u6709\u610f\u8bc6\u7684\u590d\u4e60\u9ad8\u9891\u5185\u5bb9\u3002")),(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u600e\u4e48\u590d\u4e60")),(0,a.kt)("p",{parentName:"li"},"\u5b66\u4e60\u77e5\u8bc6\u70b9\u65f6\u901a\u5e38\u5c3d\u53ef\u80fd\u67e5\u770b\u5177\u6709\u6743\u5a01\u6027\u7684\u6587\u6863\u3001\u89c4\u8303\u3001\u6e90\u7801\u7b49\uff0c\u8fdb\u884c\u4ece\u96f6\u5230\u4e00\u7684\u8f83\u5168\u9762\u7684\u7406\u89e3\uff0c\u8017\u65f6\u8f83\u957f\uff1b\u800c\u590d\u4e60\u65f6\u66f4\u591a\u7684\u9700\u8981\u6709\u4fa7\u91cd\u70b9\uff0c\u5e94\u5f53\u8fdb\u884c\u63d0\u70bc\u548c\u603b\u7ed3\u3002\u5019\u9009\u4eba\u901a\u5e38\u57fa\u4e8e\u77e5\u8bc6\u70b9\u53bb\u67e5\u627e\u76f8\u5173\u6280\u672f\u6587\u7ae0\uff0c\u5e0c\u671b\u4ece\u5176\u4ed6\u5f00\u53d1\u8005\u7684\u6280\u672f\u6587\u7ae0\u4e2d\u83b7\u53d6\u201c\u7cbe\u534e\u201d\uff0c\u4ece\u800c\u7701\u53bb\u81ea\u5df1\u4ece\u96f6\u5b66\u4e60\u6216\u662f\u63d0\u70bc\u603b\u7ed3\u7684\u6210\u672c\u3002"),(0,a.kt)("p",{parentName:"li"},"\u7136\u800c\u5e02\u9762\u4e0a\u5145\u65a5\u7740\u826f\u83a0\u4e0d\u9f50\u7684\u6280\u672f\u6587\u7ae0\uff0c\u4e5f\u9700\u8981\u5019\u9009\u4eba\u5728\u67e5\u627e\u8d44\u6599\u65f6\u201c\u8d27\u6bd4\u4e09\u5bb6\u201d\uff0c\u8fa9\u8bc1\u5bf9\u5f85\u6587\u7ae0\u5185\u5bb9\u3002\u5373\u4fbf\u770b\u5230\u8f83\u597d\u7684\u6587\u7ae0\uff0c\u5b66\u4f1a\u77e5\u8bc6\u548c\u9762\u8bd5\u4e2d\u5c06\u77e5\u8bc6\u8fdb\u884c\u8f93\u51fa\u4e5f\u5e76\u975e\u540c\u4e00\u4e2a\u6982\u5ff5\u3002"),(0,a.kt)("p",{parentName:"li"},"\u56e0\u4e3a\u8fd9\u4e9b\u6587\u7ae0\u901a\u5e38\u8f83\u4e3a\u8be6\u7ec6\uff0c\u4ee5\u6559\u7a0b\u7684\u65b9\u5f0f\u5a13\u5a13\u9053\u6765\uff0c\u800c\u9762\u8bd5\u65f6\u8003\u5bdf\u5019\u9009\u4eba\u5bf9\u77e5\u8bc6\u70b9\u7684\u7406\u89e3\u548c\u8fd0\u7528\uff0c\u9700\u8981\u5019\u9009\u4eba\u5728\u6709\u9650\u65f6\u95f4\u5185\u8a00\u7b80\u610f\u8d45\u7684\u56de\u7b54\u95ee\u9898\u3002\u6709\u7684\u5019\u9009\u4eba\u7f16\u7a0b\u80fd\u529b\u4e0d\u5f31\uff0c\u4f46\u53e3\u5934\u8868\u8fbe\u80fd\u529b\u548c\u603b\u7ed3\u80fd\u529b\u6b20\u7f3a\u3002\u56e0\u6b64\u590d\u4e60\u9636\u6bb5\u62e5\u6709\u4e00\u4efd\u975e\u6559\u7a0b\u5411\uff0c\u800c\u662f\u9762\u8bd5\u5411\u7684\u53c2\u8003\u8bfb\u7269\u53d8\u5f97\u91cd\u8981\u8d77\u6765\u3002"))),(0,a.kt)("p",null,"\u57fa\u4e8e\u4e00\u4e9b\u5e38\u89c1\u7684\u9762\u8bd5\u6848\u4f8b\uff0c\u6211\u4eec\u603b\u7ed3\u51fa\u4ee5\u4e0b\u9762\u8bd5\u901a\u75c5\uff1a"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u9762\u8bd5\u8868\u8fbe\u80fd\u529b\u5f31\uff1a"),"\u201c\u90a3\u4e9b\u4e1c\u897f\u6211\u4f1a\u7528\uff0c\u4f46\u9762\u8bd5\u7684\u65f6\u5019\u8bb2\u4e0d\u597d\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u590d\u4e60\u6478\u4e0d\u6e05\u91cd\u70b9\uff1a"),"\u201c\u5927\u5382\u9762\u8bd5\u611f\u89c9\u5f88\u96be\uff0c\u8981\u590d\u4e60\u7684\u4e1c\u897f\u6709\u5f88\u591a\uff0c\u4e5f\u4e0d\u77e5\u9053\u4f1a\u4e0d\u4f1a\u95ee\u5230\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u5b66\u4e60\u6548\u7387\u4f4e\uff1a"),"\u201c\u5404\u79cd\u9762\u7ecf\u6211\u90fd\u6709\u770b\uff0c\u81ea\u5df1\u5927\u81f4\u5f52\u7c7b\u4e86\u9898\u76ee\u51fa\u73b0\u9891\u7387\uff0c\u4f46\u7b54\u6848\u8fd8\u8981\u518d\u67e5\u4e00\u4e0b\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u5b66\u4e60\u5174\u8da3\u4f4e\uff1a"),"\u201c\u6709\u7684\u95ee\u9898\u6211\u6709\u81ea\u5df1\u67e5\u8d44\u6599\uff0c\u8bb2\u5f97\u5f88\u8be6\u7ec6\uff0c\u4e5f\u5f88\u96be\uff0c\u5543\u4e00\u7bc7\u8981\u82b1\u5f88\u591a\u65f6\u95f4\u3002\u201d")),(0,a.kt)("p",null,"\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u65e0\u6cd5\u5bf9\u9ad8\u9891\u77e5\u8bc6\u70b9\u8fdb\u884c\u5438\u6536\u548c\u603b\u7ed3\uff0c\u53ea\u80fd\u4e0d\u65ad\u5730\u5728\u9762\u8bd5\u73af\u8282\u4e2d\u8bd5\u9519\uff0c\u673a\u4f1a\u6210\u672c\u548c\u65f6\u95f4\u6210\u672c\u6781\u9ad8\u3002"),(0,a.kt)("h2",{id:"\u5199\u4f5c\u7406\u5ff5"},"\u5199\u4f5c\u7406\u5ff5"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://user-images.githubusercontent.com/17002181/135719463-b9602209-2192-4fe5-af17-ca04f9986150.png",alt:"image"})),(0,a.kt)("p",null,"\u4e3a\u964d\u4f4e\u524d\u7aef\u5019\u9009\u4eba\u9762\u8bd5\u7684\u51c6\u5907\u548c\u8bd5\u9519\u6210\u672c\uff0c\u6211\u4eec\u64b0\u5199\u4e86\u8fd9\u672c\u4e13\u4e3a\u524d\u7aef\u9762\u8bd5\u573a\u666f\u670d\u52a1\u7684\u4e66\uff0c\u610f\u5728\u6210\u4e3a\u5019\u9009\u4eba\u7684\u6280\u672f\u9ad8\u9891\u9898\u6307\u5357\u3002\u672c\u4e66\u4e3b\u8981\u6574\u7406\u4e86\u9ad8\u9891\u9762\u8bd5\u9898\u548c\u5bf9\u5e94\u7bc7\u5e45\u53ef\u63a7\u7684\u7b54\u6848\u3002\u9ad8\u9891\u9898\u662f\u4e3a\u4e86\u63d0\u9ad8\u5019\u9009\u4eba\u590d\u4e60\u6548\u7387\uff0c\u7bc7\u5e45\u53ef\u63a7\u7684\u7b54\u6848\u5219\u8282\u7701\u5019\u9009\u4eba\u7684\u9605\u8bfb\u65f6\u95f4\uff1a"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u4ee5\u9ad8\u6548\u65b9\u5f0f\u7ec4\u5408\u9ad8\u9891\u9898\u76ee")),(0,a.kt)("p",{parentName:"li"},"\u6280\u672f\u9762\u8bd5\u65f6\u957f\u4e00\u822c\u63a7\u5236\u5728 30-60 \u5206\u949f\uff0c\u56f4\u7ed5\u4e00\u9053\u666e\u901a\u6280\u672f\u9898\u76ee\u7684\u65f6\u957f\u4e00\u822c\u5728 1-3 \u5206\u949f\uff0c\u4f1a\u6d89\u53ca\u4e0d\u540c\u77e5\u8bc6\u9762\u7684\u7ec6\u8282\u3002\u6211\u4eec\u6574\u7406\u4e86 5 \u5957\u9762\u8bd5\u9898\uff0c\u5171\u8ba1 60 \u9053\u4e0d\u540c\u7684\u9ad8\u9891\u9762\u8bd5\u9898\u3002\u6bcf\u5957\u9898\u5305\u542b\u56fa\u5b9a\u7c7b\u578b\u9898\u76ee\uff08\u5982\u57fa\u7840\u9898\u3001\u5de5\u7a0b\u5316\u9898\u3001\u7f51\u7edc\u9898\u3001\u7f16\u7801\u9898\u3001\u7efc\u5408\u9898\u7b49\uff09\u3002"),(0,a.kt)("p",{parentName:"li"},"\u901a\u8fc7\u7ec4\u5408\u4e0d\u540c\u9898\u578b\uff0c\u6a21\u62df\u4e00\u573a\u9762\u8bd5\u4e2d\u7684\u9898\u578b\u5206\u5e03\u60c5\u51b5\uff0c\u6211\u4eec\u6bcf\u5957\u9898\u90fd\u53ef\u4ee5\u66f4\u52a0\u63a5\u8fd1\u771f\u5b9e\u6280\u672f\u9762\u8bd5\u7684\u4f53\u9a8c\u3002\u56e0\u6b64\u53ef\u4ee5\u5e2e\u52a9\u5019\u9009\u4eba\u79ef\u7d2f\u7ecf\u9a8c\u3001\u63d0\u9ad8\u9762\u8bd5\u6210\u529f\u7387\u3002")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u63d0\u70bc\u9762\u8bd5\u56de\u7b54\u8981\u70b9")),(0,a.kt)("p",{parentName:"li"},"\u9762\u8bd5\u56de\u7b54\u548c\u65e5\u5e38\u77e5\u8bc6\u70b9\u5b66\u4e60\u6709\u4e00\u5b9a\u5dee\u522b\uff1a\u65e5\u5e38\u77e5\u8bc6\u70b9\u7684\u5b66\u4e60\u9700\u4e86\u89e3\u5e7f\u5ea6\u4e14\u6df1\u5165\u7ec6\u8282\uff0c\u8981\u6c42\u67e5\u9605\u5404\u79cd\u6587\u6863\u3001\u89c4\u8303\u3002\u9762\u8bd5\u56de\u7b54\u5219\u9700\u8981\u5c06\u6240\u5b66\u77e5\u8bc6\u6d53\u7f29\u4e3a\u51e0\u53e5\u8bdd\u3002"),(0,a.kt)("p",{parentName:"li"},"\u672c\u4e66\u901a\u8fc7\u7531\u6d45\u5165\u6df1\u7684\u7ec4\u7ec7\u65b9\u5f0f\uff0c\u4ee5\u300c\u76f8\u5173\u95ee\u9898\u300d\u300c\u56de\u7b54\u5173\u952e\u70b9\u300d\u300c\u77e5\u8bc6\u70b9\u6df1\u5165\u300d\u300c\u53c2\u8003\u8d44\u6599\u300d\u4e3a\u5185\u5bb9\u57fa\u7840\u5927\u7eb2\u8fdb\u884c\u68b3\u7406\u3002\u300c\u56de\u7b54\u5173\u952e\u70b9\u300d\u4f5c\u4e3a\u9ad8\u5ea6\u6982\u62ec\u7684\u603b\u7ed3\u6027\u8bed\u8a00\uff0c\u53ef\u7528\u4e8e\u7b2c\u4e00\u65f6\u95f4\u56de\u7b54\u9762\u8bd5\u5b98\u7684\u95ee\u9898\uff1b\u300c\u77e5\u8bc6\u70b9\u6df1\u5165\u300d\u4ee5\u9012\u8fdb\u65b9\u5f0f\u6df1\u5165\u89e3\u6790\uff0c\u53ef\u4f5c\u4e3a\u5f15\u5bfc\u9762\u8bd5\u5b98\u8fdb\u4e00\u6b65\u63d0\u95ee\u7684\u65b9\u5411\u3002"),(0,a.kt)("p",{parentName:"li"},"\u8bfb\u8005\u8fd8\u53ef\u4ee5\u6a21\u4eff\u672c\u4e66\u5185\u5bb9\u7684\u7f16\u6392\u65b9\u5f0f\uff0c\u7ecf\u8fc7\u7ec3\u4e60\u540e\uff0c\u7528\u66f4\u7cbe\u70bc\u7684\u8bed\u8a00\u5bf9\u5176\u4ed6\u95ee\u9898\u4f5c\u51fa\u6709\u6548\u56de\u7b54\u3002"))),(0,a.kt)("p",null,"\u603b\u4f53\u800c\u8a00\uff0c\u672c\u4e66\u5c3d\u53ef\u80fd\u4ece\u5019\u9009\u4eba\u89d2\u5ea6\u51fa\u53d1\uff0c\u4f7f\u5019\u9009\u4eba\u5feb\u901f\u83b7\u5f97\u9762\u8bd5\u5e38\u89c1\u6280\u672f\u95ee\u9898\u7684\u53c2\u8003\u6027\u56de\u7b54\uff0c\u4e5f\u63d0\u4f9b\u7ed9\u5019\u9009\u4eba\u4e00\u4e2a\u76f8\u5bf9\u7cbe\u7b80\u7684\u77e5\u8bc6\u70b9\u6df1\u5165\u603b\u7ed3\u3002"),(0,a.kt)("p",null,"\u8bfb\u8005\u901a\u8fc7\u5bf9\u4e66\u4e2d\u5185\u5bb9\u7684\u5b66\u4e60\uff0c\u5373\u4fbf\u4e0d\u80fd\u201c\u4e00\u4e66\u5728\u624b\uff0c\u5929\u4e0b\u6211\u6709\u201d\uff0c\u4e5f\u80fd\u5728\u9762\u8bd5\u4e2d\u591a\u4e00\u4efd\u4ece\u5bb9\u548c\u81ea\u4fe1\u3002\u540c\u65f6\u672c\u4e66\u4e5f\u5411\u524d\u7aef\u4ece\u4e1a\u4eba\u5458\u4f20\u9012\u4e00\u79cd\u5b66\u4e60\u65b9\u6cd5\u548c\u601d\u8def\u3002\u6bd5\u7adf\u6bcf\u4e2a\u4eba\u7684\u5b66\u4e60\u65b9\u6cd5\u4e0d\u540c\uff0c\u4e0d\u53ef\u4ee5\u673a\u68b0\u7167\u642c\u3002\u5bf9\u4e8e\u5982\u4f55\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u5c06\u590d\u4e60\u6548\u76ca\u6700\u5927\u5316\u3002\u8bfb\u8005\u53ef\u4ee5\u5c1d\u8bd5\u81ea\u884c\u5206\u6790\u573a\u666f\u7279\u70b9\u548c\u81ea\u8eab\u75db\u70b9\uff0c\u627e\u51fa\u5c5e\u4e8e\u81ea\u5df1\u7684\u5173\u952e\u8def\u5f84\u5e76\u5236\u5b9a\u590d\u4e60\u8ba1\u5212\u3002"),(0,a.kt)("h2",{id:"\u9002\u5408\u4eba\u7fa4"},"\u9002\u5408\u4eba\u7fa4"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"\u6709\u610f\u51b2\u523a\u4e92\u8054\u7f51\u5927\u5382\u7684\u524d\u7aef\u5f00\u53d1\u8005\uff0c\u53ef\u53c2\u8003\u672c\u4e66\u9898\u76ee\u548c\u7b54\u6848\u63d0\u7eb2\uff0c\u81ea\u4e3b\u6df1\u5165\u5b66\u4e60\uff0c\u67e5\u6f0f\u8865\u7f3a\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u77ed\u65f6\u95f4\u5185\u53c2\u52a0\u9762\u8bd5\u7684\u524d\u7aef\u5f00\u53d1\u8005\uff0c\u53ef\u501f\u52a9\u672c\u4e66\u5feb\u901f\u4e86\u89e3\u9762\u8bd5\u9ad8\u9891\u7684\u6280\u672f\u95ee\u9898\u548c\u76f8\u5173\u89e3\u7b54\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u524d\u7aef\u9762\u8bd5\u5b98\u53ef\u53c2\u8003\u672c\u4e66\u7684\u9898\u578b\u548c\u9898\u76ee\uff0c\u6309\u5c97\u4f4d\u9700\u6c42\u5bf9\u5019\u9009\u4eba\u8fdb\u884c\u6709\u68af\u5ea6\u7684\u8003\u5bdf\u3002")),(0,a.kt)("p",null,"\u4e00\u5343\u4e2a\u4eba\u773c\u4e2d\u6709\u4e00\u5343\u4e2a\u54c8\u59c6\u96f7\u7279\u3002\u800c\u4e00\u4e2a\u4eba\u773c\u4e2d\uff0c\u5728\u4e0d\u540c\u9636\u6bb5\u4e5f\u53ef\u4ee5\u770b\u5230\u4e0d\u4e00\u6837\u7684\u98ce\u666f\u3002"),(0,a.kt)("h2",{id:"\u4e92\u52a8\u4e0e\u52d8\u8bef"},"\u4e92\u52a8\u4e0e\u52d8\u8bef"),(0,a.kt)("p",null,"\u672c\u4e66\u76ee\u524d\u5728 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/hzfe/awesome-interview"},"GitHub")," \u4e2d\u5f00\u6e90\u4e86\u7b2c\u4e00\u7248\u5185\u5bb9\u7684\u90e8\u5206\u9898\u76ee\uff0c\u65e8\u5728\u63a5\u53d7\u5e7f\u5927\u5f00\u53d1\u8005\u7684\u68c0\u9a8c\u548c\u6536\u96c6\u8bfb\u8005\u53cd\u9988\u540e\uff0c\u80fd\u5c06\u672c\u4e66\u6253\u78e8\u5f97\u66f4\u597d\u3002"),(0,a.kt)("p",null,"\u9605\u8bfb\u65f6\u60a8\u53ef\u80fd\u4f1a\u53d1\u73b0\u5185\u5bb9\u4e0a\u7684\u9519\u8bef\uff0c\u53ef\u4ee5\u76f4\u63a5\u5728\u76f8\u5173\u7ae0\u8282\u672b\u5c3e\u7684\u8bc4\u8bba\u533a\u8fdb\u884c\u7559\u8a00\uff0c\u7559\u8a00\u5185\u5bb9\u4f1a\u88ab\u81ea\u52a8\u540c\u6b65\u5230\u4ed3\u5e93 Issues \u4e2d\u3002\u60a8\u4e5f\u53ef\u4ee5\u5728\u4ed3\u5e93 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/HZFE/awesome-interview/issues"},"Issues")," \u4e2d\u76f4\u63a5\u7559\u4e0b\u5b9d\u8d35\u610f\u89c1\u3002\u6b22\u8fce\u8bfb\u8005\u5bf9\u5185\u5bb9\u4ed3\u5e93\u8fdb\u884c ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/hzfe/awesome-interview"},"\u8ba2\u9605/Watch")," \u6216\u52a0\u5165\u7fa4\u804a\uff0c\u6211\u4eec\u4f1a\u6301\u7eed\u6dfb\u52a0\u548c\u8ba2\u6b63\u5185\u5bb9\u3002"),(0,a.kt)("img",{src:"https://user-images.githubusercontent.com/17002181/135381385-f0dc86cd-be39-4826-87dd-30ddeaaa0229.jpg",height:"200"}),(0,a.kt)("p",null,'\u6dfb\u52a0\u673a\u5668\u4eba\u5c0f\u51b0\u540e\uff0c\u56de\u590d\u5173\u952e\u8bcd"',(0,a.kt)("strong",null,"\u5251\u6307\u524d\u7aef"),'"\uff0c\u83b7\u53d6\u5165\u7fa4\u94fe\u63a5\u3002'))}c.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkjjbook=self.webpackChunkjjbook||[]).push([[3280],{3905:(e,t,r)=>{r.d(t,{Zo:()=>m,kt:()=>f});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function l(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function p(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var o=n.createContext({}),u=function(e){var t=n.useContext(o),r=t;return e&&(r="function"==typeof e?e(t):p(p({},t),e)),r},m=function(e){var t=u(e.components);return n.createElement(o.Provider,{value:t},e.children)},s="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},k=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,l=e.originalType,o=e.parentName,m=i(e,["components","mdxType","originalType","parentName"]),s=u(r),k=a,f=s["".concat(o,".").concat(k)]||s[k]||c[k]||l;return r?n.createElement(f,p(p({ref:t},m),{},{components:r})):n.createElement(f,p({ref:t},m))}));function f(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var l=r.length,p=new Array(l);p[0]=k;var i={};for(var o in t)hasOwnProperty.call(t,o)&&(i[o]=t[o]);i.originalType=e,i[s]="string"==typeof e?e:a,p[1]=i;for(var u=2;u{r.r(t),r.d(t,{assets:()=>o,contentTitle:()=>p,default:()=>c,frontMatter:()=>l,metadata:()=>i,toc:()=>u});var n=r(87462),a=(r(67294),r(3905));const l={sidebar_label:"\u524d\u8a00",sidebar_position:.5,slug:"/"},p="\u524d\u8a00",i={unversionedId:"preface",id:"preface",title:"\u524d\u8a00",description:"\u5199\u4f5c\u80cc\u666f",source:"@site/docs/preface.md",sourceDirName:".",slug:"/",permalink:"/awesome-interview/",draft:!1,tags:[],version:"current",sidebarPosition:.5,frontMatter:{sidebar_label:"\u524d\u8a00",sidebar_position:.5,slug:"/"},sidebar:"tutorialSidebar",previous:{title:"\u5173\u4e8e\u6211\u4eec",permalink:"/awesome-interview/about"},next:{title:"\u6d4f\u89c8\u5668\uff1a\u6d4f\u89c8\u5668\u8de8\u57df",permalink:"/awesome-interview/book1/browser-cross-origin"}},o={},u=[{value:"\u5199\u4f5c\u80cc\u666f",id:"\u5199\u4f5c\u80cc\u666f",level:2},{value:"\u9762\u8bd5\u75db\u70b9",id:"\u9762\u8bd5\u75db\u70b9",level:2},{value:"\u5199\u4f5c\u7406\u5ff5",id:"\u5199\u4f5c\u7406\u5ff5",level:2},{value:"\u9002\u5408\u4eba\u7fa4",id:"\u9002\u5408\u4eba\u7fa4",level:2},{value:"\u4e92\u52a8\u4e0e\u52d8\u8bef",id:"\u4e92\u52a8\u4e0e\u52d8\u8bef",level:2}],m={toc:u},s="wrapper";function c(e){let{components:t,...r}=e;return(0,a.kt)(s,(0,n.Z)({},m,r,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"\u524d\u8a00"},"\u524d\u8a00"),(0,a.kt)("h2",{id:"\u5199\u4f5c\u80cc\u666f"},"\u5199\u4f5c\u80cc\u666f"),(0,a.kt)("p",null,"\u5728\u6211\u4eec\u56e2\u961f\u4e2d\uff0c\u6bcf\u5e74\u90fd\u4f1a\u6709\u90e8\u5206\u4eba\u9700\u8981\u66f4\u6362\u65b0\u7684\u5de5\u4f5c\u73af\u5883\uff0c\u56e0\u6b64\u6211\u4eec\u7684\u804a\u5929\u8bdd\u9898\u603b\u662f\u9636\u6bb5\u6027\u7684\u53d8\u6210\u9762\u8bd5\u9898\u63a2\u8ba8\u3002\u968f\u7740\u8fd9\u51e0\u5e74\u6211\u4eec\u5bf9\u524d\u7aef\u9762\u8bd5\u65b9\u9762\u7684\u7ecf\u9a8c\u79ef\u7d2f\u548c\u603b\u7ed3\uff0c\u6211\u4eec\u4e00\u81f4\u8ba4\u4e3a\u524d\u7aef\u9762\u8bd5\u7684\u590d\u4e60\u662f\u6709\u5173\u952e\u8def\u5f84\u7684\u3002\u57fa\u4e8e\u6211\u4eec\u5185\u90e8\u9700\u8981\uff0c\u4e5f\u66fe\u8fed\u4ee3\u8fc7\u51e0\u4e2a\u9762\u8bd5\u9898\u5e93\u7248\u672c\u3002"),(0,a.kt)("p",null,"\u4ece\u9762\u8bd5\u89d2\u5ea6\u5206\u6790\uff0c\u9762\u8bd5\u6700\u5178\u578b\u7684\u7279\u70b9\u662f\u65f6\u95f4\u6709\u9650\u3002\u8fd9\u610f\u5473\u7740\u9762\u8bd5\u5b98\u548c\u5019\u9009\u4eba\u9700\u8981\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u505a\u51fa\u6700\u5927\u7a0b\u5ea6\u7684\u6709\u6548\u6c9f\u901a\u3002"),(0,a.kt)("p",null,"\u9762\u8bd5\u5b98\u5982\u4f55\u6709\u6548\u4e14\u5168\u9762\u7684\u4e86\u89e3\u5019\u9009\u4eba\uff0c\u662f\u53e6\u5916\u4e00\u95e8\u5b66\u95ee\u3002\u800c\u5019\u9009\u4eba\u7684\u6311\u6218\u5728\u4e8e\u9762\u5bf9\u95ee\u9898\u65f6\uff0c\u5982\u4f55\u5728\u4e00\u4e24\u5206\u949f\u5185\u4f5c\u51fa\u6709\u6548\u56de\u7b54\u3002\u6709\u6548\u56de\u7b54\u662f\u6307\uff1a\u7528\u4e24\u4e09\u53e5\u8bdd\u5bf9\u95ee\u9898\u4f5c\u51fa\u6982\u62ec\u6027\u56de\u7b54\uff0c\u5e76\u5f15\u5bfc\u9762\u8bd5\u5b98\u5bf9\u56de\u7b54\u4e2d\u63d0\u5230\u7684\u5173\u952e\u8bcd\u8fdb\u4e00\u6b65\u6df1\u5165\u63d0\u95ee\u3002"),(0,a.kt)("p",null,"\u4e0d\u5c11\u540c\u884c\u7684\u5fc3\u6001\u662f\uff1a\u5982\u679c\u9762\u8bd5\u95ee\u5f97\u96be\uff0c\u90a3\u4fbf\u662f\u9762\u8bd5\u9020\u822a\u6bcd\uff0c\u5de5\u4f5c\u62e7\u87ba\u4e1d\uff1b\u5982\u679c\u9762\u8bd5\u95ee\u5f97\u7b80\u5355\uff0c\u90a3\u4fbf\u662f\u4e1c\u897f\u6211\u4f1a\u7528\uff0c\u4f46\u6211\u4e0d\u4f1a\u8bf4\uff0c\u6ca1\u53d1\u6325\u597d\u3002\u7531\u4e8e\u5404\u79cd\u539f\u56e0\uff0c\u5931\u53bb\u4e86\u66f4\u591a\u7684\u9009\u62e9\u3002\u672c\u4e66\u5e0c\u671b\u80fd\u591f\u5e2e\u52a9\u5927\u5bb6\u5c3d\u53ef\u80fd\u89e3\u51b3\u8fd9\u65b9\u9762\u7684\u95ee\u9898\uff0c\u8ba9\u524d\u7aef\u5f00\u53d1\u8005\u5728\u9762\u8bd5\u590d\u4e60\u9636\u6bb5\u4e8b\u534a\u529f\u500d\uff0c\u4ee5\u66f4\u597d\u7684\u72b6\u6001\u8fdb\u884c\u9762\u8bd5\u3002"),(0,a.kt)("p",null,"\u57fa\u4e8e\u4ee5\u4e0a\u5199\u4f5c\u80cc\u666f\uff0c\u6211\u4eec\u53ef\u4ee5\u8fbe\u6210\u4e24\u4e2a\u5171\u8bc6\uff1a"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u9762\u8bd5\u65f6\u95f4\u603b\u662f\u6709\u9650\u7684")),(0,a.kt)("p",{parentName:"li"},"\u56f4\u7ed5\u4e00\u9053\u666e\u901a\u6280\u672f\u9898\u76ee\u7684\u65f6\u957f\u4e00\u822c\u5728 1-3 \u5206\u949f\uff0c\u4e00\u8f6e\u6280\u672f\u9762\u8bd5\u7684\u65f6\u957f\u4e00\u822c\u63a7\u5236\u5728 30-60 \u5206\u949f\uff0c\u9762\u8bd5\u9898\u76ee\u901a\u5e38\u6d89\u53ca\u4e0d\u540c\u77e5\u8bc6\u9762\u3002")),(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u95ee\u9898\u7684\u56de\u7b54\u4e00\u822c\u662f\u81ea\u9876\u5411\u4e0b\u7684")),(0,a.kt)("p",{parentName:"li"},"\u4ee5\u4e00\u4e2a\u6982\u62ec\u6027\u8f83\u5f3a\u7684\u56de\u7b54\u8fdb\u884c\u53cd\u9988\uff0c\u9762\u8bd5\u5b98\u83b7\u5f97\u53cd\u9988\u540e\uff0c\u901a\u5e38\u4f1a\u57fa\u4e8e\u5019\u9009\u4eba\u56de\u7b54\u4e2d\u63d0\u5230\u7684\u5173\u952e\u70b9\u6216\u9762\u8bd5\u5b98\u8ba4\u4e3a\u7684\u5176\u4ed6\u5173\u952e\u70b9\u5c55\u5f00\u8fdb\u4e00\u6b65\u63d0\u95ee\u3002"))),(0,a.kt)("h2",{id:"\u9762\u8bd5\u75db\u70b9"},"\u9762\u8bd5\u75db\u70b9"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://user-images.githubusercontent.com/17002181/132717020-3fa8010a-ae1d-4921-88f9-5563b5b53c22.png",alt:"image"})),(0,a.kt)("p",null,"\u4e0a\u9762\u8fd9\u5f20\u7ed3\u6784\u56fe\uff0c\u6211\u4eec\u68b3\u7406\u4e86\u4ece\u51c6\u5907\u9762\u8bd5\u5230\u6700\u7ec8\u83b7\u5f97 Offer \u7684\u5927\u81f4\u8fc7\u7a0b\u3002\u8f83\u591a\u4eba\u7684\u590d\u4e60\u65b9\u6cd5\u4e3b\u8981\u6765\u6e90\u4e8e\u7cfb\u7edf\u5730\u590d\u4e60\u6240\u6709\u77e5\u8bc6\u70b9\uff0c\u6216\u8005\u6709\u76ee\u7684\u6027\u7684\u67e5\u627e\u9762\u7ecf\u5e76\u590d\u4e60\u5bf9\u5e94\u7684\u77e5\u8bc6\u70b9\u3002\u5728\u65f6\u95f4\u5145\u5206\u7684\u60c5\u51b5\u4e0b\uff0c\u7cfb\u7edf\u590d\u4e60\u5728\u590d\u4e60\u9636\u6bb5\u521d\u671f\u4f1a\u6709\u4e00\u5b9a\u6210\u6548\u3002\u4f46\u662f\u4ec5\u505c\u7559\u5728\u8fd9\u4e00\u6b65\uff0c\u4e5f\u8bb8\u5e76\u4e0d\u662f\u6700\u597d\u7684\u9009\u62e9\u3002"),(0,a.kt)("p",null,"\u6211\u4eec\u53ef\u4ee5\u4ece\u4ee5\u4e0b\u89d2\u5ea6\u5256\u6790\u9762\u8bd5\u590d\u4e60\u9636\u6bb5\u7684\u5173\u6ce8\u70b9\uff1a"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u590d\u4e60\u4ec0\u4e48")),(0,a.kt)("p",{parentName:"li"},"\u5019\u9009\u4eba\u901a\u5e38\u4f1a\u7cfb\u7edf\u5730\u67e5\u770b\u77e5\u8bc6\u70b9\u603b\u7ed3\uff0c\u800c\u5e02\u9762\u4e0a\u5927\u90e8\u5206\u7684\u77e5\u8bc6\u70b9\u603b\u7ed3\uff0c\u63d0\u7eb2\u6392\u5217\u987a\u5e8f\u4e3a\u7531\u6613\u5230\u96be\uff0c\u901a\u5e38\u4ece\u57fa\u672c\u6570\u636e\u7c7b\u578b\u7ae0\u8282\u5f00\u59cb\u8bb2\u89e3\uff0c\u5185\u5bb9\u6bd4\u8f83\u5197\u957f\u3002\u7cfb\u7edf\u590d\u4e60\u6709\u4e00\u5b9a\u7684\u5fc5\u8981\u6027\uff0c\u4f46\u662f\u5728\u4e3b\u6d41\u9762\u8bd5\u4e2d\u4e00\u822c\u6709\u660e\u786e\u9762\u8bd5\u8303\u56f4\u548c\u8003\u5bdf\u9891\u7387\u3002\u56e0\u6b64\u590d\u4e60\u9636\u6bb5\u5e94\u8be5\u6709\u610f\u8bc6\u7684\u590d\u4e60\u9ad8\u9891\u5185\u5bb9\u3002")),(0,a.kt)("li",{parentName:"ol"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u600e\u4e48\u590d\u4e60")),(0,a.kt)("p",{parentName:"li"},"\u5b66\u4e60\u77e5\u8bc6\u70b9\u65f6\u901a\u5e38\u5c3d\u53ef\u80fd\u67e5\u770b\u5177\u6709\u6743\u5a01\u6027\u7684\u6587\u6863\u3001\u89c4\u8303\u3001\u6e90\u7801\u7b49\uff0c\u8fdb\u884c\u4ece\u96f6\u5230\u4e00\u7684\u8f83\u5168\u9762\u7684\u7406\u89e3\uff0c\u8017\u65f6\u8f83\u957f\uff1b\u800c\u590d\u4e60\u65f6\u66f4\u591a\u7684\u9700\u8981\u6709\u4fa7\u91cd\u70b9\uff0c\u5e94\u5f53\u8fdb\u884c\u63d0\u70bc\u548c\u603b\u7ed3\u3002\u5019\u9009\u4eba\u901a\u5e38\u57fa\u4e8e\u77e5\u8bc6\u70b9\u53bb\u67e5\u627e\u76f8\u5173\u6280\u672f\u6587\u7ae0\uff0c\u5e0c\u671b\u4ece\u5176\u4ed6\u5f00\u53d1\u8005\u7684\u6280\u672f\u6587\u7ae0\u4e2d\u83b7\u53d6\u201c\u7cbe\u534e\u201d\uff0c\u4ece\u800c\u7701\u53bb\u81ea\u5df1\u4ece\u96f6\u5b66\u4e60\u6216\u662f\u63d0\u70bc\u603b\u7ed3\u7684\u6210\u672c\u3002"),(0,a.kt)("p",{parentName:"li"},"\u7136\u800c\u5e02\u9762\u4e0a\u5145\u65a5\u7740\u826f\u83a0\u4e0d\u9f50\u7684\u6280\u672f\u6587\u7ae0\uff0c\u4e5f\u9700\u8981\u5019\u9009\u4eba\u5728\u67e5\u627e\u8d44\u6599\u65f6\u201c\u8d27\u6bd4\u4e09\u5bb6\u201d\uff0c\u8fa9\u8bc1\u5bf9\u5f85\u6587\u7ae0\u5185\u5bb9\u3002\u5373\u4fbf\u770b\u5230\u8f83\u597d\u7684\u6587\u7ae0\uff0c\u5b66\u4f1a\u77e5\u8bc6\u548c\u9762\u8bd5\u4e2d\u5c06\u77e5\u8bc6\u8fdb\u884c\u8f93\u51fa\u4e5f\u5e76\u975e\u540c\u4e00\u4e2a\u6982\u5ff5\u3002"),(0,a.kt)("p",{parentName:"li"},"\u56e0\u4e3a\u8fd9\u4e9b\u6587\u7ae0\u901a\u5e38\u8f83\u4e3a\u8be6\u7ec6\uff0c\u4ee5\u6559\u7a0b\u7684\u65b9\u5f0f\u5a13\u5a13\u9053\u6765\uff0c\u800c\u9762\u8bd5\u65f6\u8003\u5bdf\u5019\u9009\u4eba\u5bf9\u77e5\u8bc6\u70b9\u7684\u7406\u89e3\u548c\u8fd0\u7528\uff0c\u9700\u8981\u5019\u9009\u4eba\u5728\u6709\u9650\u65f6\u95f4\u5185\u8a00\u7b80\u610f\u8d45\u7684\u56de\u7b54\u95ee\u9898\u3002\u6709\u7684\u5019\u9009\u4eba\u7f16\u7a0b\u80fd\u529b\u4e0d\u5f31\uff0c\u4f46\u53e3\u5934\u8868\u8fbe\u80fd\u529b\u548c\u603b\u7ed3\u80fd\u529b\u6b20\u7f3a\u3002\u56e0\u6b64\u590d\u4e60\u9636\u6bb5\u62e5\u6709\u4e00\u4efd\u975e\u6559\u7a0b\u5411\uff0c\u800c\u662f\u9762\u8bd5\u5411\u7684\u53c2\u8003\u8bfb\u7269\u53d8\u5f97\u91cd\u8981\u8d77\u6765\u3002"))),(0,a.kt)("p",null,"\u57fa\u4e8e\u4e00\u4e9b\u5e38\u89c1\u7684\u9762\u8bd5\u6848\u4f8b\uff0c\u6211\u4eec\u603b\u7ed3\u51fa\u4ee5\u4e0b\u9762\u8bd5\u901a\u75c5\uff1a"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u9762\u8bd5\u8868\u8fbe\u80fd\u529b\u5f31\uff1a"),"\u201c\u90a3\u4e9b\u4e1c\u897f\u6211\u4f1a\u7528\uff0c\u4f46\u9762\u8bd5\u7684\u65f6\u5019\u8bb2\u4e0d\u597d\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u590d\u4e60\u6478\u4e0d\u6e05\u91cd\u70b9\uff1a"),"\u201c\u5927\u5382\u9762\u8bd5\u611f\u89c9\u5f88\u96be\uff0c\u8981\u590d\u4e60\u7684\u4e1c\u897f\u6709\u5f88\u591a\uff0c\u4e5f\u4e0d\u77e5\u9053\u4f1a\u4e0d\u4f1a\u95ee\u5230\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u5b66\u4e60\u6548\u7387\u4f4e\uff1a"),"\u201c\u5404\u79cd\u9762\u7ecf\u6211\u90fd\u6709\u770b\uff0c\u81ea\u5df1\u5927\u81f4\u5f52\u7c7b\u4e86\u9898\u76ee\u51fa\u73b0\u9891\u7387\uff0c\u4f46\u7b54\u6848\u8fd8\u8981\u518d\u67e5\u4e00\u4e0b\u3002\u201d"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"\u5b66\u4e60\u5174\u8da3\u4f4e\uff1a"),"\u201c\u6709\u7684\u95ee\u9898\u6211\u6709\u81ea\u5df1\u67e5\u8d44\u6599\uff0c\u8bb2\u5f97\u5f88\u8be6\u7ec6\uff0c\u4e5f\u5f88\u96be\uff0c\u5543\u4e00\u7bc7\u8981\u82b1\u5f88\u591a\u65f6\u95f4\u3002\u201d")),(0,a.kt)("p",null,"\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u65e0\u6cd5\u5bf9\u9ad8\u9891\u77e5\u8bc6\u70b9\u8fdb\u884c\u5438\u6536\u548c\u603b\u7ed3\uff0c\u53ea\u80fd\u4e0d\u65ad\u5730\u5728\u9762\u8bd5\u73af\u8282\u4e2d\u8bd5\u9519\uff0c\u673a\u4f1a\u6210\u672c\u548c\u65f6\u95f4\u6210\u672c\u6781\u9ad8\u3002"),(0,a.kt)("h2",{id:"\u5199\u4f5c\u7406\u5ff5"},"\u5199\u4f5c\u7406\u5ff5"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://user-images.githubusercontent.com/17002181/135719463-b9602209-2192-4fe5-af17-ca04f9986150.png",alt:"image"})),(0,a.kt)("p",null,"\u4e3a\u964d\u4f4e\u524d\u7aef\u5019\u9009\u4eba\u9762\u8bd5\u7684\u51c6\u5907\u548c\u8bd5\u9519\u6210\u672c\uff0c\u6211\u4eec\u64b0\u5199\u4e86\u8fd9\u672c\u4e13\u4e3a\u524d\u7aef\u9762\u8bd5\u573a\u666f\u670d\u52a1\u7684\u4e66\uff0c\u610f\u5728\u6210\u4e3a\u5019\u9009\u4eba\u7684\u6280\u672f\u9ad8\u9891\u9898\u6307\u5357\u3002\u672c\u4e66\u4e3b\u8981\u6574\u7406\u4e86\u9ad8\u9891\u9762\u8bd5\u9898\u548c\u5bf9\u5e94\u7bc7\u5e45\u53ef\u63a7\u7684\u7b54\u6848\u3002\u9ad8\u9891\u9898\u662f\u4e3a\u4e86\u63d0\u9ad8\u5019\u9009\u4eba\u590d\u4e60\u6548\u7387\uff0c\u7bc7\u5e45\u53ef\u63a7\u7684\u7b54\u6848\u5219\u8282\u7701\u5019\u9009\u4eba\u7684\u9605\u8bfb\u65f6\u95f4\uff1a"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u4ee5\u9ad8\u6548\u65b9\u5f0f\u7ec4\u5408\u9ad8\u9891\u9898\u76ee")),(0,a.kt)("p",{parentName:"li"},"\u6280\u672f\u9762\u8bd5\u65f6\u957f\u4e00\u822c\u63a7\u5236\u5728 30-60 \u5206\u949f\uff0c\u56f4\u7ed5\u4e00\u9053\u666e\u901a\u6280\u672f\u9898\u76ee\u7684\u65f6\u957f\u4e00\u822c\u5728 1-3 \u5206\u949f\uff0c\u4f1a\u6d89\u53ca\u4e0d\u540c\u77e5\u8bc6\u9762\u7684\u7ec6\u8282\u3002\u6211\u4eec\u6574\u7406\u4e86 5 \u5957\u9762\u8bd5\u9898\uff0c\u5171\u8ba1 60 \u9053\u4e0d\u540c\u7684\u9ad8\u9891\u9762\u8bd5\u9898\u3002\u6bcf\u5957\u9898\u5305\u542b\u56fa\u5b9a\u7c7b\u578b\u9898\u76ee\uff08\u5982\u57fa\u7840\u9898\u3001\u5de5\u7a0b\u5316\u9898\u3001\u7f51\u7edc\u9898\u3001\u7f16\u7801\u9898\u3001\u7efc\u5408\u9898\u7b49\uff09\u3002"),(0,a.kt)("p",{parentName:"li"},"\u901a\u8fc7\u7ec4\u5408\u4e0d\u540c\u9898\u578b\uff0c\u6a21\u62df\u4e00\u573a\u9762\u8bd5\u4e2d\u7684\u9898\u578b\u5206\u5e03\u60c5\u51b5\uff0c\u6211\u4eec\u6bcf\u5957\u9898\u90fd\u53ef\u4ee5\u66f4\u52a0\u63a5\u8fd1\u771f\u5b9e\u6280\u672f\u9762\u8bd5\u7684\u4f53\u9a8c\u3002\u56e0\u6b64\u53ef\u4ee5\u5e2e\u52a9\u5019\u9009\u4eba\u79ef\u7d2f\u7ecf\u9a8c\u3001\u63d0\u9ad8\u9762\u8bd5\u6210\u529f\u7387\u3002")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("strong",{parentName:"p"},"\u63d0\u70bc\u9762\u8bd5\u56de\u7b54\u8981\u70b9")),(0,a.kt)("p",{parentName:"li"},"\u9762\u8bd5\u56de\u7b54\u548c\u65e5\u5e38\u77e5\u8bc6\u70b9\u5b66\u4e60\u6709\u4e00\u5b9a\u5dee\u522b\uff1a\u65e5\u5e38\u77e5\u8bc6\u70b9\u7684\u5b66\u4e60\u9700\u4e86\u89e3\u5e7f\u5ea6\u4e14\u6df1\u5165\u7ec6\u8282\uff0c\u8981\u6c42\u67e5\u9605\u5404\u79cd\u6587\u6863\u3001\u89c4\u8303\u3002\u9762\u8bd5\u56de\u7b54\u5219\u9700\u8981\u5c06\u6240\u5b66\u77e5\u8bc6\u6d53\u7f29\u4e3a\u51e0\u53e5\u8bdd\u3002"),(0,a.kt)("p",{parentName:"li"},"\u672c\u4e66\u901a\u8fc7\u7531\u6d45\u5165\u6df1\u7684\u7ec4\u7ec7\u65b9\u5f0f\uff0c\u4ee5\u300c\u76f8\u5173\u95ee\u9898\u300d\u300c\u56de\u7b54\u5173\u952e\u70b9\u300d\u300c\u77e5\u8bc6\u70b9\u6df1\u5165\u300d\u300c\u53c2\u8003\u8d44\u6599\u300d\u4e3a\u5185\u5bb9\u57fa\u7840\u5927\u7eb2\u8fdb\u884c\u68b3\u7406\u3002\u300c\u56de\u7b54\u5173\u952e\u70b9\u300d\u4f5c\u4e3a\u9ad8\u5ea6\u6982\u62ec\u7684\u603b\u7ed3\u6027\u8bed\u8a00\uff0c\u53ef\u7528\u4e8e\u7b2c\u4e00\u65f6\u95f4\u56de\u7b54\u9762\u8bd5\u5b98\u7684\u95ee\u9898\uff1b\u300c\u77e5\u8bc6\u70b9\u6df1\u5165\u300d\u4ee5\u9012\u8fdb\u65b9\u5f0f\u6df1\u5165\u89e3\u6790\uff0c\u53ef\u4f5c\u4e3a\u5f15\u5bfc\u9762\u8bd5\u5b98\u8fdb\u4e00\u6b65\u63d0\u95ee\u7684\u65b9\u5411\u3002"),(0,a.kt)("p",{parentName:"li"},"\u8bfb\u8005\u8fd8\u53ef\u4ee5\u6a21\u4eff\u672c\u4e66\u5185\u5bb9\u7684\u7f16\u6392\u65b9\u5f0f\uff0c\u7ecf\u8fc7\u7ec3\u4e60\u540e\uff0c\u7528\u66f4\u7cbe\u70bc\u7684\u8bed\u8a00\u5bf9\u5176\u4ed6\u95ee\u9898\u4f5c\u51fa\u6709\u6548\u56de\u7b54\u3002"))),(0,a.kt)("p",null,"\u603b\u4f53\u800c\u8a00\uff0c\u672c\u4e66\u5c3d\u53ef\u80fd\u4ece\u5019\u9009\u4eba\u89d2\u5ea6\u51fa\u53d1\uff0c\u4f7f\u5019\u9009\u4eba\u5feb\u901f\u83b7\u5f97\u9762\u8bd5\u5e38\u89c1\u6280\u672f\u95ee\u9898\u7684\u53c2\u8003\u6027\u56de\u7b54\uff0c\u4e5f\u63d0\u4f9b\u7ed9\u5019\u9009\u4eba\u4e00\u4e2a\u76f8\u5bf9\u7cbe\u7b80\u7684\u77e5\u8bc6\u70b9\u6df1\u5165\u603b\u7ed3\u3002"),(0,a.kt)("p",null,"\u8bfb\u8005\u901a\u8fc7\u5bf9\u4e66\u4e2d\u5185\u5bb9\u7684\u5b66\u4e60\uff0c\u5373\u4fbf\u4e0d\u80fd\u201c\u4e00\u4e66\u5728\u624b\uff0c\u5929\u4e0b\u6211\u6709\u201d\uff0c\u4e5f\u80fd\u5728\u9762\u8bd5\u4e2d\u591a\u4e00\u4efd\u4ece\u5bb9\u548c\u81ea\u4fe1\u3002\u540c\u65f6\u672c\u4e66\u4e5f\u5411\u524d\u7aef\u4ece\u4e1a\u4eba\u5458\u4f20\u9012\u4e00\u79cd\u5b66\u4e60\u65b9\u6cd5\u548c\u601d\u8def\u3002\u6bd5\u7adf\u6bcf\u4e2a\u4eba\u7684\u5b66\u4e60\u65b9\u6cd5\u4e0d\u540c\uff0c\u4e0d\u53ef\u4ee5\u673a\u68b0\u7167\u642c\u3002\u5bf9\u4e8e\u5982\u4f55\u5728\u6709\u9650\u65f6\u95f4\u5185\uff0c\u5c06\u590d\u4e60\u6548\u76ca\u6700\u5927\u5316\u3002\u8bfb\u8005\u53ef\u4ee5\u5c1d\u8bd5\u81ea\u884c\u5206\u6790\u573a\u666f\u7279\u70b9\u548c\u81ea\u8eab\u75db\u70b9\uff0c\u627e\u51fa\u5c5e\u4e8e\u81ea\u5df1\u7684\u5173\u952e\u8def\u5f84\u5e76\u5236\u5b9a\u590d\u4e60\u8ba1\u5212\u3002"),(0,a.kt)("h2",{id:"\u9002\u5408\u4eba\u7fa4"},"\u9002\u5408\u4eba\u7fa4"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"\u6709\u610f\u51b2\u523a\u4e92\u8054\u7f51\u5927\u5382\u7684\u524d\u7aef\u5f00\u53d1\u8005\uff0c\u53ef\u53c2\u8003\u672c\u4e66\u9898\u76ee\u548c\u7b54\u6848\u63d0\u7eb2\uff0c\u81ea\u4e3b\u6df1\u5165\u5b66\u4e60\uff0c\u67e5\u6f0f\u8865\u7f3a\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u77ed\u65f6\u95f4\u5185\u53c2\u52a0\u9762\u8bd5\u7684\u524d\u7aef\u5f00\u53d1\u8005\uff0c\u53ef\u501f\u52a9\u672c\u4e66\u5feb\u901f\u4e86\u89e3\u9762\u8bd5\u9ad8\u9891\u7684\u6280\u672f\u95ee\u9898\u548c\u76f8\u5173\u89e3\u7b54\u3002"),(0,a.kt)("li",{parentName:"ul"},"\u524d\u7aef\u9762\u8bd5\u5b98\u53ef\u53c2\u8003\u672c\u4e66\u7684\u9898\u578b\u548c\u9898\u76ee\uff0c\u6309\u5c97\u4f4d\u9700\u6c42\u5bf9\u5019\u9009\u4eba\u8fdb\u884c\u6709\u68af\u5ea6\u7684\u8003\u5bdf\u3002")),(0,a.kt)("p",null,"\u4e00\u5343\u4e2a\u4eba\u773c\u4e2d\u6709\u4e00\u5343\u4e2a\u54c8\u59c6\u96f7\u7279\u3002\u800c\u4e00\u4e2a\u4eba\u773c\u4e2d\uff0c\u5728\u4e0d\u540c\u9636\u6bb5\u4e5f\u53ef\u4ee5\u770b\u5230\u4e0d\u4e00\u6837\u7684\u98ce\u666f\u3002"),(0,a.kt)("h2",{id:"\u4e92\u52a8\u4e0e\u52d8\u8bef"},"\u4e92\u52a8\u4e0e\u52d8\u8bef"),(0,a.kt)("p",null,"\u672c\u4e66\u76ee\u524d\u5728 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/hzfe/awesome-interview"},"GitHub")," \u4e2d\u5f00\u6e90\u4e86\u7b2c\u4e00\u7248\u5185\u5bb9\u7684\u90e8\u5206\u9898\u76ee\uff0c\u65e8\u5728\u63a5\u53d7\u5e7f\u5927\u5f00\u53d1\u8005\u7684\u68c0\u9a8c\u548c\u6536\u96c6\u8bfb\u8005\u53cd\u9988\u540e\uff0c\u80fd\u5c06\u672c\u4e66\u6253\u78e8\u5f97\u66f4\u597d\u3002"),(0,a.kt)("p",null,"\u9605\u8bfb\u65f6\u60a8\u53ef\u80fd\u4f1a\u53d1\u73b0\u5185\u5bb9\u4e0a\u7684\u9519\u8bef\uff0c\u53ef\u4ee5\u76f4\u63a5\u5728\u76f8\u5173\u7ae0\u8282\u672b\u5c3e\u7684\u8bc4\u8bba\u533a\u8fdb\u884c\u7559\u8a00\uff0c\u7559\u8a00\u5185\u5bb9\u4f1a\u88ab\u81ea\u52a8\u540c\u6b65\u5230\u4ed3\u5e93 Issues \u4e2d\u3002\u60a8\u4e5f\u53ef\u4ee5\u5728\u4ed3\u5e93 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/HZFE/awesome-interview/issues"},"Issues")," \u4e2d\u76f4\u63a5\u7559\u4e0b\u5b9d\u8d35\u610f\u89c1\u3002\u6b22\u8fce\u8bfb\u8005\u5bf9\u5185\u5bb9\u4ed3\u5e93\u8fdb\u884c ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/hzfe/awesome-interview"},"\u8ba2\u9605/Watch")," \u6216\u52a0\u5165\u7fa4\u804a\uff0c\u6211\u4eec\u4f1a\u6301\u7eed\u6dfb\u52a0\u548c\u8ba2\u6b63\u5185\u5bb9\u3002"))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.e165dbd3.js b/assets/js/runtime~main.a75952d5.js similarity index 98% rename from assets/js/runtime~main.e165dbd3.js rename to assets/js/runtime~main.a75952d5.js index d5d095c..144c679 100644 --- a/assets/js/runtime~main.e165dbd3.js +++ b/assets/js/runtime~main.a75952d5.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,d,f,t,r={},b={};function c(e){var a=b[e];if(void 0!==a)return a.exports;var d=b[e]={id:e,loaded:!1,exports:{}};return r[e].call(d.exports,d,d.exports,c),d.loaded=!0,d.exports}c.m=r,c.c=b,e=[],c.O=(a,d,f,t)=>{if(!d){var r=1/0;for(i=0;i=t)&&Object.keys(c.O).every((e=>c.O[e](d[o])))?d.splice(o--,1):(b=!1,t0&&e[i-1][2]>t;i--)e[i]=e[i-1];e[i]=[d,f,t]},c.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return c.d(a,{a:a}),a},d=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,c.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var t=Object.create(null);c.r(t);var r={};a=a||[null,d({}),d([]),d(d)];for(var b=2&f&&e;"object"==typeof b&&!~a.indexOf(b);b=d(b))Object.getOwnPropertyNames(b).forEach((a=>r[a]=()=>e[a]));return r.default=()=>e,c.d(t,r),t},c.d=(e,a)=>{for(var d in a)c.o(a,d)&&!c.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},c.f={},c.e=e=>Promise.all(Object.keys(c.f).reduce(((a,d)=>(c.f[d](e,a),a)),[])),c.u=e=>"assets/js/"+({53:"935f2afb",221:"dfd24482",334:"505fc875",397:"e3741bf5",439:"02fa4020",737:"90b799f4",1050:"da95f3d6",1293:"9f1c36eb",1440:"3bd79dc4",1734:"dd5deefd",1906:"02fefe41",2299:"605371c8",2350:"9e3afa9a",2575:"99c95826",2596:"c48aeec7",2617:"6b15a8e7",2872:"972d49dd",3085:"1f391b9e",3253:"667a1a38",3280:"e31563f4",3751:"57076a74",3917:"3d604b8e",3989:"e420c2e8",4051:"e9fc5b99",4109:"f4f9ee34",4501:"abf449ea",5131:"5825b5f0",5211:"b728f6fe",5365:"59da24a9",5582:"7c39e10e",5722:"0ba2ede9",5851:"a757db9b",5986:"d5444868",6015:"6b1ae1c5",6099:"312ed758",6238:"d9cd0856",6283:"b948ee85",6553:"f930e7e8",6585:"d04fa17f",6686:"d9d15992",6695:"26d83c4c",6756:"520898ab",7184:"92d10100",7198:"bfa6c7fa",7259:"8584d295",7530:"223d151b",7544:"216712ef",7802:"31bf44dd",7920:"1a4e3797",8670:"ab21f6e2",8711:"4f33924b",8765:"5ba709b9",9429:"5cddde15",9500:"2ad5369a",9514:"1be78505",9591:"d4358da1",9791:"e2f5eafd"}[e]||e)+"."+{53:"ae96f8c3",221:"a8c9edca",230:"34dddfc8",272:"a34d203d",334:"611070a6",397:"5a137d01",439:"d80415df",737:"6601a4ef",1050:"ff213c15",1293:"f2d83d86",1440:"81d1364b",1734:"039b46a5",1906:"b3f9381e",2299:"71cb6ac4",2350:"0b52dd30",2384:"4861d8b1",2575:"de80d85d",2596:"94e91701",2617:"370eb108",2872:"e496a32b",3085:"a00364fc",3253:"0def7602",3280:"3b2fb9de",3751:"a9e996b1",3917:"a7165cd6",3989:"d885ff56",4051:"babb79cb",4109:"40b1280a",4501:"b1986ef7",4972:"bea3865f",5131:"c4348d51",5178:"6dd866d0",5211:"bba33834",5283:"79fe536c",5365:"b2082b98",5582:"72217821",5722:"56884b73",5851:"62dd77de",5986:"acde29ec",6015:"73c2607f",6099:"c38443de",6238:"cc532560",6283:"51069a20",6511:"4f41f0a7",6553:"1501d559",6585:"2a5decdb",6686:"54894bba",6695:"edef56f4",6756:"4d2ef29d",7184:"a6f541cd",7198:"1eac726c",7259:"ebf32f7e",7530:"a2893301",7544:"2205d07e",7802:"fe411772",7920:"10deaa27",8624:"ef1f3195",8670:"e3bd364c",8711:"6c95a067",8765:"3ab20d87",8894:"446f680f",9429:"3da6324e",9500:"582e9919",9514:"418dd524",9591:"05e70cb8",9791:"57ff7439"}[e]+".js",c.miniCssF=e=>{},c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},t="jjbook:",c.l=(e,a,d,r)=>{if(f[e])f[e].push(a);else{var b,o;if(void 0!==d)for(var n=document.getElementsByTagName("script"),i=0;i{b.onerror=b.onload=null,clearTimeout(s);var t=f[e];if(delete f[e],b.parentNode&&b.parentNode.removeChild(b),t&&t.forEach((e=>e(d))),a)return a(d)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:b}),12e4);b.onerror=u.bind(null,b.onerror),b.onload=u.bind(null,b.onload),o&&document.head.appendChild(b)}},c.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),c.p="/awesome-interview/",c.gca=function(e){return e={"935f2afb":"53",dfd24482:"221","505fc875":"334",e3741bf5:"397","02fa4020":"439","90b799f4":"737",da95f3d6:"1050","9f1c36eb":"1293","3bd79dc4":"1440",dd5deefd:"1734","02fefe41":"1906","605371c8":"2299","9e3afa9a":"2350","99c95826":"2575",c48aeec7:"2596","6b15a8e7":"2617","972d49dd":"2872","1f391b9e":"3085","667a1a38":"3253",e31563f4:"3280","57076a74":"3751","3d604b8e":"3917",e420c2e8:"3989",e9fc5b99:"4051",f4f9ee34:"4109",abf449ea:"4501","5825b5f0":"5131",b728f6fe:"5211","59da24a9":"5365","7c39e10e":"5582","0ba2ede9":"5722",a757db9b:"5851",d5444868:"5986","6b1ae1c5":"6015","312ed758":"6099",d9cd0856:"6238",b948ee85:"6283",f930e7e8:"6553",d04fa17f:"6585",d9d15992:"6686","26d83c4c":"6695","520898ab":"6756","92d10100":"7184",bfa6c7fa:"7198","8584d295":"7259","223d151b":"7530","216712ef":"7544","31bf44dd":"7802","1a4e3797":"7920",ab21f6e2:"8670","4f33924b":"8711","5ba709b9":"8765","5cddde15":"9429","2ad5369a":"9500","1be78505":"9514",d4358da1:"9591",e2f5eafd:"9791"}[e]||e,c.p+c.u(e)},(()=>{var e={1303:0,532:0};c.f.j=(a,d)=>{var f=c.o(e,a)?e[a]:void 0;if(0!==f)if(f)d.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var t=new Promise(((d,t)=>f=e[a]=[d,t]));d.push(f[2]=t);var r=c.p+c.u(a),b=new Error;c.l(r,(d=>{if(c.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var t=d&&("load"===d.type?"missing":d.type),r=d&&d.target&&d.target.src;b.message="Loading chunk "+a+" failed.\n("+t+": "+r+")",b.name="ChunkLoadError",b.type=t,b.request=r,f[1](b)}}),"chunk-"+a,a)}},c.O.j=a=>0===e[a];var a=(a,d)=>{var f,t,r=d[0],b=d[1],o=d[2],n=0;if(r.some((a=>0!==e[a]))){for(f in b)c.o(b,f)&&(c.m[f]=b[f]);if(o)var i=o(c)}for(a&&a(d);n{"use strict";var e,a,d,f,t,r={},b={};function c(e){var a=b[e];if(void 0!==a)return a.exports;var d=b[e]={id:e,loaded:!1,exports:{}};return r[e].call(d.exports,d,d.exports,c),d.loaded=!0,d.exports}c.m=r,c.c=b,e=[],c.O=(a,d,f,t)=>{if(!d){var r=1/0;for(i=0;i=t)&&Object.keys(c.O).every((e=>c.O[e](d[o])))?d.splice(o--,1):(b=!1,t0&&e[i-1][2]>t;i--)e[i]=e[i-1];e[i]=[d,f,t]},c.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return c.d(a,{a:a}),a},d=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,c.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var t=Object.create(null);c.r(t);var r={};a=a||[null,d({}),d([]),d(d)];for(var b=2&f&&e;"object"==typeof b&&!~a.indexOf(b);b=d(b))Object.getOwnPropertyNames(b).forEach((a=>r[a]=()=>e[a]));return r.default=()=>e,c.d(t,r),t},c.d=(e,a)=>{for(var d in a)c.o(a,d)&&!c.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},c.f={},c.e=e=>Promise.all(Object.keys(c.f).reduce(((a,d)=>(c.f[d](e,a),a)),[])),c.u=e=>"assets/js/"+({53:"935f2afb",221:"dfd24482",334:"505fc875",397:"e3741bf5",439:"02fa4020",737:"90b799f4",1050:"da95f3d6",1293:"9f1c36eb",1440:"3bd79dc4",1734:"dd5deefd",1906:"02fefe41",2299:"605371c8",2350:"9e3afa9a",2575:"99c95826",2596:"c48aeec7",2617:"6b15a8e7",2872:"972d49dd",3085:"1f391b9e",3253:"667a1a38",3280:"e31563f4",3751:"57076a74",3917:"3d604b8e",3989:"e420c2e8",4051:"e9fc5b99",4109:"f4f9ee34",4501:"abf449ea",5131:"5825b5f0",5211:"b728f6fe",5365:"59da24a9",5582:"7c39e10e",5722:"0ba2ede9",5851:"a757db9b",5986:"d5444868",6015:"6b1ae1c5",6099:"312ed758",6238:"d9cd0856",6283:"b948ee85",6553:"f930e7e8",6585:"d04fa17f",6686:"d9d15992",6695:"26d83c4c",6756:"520898ab",7184:"92d10100",7198:"bfa6c7fa",7259:"8584d295",7530:"223d151b",7544:"216712ef",7802:"31bf44dd",7920:"1a4e3797",8670:"ab21f6e2",8711:"4f33924b",8765:"5ba709b9",9429:"5cddde15",9500:"2ad5369a",9514:"1be78505",9591:"d4358da1",9791:"e2f5eafd"}[e]||e)+"."+{53:"ae96f8c3",221:"a8c9edca",230:"34dddfc8",272:"a34d203d",334:"611070a6",397:"5a137d01",439:"d80415df",737:"6601a4ef",1050:"ff213c15",1293:"f2d83d86",1440:"81d1364b",1734:"039b46a5",1906:"b3f9381e",2299:"71cb6ac4",2350:"0b52dd30",2384:"4861d8b1",2575:"de80d85d",2596:"94e91701",2617:"370eb108",2872:"e496a32b",3085:"a00364fc",3253:"0def7602",3280:"d05d9934",3751:"a9e996b1",3917:"a7165cd6",3989:"d885ff56",4051:"babb79cb",4109:"40b1280a",4501:"b1986ef7",4972:"bea3865f",5131:"c4348d51",5178:"6dd866d0",5211:"bba33834",5283:"79fe536c",5365:"b2082b98",5582:"72217821",5722:"56884b73",5851:"62dd77de",5986:"acde29ec",6015:"73c2607f",6099:"c38443de",6238:"cc532560",6283:"51069a20",6511:"4f41f0a7",6553:"1501d559",6585:"2a5decdb",6686:"54894bba",6695:"edef56f4",6756:"4d2ef29d",7184:"a6f541cd",7198:"1eac726c",7259:"ebf32f7e",7530:"a2893301",7544:"2205d07e",7802:"fe411772",7920:"10deaa27",8624:"ef1f3195",8670:"e3bd364c",8711:"6c95a067",8765:"3ab20d87",8894:"446f680f",9429:"3da6324e",9500:"582e9919",9514:"418dd524",9591:"05e70cb8",9791:"57ff7439"}[e]+".js",c.miniCssF=e=>{},c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},t="jjbook:",c.l=(e,a,d,r)=>{if(f[e])f[e].push(a);else{var b,o;if(void 0!==d)for(var n=document.getElementsByTagName("script"),i=0;i{b.onerror=b.onload=null,clearTimeout(s);var t=f[e];if(delete f[e],b.parentNode&&b.parentNode.removeChild(b),t&&t.forEach((e=>e(d))),a)return a(d)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:b}),12e4);b.onerror=u.bind(null,b.onerror),b.onload=u.bind(null,b.onload),o&&document.head.appendChild(b)}},c.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),c.p="/awesome-interview/",c.gca=function(e){return e={"935f2afb":"53",dfd24482:"221","505fc875":"334",e3741bf5:"397","02fa4020":"439","90b799f4":"737",da95f3d6:"1050","9f1c36eb":"1293","3bd79dc4":"1440",dd5deefd:"1734","02fefe41":"1906","605371c8":"2299","9e3afa9a":"2350","99c95826":"2575",c48aeec7:"2596","6b15a8e7":"2617","972d49dd":"2872","1f391b9e":"3085","667a1a38":"3253",e31563f4:"3280","57076a74":"3751","3d604b8e":"3917",e420c2e8:"3989",e9fc5b99:"4051",f4f9ee34:"4109",abf449ea:"4501","5825b5f0":"5131",b728f6fe:"5211","59da24a9":"5365","7c39e10e":"5582","0ba2ede9":"5722",a757db9b:"5851",d5444868:"5986","6b1ae1c5":"6015","312ed758":"6099",d9cd0856:"6238",b948ee85:"6283",f930e7e8:"6553",d04fa17f:"6585",d9d15992:"6686","26d83c4c":"6695","520898ab":"6756","92d10100":"7184",bfa6c7fa:"7198","8584d295":"7259","223d151b":"7530","216712ef":"7544","31bf44dd":"7802","1a4e3797":"7920",ab21f6e2:"8670","4f33924b":"8711","5ba709b9":"8765","5cddde15":"9429","2ad5369a":"9500","1be78505":"9514",d4358da1:"9591",e2f5eafd:"9791"}[e]||e,c.p+c.u(e)},(()=>{var e={1303:0,532:0};c.f.j=(a,d)=>{var f=c.o(e,a)?e[a]:void 0;if(0!==f)if(f)d.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var t=new Promise(((d,t)=>f=e[a]=[d,t]));d.push(f[2]=t);var r=c.p+c.u(a),b=new Error;c.l(r,(d=>{if(c.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var t=d&&("load"===d.type?"missing":d.type),r=d&&d.target&&d.target.src;b.message="Loading chunk "+a+" failed.\n("+t+": "+r+")",b.name="ChunkLoadError",b.type=t,b.request=r,f[1](b)}}),"chunk-"+a,a)}},c.O.j=a=>0===e[a];var a=(a,d)=>{var f,t,r=d[0],b=d[1],o=d[2],n=0;if(r.some((a=>0!==e[a]))){for(f in b)c.o(b,f)&&(c.m[f]=b[f]);if(o)var i=o(c)}for(a&&a(d);nvar _hmt=_hmt||[];!function(){var e=document.createElement("script");e.src="https://hm.baidu.com/hm.js?c7cd0fd77ac518cc6ef46461cdc9524b";var c=document.getElementsByTagName("script")[0];c.parentNode.insertBefore(e,c)}() - +

平衡二叉树

题目描述

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过 1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3, 9, 20, null, null, 15, 7]

3
/ \
9 20
/ \
15 7
返回 true。

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

1
/ \
2 2
/ \
3 3
/ \
4 4
返回 false。

限制:0 <= 树的结点个数 <= 10000

基本知识点

二叉树的每个节点最多有两个子节点,平衡二叉树中任意一个节点的左右子树高度相差不能大于 1,满二叉树和完全二叉树都是平衡二叉树,普通二叉树有可能是平衡二叉树。

题解

解法一

思路

若想判断二叉树是不是平衡二叉树,只需要判断左右子树的高度差是不是不超过 1 即可。同时,要满足一个树是平衡二叉树,它的子树也必须是平衡二叉树。我们可以从根结点开始,通过递归来求得子树的高度,以及子树是否是平衡二叉树,以此来结合判断二叉树是否是平衡二叉树。

代码

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val === undefined ? 0 : val)
* this.left = (left === undefined ? null : left)
* this.right = (right === undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
const isBalanced = function (root) {
if (root === null) {
return true;
} else {
return (
Math.abs(height(root.left) - height(root.right)) <= 1 &&
isBalanced(root.left) &&
isBalanced(root.right)
);
}
};

const height = function (root) {
if (root === null) {
return 0;
} else {
return Math.max(height(root.left), height(root.right)) + 1;
}
};

时间复杂度分析

该方法最坏的情况是每个父节点都只有一个子节点,这样树的高度时间复杂度为 O(n),即“链表”的长度。而第 d 层调用 height 函数的时间复杂度是 O(d),所以整体的时间复杂度为高度时间复杂度 * 调用 height 函数的时间复杂度,即 O(n^2)。

空间复杂度分析

空间复杂度取决于递归调用的层数,不会超过 n 层,所以空间复杂度是 O(n)。

解法二

思路

上面的方法是自顶而下的,这样其实就会导致每层的高度都要重复计算。那么,我们可以使用后序遍历,这样每个节点的高度就能根据前面的结果算出来。

代码

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val === undefined ? 0 : val)
* this.left = (left === undefined ? null : left)
* this.right = (right === undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isBalanced = function (root) {
return height(root) != -1;
};

var height = function (root) {
if (root == null) {
return 0;
}

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)。

空间复杂度分析

空间复杂度取决于递归调用的层数,不会超过 n 层,所以空间复杂度是 O(n)。

Loading script...
- + \ No newline at end of file diff --git a/book1/browser-cross-origin.html b/book1/browser-cross-origin.html index ea7b631..428b36f 100644 --- a/book1/browser-cross-origin.html +++ b/book1/browser-cross-origin.html @@ -10,13 +10,13 @@ - +

浏览器跨域

相关问题

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

回答关键点

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 是一个相对古老的跨域解决方案,只支持 GET 请求。主要是利用了浏览器加载 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 96bd7fc..4410cd5 100644 --- a/book1/browser-repain-reflow.html +++ b/book1/browser-repain-reflow.html @@ -10,13 +10,13 @@ - +

浏览器的重排重绘

相关问题

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

回答关键点

渲染性能 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 edf1165..2e3231e 100644 --- a/book1/coding-promise.html +++ b/book1/coding-promise.html @@ -10,13 +10,13 @@ - +

实现一个符合 Promises/A+ 规范的 Promise

这是一道有着成熟的业界规范的 coding 题,完成这道题的前置知识就是要了解什么是 Promises/A+

这道题的难点就在于它是有规范的,任何一个不满足所有规范条件的解答都是错误的。同时,成熟的规范也配套了成熟的测试用例,官方提供了 872 个测试用例针对规范中的所有条件一一进行检测,哪怕只有一条失败,那也是错误的解答。

而这道题的答题关键也恰恰是因为它是有规范的,只要我们对于规范了然于胸,那么编写代码自然也是水到渠成。因为官方规范提供了一个符合 Promises/A+ 规范的 Promise 应该具有的全部条件,并且在 Requirements 一节中结构清晰、逻辑充分的表述了出来,我们只需将规范中的文字转变为代码,就能够实现一个 Promises/A+ 规范的 Promise。

编写代码

因为规范条例较多,我们拆解成三块来理解记忆,分别是:基础框架、then 方法和 Promise 处理程序。

每一块由两部分构成:

  • 流程图:展示了代码逻辑的关键步骤,也是优先需要理解记忆的点。
  • 实现代码:展示了代码逻辑的具体细节,是对关键步骤的完善补全。

其中,涉及到规范条例的点会注明规范序号

再次强调,本题的答题关键是熟悉规范!磨刀不误砍柴工,务必先熟悉!熟悉!熟悉!

1. 基础框架

1.1 流程图

基本框架

1.2 实现代码

function Promise(executor) {
// 2.1. Promise 的状态
// Promise 必须处于以下三种状态之一:pending,fulfilled 或者 rejected。
this.state = "pending";
// 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行。
this.onFulfilledCallback = [];
// 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
this.onRejectedCallback = [];

const self = this;

function resolve(value) {
setTimeout(function () {
// 2.1.1. 当 Promise 处于 pending 状态时:
// 2.1.1.1. 可以转换到 fulfilled 或 rejected 状态。
// 2.1.2. 当 Promise 处于 fulfilled 状态时:
// 2.1.2.1. 不得过渡到任何其他状态。
// 2.1.2.2. 必须有一个不能改变的值。
if (self.state === "pending") {
self.state = "fulfilled";
self.data = value;
// 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行。
for (let i = 0; i < self.onFulfilledCallback.length; i++) {
self.onFulfilledCallback[i](value);
}
}
});
}

function reject(reason) {
setTimeout(function () {
// 2.1.1. 当 Promise 处于 pending 状态时:
// 2.1.1.1. 可以转换到 fulfilled 或 rejected 状态。
// 2.1.3. 当 Promise 处于 rejected 状态时:
// 2.1.2.1. 不得过渡到任何其他状态。
// 2.1.2.2. 必须有一个不能改变的值。
if (self.state === "pending") {
self.state = "rejected";
self.data = reason;
// 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
for (let i = 0; i < self.onRejectedCallback.length; i++) {
self.onRejectedCallback[i](reason);
}
}
});
}

// 补充说明:用户传入的函数可能也会执行异常,所以这里用 try...catch 包裹
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

2. then 方法

2.1 流程图

then 方法

2.2 实现代码

// 2.2. then 方法
// 一个 promise 必须提供一个 then 方法来访问其当前值或最终值或 rejected 的原因。
// 一个 promise 的 then 方法接受两个参数:
// promise.then(onFulfilled, onRejected)
Promise.prototype.then = function (onFulfilled, onRejected) {
const self = this;

let promise2;
// 2.2.7. then 必须返回一个 promise
return (promise2 = new Promise(function (resolve, reject) {
// 2.2.2. 如果 onFulfilled 是一个函数:
// 2.2.2.1. 它必须在 promise 的状态变为 fulfilled 后被调用,并将 promise 的值作为它的第一个参数。
// 2.2.2.2. 它一定不能在 promise 的状态变为 fulfilled 前被调用。
// 2.2.2.3. 它最多只能被调用一次。
if (self.state === "fulfilled") {
// 2.2.4. onFulfilled 或 onRejected 在执行上下文堆栈仅包含平台代码之前不得调用。
// 3.1. 这可以通过“宏任务”机制(例如 setTimeout 或 setImmediate)或“微任务”机制(例如 MutationObserver 或 process.nextTick)来实现。
setTimeout(function () {
// 2.2.1. onFulfilled 和 onRejected 都是可选参数:
// 2.2.1.1. 如果 onFulfilled 不是一个函数,它必须被忽略。
if (typeof onFulfilled === "function") {
try {
// 2.2.2.1. 它必须在 promise 的状态变为 fulfilled 后被调用,并将 promise 的值作为它的第一个参数。
// 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
const x = onFulfilled(self.data);
// 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
reject(e);
}
} else {
// 2.2.7.3. 如果 onFulfilled 不是一个函数且 promise1 为 fulfilled 状态,promise2 必须用和 promise1 一样的值来变为 fulfilled 状态。
resolve(self.data);
}
});
}
// 2.2.3. 如果 onRejected 是一个函数,
// 2.2.3.1. 它必须在 promise 的状态变为 rejected 后被调用,并将 promise 的 reason 作为它的第一个参数。
// 2.2.3.2. 它一定不能在 promise 的状态变为 rejected 前被调用。
// 2.2.3.3. 它最多只能被调用一次。
else if (self.state === "rejected") {
// 2.2.4. onFulfilled 或 onRejected 在执行上下文堆栈仅包含平台代码之前不得调用。
// 3.1. 这可以通过“宏任务”机制(例如 setTimeout 或 setImmediate)或“微任务”机制(例如 MutationObserver 或 process.nextTick)来实现。
setTimeout(function () {
// 2.2.1. onFulfilled 和 onRejected 都是可选参数:
// 2.2.1.2. 如果 onRejected 不是一个函数,它必须被忽略。
if (typeof onRejected === "function") {
try {
// 2.2.3.1. 它必须在 promise 的状态变为 rejected 后被调用,并将 promise 的 reason 作为它的第一个参数。
// 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
const x = onRejected(self.data);
// 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
reject(e);
}
}
// 2.2.7.4. 如果 onRejected 不是一个函数且 promise1 为 rejected 状态,promise2 必须用和 promise1 一样的 reason 来变为 rejected 状态。
else {
reject(self.data);
}
});
} else if (self.state === "pending") {
// 2.2.6. then 可能会被同一个 promise 多次调用。

// 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行。
self.onFulfilledCallback.push(function (promise1Value) {
if (typeof onFulfilled === "function") {
try {
// 2.2.2.1. 它必须在 promise 的状态变为 fulfilled 后被调用,并将 promise 的值作为它的第一个参数。
// 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
const x = onFulfilled(self.data);
// 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
reject(e);
}
}
// 2.2.7.3. 如果 onFulfilled 不是一个函数且 promise1 为 fulfilled 状态,promise2 必须用和 promise1 一样的值来变为 fulfilled 状态。
else {
resolve(promise1Value);
}
});
// 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
self.onRejectedCallback.push(function (promise1Reason) {
if (typeof onRejected === "function") {
try {
// 2.2.3.1. 它必须在 promise 的状态变为 rejected 后被调用,并将 promise 的 reason 作为它的第一个参数。
// 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
const x = onRejected(self.data);
// 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
reject(e);
}
}
// 2.2.7.4. 如果 onRejected 不是一个函数且 promise1 为 rejected 状态,promise2 必须用和 promise1 一样的 reason 来变为 rejected 状态。
else {
reject(promise1Reason);
}
});
}
}));
};

3. Promise 处理程序

3.1 流程图

Promise 处理程序

3.2 实现代码

// 2.3. Promise 处理程序
// Promise 处理程序是一个将 promise 和 value 作为输入的抽象操作,我们将其表示为 [[Resolve]](promise, x)。
// 补充说明:这里我们将 resolve 和 reject 也传入进来,因为后续要根据不同的逻辑对 promise 执行 fulfill 或 reject 操作。
function promiseResolutionProcedure(promise2, x, resolve, reject) {
// 2.3.1. 如果 promise 和 x 引用的是同一个对象,promise 将以一个 TypeError 作为 reason 来进行 reject。
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise"));
}

// 2.3.2. 如果 x 是一个 promise,根据它的状态:
if (x instanceof Promise) {
// 2.3.2.1. 如果 x 的状态为 pending,promise 必须保持 pending 状态直到 x 的状态变为 fulfilled 或 rejected。
if (x.state === "pending") {
x.then(function (value) {
promiseResolutionProcedure(promise2, value, resolve, reject);
}, reject);
}
// 2.3.2.2. 如果 x 的状态为 fulfilled,那么 promise 也用同样的值来执行 fulfill 操作。
else if (x.state === "fulfilled") {
resolve(x.data);
}
// 2.3.2.3. 如果 x 的状态为 rejected,那么 promise 也用同样的 reason 来执行 reject 操作。
else if (x.state === "rejected") {
reject(x.data);
}
return;
}

// 2.3.3. 除此之外,如果 x 是一个对象或者函数,
if (x && (typeof x === "object" || typeof x === "function")) {
// 2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用,或者多次调用同样的参数,则第一次调用优先,任何之后的调用都将被忽略。
let isCalled = false;

try {
// 2.3.3.1. 声明一个 then 变量来保存 then
let then = x.then;
// 2.3.3.3. 如果 then 是一个函数,将 x 作为 this 来调用它,第一个参数为 resolvePromise,第二个参数为 rejectPromise,其中:
if (typeof then === "function") {
then.call(
x,
// 2.3.3.3.1. 假设 resolvePromise 使用一个名为 y 的值来调用,运行 promise 处理程序 [[Resolve]](promise, y)。
function resolvePromise(y) {
// 2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用,或者多次调用同样的参数,则第一次调用优先,任何之后的调用都将被忽略。
if (isCalled) return;
isCalled = true;
return promiseResolutionProcedure(promise2, y, resolve, reject);
},
// 2.3.3.3.2. 假设 rejectPromise 使用一个名为 r 的 reason 来调用,则用 r 作为 reason 对 promise 执行 reject 操作。
function rejectPromise(r) {
// 2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用,或者多次调用同样的参数,则第一次调用优先,任何之后的调用都将被忽略。
if (isCalled) return;
isCalled = true;
return reject(r);
}
);
}
// 2.3.3.4. 如果 then 不是一个函数,使用 x 作为值对 promise 执行 fulfill 操作。
else {
resolve(x);
}
} catch (e) {
// 2.3.3.2. 如果检索 x.then 的结果抛出异常 e,使用 e 作为 reason 对 promise 执行 reject 操作。
// 2.3.3.3.4. 如果调用 then 时抛出一个异常 e,
// 2.3.3.3.4.1. 如果 resolvePromise 或 rejectPromise 已经被调用过了,则忽略异常。
if (isCalled) return;
isCalled = true;
// 2.3.3.3.4.2. 否则,使用 e 作为 reason 对 promise 执行 reject 操作。
reject(e);
}
}
// 2.3.4. 如果 x 不是一个对象或者函数,使用 x 作为值对 promise 执行 fulfill 操作。
else {
resolve(x);
}
}

4. 完整代码

function Promise(executor) {
this.state = "pending";
this.onFulfilledCallback = [];
this.onRejectedCallback = [];

const self = this;

function resolve(value) {
setTimeout(function () {
if (self.state === "pending") {
self.state = "fulfilled";
self.data = value;
for (let i = 0; i < self.onFulfilledCallback.length; i++) {
self.onFulfilledCallback[i](value);
}
}
});
}

function reject(reason) {
setTimeout(function () {
if (self.state === "pending") {
self.state = "rejected";
self.data = reason;
for (let i = 0; i < self.onRejectedCallback.length; i++) {
self.onRejectedCallback[i](reason);
}
}
});
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

Promise.prototype.then = function (onFulfilled, onRejected) {
const self = this;

let promise2;

return (promise2 = new Promise(function (resolve, reject) {
if (self.state === "fulfilled") {
setTimeout(function () {
if (typeof onFulfilled === "function") {
try {
const x = onFulfilled(self.data);

promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
} else {
resolve(self.data);
}
});
} else if (self.state === "rejected") {
setTimeout(function () {
if (typeof onRejected === "function") {
try {
const x = onRejected(self.data);

promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
} else {
reject(self.data);
}
});
} else if (self.state === "pending") {
self.onFulfilledCallback.push(function (promise1Value) {
if (typeof onFulfilled === "function") {
try {
const x = onFulfilled(self.data);

promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
} else {
resolve(promise1Value);
}
});

self.onRejectedCallback.push(function (promise1Reason) {
if (typeof onRejected === "function") {
try {
const x = onRejected(self.data);

promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
} else {
reject(promise1Reason);
}
});
}
}));
};

function promiseResolutionProcedure(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise"));
}

if (x instanceof Promise) {
if (x.state === "pending") {
x.then(function (value) {
promiseResolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else if (x.state === "fulfilled") {
resolve(x.data);
} else if (x.state === "rejected") {
reject(x.data);
}
return;
}

if (x && (typeof x === "object" || typeof x === "function")) {
let isCalled = false;

try {
let then = x.then;

if (typeof then === "function") {
then.call(
x,
function resolvePromise(y) {
if (isCalled) return;
isCalled = true;
return promiseResolutionProcedure(promise2, y, resolve, reject);
},
function rejectPromise(r) {
if (isCalled) return;
isCalled = true;
return reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (isCalled) return;
isCalled = true;
reject(e);
}
} else {
resolve(x);
}
}

module.exports = Promise;

测试代码

开头我们就说过,Promises/A+ 规范配套了成熟的测试用例,我们必须全部通过才算代码编写正确。下面我们就用 872 个官方测试用例来测试一下我们的完整代码是否符合 Promises/A+ 规范。

1. 暴露一个简单的适配器接口

// test.js

// 导入我们写好的 promise
const Promise = require("./promise.js");

// 根据官方文档暴露一个 deferred 方法,返回一个包含 promise、resolve、reject 的对象
Promise.deferred = function () {
const obj = {};

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 07a7006..6c0b06b 100644 --- a/book1/css-bfc.html +++ b/book1/css-bfc.html @@ -10,13 +10,13 @@ - +

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 e10eadd..7333a94 100644 --- a/book1/engineer-webpack-workflow.html +++ b/book1/engineer-webpack-workflow.html @@ -10,7 +10,7 @@ - + @@ -18,7 +18,7 @@

webpack 工作流程

本篇内容和配置基于 webpack v5.52.1 讲解

相关问题

  • webpack 工作流程是怎样的
  • webpack 在不同阶段做了什么事情

回答关键点

模块化 打包 依赖生成 工程化

webpack 是一种模块打包工具,可以将各类型的资源,例如图片、CSS、JS 等,转译组合为 JS 格式的 bundle 文件。

webpack

图片来源 webpack 官网

webpack 构建的核心任务是完成内容转化和资源合并。主要包含以下 3 个阶段:

  1. 初始化阶段
    • 初始化参数:从配置文件、配置对象和 Shell 参数中读取并与默认参数进行合并,组合成最终使用的参数。
    • 创建编译对象:用上一步得到的参数创建 Compiler 对象。
    • 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等。
  2. 构建阶段
    • 开始编译:执行 Compiler 对象的 run 方法,创建 Compilation 对象。
    • 确认编译入口:进入 entryOption 阶段,读取配置的 Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。
    • 编译模块(make): 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转化为 AST 对象,然后递归分析依赖,依次处理全部文件。
    • 完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图。
  3. 生成阶段
    • 输出资源(seal):根据入口和模块之间的依赖关系,组装成多个包含多个模块的 Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输出内容的最后机会。
    • 写入文件系统(emitAssets):确定好输出内容后,根据配置的 output 将内容写入文件系统。

知识点深入

1. webpack 初始化过程

从 webpack 项目 webpack.js 文件 webpack 方法出发,可以看到初始化过程如下: webpack 初始化流程图

  1. 将命令行参数和用户的配置文件进行合并。
  2. 调用 getValidateSchema 对配置进行校验。
  3. 调用 createCompiler 创建 Compiler 对象。
    1. 将用户配置和默认配置进行合并处理。
    2. 实例化 Compiler。
    3. 实例化 NodeEnvironmentPlugin。
    4. 处理用户配置的 plugins,执行 plugin 的 apply 方法。
    5. 触发 environment 和 afterEnvironment 上注册的事件。
    6. 注册 webpack 内部插件。
    7. 触发 initialize 事件。
// lib/webpack.js 122 行 部分代码省略处理
const create = () => {
if (!webpackOptionsSchemaCheck(options)) {
// 校验参数
getValidateSchema()(webpackOptionsSchema, options);
}
// 创建 compiler 对象
compiler = createCompiler(webpackOptions);
};

// lib/webpack.js 57 行
const createCompiler = (rawOptions) => {
// 统一合并处理参数
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// 实例化 compiler
const compiler = new Compiler(options.context);
// 把 options 挂载到对象上
compiler.options = options;
// NodeEnvironmentPlugin 是对 fs 模块的封装,用来处理文件输入输出等
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging,
}).apply(compiler);
// 注册用户配置插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
// 触发 environment 和 afterEnvironment 上注册的事件
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 注册 webpack 内置插件
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};

2. webpack 构建阶段做了什么

在 webpack 函数执行完之后,就到主要的构建阶段,首先执行 compiler.run(),然后触发一系列钩子函数,执行 compiler.compile()。 webpack 执行流程

  1. 在实例化 compiler 之后,执行 compiler.run()。
  2. 执行 newCompilation 函数,调用 createCompilation 初始化 Compilation 对象。
  3. 执行 _addEntryItem 将入口文件存入 this.entries(map 对象),遍历 this.entries 对象构建 chunk。
  4. 执行 handleModuleCreation,开始创建模块实例。
  5. 执行 moduleFactory.create 创建模块。
    1. 执行 factory.hooks.factorize.call 钩子,然后会调用 ExternalModuleFactoryPlugin 中注册的钩子,用于配置外部文件的模块加载方式。
    2. 使用 enhanced-resolve 解析模块和 loader 的真实绝对路径。
    3. 执行 new NormalModule()创建 module 实例。
  6. 执行 addModule,存储 module。
  7. 执行 buildModule,添加模块到模块队列 buildQueue,开始构建模块, 这里会调用 normalModule 中的 build 开启构建。
    1. 创建 loader 上下文。
    2. 执行 runLoaders,通过 enhanced-resolve 解析得到的模块和 loader 的路径获取函数,执行 loader。
    3. 生成模块的 hash。
  8. 所有依赖都解析完毕后,构建阶段结束。
  // 构建过程涉及流程比较复杂,代码会做省略

// lib/webpack.js 1284行
// 开启编译流程
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});

// lib/compiler.js 1081行
// 开启编译流程
compile(callback) {
const params = this.newCompilationParams();
// 创建 Compilation 对象
const Compilation = this.newCompilation(params);
}

// lib/Compilation.js 1865行
// 确认入口文件
addEntry() {
this._addEntryItem();
}

// lib/Compilation.js 1834行
// 开始创建模块流程,创建模块实例
addModuleTree() {
this.handleModuleCreation()
}

// lib/Compilation.js 1548行
// 开始创建模块流程,创建模块实例
handleModuleCreation() {
this.factorizeModule()
}

// 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 082177a..59195c0 100644 --- a/book1/frame-vue-computed-watch.html +++ b/book1/frame-vue-computed-watch.html @@ -10,13 +10,13 @@ - +

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 963a937..190cedf 100644 --- a/book1/frame-vue-data-binding.html +++ b/book1/frame-vue-data-binding.html @@ -10,13 +10,13 @@ - +

Vue 的数据绑定机制

相关问题

  • Vue 是如何实现数据劫持的
  • Vue 是如何实现双向绑定的
  • MVVM 是什么

回答关键点

响应式对象 数据劫持 双向绑定 MVVM(Model-View-ViewModel) 发布/订阅模式(publish-subscribe pattern)

响应式对象

Vue2 通过 Object.defineProperty,Vue3 通过 Proxy 来劫持 state 中各个属性的 setter、getter,通过 getter 收集依赖。当 state 中的数据发生变动之后发布通知给订阅者更新数据。

双向绑定

Vue 通过 v-model 实现双向绑定。v-model 实际是 v-bind:xxxv-on:xxx 的语法糖。当触发元素对应的事件(如 input、change 等)时更新数据(ViewModel),当数据(ViewModel)更新时同步更新到元素的对应属性(View)上。

MVVM(Model-View-ViewModel)

MVVM 模式是一种软件架构模式,相比 MVC 模式多了一个 ViewModel 层。有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来。

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交互。
  • View:视图层,将数据通过 UI 展现出来。
  • ViewModel:视图模型层,连接 Model 层和 View 层。

知识点深入

1. 前置概念

在详细说明原理之前,需要对以下概念有一定的认知:

  1. Dep:实现发布订阅模式的模块。
  2. Watcher:订阅更新和触发视图更新的模块。

2. 实现原理

image

上图是 Vue 官网描述 Vue 内数据变化与发布更新的流程,我们以响应式对象、依赖收集、数据更新的顺序详细说明整个过程。

2.1 响应式对象

Vue2 通过 Object.defineProperty,Vue3 通过 Proxy 来劫持 state 中各个属性的 getter、setter。其中 getter 中主要是通过 Dep 收集依赖这个属性的订阅者,setter 中则是在属性变化后通知 Dep 收集到的订阅者,派发更新。

以下是 Dep 的伪代码:

export default class Dep {
static target?: Watcher;
subs: Array<Watcher>;

constructor () {
this.subs = []
}

/**
* 添加订阅者(watcher)
**/
addSub (sub: Watcher) {
this.subs.push(sub)
}

/**
* 移除订阅者(watcher)
**/
removeSub (sub: Watcher) {
remove(this.subs, sub)
}

/**
* 添加订阅(调用订阅者上的 addDep 方法)
**/
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

/**
* 遍历通知订阅者更新
**/
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Dep.target = null

以下是生成响应式对象的伪代码:

/**
* 生成响应式对象
* 为了方便理解,以下代码略有修改,省略了部分不相关内容
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 每次 get 时如果有订阅者则添加订阅
if (Dep.target) {
dep.depend()
}
return val
},
set: function reactiveSetter (newVal) {
val = newVal
// 每次更新数据之后广播更新
dep.notify()
}
})
}

2.2 依赖收集

Vue 会在需要使用到属性的地方新建一个 Watcher 的实例 watcher,watcher 实例化时会读取对应属性的内容,从而触发 1.1 中的 getter,将 watcher 注册进 Dep 中。

以下是 Watcher 的伪代码:

/**
* 为了方便理解,以下代码略有修改,省略了部分不相关内容
*/
export default class Watcher {
vm: Component;
getter: Function;
value: any;
cb: Function;

constructor (
vm: Component,
exp: string
) {
this.vm = vm
this.cb = cb
// 获取表达式对应的属性的 getter
this.getter = parsePath(exp)
this.value = this.get()
}

/**
* 获取最新的值
**/
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 24bcb37..a2c2118 100644 --- a/book1/js-closures.html +++ b/book1/js-closures.html @@ -10,14 +10,14 @@ - +

闭包的作用和原理

相关问题

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

回答关键点

作用域 引用 函数

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

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

知识点深入

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 8cf651e..ee7d5d0 100644 --- a/book1/js-module-specs.html +++ b/book1/js-module-specs.html @@ -10,14 +10,14 @@ - +

前端模块化规范

相关问题

  • JavaScript 主要有哪几种模块化规范
  • AMD / CMD 有什么异同
  • ESM 是什么
  • 模块化解决了什么问题/痛点

回答关键点

CommonJS AMD CMD UMD ESM

  • CommonJS[1]: 主要是 Node.js 使用,通过 require 同步加载模块,exports 导出内容。
  • AMD[2]: 主要是浏览器端使用,通过 define 定义模块和依赖,require 异步加载模块,推崇依赖前置
  • CMD[3]: 和 AMD 比较类似,主要是浏览器端使用,通过 require 异步加载模块,exports 导出内容,推崇依赖就近
  • UMD[4]: 通用模块规范,是 CommonJS、AMD 两个规范的大融合,是跨平台的解决方案。
  • ESM[5]: 官方模块化规范,现代浏览器原生支持,通过 import 异步加载模块,export 导出内容。

知识点深入

1. 为什么需要模块化和模块化规范

模块化可以解决代码之间的变量、函数、对象等命名的冲突/污染问题,良好的模块化设计可以降低代码之间的耦合关系,提高代码的可维护性、可扩展性以及复用性。

模块化规范的作用是为了规范 JavaScript 模块的定义和加载机制,以统一的方式导出和加载模块,降低学习使用成本,提高开发效率。

2. 各种模块化规范的细节

2.1 CommonJS

CommonJS 主要是 Node.js 使用,通过 require 同步加载模块,exports 导出内容。在 CommonJS 规范下,每一个 JS 文件都是独立的模块,每个模块都有独立的作用域,模块里的本地变量都是私有的。

示例

// hzfe.js
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};
module.exports.getHZFEMember = getHZFEMember;

// index.js
const hzfe = require("./hzfe.js");
console.log(hzfe.getHZFEMember()); // HZFE Member: 17

使用场景

CommonJS 主要在服务端(如:Node.js)使用,也可通过打包工具打包之后在浏览器端使用。

加载方式

CommonJS 通过同步的方式加载模块,首次加载会缓存结果,后续加载则是直接读取缓存结果。

优缺点

优点

  • 简单易用
  • 可以在任意位置 require 模块
  • 支持循环依赖

缺点

  • 同步的加载方式不适用于浏览器端
  • 浏览器端使用需要打包
  • 难以支持模块静态分析

2.2 AMD (Asynchronous Module Definition)

AMD,即异步模块定义。AMD 定义了一套 JavaScript 模块依赖异步加载标准,用来解决浏览器端模块加载的问题。AMD 主要是浏览器端使用,通过 define 定义模块和依赖,require 异步加载模块,推崇依赖前置

AMD 模块通过 define 函数定义在闭包中:

/**
* define
* @param id 模块名
* @param dependencies 依赖列表
* @param factory 模块的具体内容/具体实现
*/
define(id?: string, dependencies?: string[], factory: Function | Object);

示例

// hzfe.js
define("hzfe", [], function () {
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

return {
getHZFEMember,
};
});

// index.js
require(["hzfe"], function (hzfe) {
// 依赖前置
console.log(hzfe.getHZFEMember()); // HZFE Member: 17
});

使用场景

AMD 主要在浏览器端中使用,通过符合 AMD 标准的 JavaScript 库(如:RequireJs)加载模块。

加载方式

AMD 通过异步的方式加载模块,每加载一个模块实际就是加载对应的 JS 文件。

优缺点

优点

  • 依赖异步加载,更快的启动速度
  • 支持循环依赖
  • 支持插件

缺点

  • 语法相对复杂
  • 依赖加载器
  • 难以支持模块静态分析

具体实现

2.3 CMD (Common Module Definition)

CMD,即通用模块定义。CMD 定义了一套 JavaScript 模块依赖异步加载标准,用来解决浏览器端模块加载的问题。CMD 主要是浏览器端使用,通过 define 定义模块和依赖,require 异步加载模块,推崇依赖就近

CMD 模块通过 define 函数定义在闭包中:

/**
* define
* @param id 模块名
* @param dependencies 依赖列表
* @param factory 模块的具体内容/具体实现
*/
define(id?: string, dependencies?: string[], factory: Function | Object);

示例

// hzfe.js
define("hzfe", [], function () {
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

exports.getHZFEMember = getHZFEMember;
});

// index.js
define(function (require, exports) {
const hzfe = require("hzfe"); // 依赖就近
console.log(hzfe.getHZFEMember()); // HZFE Member: 17
});

使用场景

CMD 主要在浏览器端中使用,通过符合 CMD 标准的 JavaScript 库(如 sea.js)加载模块。

加载方式

CMD 通过异步的方式加载模块,每加载一个模块实际就是加载对应的 JS 文件。

优缺点

优点

  • 依赖异步加载,更快的启动速度
  • 支持循环依赖
  • 依赖就近
  • 与 CommonJS 保持很大的兼容性

缺点

  • 语法相对复杂
  • 依赖加载器
  • 难以支持模块静态分析

具体实现

2.4 UMD (Universal Module Definition)

UMD,即通用模块定义。UMD 主要为了解决 CommonJS 和 AMD 规范下的代码不通用的问题,同时还支持将模块挂载到全局,是跨平台的解决方案。

示例

// hzfe.js
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["exports", "hzfe"], factory);
} else if (
typeof exports === "object" &&
typeof exports.nodeName !== "string"
) {
// CommonJS
factory(exports, require("hzfe"));
} else {
// Browser globals
factory((root.commonJsStrict = {}), root.hzfe);
}
})(typeof self !== "undefined" ? self : this, function (exports, b) {
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

exports.getHZFEMember = getHZFEMember;
});

// index.js
const hzfe = require("./hzfe.js");
console.log(hzfe.getHZFEMember()); // HZFE Member: 17

使用场景

UMD 可同时在服务器端和浏览器端使用。

加载方式

UMD 加载模块的方式取决于所处的环境,Node.js 同步加载,浏览器端异步加载。

优缺点

优点

  • 跨平台兼容

缺点

  • 代码量稍大

2.5 ESM (ECMAScript Module)

ESM,即 ESModule、ECMAScript Module。官方模块化规范,现代浏览器原生支持,通过 import 加载模块,export 导出内容。 示例

// hzfe.js
const hzfeMember = 17;
export const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

// index.js
import * as hzfe from "./hzfe.js";
console.log(hzfe.getHZFEMember()); // HZFE Member: 17

使用场景

ESM 在支持的浏览器环境下可以直接使用,在不支持的端需要编译/打包后使用。

加载方式

ESM 加载模块的方式同样取决于所处的环境,Node.js 同步加载,浏览器端异步加载。

优缺点

优点

  • 支持同步/异步加载
  • 语法简单
  • 支持模块静态分析
  • 支持循环引用

缺点

  • 兼容性不佳

扩展阅读

1. 静态分析

静态程序分析(Static program analysis)是指在不运行程序的条件下,进行程序分析的方法。 静态程序分析 - Wiki

简而言之,前文里提到的静态分析就是指在运行代码之前就可判断出代码内有哪些代码使用到了,哪些没有使用到。

2. 模块化与工程化:webpack

webpack 同时支持 CommonJS、AMD 和 ESM 三种模块化规范的打包。根据不同规范 webpack 会将模块处理成不同的产物。

CommonJS

(function (module, exports) {
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

module.exports = getHZFEMember;
});

AMD

(function (module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, // 依赖列表
__WEBPACK_AMD_DEFINE_RESULT__; // factory 返回值

__WEBPACK_AMD_DEFINE_ARRAY__ = [];

// 执行 factory
__WEBPACK_AMD_DEFINE_RESULT__ = function () {
const hzfeMember = 17;
const getHZFEMember = () => {
return `HZFE Member: ${hzfeMember}`;
};

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 bcfe701..6e4d402 100644 --- a/book1/network-security.html +++ b/book1/network-security.html @@ -10,13 +10,13 @@ - +

前端安全

相关问题

  • 如何防范 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,不会给服务器压力。
  • 设置 Cookie 的 SameSite 属性可以用来限制第三方 Cookie 的使用,可选值有 Strict、Lax、None。
    • Strict:完全禁止第三方 Cookie。
    • Lax:只允许链接、预加载请求和 GET 表单的场景下发送第三方 Cookie。
    • None:关闭 SameSite 属性。
  • 设置白名单,仅允许安全域名请求
  • 增加验证码验证

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 e8b6969..2ae580c 100644 --- a/book1/topic-enter-url-display-xx.html +++ b/book1/topic-enter-url-display-xx.html @@ -10,14 +10,14 @@ - +

浏览器从输入网址到页面展示的过程

回答关键点

URL DNS TCP 渲染

浏览器从输入网址到渲染页面主要分为以下几个过程

  • URL 输入
  • DNS 解析
  • 建立 TCP 连接
  • 发送 HTTP / HTTPS 请求(建立 TLS 连接)
  • 服务器响应请求
  • 浏览器解析渲染页面
  • HTTP 请求结束,断开 TCP 连接

知识点深入

1. URL 输入

URL地址

URL(统一资源定位符,Uniform Resource Locator)用于定位互联网上资源,俗称网址。

我们在地址栏输入 HZFE 官方网址 hzfe.org 后敲下回车,浏览器会对输入的信息进行以下判断:

  1. 检查输入的内容是否是一个合法的 URL 链接。
  2. 是,则判断输入的 URL 是否完整。如果不完整,浏览器可能会对域进行猜测,补全前缀或者后缀。
  3. 否,将输入内容作为搜索条件,使用用户设置的默认搜索引擎来进行搜索。

大部分浏览器会从历史记录、书签等地方开始查找我们输入的网址,并给出智能提示。

2. DNS(Domain Name System)解析

因为浏览器不能直接通过域名找到对应的服务器 IP 地址,所以需要进行 DNS 解析,查找到对应的 IP 地址进行访问。

DNS 解析流程如下:

DNS 解析

  1. 在浏览器中输入 hzfe.org 域名,操作系统检查浏览器缓存和本地的 hosts 文件中,是否有这个网址记录,有则从记录里面找到对应的 IP 地址,完成域名解析。
  2. 查找本地 DNS 解析器缓存中,是否有这个网址记录,有则从记录里面找到对应的 IP 地址,完成域名解析。
  3. 使用 TCP/IP 参数中设置的 DNS 服务器进行查询。如果要查询的域名包含在本地配置区域资源中,则返回解析结果,完成域名解析。
  4. 检查本地 DNS 服务器是否缓存该网址记录,有则返回解析结果,完成域名解析。
  5. 本地 DNS 服务器发送查询报文至根 DNS 服务器,根 DNS 服务器收到请求后,用顶级域 DNS 服务器地址进行响应。
  6. 本地 DNS 服务器发送查询报文至顶级域 DNS 服务器。顶级域 DNS 服务器收到请求后,用权威 DNS 服务器地址进行响应。
  7. 本地 DNS 服务器发送查询报文至权威 DNS 服务器,权威 DNS 服务器收到请求后,用 hzfe.org 的 IP 地址进行响应,完成域名解析。

查询通常遵循以上流程,从请求主机到本地 DNS 服务器的查询是递归查询,DNS 服务器获取到所需映射的查询过程是迭代查询。

3. 建立 TCP 连接

世界上几乎所有的 HTTP 通信都是由 TCP/IP 承载的,TCP/IP 是全球计算机及网络设备都在使用的一种常用的分组交换网络分层。 HTTP 的连接实际上就是 TCP 连接以及其使用规则。 –《HTTP 权威指南》

当浏览器获取到服务器的 IP 地址后,浏览器会用一个随机的端口(1024 < 端口 < 65535)向服务器 80 端口发起 TCP 连接请求(注:HTTP 默认约定 80 端口,HTTPS 为 443 端口)。这个连接请求到达服务端后,通过 TCP 三次握手,建立 TCP 的连接。

3.1 分层模型

    ----------------------------------
7| 应用层 | | HTTP |

6| 表示层 | 应用层 |

5| 会话层 | | |
---------------------------------
4| 传输层 | 传输层 | TCP TLS |
---------------------------------
3| 网络层 | 网络层 | IP |
---------------------------------
2| 数据链路层
| 链路层
1| 物理层
--------------------------------
  [OSI] | [TCP/IP]

3.2 TCP 三次握手

# SYN 是建立连接时的握手信号,TCP 中发送第一个 SYN 包的为客户端,接收的为服务端
# TCP 中,当发送端数据到达接收端时,接收端返回一个已收到消息的通知。这个消息叫做确认应答 ACK

假设有客户端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 04c8aa4..c788184 100644 --- a/book2/algorithm-reverse-linked-list.html +++ b/book2/algorithm-reverse-linked-list.html @@ -10,13 +10,13 @@ - +

反转链表

题目描述

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

反转链表

示例:


输入: 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 da0741f..b5a5b1b 100644 --- a/book2/browser-garbage.html +++ b/book2/browser-garbage.html @@ -10,13 +10,13 @@ - +

垃圾回收机制

相关问题

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

回答关键点

引用计数法 标记清除法 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 e7fec86..d0c073f 100644 --- a/book2/browser-render-mechanism.html +++ b/book2/browser-render-mechanism.html @@ -10,7 +10,7 @@ - + @@ -18,7 +18,7 @@

浏览器渲染机制

相关问题

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

回答关键点

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 304c667..da83902 100644 --- a/book2/coding-throttle-debounce.html +++ b/book2/coding-throttle-debounce.html @@ -10,13 +10,13 @@ - +

实现节流防抖函数

节流

1. 基本概念

throttle(func, wait)

每 wait 毫秒内最多只调用一次 func。

2. 应用场景

  • 搜索框输入时的实时联想。
  • 监听 scroll 事件计算位置信息。

3. 流程图

节流

4. 编写代码

function throttle(func, wait) {
let lastTime = 0;
let timer = null;

return function () {
if (timer) {
clearTimeout(timer);
timer = null;
}

let self = this;
let args = arguments;
let nowTime = +new Date();

const remainWaitTime = wait - (nowTime - lastTime);

if (remainWaitTime <= 0) {
lastTime = nowTime;
func.apply(self, args);
} else {
timer = setTimeout(function () {
lastTime = +new Date();
func.apply(self, args);
timer = null;
}, remainWaitTime);
}
};
}

防抖

1. 基本概念

debounce(func, wait)

自最近一次触发后延迟 wait 毫秒调用 func。

2. 应用场景

  • 注册时输入完用户名后检测是否被占用。
  • 监听 resize 事件计算尺寸信息。

3. 流程图

防抖

4. 编写代码

function debounce(func, wait) {
let timer = null;

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 6ceb833..39324d8 100644 --- a/book2/css-preprocessor.html +++ b/book2/css-preprocessor.html @@ -10,13 +10,13 @@ - +

谈谈 CSS 预处理器

相关问题

  • CSS 主要有哪些预处理器
  • 为什么需要用预处理器
  • 各预处理器优缺点

回答关键点

Sass Less Stylus PostCSS 工程化 提升效率

CSS 本身不属于可编程语言,当前端项目逐渐庞大之后 CSS 的维护也愈加困难。CSS 预处理器所做的本质上是为 CSS 增加一些可编程的特性,通过变量嵌套简单的程序逻辑计算函数等特性,通过工程化的手段让 CSS 更易维护,提升开发效率。

目前主流的 CSS 预处理器主要有 Sass、Less、Stylus、PostCSS。

知识点深入

1. PostCSS[1]

PostCSS 是目前最为流行的 CSS 预/后处理器。PostCSS 本体功能比较单一,它提供一种用 JavaScript 来处理 CSS 的方式。PostCSS 会把 CSS 解析成 AST(Abstract Syntax Tree 抽象语法树),之后由其他插件进行不同的处理。

功能

PostCSS 本体功能比较单一,大多数的 CSS 处理功能都由插件提供,下面是一些常用的插件:

优点

  • 插件系统完善,扩展性强。
  • 配合插件功能齐全。
  • 生态优秀。

缺点

  • 配置相对复杂。

2. Sass[2]

Sass 在完全兼容 CSS 语法的前提下,给 CSS 提供了变量、嵌套、混合、操作符、自定义函数等可编程能力。

功能

Sass 常用的有几种功能:

  • 变量:变量中可以存储颜色、字体或任何 CSS 值。
  • 嵌套:可嵌套 CSS 选择器,提供清晰的层次结构。
  • 混合:可以定义&重用代码块。
  • 扩展/集成:可以在一个选择器内继承另一个选择器。
  • 操作符:可以在 CSS 中使用操作符进行计算。
  • 条件/循环语句:可以循环/条件生成 CSS。
  • 自定义函数:可以自定义复杂操作的函数。

优点

  • 使用广泛。
  • 功能支持完善。
  • 可编程能力强。

缺点

  • CSS 的复杂度不可控。
  • node-sass 国内安装不易(非 Sass 本身的缺点,dart-sass 可代替)。

3. Less[3]

Less 和 Sass 类似,完全兼容 CSS 语法,并给 CSS 提供了变量、嵌套、混合、运算等可编程能力。Less 通过 JavaScript 实现,可在浏览器端直接使用。

功能

Less 常用的有几种功能:

  • 变量:变量中可以存储颜色、字体或任何 CSS 值。
  • 嵌套:可嵌套 CSS 选择器,提供清晰的层次结构。
  • 混合:可以定义&重用的代码块。
  • 扩展/集成:可以在一个选择器内继承另一个选择器。
  • 运算:可以在 CSS 中进行计算。
  • 条件/循环语句:可以循环/条件生成 CSS。

优点

  • 使用广泛。
  • 可以在浏览器中运行,容易实现主题定制功能。

缺点

  • 不支持自定义函数(可通过 mixins 实现简单逻辑)。
  • 编程能力相对较弱。

4. Stylus[4]

Stylus 基础功能和 Sass / Less 十分类似。Stylus 的特点是冒号、分号、逗号和括号都是可选项,所以可以写出非常简洁的 CSS,示例如下:

body
background-color: #000

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 0368040..677ec95 100644 --- a/book2/engineer-babel.html +++ b/book2/engineer-babel.html @@ -10,13 +10,13 @@ - +

Babel 的原理

相关问题

  • Babel 是什么
  • Babel 有什么用
  • 压缩代码如何实现

回答关键点

JS 编译器 AST 插件系统

Babel 是 JavaScript 编译器:他能让开发者在开发过程中,直接使用各类方言(如 TS、Flow、JSX)或新的语法特性,而不需要考虑运行环境,因为 Babel 可以做到按需转换为低版本支持的代码;Babel 内部原理是将 JS 代码转换为 AST,对 AST 应用各种插件进行处理,最终输出编译后的 JS 代码。

知识点深入

1. AST 抽象语法树

简单定义:以树的形式来表现编程语言的语法结构。

image

利用在线 playground 调试,可以对 AST 有个直观感受:生成的树有多个节点,节点有不同的类型,不同类型节点有不同的属性。

const custom = "HZFE";

image

AST 是源代码的高效表示,能便捷的表示大多数编程语言的结构。适用于做代码分析或转换等需求。之所以用树来进行分析或转换,是因为树能使得程序中的每一节点恰好被访问一次(前序或后序遍历)。

常见使用场景:代码压缩混淆功能可以借助 AST 来实现:分析 AST,基于各种规则进行优化(如 IF 语句优化;移除不可访问代码;移除 debugger 等),从而生成更小的 AST 树,最终输出精简的代码结果。

2. Babel 编译流程

三大步骤

image

  1. 解析阶段:Babel 默认使用 @babel/parser 将代码转换为 AST。解析一般分为两个阶段:词法分析和语法分析。

    • 词法分析:对输入的字符序列做标记化(tokenization)操作。
    • 语法分析:处理标记与标记之间的关系,最终形成一颗完整的 AST 结构。
  2. 转换阶段:Babel 使用 @babel/traverse 提供的方法对 AST 进行深度优先遍历,调用插件对关注节点的处理函数,按需对 AST 节点进行增删改操作。

  3. 生成阶段:Babel 默认使用 @babel/generator 将上一阶段处理后的 AST 转换为代码字符串。

3. Babel 插件系统

Babel 的核心模块 @babel/core,@babel/parser,@babel/traverse 和 @babel/generator 提供了完整的编译流程。而具体的转换逻辑需要插件来完成。

在使用 Babel 时,我们可通过配置文件指定 plugin 和 preset。而 preset 可以是 plugin 和 preset 以及其他配置的集合。Babel 会递归读取 preset,最终获取一个大的 plugins 数组,用于后续使用。

常见 presets

  • @babel/preset-env
  • @babel/preset-typescript
  • @babel/preset-react
  • @babel/preset-flow

最常见的 @babel/preset-env 预设,包含了一组最新浏览器已支持的 ES 语法特性,并且可以通过配置目标运行环境范围,自动按需引入插件。

编写 Babel 插件

Babel 插件的写法是借助访问者模式(Visitor Pattern)对关注的节点定义处理函数。参考一个简单 Babel 插件例子:

module.exports = function () {
return {
pre() {},
// 在 visitor 下挂载各种感兴趣的节点类型的监听方法
visitor: {
/**
* 对 Identify 类型的节点进行处理
* @param {NodePath} path
*/
Identifier(path) {
path.node.name = path.node.name.toUpperCase();
},
},
post() {},
};
};

使用该 Babel 插件的效果如下:

// input

// index.js
function hzfe() {}

// .babelrc
{
"plugins": ["babel-plugin-yourpluginname"]
}
// output
function 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 2b158f1..552fe74 100644 --- a/book2/frame-react-fiber.html +++ b/book2/frame-react-fiber.html @@ -10,7 +10,7 @@ - + @@ -18,7 +18,7 @@

React Fiber 的作用和原理

相关问题

  • Fiber 是什么
  • 谈谈你对 Fiber 的了解
  • Fiber 对 React 的使用带来了什么影响

回答关键点

调度 深度优先遍历

Fiber 是 React 16 中采用的新协调(reconciliation)引擎,主要目标是支持虚拟 DOM 的渐进式渲染。

Fiber 将原有的 Stack Reconciler 替换为 Fiber Reconciler,提高了复杂应用的可响应性和性能。主要通过以下方式达成目标:

  • 对大型复杂任务的分片。
  • 对任务划分优先级,优先调度高优先级的任务。
  • 调度过程中,可以对任务进行挂起、恢复、终止等操作。

Fiber 对现有代码的影响: 由于 Fiber 采用了全新的调度方式,任务的更新过程可能会被打断,这意味着在组件更新过程中,render 及其之前的生命周期函数可能会调用多次。因此,在下列生命周期函数中不应出现副作用。

  • shouldComponentUpdate
  • React 16 中已经声明废弃的钩子
    • componentWillMount(UNSAFE_componentWillMount)
    • componentWillReceiveProps(UNSAFE_componentWillReceiveProps)
    • componentWillUpdate(UNSAFE_componentWillUpdate)

知识点深入

1. React 是如何工作的

import React from "react";
import ReactDOM from "react-dom";

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(以 60Hz 频率的显示器为例,浏览器绘制一帧的最小时间间隔为 1/60s 约等于 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, 并且构建 workInProgress 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 604ceeb..3c4ad21 100644 --- a/book2/frame-react-hoc-hooks.html +++ b/book2/frame-react-hoc-hooks.html @@ -10,14 +10,14 @@ - +

HOC vs Render Props vs Hooks

相关问题

  • 什么是 HOC / Render Props / Hooks
  • 为什么需要 HOC / Render Props / Hooks
  • 如何提高代码复用性
  • Hooks 的实现原理
  • Hooks 相比其他方案有什么优势

回答关键点

复用性

HOC / Render Props / Hooks 三种写法都可以提高代码的复用性,但实现方法不同:HOC 是对传入的组件进行增强后,返回新的组件给开发者;Render Props 是指将一个返回 React 组件的函数,作为 prop 传给另一个 React 组件的共享代码的技术;Hooks 是 React 提供的一组 API,使开发者可以在不编写 class 的情况下使用 state 和其他 React 特性。

知识点深入

1. HOC (Higher Order Component,即高阶组件)

HOC 是 React 中复用代码的编程模式。具体来说,高阶组件是一个纯函数,它接收一个组件并返回一个新的组件。常见例子:React Redux 的 connect,将 Redux Store 和 React 组件联系起来。

// react-redux connect 例子
const ConnectedMyComponent = connect(mapState)(MyComponent);
// 实现一个简单的 HOC 例子
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
}

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)的规则,会造成项目臃肿和难以维护的问题。较早的方案是使用 HOC / Render Props 进行重构,然而这两种方案都会改变组件层级,容易形成“嵌套地狱”,使得代码的可读性下降。而 React Hooks 则很好地解决了这个问题。

方案优劣

为辅助理解,可参考以下图片。图中所示为下拉列表功能的三种不同实现,相比于使用一个 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 75df1ee..085fcca 100644 --- a/book2/js-inherite.html +++ b/book2/js-inherite.html @@ -10,14 +10,14 @@ - +

ES5、ES6 如何实现继承

相关问题

  • 关于 ES5 和 ES6 的继承问题
  • 原型链概念

回答关键点

原型链继承 构造函数继承 ES6 类继承

继承是指子类型具备父类型的属性和行为,使代码得以复用,做到设计上的分离。JavaScript 中的继承主要通过原型链和构造函数来实现。常见的继承方法有:ES6 中 class 的继承、原型链继承、寄生组合式继承等。

知识点深入

1. 原型链

原型链的本质是拓展原型搜索机制。每个实例对象都有一个私有属性 __proto__。该属性指向它的构造函数的原型对象 prototype。该原型对象的 __proto__ 也可以指向其他构造函数的 prototype。依次层层向上,直到一个对象的 __proto__ 指向 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或直到这个链表结束(Object.prototype.__proto__ === null)。

2. 原型链继承

原型链继承的思想:一个引用类型继承另一个引用类型的属性和方法

function SuperType() {
this.b = [1, 2, 3];
}

function SubType() {}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

var sub1 = new SubType();
var sub2 = new SubType();

// 这里对引用类型的数据进行操作
sub1.b.push(4);

console.log(sub1.b); // [1,2,3,4]
console.log(sub2.b); // [1,2,3,4]
console.log(sub1 instanceof SuperType); // true

优点:

  1. 父类新增原型方法/原型属性,子类都能访问到。
  2. 简单、易于实现。

缺点:

  1. 无法实现多继承。
  2. 由于原型中的引用值被共享,导致实例上的修改会直接影响到原型。
  3. 创建子类实例时,无法向父类构造函数传参。

3. 构造函数继承

构造函数继承的思想:子类型构造函数中调用父类的构造函数,使所有需要继承的属性都定义在实例对象上

function SuperType(name) {
this.name = name;
this.b = [1, 2, 3];
}

SuperType.prototype.say = function () {
console.log("HZFE");
};

function SubType(name) {
SuperType.call(this, name);
}

var sub1 = new SubType();
var sub2 = new SubType();

// 传递参数
var sub3 = new SubType("Hzfe");

sub1.say(); // 使用构造函数继承并没有访问到原型链,say 方法不能调用

console.log(sub3.name); // Hzfe

sub1.b.push(4);

// 解决了原型链继承中子类实例共享父类引用属性的问题
console.log(sub1.b); // [1,2,3,4]
console.log(sub2.b); // [1,2,3]
console.log(sub1 instanceof SuperType); // false

优点:

  1. 解决了原型链继承中子类实例共享父类引用属性的问题。
  2. 可以在子类型构造函数中向父类构造函数传递参数。
  3. 可以实现多继承(call 多个父类对象)。

缺点:

  1. 实例并不是父类的实例,只是子类的实例。
  2. 只能继承父类的实例属性和方法,不能继承原型属性和方法。
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。

4. 组合继承(伪经典继承)

组合继承的思想:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承

function SuperType(name) {
this.name = name;
this.a = "HZFE";
this.b = [1, 2, 3, 4];
}

SuperType.prototype.say = function () {
console.log("HZFE");
};

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

SubType.prototype = new SuperType(); // 第一次调用 SuperType
SubType.prototype.constructor = SubType;

优点:

  1. 可以继承实例属性/方法,也可以继承原型属性/方法。
  2. 不存在引用属性共享问题。
  3. 可传参
  4. 函数可复用

缺点:

  1. 调用了两次父类构造函数(耗内存),生成了两份实例。

5. 寄生组合式继承

寄生组合式继承的思想:借用构造函数来继承属性,使用混合式原型链继承方法

// 在函数内部,第一步创建父类原型的一个副本,第二部是为创建的副本添加 constructor 属性,
// 从而弥补因重写而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给予类型的原型。
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}

function SuperType(name) {
this.name = name;
}

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

function SubType(name, num) {
SuperType.call(this, name);
this.num = num;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayNum = function () {
console.log(this.num);
};

优点:

  1. 只调用了一次 SuperType 构造函数,避免了在 SubType.prototype 上创建不必要的属性。
  2. 能够正常使用 instanceof 和 isPrototypeOf()。

缺点:

  1. 实现较为复杂

6. ES6 中 class 的继承

ES6 中引入了 class 关键字, class 可以通过 extends 关键字实现继承,还可以通过 static 关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 需要注意的是:class 关键字只是原型的语法糖, JavaScript 继承仍然是基于原型实现的。

class Pet {
constructor(name, age) {
this.name = name;
this.age = age;
}

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 22cac29..108b510 100644 --- a/book2/js-new.html +++ b/book2/js-new.html @@ -10,13 +10,13 @@ - +

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 4a3d432..35d7dff 100644 --- a/book2/network-http-cache.html +++ b/book2/network-http-cache.html @@ -10,13 +10,13 @@ - +

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 5325a47..3d0b116 100644 --- a/book2/topic-multi-pics-site-optimize.html +++ b/book2/topic-multi-pics-site-optimize.html @@ -10,13 +10,13 @@ - +

多图站点性能优化

回答关键点

图片优化 传输优化 加载策略

提高网站性能的一项重要指标是提高访问速度,这与用户留存率和转换率呈正相关。根据 HTTPArchive 的数据可知,图像是大多数网站需求最多的资源类型,通常比其他资源占用更多带宽。在多图站点中,图片资源对于页面的加载和整体的用户体验有更明显的影响。最常见的问题是图片加载慢。对应的优化策略包括:

  • 图片优化:进行图片压缩/缩放和选择正确的图片格式。
  • 网络传输优化:使用 HTTP/2 和 CDN 服务。
  • 图片加载策略优化:按需使用懒加载、预加载,响应式图片加载等策略。

知识点深入

1. 图片优化

1.1 选择合适的图片格式

为控制篇幅,以下提到的图片格式,为截止至 2021 年 8 月,市场份额大于 0.1% 的格式。

  1. JPEG 的压缩效率高,是一种高效且轻巧的有损压缩图片格式。但不适合对矢量或对比度强的图像压缩,会有明显的图片质量下降。超过一定的压缩阈值,压缩的图像也会出现明显的图片质量下降。

  2. PNG 是一种无损压缩的高保真图片格式。相比 JPEG 有更强色彩表现力,且支持透明通道。

  3. GIF 是一种最多支持 256 种颜色的 8 位无损图片格式,支持动图。

  4. WebP 是一种同时提供有损压缩与无损压缩的图片格式。不仅支持透明图片,有优秀的色彩表现,也能支持动画。支持无损压缩且通常比 PNG 格式的相同图像小 26%。支持有损压缩且比视觉上相似压缩水平的 JPEG 图像平均小 25-35%。但是浏览器兼容性差。

  5. SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics)。适合非照片类型的图片的缩放或高保真场景。

图中所示为 2012 年 1 月至 2021 年 8 月的主流图片格式的使用趋势。

image

图片来源 w3techs.com

按需选择更高效的图片格式,不仅能提升用户视觉体验,也可以提升网站加载效率。在选用图片格式时,一般可以基于一些简单规则来筛选:在兼容性支持的情况下,可以选用 WebP,否则可以通过动图和透明度两个需求点来进行筛选:

  • 动图

    可以使用 GIF 或者是视频格式。前者的问题在于支持的色彩少,低帧率低分辨率,文件体积利用率低,而视频方案则可以避免这些问题。也可以使用 APNG,支持更多色彩的前提下,和对应 GIF 格式的文件体积相近。

  • 透明度

    PNG 和 GIF 都支持透明图片,可以按需使用。

在没有透明和动画需求的情况下,JPEG 格式图片胜任大部分场景,如果对图片的展示质量有较高要求时,可使用 PNG 格式图片。

绘制 LOGO、ICON 等非照片的图片内容时,一般使用 SVG 格式。比如 iconfont 等矢量图标管理平台中大量使用 SVG 格式。

1.2 图片压缩和缩放处理

由于实际应用场景的差异,对应图片的布局大小以及图片细节要求各有不同,大量未经压缩或缩放调整的图片会使网页加载许多不必要的字节,且对用户的视觉效果没有太大的提升。前端常见的压缩和缩放的处理方案包括:

  • 静态资源通过工具(比如 imagemin)按需进行有损或无损压缩。
  • 将用户上传的图片绘制到 Canvas 画布上,利用CanvasRenderingContext2D.drawImage(image, dx, dy, dWidth, dHeight) API 进行图片缩放;利用 HTMLCanvasElement.toDataURL(type, encoderOptions) API 进行有损压缩。
  • 根据用户侧的显示需求(如头像、缩略图、商品图等),通过对象存储服务(如七牛、阿里云 OSS)所提供的压缩或缩放等功能处理后返回使用。

2. 网络传输优化

2.1 使用 HTTP/2 协议

使用 HTTP/1.X 协议时,浏览器有同源最大并发连接数的限制,且 HTTP/1.X 不支持多路复用,因此一个多图站点想要获得较完整的视觉呈现,会有一定程度的延迟:所有的资源请求(包括图片资源)会进入先进先出(FIFO)队列等待被下载。

image

使用 HTTP/2 前的常见优化方案包括:

  • 使用精灵图 / 雪碧图,减少 HTTP 请求数。
  • 10kb 大小以内的图片资源使用 base64 编码,减少 HTTP 请求数。
  • 通过使用多个域名,开启多个 TCP 连接,突破浏览器同源最大并发连接数的限制。

由于 HTTP/2 支持多路复用,因此使用 HTTP/2 可以进一步减少网络延迟,更加快速的加载图片资源。

如下图所示,观察 Connection ID 一列可知,使用 HTTP/2 的情况下,资源重用同一条 TCP 连接,并发请求大量图片资源。

image

2.2 使用 CDN

CDN 将源站资源缓存到各加速节点后,用户请求源站资源时无需回源,可就近获取 CDN 节点上已缓存的资源,从而提高资源访问速度,分担源站压力。常见的 CDN 服务还支持对图片进行压缩、缩放、裁剪等图像处理功能。

3. 图片加载策略优化

3.1 图片懒加载

懒加载的策略是推迟加载离屏图片资源,从而减少资源请求数。实现懒加载的主流方案有:

  • 使用 img 标签的 loading 属性。
  • 使用 Intersection Observer API。
  • 使用 scroll、resize 和 orientationchange 事件。

后两种方案的实现原理是通过在 img 标签上添加 data-src 或其他自定义属性存放图片链接,而 src 属性不被设置或设置为占位图链接。通过 Intersection Observer 或 scroll 等 API 检测离屏图片是否滚动到预期位置,如果是则将 data-src 的值赋给 src 属性,从而达到懒加载的目的。

一般使用图片懒加载时,图片的占位处会使用各种方式来提升用户体验:

  • 色块 / 骨架屏占位。
  • LOGO 等品牌元素做默认图片。
  • 使用图片缩略图做模糊效果占位。

img loading

从 Chrome 76+ 版本起,开发者可以使用 loading 属性来推迟加载可通过滚动进入视口内的离屏图像。通过给 loading 属性设置 lazy 值,可以推迟加载资源,直到它与视口达到一定距离。caniuse.com 可查阅跨浏览器兼容性支持的详细信息。不支持 loading 属性的浏览器会忽略该属性,不会产生副作用。

<img src="image.png" loading="lazy" alt="" width="200" height="200" />

Intersection Observer

Intersection Observer API 可用于异步观察目标元素与祖先元素或与顶级文档视口的交叉点变化。

<img data-src="https://hzfe-blah.com/anyphoto1.jpg" />
<img data-src="https://hzfe-blah.com/anyphoto2.jpg" />
const config = {
/** any option */
};

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 时需要注意可能存在跨域问题。

// 动态创建 Image
function 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 cd89cf4..470ef80 100644 --- a/book3/algorithm-binary-tree-k.html +++ b/book3/algorithm-binary-tree-k.html @@ -10,13 +10,13 @@ - +

二叉搜索树的第 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 646eadf..ce8dedc 100644 --- a/book3/browser-event-loop.html +++ b/book3/browser-event-loop.html @@ -10,13 +10,13 @@ - +

浏览器事件循环

相关问题

  • 什么是浏览器事件循环
  • 浏览器为什么需要事件循环
  • 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. 任务与微任务

异步任务被分为两类:任务(任务队列中的任务,非微任务)与微任务(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 491b529..b9c4be8 100644 --- a/book3/browser-memory-leaks.html +++ b/book3/browser-memory-leaks.html @@ -10,13 +10,13 @@ - +

如何定位内存泄露

相关问题

  • 垃圾回收机制

回答关键点

垃圾回收 DevTools

内存泄漏是指不再使用的内存,没有被垃圾回收机制回收。当内存泄漏很大或足够频繁时,用户会有所感知:轻则影响应用性能,表现为迟缓卡顿;重则导致应用崩溃,表现为无法正常使用。为了避免内存泄漏带来的不良影响,需要对垃圾回收机制进行了解,掌握内存泄漏分析方法,完善线上相关监控措施。

内存泄漏定位和分析一般需要辅助工具,比如 Chrome DevTools。开发者可以通过 DevTools 记录页面活动概况,生成可视化分析结果,从时间轴中直观了解内存泄漏情况;利用 DevTools 获取若干次内存快照,检查内存堆栈变化;以及使用 Chrome 任务管理器,实时监控内存的使用情况。

知识点深入

1. 排查内存泄漏常见问题

在 JavaScript 中,当一些不再需要的数据仍然可达时,V8 会认为这些数据仍在被使用,不会释放内存。为了调试内存泄漏,我们需要找到被错误保留的数据,并确保 V8 能够将其清理掉。

代码量较小时,开发者通常可以基于以下基本原则进行快速自查:

  1. 是否滥用全局变量,没有手动回收。
  2. 是否没有正确销毁定时器、闭包。
  3. 是否没有正确监听事件和销毁事件。

除此之外,开发者可以借助外部工具进行内存泄漏排查。

2. 使用 Chrome DevTools 定位内存泄漏

Performance

image

打开准备分析的页面和 DevTools 的 Performance 面板,勾选 Memory 并开始录制,在模拟用户操作一段时间后结束录制,DevTools 会将这段时间内的页面行为活动进行记录和分析。

通过生成的结果可以直观查看到内存时间线,了解内存随时间的占用变化,如果内存占用曲线成阶梯状一直上升,则可能存在内存泄漏。按需选取时间线中的区域片段,检查对应时间段内的活动类型和时间占用,作为排查和定位内存泄漏的辅助办法。

Memory

image

打开准备分析的页面和 DevTools 的 Memory 面板,按需生成快照。每个快照的内容是快照时刻,进行一次垃圾回收后,应用中所有可达的对象。

当开发者明确知道与内存泄漏关联的用户交互步骤时,可以生成多次内存快照进行对比,排查出泄漏的对象:在做用户交互操作之前,进行一次正常内存堆栈信息的快照;在做用户交互操作中或操作结束时,进行内存快照。使用 Comparison 视图或使用 filter 按需查看快照之间的差异。

上面的图中使用 filter 检查快照 2 和快照 3 的差异,通过结果可知在两个快照之间持续被分配 clickCallback 闭包。通过点击文件路径可以定位到内存泄漏的代码。

image

3. Node.js 中的内存泄漏定位

如果需要定位 Node.js 中的内存泄漏,启动 Node.js 时带上 --inspect 参数,以便利用 Chrome DevTools 工具生成 Memory 快照数据。如图所示,启动 Node.js 服务后,打开 Chrome DevTools,会有 Node 标识,点击可以打开 Node 专用 DevTools。

image

除此之外,也可以借助第三方包 heapdump 生成快照文件,导入至 Chrome DevTools 中的 Memory 进行快照对比。

启动 Node.js 时带上 --expose-gc 参数以便调用 global.gc() 方法触发垃圾回收。借助 process.memoryUsage().heapUsed 检查内存大小,作为内存泄漏的辅助判断。

const heapdump = require("heapdump");

const capture = function () {
global.gc();
heapdump.writeSnapshot("./HZFE_HEAPSNAPSHOT/" + Date.now() + ".heapsnapshot");
console.log("heapUsed:", process.memoryUsage().heapUsed);
};

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 bc31966..29c5cd4 100644 --- a/book3/coding-arr-to-tree.html +++ b/book3/coding-arr-to-tree.html @@ -10,14 +10,14 @@ - +

将列表还原为树状结构

需求来源分析:

在需要存储树结构的情况下,一般由于使用的关系型数据库(如 MySQL),是以类似表格的扁平化方式存储数据。因此不会直接将树结构存储在数据库中,通常是通过邻接表、路径枚举、嵌套集或闭包表来存储。

其中,邻接表是最常用的方案之一,其存储模型如下:

idpiddata
10a
21b
31c

该模型代表了如下的树状结构:

{
id: 1,
pid: 0,
data: 'a',
children: [
{id: 2, pid: 1, data: 'b'},
{id: 3, pid: 1, data: 'c'},
]
}

大部分情况下,会交给应用程序来构造树结构。

典型题目

const list = [
{ pid: null, id: 1, data: "1" },
{ pid: 1, id: 2, data: "2-1" },
{ pid: 1, id: 3, data: "2-2" },
{ pid: 2, id: 4, data: "3-1" },
{ pid: 3, id: 5, data: "3-2" },
{ pid: 4, id: 6, data: "4-1" },
];

解法

解法一

递归解法:该方法简单易懂,从根节点出发,每一轮迭代找到 pid 为当前节点 id 的节点,作为当前节点的 children,递归进行。

function listToTree(
list,
pid = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
return list.reduce((root, item) => {
// 遍历每一项,如果该项与当前 pid 匹配,则递归构建该项的子树
if (item[pidName] === pid) {
const children = listToTree(list, item[idName]);
if (children.length) {
item[childName] = children;
}
return [...root, item];
}
return root;
}, []);
}

时间复杂度分析:最坏的情况下,这棵树退化为链表,且倒序排列。每一轮迭代需要在最后面才找到目标节点。假设有 n 个元素,那么总迭代次数为 n+(n-1) + (n-2) + ... + 1,时间复杂度为 O(n^2)。

解法二

迭代法:利用对象在 js 中是引用类型的原理。第一轮遍历将所有的项,将项的 id 与项自身在字典中建立映射,为后面的立即访问做好准备。 由于操作的每一项都是对象,结果集 root 中的每一项和字典中相同 id 对应的项实际上指向的是同一块数据。后续的遍历中,直接对字典进行操作,操作同时会反应到 root 中。

function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空间换时间,用于将所有项的 id 及自身记录到字典中
const root = [];

list.forEach((item) => {
record[item[idName]] = item; // 记录 id 与项的映射
item[childName] = [];
});

list.forEach((item) => {
if (item[pidName] === rootId) {
root.push(item);
} else {
// 由于持有的是引用,record 中相关元素的修改,会在反映在 root 中。
record[item[pidName]][childName].push(item);
}
});

return root;
}

record 字典 与 root 结果集的参考内存引用关系如图所示:

image

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

解法二变体

变体一

在解法二的基础上,将两轮迭代合并成一轮迭代。采用边迭代边构建的方式:

function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空间换时间,用于将所有项的 id 及自身记录到字典中
const root = [];

list.forEach((item) => {
const id = item[idName];
const parentId = item[pidName];

// 如果该项不在 record 中,则放入 record。如果该项已存在 (可能由别的项构建 pid 加入),则合并该项和已存在的数据
record[id] = !record[id] ? item : { ...item, ...record[id] };

const treeItem = record[id];

if (parentId === rootId) {
// 如果是根元素,则加入结果集
root.push(treeItem);
} else {
// 如果父元素不存在,则初始化父元素
if (!record[parentId]) {
record[parentId] = {};
}
// 如果父元素没有 children, 则初始化
if (!record[parentId][childName]) {
record[parentId][childName] = [];
}

record[parentId][childName].push(treeItem);
}
});

return root;
}

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

变体二

record 字典仅记录 id 与 children 的映射关系,代码更精简:

function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空间换时间,仅用于记录 children
const root = [];

list.forEach((item) => {
const newItem = Object.assign({}, item); // 如有需要,可以复制 item ,可以不影响 list 中原有的元素。
const id = newItem[idName];
const parentId = newItem[pidName];

// 如果当前 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 78be824..e422a12 100644 --- a/book3/css-mobile-adaptive.html +++ b/book3/css-mobile-adaptive.html @@ -10,13 +10,13 @@ - +

移动端自适应的常见手段

相关问题

  • 介绍 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 8fb5bf5..0be6086 100644 --- a/book3/engineer-webpack-loader.html +++ b/book3/engineer-webpack-loader.html @@ -10,13 +10,13 @@ - +

谈下 webpack loader 的机制

相关问题

  • webpack loader 是如何工作的
  • 如何编写 webpack loader

回答关键点

转换 生命周期 chunk

webpack 本身只能处理 JavaScript 和 JSON 文件,而 loader 为 webpack 添加了处理其他类型文件的能力。loader 将其他类型的文件转换成有效的 webpack modules(如 ESmodule、CommonJS、AMD),webpack 能消费这些模块,并将其添加到依赖关系图中。

loader 本质上是一个函数,该函数对接收到的内容进行转换,返回转换后的结果。

常见的 loader 有:

  • raw-loader:加载文件原始内容。
  • file-loader:将引用文件输出到目标文件夹中,在代码中通过相对路径引用输出的文件。
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式将文件内容注入到代码中。
  • babel-loader:将 ES 较新的语法转换为浏览器可以兼容的语法。
  • style-loader:将 CSS 代码注入到 JavaScript 中,通过 DOM 操作加载 CSS。
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。

使用 loader 的方式主要有两种:

  1. 在 webpack.config.js 文件中配置,通过在 module.rules 中使用 test 匹配要转换的文件类型,使用 use 指定要使用的 loader。
module.exports = {
module: {
rules: [{ test: /\.ts$/, use: "ts-loader" }],
},
};
  1. 内联使用
import Styles from "style-loader!css-loader?modules!./styles.css";

知识点深入

1. 编写 webpack loader

1.1 同步 loader

同步转换内容后,可以通过 return 或调用 this.callback 返回结果。

export default function loader(content, map, meta) {
return someSyncOperation(content);
}

通过 this.callback 可以返回除内容以外的其他信息(如 sourcemap)。

export default function loader(content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 时,始终返回 undefined
}

1.2 异步 loader

通过 this.async 可以获取异步操作的回调函数,并在回调函数中返回结果。

export default function (content, map, meta) {
const callback = this.async();
someAsyncOperation(content, (err, result, sourceMaps, meta) => {
if (err) return callback(err);
callback(null, result, sourceMaps, meta);
});
}

除非计算很小,否则对于 Node.js 这种单线程环境,尽可能使用异步 loader。

1.3 loader 开发辅助工具及 loaderContext

loader-utilsschema-utils,可以使获取及验证传递给 loader 的参数的工作简单化。

import { getOptions } from "loader-utils";
import { validate } from "schema-utils";

const schema = {
type: "object",
properties: {
test: {
type: "string",
},
},
};

export default function (source) {
const options = getOptions(this);

validate(schema, options, {
name: "Example Loader",
baseDataPath: "options",
});

// Apply some transformations to the source...

return `export default ${JSON.stringify(source)}`;
}

loader-utils 主要有以下工具方法:

  • parseQuery:解析 loader 的 query 参数,返回一个对象。
  • stringifyRequest:将请求的资源转换为可以在 loader 生成的代码中 require 或 import 使用的相对路径字符串,同时避免绝对路径导致重新计算 hash 值。
    loaderUtils.stringifyRequest(this, "./test.js");
    // "\"./test.js\""
  • urlToRequest:将请求的资源路径转换成 webpack 可以处理的形式。
    const url = "~path/to/module.js";
    const request = loaderUtils.urlToRequest(url); // "path/to/module.js"
  • interpolateName:对文件名模板进行插值。
    // loaderContext.resourcePath = "/absolute/path/to/app/js/hzfe.js"
    loaderUtils.interpolateName(loaderContext, "js/[hash].script.[ext]", { content: ... });
    // => js/9473fdd0d880a43c21b7778d34872157.script.js
  • getHashDigest:获取文件内容的 hash 值。

在编写 loader 的过程中,还可以利用 loaderContext 对象来获取 loader 的相关信息和进行一些高级的操作,常见的属性和方法有:

  • this.addDependency:加入一个文件,作为 loader 产生的结果的依赖,使其在有任何变化时可以被监听到,从而触发重新编译。
  • this.async:告诉 loader-runner 这个 loader 将会异步的执行回调。
  • this.cacheable:默认情况下,将 loader 的处理结果标记为可缓存。传入 false 可以关闭 loader 处理结果的缓存能力。
  • this.fs:用于访问 compilation 的 inputFileSystem 属性。
  • this.getOptions:提取 loader 的配置选项。从 webpack 5 开始,可以获取到 loader 上下文对象,用于替代 loader-utils 中的 getOptions 方法。
  • this.mode: webpack 的运行模式,可以是 "development" 或 "production"。
  • this.query:如果 loader 配置了 options 对象,则指向这个对象。如果 loader 没有 options,而是以 query 字符串作为参数,query 则是一个以 ? 开头的字符串。

以上内容是编写一个 loader 的关键点,想要学习更详细的关于编写 loader 的指导,可以参考官方的 guidelines

2. webpack loader 工作机制

2.1 根据 module.rules 解析 loader 加载规则

当 webpack 处理一个模块(module)时,会根据配置文件中 module.rules 的规则,使用 loader 处理对应资源,得到可供 webpack 使用的 JavaScript 模块。

根据具体的配置情况,loader 会有不同的类型,可以影响 loader 的执行顺序。具体类型如下所示:

rules: [
// pre 前置 loader
{ enforce: "pre", test: /\.js$/, loader: "eslint-loader" },
// normal loader
{ test: /\.js$/, loader: "babel-loader" },
// post 后置 loader
{ enforce: "post", test: /\.js$/, loader: "eslint-loader" },
];

以及内联使用的 inline loader:

import "style-loader!css-loader!sass-loader!./hzfe.scss";

在正常的执行流程中,这些不同类型的 loader 的执行顺序是:pre -> normal -> inline -> post。在下一节将会提到的 pitch 流程中,这些 loader 的执行顺序是反过来的:post -> inline -> normal -> pre

对于内联 loader,可以通知修饰前缀改变 loader 的执行顺序:

// ! 前缀会禁用 normal loader
import { HZFE } from "!./hzfe.js";
// -! 前缀会禁用 pre loader 和 normal loader
import { HZFE } from "-!./hzfe.js";
// !! 前缀会禁用 pre、normal 和 post loader
import { HZFE } from "!!./hzfe.js";

一般情况下,! 前缀和 inline loader 一起使用仅出现在 loader(如 style-loader)生成的代码中,webpack 官方不建议用户同时使用 inline loader 和 ! 前缀。

webpack rules 中配置的 loader 可以是多个链式串联的。在正常流程中,链式 loader 会按照从后往前的顺序执行。

  • 最后的 loader 最先执行,它接收的是资源文件(resource file)的内容。
  • 第一个 loader 最后执行,它将返回 JavaScript 模块和可选的 source map。
  • 位于中间的 loader,对接收和返回没有特定要求,只要能处理之前 loader 返回的内容,产出下一个 loader 能够理解的内容就可以。

2.2 loader-runner 的执行流程

webpack 调用 loader 的时机在触发 compilation 的 buildModule 钩子之后。webpack 会在 NormalModule.js 中,调用 runLoaders 运行 loader:

runLoaders(
{
resource: this.resource, // 资源文件的路径,可以有查询字符串。如:'./test.txt?query'
loaders: this.loaders, // loader 的路径。
context: loaderContext, // 传递给 loader 的上下文
processResource: (loaderContext, resourcePath, callback) => {
// 获取资源的方式,有 scheme 的文件通过 readResourceForScheme 读取,否则通过 fs.readFile 读取。
const resource = loaderContext.resource;
const scheme = getScheme(resource);
if (scheme) {
hooks.readResourceForScheme
.for(scheme)
.callAsync(resource, this, (err, result) => {
// ...
return callback(null, result);
});
} else {
loaderContext.addDependency(resourcePath);
fs.readFile(resourcePath, callback);
}
},
},
(err, result) => {
// 当 loader 转换完成后,会将结果返回到 webpack 中继续处理。
processResult(err, result.result);
}
);

runLoaders 函数来自 loader-runner 包。在介绍 runLoaders 的具体流程之前,先介绍一下 pitch 阶段,上一节中所讲的这种从后往前执行 loader 的流程,一般叫做 normal 阶段。与之相对的,还有一种叫做 pitch 阶段的流程。

一个 loader 如果在导出的函数的 pitch 属性上挂在了方法,那这个方法将在 pitch 阶段执行。pitch 阶段不同于 normal 阶段,pitch 阶段的执行顺序是从前往后的,整个流程类似浏览器事件模型或洋葱模型,pitch 阶段先从前往后执行 loader,然后再进入 normal 阶段从后往前执行 loader。注意,pitch 阶段一般不返回值,一旦 pitch 阶段有 loader 返回值,则从这里开始进入从后往前执行的 normal 阶段。

loader-runner 的具体流程如下:

  1. 处理从 webpack 接收的 context,继续添加必要的属性和辅助方法。

  2. iteratePitchingLoaders 处理 pitch loader。

    如果我们给一个 module 配置了三个 loader,每个 loader 都配置了 pitch 函数:

    module.exports = {
    //...
    module: {
    rules: [
    {
    //...
    use: ["a-loader", "b-loader", "c-loader"],
    },
    ],
    },
    };

    那么处理这个 module 的流程如下:

    |- a-loader `pitch`
    |- b-loader `pitch`
    |- c-loader `pitch`
    |- requested module is picked up as a dependency
    |- c-loader normal execution
    |- b-loader normal execution
    |- a-loader normal execution

    如果 b-loader 在 pitch 中提前返回了值,那么流程如下:

    |- a-loader `pitch`
    |- b-loader `pitch` returns a module
    |- a-loader normal execution
  3. iterateNormalLoaders 处理 normal loader。

    当 pitch loader 的流程处理完后,就来到了处理 normal loader 的流程。处理 normal loader 的流程和 pitch loader 相似,只是从后往前迭代。

    iterateNormalLoaders 和 iteratePitchingLoaders 都会调用 runSyncOrAsync 来执行 loader。runSyncOrAsync 会提供 context.async,这是一个返回 callback 的 async 函数,用于异步处理。

3. 常见 webpack loader 原理解析

loader 本身的操作并不复杂,就是一个负责转换其他资源到 JavaScript 模块的函数。

3.1 raw-loader 分析

该 loader 是功能非常简单的同步 loader,它的核心步骤是从文件原始内容中取得序列化的字符串,修复 JSON 序列化特殊字符时的 bug,添加导出语句,使其成为 JavaScript 模块。

该 loader 在 webpack 5 中已废弃,直接使用 asset modules 的功能代替即可。该 loader 源码如下:

import { getOptions } from "loader-utils";
import { validate } from "schema-utils";

import schema from "./options.json";

export default function rawLoader(source) {
const options = getOptions(this);

validate(schema, options, {
name: "Raw Loader",
baseDataPath: "options",
});

const json = JSON.stringify(source)
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");

const esModule =
typeof options.esModule !== "undefined" ? options.esModule : true;

return `${esModule ? "export default" : "module.exports ="} ${json};`;
}

3.2 babel-loader 分析

babel loader 是一个综合了同步和异步的 loader,在使用缓存配置时以异步模式运行,否则以同步方式运行。该 loader 的主要源码如下:

// imports ...
// ...

const transpile = function (source, options) {
// ...

let result;
try {
result = babel.transform(source, options);
} catch (error) {
// ...
}
// ...

return {
code: code,
map: map,
metadata: metadata,
};
};

// ...

module.exports = function (source, inputSourceMap) {
// ...

if (cacheDirectory) {
const callback = this.async();
return cache(
{
directory: cacheDirectory,
identifier: cacheIdentifier,
source: source,
options: options,
transform: transpile,
},
(err, { code, map, metadata } = {}) => {
if (err) return callback(err);

metadataSubscribers.forEach((s) => passMetadata(s, this, metadata));

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}",
"",
]);
// Exports
export 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 8953fec..36c0f66 100644 --- a/book3/frame-diff.html +++ b/book3/frame-diff.html @@ -10,13 +10,13 @@ - +

常见框架的 Diff 算法

相关问题

  • 虚拟 DOM 是什么
  • 虚拟 DOM 的作用
  • 讲一下 Vue 的 Diff 算法

回答关键点

虚拟 DOM 时间复杂度O(n)

现代网站大多具有复杂布局,大量的节点和交互操作等特征,直接操作 DOM 方法不当带来的性能问题不可忽视。虚拟 DOM 的本质是 JavaScript 对象,它可以代表 DOM 的一部分特征,是 DOM 的抽象简化版本。通过预先操作虚拟 DOM,在某个时机找出和真实 DOM 之间的差异部分并重新渲染,来提升操作真实 DOM 的性能和效率。

为达到这个目的,还需要关注两个问题:什么时候重新渲染,怎么高效选择重新渲染的范围。找出需要重新渲染的范围,就是 Diff 的过程。React 和 Vue 的 Diff 算法思路基本一致,只对同层节点进行比较,利用唯一标识符对节点进行区分。

知识点深入

1. Diff 算法

两棵树的比对和更新,涉及到树编辑距离(Tree Editing Distance)算法:将一棵树转化为另一棵树的最小操作成本。操作类型包括:删除、插入、修改。时间复杂度为 O(n^3)。

为了降低时间复杂度,React 和 Vue 的思路是基于以下两个假设条件,缩减递归迭代规模,将 Diff 算法的时间复杂度降低为 O(n):

  1. 相同类型的组件产生相同的 DOM 结构,反之亦然。所以不同类型组件的结构不需要进一步递归 Diff。
  2. 同一层级的一组节点,可以通过唯一标识符进行区分。

2. React Reconciliation

在 React 中,将虚拟 DOM 和真实 DOM 进行比对然后同步的过程被称为 Reconciliation(调和),Fiber 是 React 16 中新的调和引擎。它的主要目标是实现虚拟 DOM 的增量渲染。

Diff 的大致过程是,当对比两棵虚拟 DOM 树时,React 先对比根元素。依据根元素的类型不同,会有不同的操作:

  1. 不同类型的元素

    如果元素的类型不同,React 会抛弃旧树并建立新树。如以下情况,会导致完全重建:

    <!-- old -->
    <button class="bg-blue-100">HZFE</button>

    <!-- new -->
    <div class="bg-blue-100">HZFE</div>
  2. 相同类型的元素

    如果元素是两个相同类型的 React DOM 元素时,React 会查看两者的属性,保留 DOM 节点,只更新改变的属性。如以下情况,React 只更新颜色样式。

    <!-- old -->
    <button class="bg-blue-100 text-center">HZFE</button>

    <!-- new -->
    <button class="bg-red-100 text-center">HZFE</button>

在元素类型相同的情况下,比对完元素后,会递归元素的子元素。默认情况下,React 会同时迭代新老两个子元素列表。对于列表的更新,React 建议在列表项中标识 key 属性。避免以下低效场景:

<!-- bad -->
<!-- React 不会意识到可以保留<li>HZFE</li>和<li>Front-End</li>子树的完整,而是重写每个元素 -->

<!-- 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 e158c81..e990bf2 100644 --- a/book3/frame-react-hooks.html +++ b/book3/frame-react-hooks.html @@ -10,13 +10,13 @@ - +

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 属性上的循环链表记住所有的更新操作,并在 update 阶段依次执行循环链表中的所有更新操作,最终拿到最新的 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 a3a4a20..f31f6df 100644 --- a/book3/js-async.html +++ b/book3/js-async.html @@ -10,13 +10,13 @@ - +

JavaScript 异步编程

相关问题

  • JavaScript 异步编程方案有哪些
  • JavaScript 异步编程方案各有什么优缺点

回答关键点

阻塞 事件循环 回调函数

JavaScript 是一种同步的、阻塞的、单线程的语言,一次只能执行一个任务。但浏览器定义了非同步的 Web APIs,将回调函数插入到事件循环,实现异步任务的非阻塞执行。常见的异步方案有异步回调、定时器、发布/订阅模式、Promise、生成器 Generator、async/await 以及 Web Worker。

知识点深入

1. 异步回调

异步回调函数作为参数传递给在后台执行的其他函数。当后台运行的代码结束,就调用回调函数,通知工作已经完成。具体示例如下:

// 第一个参数是监听的事件类型,第二个就是事件发生时调用的回调函数。
btn.addEventListener("click", () => {
console.log("You clicked me!");

const pElem = document.createElement("p");
pElem.textContent = "hello, hzfe.";
document.body.appendChild(pElem);
});

异步回调是编写和处理 JavaScript 异步逻辑的最常用方式,也是最基础的异步模式。但是随着 JavaScript 的发展,异步回调的问题也不容忽视:

  1. 回调表达异步流程的方式是非线性的,非顺序的,理解成本较高。
  2. 回调会受到控制反转的影响。因为回调的控制权在第三方(如 Ajax),由第三方来调用回调函数,无法确定调用是否符合预期。
  3. 多层嵌套回调会产生回调地狱(callback hell)。

2. 定时器:setTimeout/setInterval/requestAnimationFrame

这三个都可以用异步方式运行代码。主要特征如下:

  1. setTimeout:经过任意时间后运行函数,递归 setTimeout 在 JavaScript 线程不阻塞的情况下可保证执行间隔相同
  2. setInterval:允许重复执行一个函数,并设置时间间隔,不能保证执行间隔相同
  3. requestAnimationFrame:以当前浏览器/系统的最佳帧速率重复且高效地运行函数的方法。一般用于处理动画效果。

setInterval 会按设定的时间间隔固定调用,其中 setInterval 里面的代码的执行时间也包含在内,所以实际间隔小于设定的时间间隔。而递归 setTimeout 是调用时才开始算时间,可以保证多次递归调用时的间隔相同。

如果当前 JavaScript 线程阻塞,轮到的 setInterval 无法执行,那么本次任务就会被丢弃。而 setTimeout 被阻塞后不会被丢弃,等到空闲时会继续执行,但无法保证执行间隔。

3. 发布/订阅模式(publish-subscribe pattern)

发布/订阅模式是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知。

上面异步回调的例子也是一个发布/订阅模式(publish-subscribe pattern)的实现。订阅 btn 的 click 事件,当 btn 被点击时向订阅者发送这个消息,执行对应的操作。

class PubSub {
constructor() {
// 存储所有订阅的事件类型及对应的订阅函数数组
// key <eventType>: value <subscribeList>[]
this.handlers = {};
}
// 订阅事件方法
on(eventType, handler) {
if (!(eventType in this.handlers)) this.handlers[eventType] = [];
this.handlers[eventType].push(handler);
}
// 消息发布方法
emit(eventType, ...handlerArgs) {
this.handlers[eventType].forEach((v) => {
v(...handlerArgs);
});
}
// 取消订阅
remove(eventType, handler) {
// 没有传入具体的事件处理函数,则移除该事件类型的所有订阅函数
// 有则在订阅数组中移除对应的函数
if (!handler) {
this.handlers[eventType].length = 0;
} else {
const key = this.handlers[eventType].findIndex((v) => v === handler);
if (key !== -1) this.handlers[eventType].splice(key, 1);
}
}
}

const test1 = new PubSub();
const fn1 = (...data) => console.log(data);
test1.on("event1", fn1);
test1.on("event1", (...data) => console.log(`fn2: ${data}`));
test1.emit("event1", "hzfe1", "hzfe2", "hzfe3");
test1.remove("event1", fn1);
// ["hzfe1", "hzfe2", "hzfe3"] fn1打印
// fn2: hzfe1,hzfe2,hzfe3

发布/订阅模式可以更细致地了解到有多少种事件类型以及每种类型对应的订阅事件,方便进一步的监听与控制。

4. Promise

Promise 提供了完成和拒绝两个状态来标识异步操作结果,通过 then 和 catch 可以分别对着两个状态进行跟踪处理。和事件监听的主要差别在于:

  1. 一个 Promise 只能成功或失败一次,一旦状态改变,就无法从成功切换到失败,反之亦然。
  2. 如果 Promise 成功或失败,那么即使在事件发生之后添加成功/失败回调,也将调用正确的回调。

Promise 使用顺序的方式来表达异步,将回调的控制权转交给了可以信任的 Promise.resolve(),同时也能够使用链式流的方式避免回调地狱的产生,解决了异步回调的问题。但 Promise 也有缺陷:

  1. 顺序错误处理:如果不设置回调函数,Promise 链中的错误很容易被忽略。
  2. 单决议:Promise 只能被决议一次(完成或拒绝),不能很好地支持多次触发的事件及数据流(支持的标准正在制定中)。
  3. 无法获取状态:处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
  4. 无法取消:一旦创建了 Promise 并注册了完成/拒绝函数,不能取消执行。

5. 生成器 Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法与传统函数完全不同,最大的特点就是可以控制函数的执行。简单示例如下:

function* helloHzfeGenerator() {
yield "hello";
yield "hzfe";
return "ending";
}

var hello = helloHzfeGenerator();

hello.next();
// { value: 'hello', done: false }

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 bc6e756..c2ae543 100644 --- a/book3/js-ts-interface-type.html +++ b/book3/js-ts-interface-type.html @@ -10,13 +10,13 @@ - +

TypeScript 中的 Interface 和 Type Alias

相关问题

  • Interface 和 Type Alias 的作用
  • Interface 和 Type Alias 的相同点
  • Interface 和 Type Alias 的区别

回答关键点

类型约束 扩展 类型合并

Interface 和 Type Alias(Type 别名,下文简称 Type)是 TypeScript 中两个非常重要且常用的概念。在程序设计中,Interface 和 Type 主要起到类型的限制和规范的作用,它们不关心实现细节,只规定和限制类或变量必须提供对应的属性和方法。

Interface 和 Type 核心的区别是 Type 不可在定义后重新添加内容,而 Interface 则总是可以扩展新内容。相比 Interface,Type 并没有实际创建一个新的类型,而是创建一个引用某个类型的名字。

知识点深入

总体来说,Interface 和 Type 两者非常相似,Interface 的特性大部分都可以使用 Type 实现,在大多数场景下都可以任意选择 Interface 或 Type 实现功能。根据这两者设计上的异同,可以总结出两者使用上的相同点和不同点。

1. Interface 和 Type 的相同点

1.1 可描述对象/函数

Interface 和 Type 都可以描述对象和函数。

// Interface
interface IHzfe {
name: string;
}
interface GetHZFE {
(): string;
}

// Type
type THzfe = {
name: string;
};
type GetHZFE = () => string;

1.2 可扩展

Interface 和 Type 都可以扩展类型。

// Interface
interface IHzfe {
name: string;
}
interface IShfe extends IHzfe {
location: string;
}

// Type
type THzfe = {
name: string;
};
type TShfe = THzfe & { location: string };

另外,Interface 的 extends 和 Type 的交叉类型有一些细微区别:extends 中的同名字段的类型必须是兼容的。而交叉类型中出现了同名字段且类型不同时,则类型一般是 never。

1.3 class Implements

Interface 和 Type 描述的类型都可以被 class 实现。

// Interface
interface IHzfe {
name: string;
}

// Type
type THzfe = {
name: string;
};

class HZFE1 implements IHzfe {
name = "HZFEStudio";
}
class HZFE2 implements THzfe {
name = "HZFEStudio";
}

2. Interface 和 Type 的不同点

2.1 基本类型别名、联合类型、元组

由于 Type 定义的实际是一个别名,所以 Type 可以描述一些基本类型、联合类型和元组的别名。

// 基本类型
type HZFEMember = number;

// 联合类型
type HZFEMemberTechStack = string | string[];

// 元组
type HZFEMember = [number, string];

2.2 声明合并

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

// Interface
interface 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 e210acf..f003aa1 100644 --- a/book3/network-http-1-2.html +++ b/book3/network-http-1-2.html @@ -10,13 +10,13 @@ - +

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 6bf6bf2..c1e344f 100644 --- a/book3/topic-white-screen-optimization.html +++ b/book3/topic-white-screen-optimization.html @@ -10,13 +10,13 @@ - +

如何减少白屏的时间

回答关键点

资源优化 预加载 服务端渲染 性能监控指标 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/book4/array-repeat-number.html b/book4/array-repeat-number.html index 0a8c6f8..0024622 100644 --- a/book4/array-repeat-number.html +++ b/book4/array-repeat-number.html @@ -10,13 +10,13 @@ - +

找到数组中重复的数字

题目描述

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例:

输入:[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

解法一: 哈希表

在线链接

从题目中分析,只需要找到任意一个重复的数字,那么可以利用哈希表,用数字的值作为 key 去存储,如果遇到已经存在的 key,则直接返回,这样就找到了重复的数字。

算法步骤

  1. 声明哈希表用来保存遍历的值。
  2. 开始遍历数组。
  3. 获取当前值,判断值是否已经存在,若存在,则返回结果并结束循环;若不存在,则继续下一步。
  4. 储存当前值进哈希表。
  5. 处理数组下一位,重复直到遍历完成。
/**
* @param {number[]} nums
* @return {number}
*/
const findRepeatNumber = function (nums) {
// 定义 hash 表
let map = new Map();
let i = 0;
while (i < nums.length) {
// 如果存在当前数字,那么直接结束循环,返回当前数字
if (map.has(nums[i])) {
return map.get(nums[i]);
}
// 储存已经遍历过的值
map.set(nums[i], nums[i]);
i++;
}
};

复杂度分析

  • 时间复杂度 O(N):遍历一遍数组既可完成。
  • 空间复杂度 O(N):需要声明哈希表进行存储。

解法二: 原地交换

在线链接

我们可以把数组的值,交换到与该值相等的索引的位置。如果交换时发现,该索引的位置已经存在与当前值相等的值,则结束操作并返回当前值。

算法步骤

  1. 遍历数组。
  2. 判断当前值和索引是否相等,如果是,直接跳过处理下一个元素,不是,则进行下一步。
  3. 判断需要交换的索引位置的值,是否和当前值相等,如果是,则返回当前值并结束,不是,则进行下一步。
  4. 将当前值和对应索引位置的元素进行交换。
  5. 依次处理完全部元素。
/**
* @param {number[]} nums
* @return {number}
*/
const findRepeatNumber = function (nums) {
let i = 0;
while (i < nums.length) {
// 如果当前值等于索引,那么直接跳过
if (nums[i] === i) {
i++;
continue;
}
// 如果需要交换的索引位置已经存在相同值,直接结束,返回当前值
if (nums[nums[i]] === nums[i]) {
return nums[i];
}
// 交换当前值,和对应索引存在的值
[nums[nums[i]], nums[i]] = [nums[i], nums[nums[i]]];
}
return -1;
};

复杂度分析

  • 时间复杂度 O(N):每个元素最多被移动 2 次。
  • 空间复杂度 O(1):不需要额外的空间完成。

参考资料

  1. 剑指 offer
Loading script...
- + \ No newline at end of file diff --git a/book4/browser-local-storage.html b/book4/browser-local-storage.html index 02699c7..c60ee6b 100644 --- a/book4/browser-local-storage.html +++ b/book4/browser-local-storage.html @@ -10,13 +10,13 @@ - +

本地存储方式及场景

相关问题

  • cookie 的优缺点
  • cookie、Web Storage 以及 IndexedDB 区别

回答关键点

cookie Web Storage IndexedDB

浏览器本地存储主要分为 cookie、Web Storage 以及 IndexedDB。其中 Web Storage 又可以分为 sessionStorage 和 localStorage。

  • cookie:为了辨别用户身份而储存在客户端上的数据(通常经过加密)。cookie 通常由服务端生成,客户端维护,主要用于维持用户的状态。cookie 会随请求发送给服务器。
  • Web Storage:以键值对的形式来存储数据,提供除 cookie 之外存储会话数据的途径。存储限制大于 cookie。
    • sessionStorage:可存储会话数据。
    • localStorage:同域之间,可跨会话的持久存储数据。
  • IndexedDB:能存储大量结构化数据,适用于高性能搜索场景。

知识点深入

cookie 通常由服务端生成,发送到客户端。客户端会将其存储起来,之后请求对应的服务端时带上。cookie 可以用于标识客户端,能够让使用无状态 HTTP 协议的服务器记住状态信息。

1.1 使用场景

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

1.2 相关属性

在收到 HTTP 请求时,服务端可以通过在响应头中增加 Set-Cookie 字段来告诉客户端存储对应的 cookie,前端也可以通过 JavaScript 来设置 cookie。之后向相同的服务端发送请求时,存储的 cookie 会作为请求头 Cookie 字段的值一起发送出去。可以通过不同属性的设置来让 cookie 拥有不同的特性:

  • Expires 属性用于设置过期时间,Max-Age 用于设置有效时间段,过期后 cookie 会被删除。
  • Secure 属性代表 cookie 只会随 HTTPS 请求发送。
  • HttpOnly 属性代表 cookie 只用于发送给服务端,无法被 JavaScript 访问。
  • Domain 属性设置可接收 cookie 的 hosts,不设置则默认为当前 host。如果设置了 Domain,子域名也被包含在内。
  • Path 属性设置可接收 cookie 的 URL path,只有包含指定路径的 url 请求才会带上 cookie。如设置为 “/”,则子路径也包含在内。
  • SameSite 属性表示跨域时 cookie 的处理策略,包括 Strict, LaxNone
    • Strcit:cookie 只会在第一方上下文中发送,不会与第三方网站发起的请求一起发送。
    • Lax:cookie 允许与顶级导航一起发送,并将与第三方网站发起的 GET 请求一起发送,这也是浏览器的默认值。
    • None:cookie 将在所有上下文中发送,即允许跨域发送。使用 None 时,需在最新的浏览器版本中同时使用 Secure 属性,否则会报错。

1.3 优点

  1. 简单易用。
  2. 不占用服务器资源。
  3. 可设置过期时间,提升安全性。

1.4 缺点

  1. cookie 会被添加在每个请求中,增加了流量消耗。
  2. cookie 在 HTTP 请求中是明文传输的,不够安全,使用 HTTPS 可避免该问题。
  3. cookie 大小限制在 4KB,复杂场景会不够用。

2. sessionStorage

sessionStorage 对象是当前源(和同源策略中的源一致,下同)下,存储会话数据的 Storage 实例。生命周期同当前页面保持一致,页面关闭时 sessionStorage 会被清空。sessionStorage 以键值对的方式存储数据,键值以字符串存储。

2.1 特点

  1. sessionStorage 只能被当前标签页访问。
  2. 页面 sessionStorage 的生命周期和浏览器行为相关:关闭浏览器或标签页清除 sessionStorage,刷新标签页或恢复浏览器页面时保留 sessionStorage。
  3. 在页面触发打开新页面时,会复制会话上下文作为新会话的上下文。
  4. 复制标签页(浏览器标签右键菜单中的复制,非复制 URL)时会复制当前 sessionStorage 到新的标签页中。

2.2 使用场景

sessionStorage 更适合用来存储生命周期和它同步的会话级别的信息。

3. localStorage

localStorage 同样也是获取当前源存储的 Storage 对象。存储的数据可以跨浏览器会话访问。

3.1 和 sessionStorage 区别

  1. localStorage 没有过期时间(隐私窗口中的 localStorage 在最后一个隐私窗口关闭时会被清空)。
  2. StorageEvent 只能监听同源页面的 localStorage 的改变,无法监听 sessionStorage 的改变。

3.2 使用场景

理论上 cookie 无法胜任的,可以用简单的键值对来存取的数据存储任务,都可以使用 localStorage 来处理。

4. IndexedDB

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括 File 和 Blob 对象)。该 API 使用索引实现对数据的高性能搜索,使用上接近于数据库。能够解决 Web Storage 在存储大量的结构化数据时存储容量小,搜索速度慢等问题。

4.1 特点

  1. 键值对存储
  2. 遵守同源策略
  3. 支持二进制存储:不仅可以存储字符串,也可以存储 File 或 Blob 对象。
  4. 同步与异步:IndexedDB 操作默认为异步操作。IndexedDB 也有用于 Web Worker 的同步 API,但因为不清楚是都需要目前已从规范中移除,有相关需求时可以引入。
  5. 存储空间大:IndexedDB 的最大存储空间是动态的,取决于硬盘大小,最大空间取决于浏览器的实现。

4.2 使用场景

  1. 存储数据量很大
  2. 存储数据为结构化数据
  3. 对数据搜索有性能要求

4.3 使用须知

IndexedDB 功能虽然很强大,但相关 API 使用起来相对来说比较复杂,需要一定的数据库开发经验,在应对简单场景时开发成本过高。

5. cookie、Web Storage(sessionStroage/localStorage) 以及 IndexedDB 区别

共同点:都是保存数据于浏览器端,遵循同源策略。

不同点:生命周期,存储空间最大值及与服务端交互方式。

特性cookiesessionStoragelocalStorageIndexedDB
生命周期可设置失效时间,默认为浏览器会话结束时清除 cookie页面会话结束时清除 sessionStorage持久存储永久保存
存储空间最大值4KB一般为 5MB同 sessionStorage取决于用户设备容量和浏览器限额设置
是否与服务端交互随请求发送给服务端,可设置多种属性控制保存在浏览器端,不与服务端交互同 sessionStorage保存在浏览器端,不与服务端交互

拓展阅读

1. Cache API

Cache API 为缓存的 Request/Response 对象提供存储机制。Cache API 和 workers 一样,是暴露在 Window 作用域下的,虽然被定义在 service worker 标准中,但是也可以脱离 service worker 单独使用。

1.1 特点

  1. Window 和 WorkerGlobalScope 都提供了 caches 对象,caches 提供了一系列异步方法,可以创建和操作 Cache 对象。
  2. 一个源可以有多个不同名称的 Cache 对象,同一域名下的 cacheName + Cache 由同一 name to cache map 管理。
  3. Cache 不能在不同域名之间共享,完全独立于浏览器的 HTTP cache,但同一域名下的 Window 对象和 ServiceWorker 对象可以共用。
  4. Cache 完全由开发者控制,增加、删除、更新等操作都需要由开发者去执行。由于浏览器都硬性限制了一个域下缓存数据的大小,需要定期清理缓存条目。

1.2 使用场景

ServiceWorker 或对离线体验要求较高的场景比较适合使用 Cache API。

1.3 使用须知

  1. Cache.put, Cache.add 和 Cache.addAll 只能在 GET 请求下使用。
  2. 自 Chrome 46 版本起,Cache API 不支持 HTTP 请求,只保存 HTTPS 请求。
  3. 匹配算法主要取决于缓存值中的 VARY Header,因此匹配新的 key 时需要查看缓存中条目的键和值。

参考资料

  1. Client-side storage
  2. HTTP cookie
  3. IndexedDB API
  4. Cache
Loading script...
- + \ No newline at end of file diff --git a/book4/browser-router.html b/book4/browser-router.html index a8fdf0c..fc20c9f 100644 --- a/book4/browser-router.html +++ b/book4/browser-router.html @@ -10,13 +10,13 @@ - +

前端路由实现

相关问题

  • 路由是什么
  • 前端路由的实现方式和实现原理
  • 前端路由和服务端路由的区别
  • 前端路由的优势
  • 前端路由的不足

回答关键点

路由 Hash 路由 History 路由 无刷新 SPA(Single-page application,单页面 Web 应用)

路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。 —— 维基百科

对于 Web 开发来说,路由的实质是 URL 到对应的处理程序的映射。

Web 路由既可以由服务端,也可以由前端实现。其中前端路由根据实现方式的不同,可以分为 Hash 路由History 路由

前端路由对于服务端路由来说,最显著的特点是页面可以在无刷新的情况下进行页面的切换。基于前端路由的这一特点,诞生了一种无刷新的单页应用开发模式 SPA。SPA 通过前端路由避免了页面的切换打断用户体验,让 Web 应用的体验更接近一个桌面应用程序。

知识点深入

前端路由根据具体实现方式的不同,可以分为 Hash 路由History 路由 两种,这两种实现方式各有其优势和局限性。

1. Hash 路由

一个 URI 的组成如下所示。其中的 fragment 部分就是 Hash 路由所读取的内容。

     foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
| _____________________|__
/ \ / \
urn:example:animal:ferret:nose

fragment 本质是用来标识次级资源,fragment 有以下特点:

  • 修改 fragment 的内容不会触发网页重载。
  • 修改 fragment 的内容会改变浏览器的历史记录。
  • 修改 fragment 的内容会触发浏览器的 onhashchange 事件。

基于 fragment 的以上特点,可实现基于 Hash 的前端路由。

1.1 实现原理

我们可以通过监听 hashchange 事件来监听页面 hash 的变化,通过解析 hash 的值来切换页面。示例如下:

/**
* 解析 hash
* @param hash
* @returns
*/
function parseHash(hash) {
// 去除 # 号
hash = hash.replace(/^#/, "");

// 简单解析示例
const parsed = hash.split("?");

// 返回 hash 的 path 和 query
return {
pathname: parsed[0],
search: parsed[1],
};
}

/**
* 监听 hash 变化
* @returns
*/
function onHashChange() {
// 解析 hash
const { pathname, search } = parseHash(location.hash);

// 切换页面内容
switch (pathname) {
case "/home":
document.body.innerHTML = `Hello ${search}`;
return;
default:
return;
}
}

window.addEventListener("hashchange", onHashChange);

1.2 优缺点

Hash 路由由于通过监听 hash 变化实现,所以有以下优势和不足:

优点

  1. 兼容性最佳。
  2. 无需服务端配置。

缺点

  1. 服务端无法获取 hash 部分内容。
  2. 可能和锚点功能冲突。
  3. SEO 不友好。

2. History 路由

Hash 路由是一个相对“Hack”的方式,利用了 fragment 来实现路由功能。而 History 路由则是通过浏览器原生提供的操作 History 的能力来实现的路由功能。

2.1 实现原理

History 路由核心主要依赖 History API 里的两个方法和一个事件,其中两个方法用于操作浏览器的历史记录,事件用于监听历史记录的切换:

方法

  • history.pushState:将给定的 Data 添加到当前标签页的历史记录栈中。
  • history.replaceState:将给定的 Data 更新到历史记录栈中最新的一条记录中。

事件

  • popstate:监听历史记录的变化。

通过以上 API 即可实现一个前端路由,示例如下:

/**
* 监听 history 变化
* @returns
*/
function onHistoryChange() {
// 解析 location
const { pathname, search } = location;

// 根据页面不同执行不同内容
switch (pathname) {
case "/home":
document.body.innerHTML = `Hello ${search.replace(/^\?/, "")}`;
return;
default:
document.body.innerHTML = `Hello World`;
return;
}
}

/**
* 页面跳转
* @returns
*/
function pushState(target) {
history.pushState(null, "", target);
onHistoryChange();
}

// 3 秒后路由跳转
setTimeout(() => {
pushState("/home?name=HZFEStudio");
}, 3000);

// 6 秒后返回
setTimeout(() => {
history.back();
}, 6000);

window.addEventListener("popstate", onHistoryChange);

2.2 优缺点

History 路由由于通过 History API 实现,所以有以下优势和不足:

优点

  1. 服务端可获取完整的链接和参数。
  2. 前端监控友好。
  3. SEO 相对 Hash 路由友好。

缺点

  1. 兼容性稍弱。
  2. 需要服务端额外配置(各 path 均指向同一个 HTML)。

3. 前端路由的优缺点

前端路由是前后端分离的开发模式的产物,对比服务端路由,前端路由的实现方式有以下优势和不足:

优点

  1. 无刷新切换内容,用户体验更佳。
  2. 减轻服务端压力。

缺点

  1. 初次加载耗时长。
  2. SEO 效果不佳。

参考资料

  1. Window: hashchange event
Loading script...
- + \ No newline at end of file diff --git a/book4/coding-apply-call-bind.html b/book4/coding-apply-call-bind.html index 5c1a88d..533153a 100644 --- a/book4/coding-apply-call-bind.html +++ b/book4/coding-apply-call-bind.html @@ -10,13 +10,13 @@ - +

实现 apply/call/bind

apply

1. 语法

func.apply(thisArg, [argsArray]);

参数

thisArg

在 func 函数运行时使用的 this 值。

argsArray

一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。

返回值

使用指定的 this 值和参数调用函数的结果。

2. 流程图

apply

3. 编写代码

Function.prototype.apply = function (thisArg, argsArray) {
if (typeof this !== "function") {
throw new TypeError(
"Function.prototype.apply was called on which is not a function"
);
}

if (thisArg === undefined || thisArg === null) {
thisArg = window;
} else {
thisArg = Object(thisArg);
}

// 将 func 放入 thisArg 内,这样在调用 thisArg[func] 时 this 自然就指向了 thisArg
const func = Symbol("func");
thisArg[func] = this;

let result;

if (argsArray && typeof argsArray === "object" && "length" in argsArray) {
// 此处使用 Array.from 包裹让其支持形如 { length: 1, 0: 1 } 这样的类数组对象,直接对 argsArray 展开将会执行出错
result = thisArg[func](...Array.from(argsArray));
} else if (argsArray === undefined || argsArray === null) {
result = thisArg[func]();
} else {
throw new TypeError("CreateListFromArrayLike called on non-object");
}

delete thisArg[func];

return result;
};

call

1. 语法

func.call(thisArg, arg1, arg2, ...)

参数

thisArg

在 func 函数运行时使用的 this 值。

arg1, arg2, ...

指定的参数列表。

返回值

使用指定的 this 值和参数调用函数的结果。

2. 流程图

call

3. 编写代码

Function.prototype.call = function (thisArg, ...argsArray) {
if (typeof this !== "function") {
throw new TypeError(
"Function.prototype.call was called on which is not a function"
);
}

if (thisArg === undefined || thisArg === null) {
thisArg = window;
} else {
thisArg = Object(thisArg);
}

// 将 func 放入 thisArg 内,这样在调用 thisArg[func] 时 this 自然就指向了 thisArg
const func = Symbol("func");
thisArg[func] = this;

let result;

if (argsArray.length) {
result = thisArg[func](...argsArray);
} else {
result = thisArg[func]();
}

delete thisArg[func];

return result;
};

bind

1. 语法

func.bind(thisArg[, arg1[, arg2[, ...]]])

参数

thisArg

调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用 new 运算符构造绑定函数,则忽略该值。

arg1, arg2, ...

当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值

返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

2. 流程图

bind

3. 编写代码

Function.prototype.bind = function (thisArg, ...argsArray) {
if (typeof this !== "function") {
throw new TypeError(
"Function.prototype.bind was called on which is not a function"
);
}

if (thisArg === undefined || thisArg === null) {
thisArg = window;
} else {
thisArg = Object(thisArg);
}

const func = this;

const bound = function (...boundArgsArray) {
let isNew = false;

// 如果 func 不是构造器,直接使用 instanceof 将出错,所以需要用 try...catch 包裹
try {
isNew = this instanceof func;
} catch (error) {}

return func.apply(isNew ? this : thisArg, argsArray.concat(boundArgsArray));
};

const Empty = function () {};
Empty.prototype = this.prototype;
bound.prototype = new Empty();

return bound;
};

参考资料

  1. Function.prototype.apply()
  2. Function.prototype.call()
  3. Function.prototype.bind()
Loading script...
- + \ No newline at end of file diff --git a/book4/css-vertical-horizontal-center.html b/book4/css-vertical-horizontal-center.html index a71ae4a..ebacfb8 100644 --- a/book4/css-vertical-horizontal-center.html +++ b/book4/css-vertical-horizontal-center.html @@ -10,13 +10,13 @@ - +

水平垂直居中方案

以下方案均基于如下布局和基础样式:

<div id="parent">
<div id="children">HZFE</div>
</div>
#parent {
background: red;
height: 600px;
}

#children {
background: blue;
}

flex

适用场景:子元素宽高不固定、子元素宽高固定

#parent {
display: flex;
justify-content: center;
align-items: center;
}

grid

适用场景:子元素宽高不固定、子元素宽高固定

#parent {
display: grid;
justify-content: center;
align-items: center;
}

相对定位

1. transform

适用场景:子元素宽高不固定、子元素宽高固定

#children {
display: inline-block;
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

适用场景:子元素宽高固定

#children {
width: 400px;
height: 300px;
position: relative;
top: 50%;
left: 50%;
transform: translate(-200px, -150px);
}

2. calc

适用场景:子元素宽高固定

#children {
width: 400px;
height: 300px;
position: relative;
top: calc(50% - 150px);
left: calc(50% - 200px);
}

绝对定位

1. transform

适用场景:子元素宽高不固定、子元素宽高固定

#parent {
position: relative;
}

#children {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

适用场景:子元素宽高固定

#parent {
position: relative;
}

#children {
width: 400px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-200px, -150px);
}

2. calc

适用场景:子元素宽高固定

#parent {
position: relative;
}

#children {
width: 400px;
height: 300px;
position: absolute;
top: calc(50% - 150px);
left: calc(50% - 200px);
}

3. 负外边距

适用场景:子元素宽高固定

#parent {
position: relative;
}

#children {
width: 400px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -150px;
margin-left: -200px;
}

4. 自动外边距

适用场景:子元素宽高固定

#parent {
position: relative;
}

#children {
width: 400px;
height: 300px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}

table-cell

适用场景:子元素宽高不固定、子元素宽高固定

#parent {
width: 800px;
display: table-cell;
text-align: center;
vertical-align: middle;
}

#children {
display: inline-block;
}

line-height

前提:父元素高度固定

适用场景:子元素宽高不固定、子元素宽高固定

#parent {
line-height: 600px;
text-align: center;
}

#children {
line-height: 1.5;
display: inline-block;
vertical-align: middle;
}
Loading script...
- + \ No newline at end of file diff --git a/book4/engineer-front-end-testing.html b/book4/engineer-front-end-testing.html index 574f4ef..0c4be2b 100644 --- a/book4/engineer-front-end-testing.html +++ b/book4/engineer-front-end-testing.html @@ -10,7 +10,7 @@ - + @@ -19,7 +19,7 @@ image

单元测试

是指对软件中独立单元(通常是一个函数、一个模块或一组紧密关联的功能集合)进行测试的方法。单元测试至少应该对模块暴露的公共接口进行测试,同时不应与代码的具体实现耦合得太紧密(如 Element 的 classname)。单元测试中经常会用到被称为 Mocks 或 Stubs 的 fake 实现,这可以使测试免于影响真实的系统,或者让测试运行得更快。

关于测试的 mock,一般有两种不同的意见:一派认为对当前测试点关注之外的东西都可以进行 mock,以实现完美的隔离,来达到避免副作用和进行复杂的测试环境设置的目的;另一派认为要尽量少的在测试中使用 mock,因为 mock 可能与真实的接口存在不一致的地方,而这很难察觉到。在实际的工作中,都会综合考虑这两派的意见,合理谨慎的运用 mock。通常测试覆盖率达到 100% 并不意味着软件就是绝对可靠的。

集成测试

一个完整的软件通常需要很多子系统配合工作。比如对后端来说,包含数据库、文件系统及其他网络调用;对前端来说,包含 UI 组件,数据仓库,网络请求等。正如单元测试难以精确界定一个单元的范围一样,集成测试在边界的确定中比单元测试更加模糊。集成测试一般是指对系统和外部功能的集成整体进行的测试。

另一方面,集成测试的含义通常比单元测试更模糊,如何确定测试的边界往往随着项目的具体情况而各有不同。集成测试中通常有着和单元测试重复的部分。有一种观点是认为可以用契约测试(Contract Tests)代替部分集成测试。

在现代化的组织架构中,随着软件的规模越来越大,开发部门也会分成各个规模较小的团队,各自负责相对独立的部分,构建高内聚、低耦合的服务。不同的服务之间通常是通过接口互相通信的,比如通过 HTTP 进行 REST 通信,使用 gRPC 来进行 RPC 通信,或者使用消息队列来进行通信。这时,可以考虑使用契约测试,来对各个子系统提供的接口进行测试。

E2E 测试

测试金字塔的最顶端一般是 E2E 测试,在进行测试时,一般有测试人员手动进行的测试,也有自动化的工具进行的测试。

E2E 测试中的一条用例会覆盖一条完整的用户操作流程。对于 web 程序来说,我们会测试用户的输入是否触发了正确的动作、数据是否呈现给了用户、UI 状态是否按照预期改变等等。

Web 应用程序的 E2E 测试通常是使用 WebDriver 来驱动实际的浏览器运行来进行测试的,这通常意味着 E2E 测试会很慢。所以我们在功能点能够被前面两种测试覆盖的时候,尽量采用它们来进行测试。

如何编写测试

最理想的状态是践行测试驱动开发,采用先红后绿的流程开发软件。这比写完功能代码后再补充测试要好,因为后补测试用例是在功能代码已经完成的情况下进行的,这也就是所谓的白盒测试。

白盒测试可能面临某些思维盲点(例如一旦从一堆不规则的图形中找出了五角星,后面再次看到时就很难再忽略它了),可能会导致测试覆盖的不够全面。白盒测试也可能过多的测试边缘情况,而忽略了业务主流程。 image

图片来源 像用户一样测试:打破知识诅咒

TDD 有四个关键点:

  1. 思考(Think):对业务需求进行拆解,将需求分解为有上下文、行为和预期结果的一个或多个任务列表,并保证列表中的每个任务是简单的、能快速实现的。
  2. 红(Red bar):从上一步的任务列表中挑选任务,为该任务快速的编写测试代码,然后不断循环的完成所有任务到测试用例的转换。此时,运行测试代码时的断言是会失败的。
  3. 绿(Green bar):以最快的速度让测试变绿,这通常意味着不需要考虑复杂的设计模式,不做过度设计,不写多余的代码,只需要符合一些简单的设计原则就行了。
  4. 重构(Refactor):在上面所有的测试都通过以后,现在可以对代码开始重构(识别代码中的坏味道(Code smell)、应用更健壮的设计模式等),而不必担心破坏之前的功能。
  5. 当准备好添加新的功能时,重新开始上面的循环。不断添加小的增量是实践 TDD 的关键。

测试的结构

对于所有的测试代码,一般都会遵循以下的结构。

  1. 设置测试数据。
  2. 调用测试方法。
  3. 断言期望的结果已经返回。

这个结构通常被记为 "Arrange, Act, Assert" 的 3A 短语。另外有一个来自 BDD(行为驱动开发)的一组短语是 "Given, When, Then"。

测试工具

单元测试工具

测试框架:常用的测试框架有 Jest,Mocha,Jasmine,AVA 等,大部分测试框架都提供了类似的功能,一般包括以下这些方面:

  • 测试块的组织,比如 describe 和 it/test 语法。
  • 每个 describe 块的 before[All],after[All],beforeEach,afterEach 钩子。
  • 全局的 setup 和 teardown 设置。
  • 匹配语法,各个框架一般拥有相似的接口,而这个风格一般继承自 Chai 这个框架。
  • Mock 功能的支持,对定时器的 mock、对函数的 mock、对模块的 mock。

被 Chai 发扬光大的三种匹配语法:

// Assert 经典语法,提供了对被测试对象的各个维度的断言语法。
assert.typeOf(foo, "string");
assert.equal(foo, "bar");
assert.lengthOf(foo, 3);
assert.property(tea, "flavors");
assert.lengthOf(tea.flavors, 3);

// 下面两种都是 BDD 风格的断言方式,它们都使用了更接近自然表达的语法来进行匹配。
// 在 node.js 和现代浏览器中,它们几乎没有区别。
// Should 采用了修改 Object 原型的方式实现到链式语法,这可能在古老的 IE 浏览器或某些嵌入式 JS 运行时中不能正常工作。

// Expect
expect(foo).to.be.a("string");
expect(foo).to.equal("bar");
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property("flavors").with.lengthOf(3);

// Should
foo.should.be.a("string");
foo.should.equal("bar");
foo.should.have.lengthOf(3);
tea.should.have.property("flavors").with.lengthOf(3);

其他拥有的测试工具:

  • 针对具体 UI 框架的测试插件,如针对 React 的 @testing-library/reactenzymereact-test-renderer 等。
  • 模拟 DOM 环境的 jsdom
  • 践行 BDD 的代表 Cucumber.js
  • 收集测试覆盖率的辅助工具 istanbul,或使用 v8 引擎内置的覆盖率功能的 c8

测试覆盖率收集原理:

istanbul.js 提供了名为 nyc 的工具,执行 nyc instrument <input> <output> 即可完成对代码的插桩。在代码实际运行时,插桩代码会统计源代码各部分的运行次数,来统计覆盖率。

例子:

greeting.js
function greeting(name = "HZFE") {
if (DEBUG) {
name = `${name} (DEBUG)`;
}

const foo = DEBUG ? "(DEBUG) foo" : "foo";

for (const i = 0; i < name.length; i++) {
name += "!";
}

return `Hello ${name}`;
}

经过 nyc instrument 后,大致代码如下:

output.js
function cov_g4lly5ec() {
// ...
var coverageData = {
path: "...",
statementMap: {
0: { start: { line: 2, column: 2 }, end: { line: 4, column: 3 } },
// ...
},
fnMap: {
0: {
name: "greeting",
decl: { start: { line: 1, column: 9 }, end: { line: 1, column: 17 } },
loc: { start: { line: 1, column: 33 }, end: { line: 13, column: 1 } },
line: 1,
},
},
branchMap: {
0: {
loc: { start: { line: 1, column: 18 }, end: { line: 1, column: 31 } },
type: "default-arg",
locations: [
{ start: { line: 1, column: 25 }, end: { line: 1, column: 31 } },
],
line: 1,
},
// ...
},
s: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 },
f: { 0: 0 },
b: { 0: [0], 1: [0, 0], 2: [0, 0] },
// ...
};
// ...
return actualCoverage;
}
cov_g4lly5ec();
function greeting(name = (cov_g4lly5ec().b[0][0]++, "HZFE")) {
cov_g4lly5ec().f[0]++;
cov_g4lly5ec().s[0]++;
if (DEBUG) {
cov_g4lly5ec().b[1][0]++;
cov_g4lly5ec().s[1]++;
name = `${name} (DEBUG)`;
} else {
cov_g4lly5ec().b[1][1]++;
}
const foo =
(cov_g4lly5ec().s[2]++,
DEBUG
? (cov_g4lly5ec().b[2][0]++, "(DEBUG) foo")
: (cov_g4lly5ec().b[2][1]++, "foo"));
cov_g4lly5ec().s[3]++;
for (const i = (cov_g4lly5ec().s[4]++, 0); i < name.length; i++) {
cov_g4lly5ec().s[5]++;
name += "!";
}
cov_g4lly5ec().s[6]++;
return `Hello ${name}`;
}

istanbul 在解析源代码时,会在函数体、默认参数、分支语句等各处位置插入记录执行次数的代码,在三元运算符等特殊位置会利用 JS 逗号运算符的特性插入统计代码。

例,在cov_g4lly5ec().b[1][0]++; 之中的 b 代表 branch(分支),1 代表第一个分支语句,1 后面紧跟的 0 代表分支的第一个叉路。依此类推,这些代码记录了 f(function),s(statement),b(branch) 的执行次数。后面的索引最终会解析到 statementMapfnMapbranchMap 中的对应项,映射到源代码中的具体位置。这样在执行完测试用例中,就能够知道源代码中每一处语法结构被运行的情况。

E2E 测试

常见工具有 Pupeteer、Nightwatch、TestCafe、Selenium、Cypress、Playwright 等。它们都是通过控制浏览器执行代码来模拟实际用户的操作,来验证 UI 的变化符合预期。E2E 测试的目的是为了尽量测试软件在真实环境下的表现,一般很少使用 mock。 基于测试在真实的浏览器下运行,和测试环境网络不稳定两个主要因素,E2E 测试通常要执行很长的时间,并且会较频繁的面临失败。在实际的持续集成环境中,一般会让 E2E 测试重试 2 到 3 次来尽可能的排除环境因素导致的失败。

在这些常用的工具中,目前使用比较广泛的三个是 Pupeteer,Cypress,Playwright。它们三个有比较突出的特点:

  • Pupeteer:Google 官方提供的测试工具,使用 Chrome DevTools Protocol 与 Chromium 系列的浏览器进行交互。所提供的 API 是比较底层的基本操作,通常需要自己封装常用的操作,但在具体的使用中相对灵活,限制较少。
  • Cypress: 高度集成的测试工具,自动等待异步操作,降低编码门槛。提供 Test Runner 可视化运行测试,记录测试步骤中的具体细节,可以查看测试代码中每一步所对应的 DOM 快照,便于 debug。基于 iframe 运行,测试代码和被测试应用处于同一运行环境,可以提高测试的可靠性。目前的限制是不支持多窗口,以及 iframe 在安全策略上的小部分无法规避的限制。
  • Playwright:微软开发的测试工具,支持除 Chromium 之外的 Firefox 和 Safari,拥有 Trace View 可以提供类似 Cypress 的强大 debug 体验。支持除了 JS/TS 以外的语言如 Python、.NET、Java,适用范围更广。

集成测试

一般情况下,集成测试所需使用的工具都涵盖在单元测试和 E2E 测试使用的工具之中了。

进行测试的最佳实践

  1. 测试应该足够快。只有测试能够快速运行得到反馈,测试人员才会乐意采用 TDD 的方式进行开发。反例是 E2E 测试,通常开发人员不会想要在本地频繁的运行 E2E 测试,因为这会浪费他们很多时间。使测试运行足够快,合理的配置测试框架,只运行改动相关的测试。
  2. 测试应该足够简单。如果测试代码写的非常复杂,也许会有疑问:“如何保证测试代码是正确的呢?”,那就可能需要对测试代码进行测试,所以在测试代码中,应该保证使用简单的代码,不要使用复杂、抽象度高的代码。对于确实需要使用复杂工具的场景,可以尝试使用久经考验的工具库。或者自己抽象一部分测试辅助工具代码,并对其进行测试。
  3. 测试应该具备良好的可读性。和上一条类似,保持简单的同时,也要保证良好的可读性。良好的可读性可以避免开发人员误解测试用例的意图,并可以最大限度发挥测试用例作为最佳文档的功能。虽然可读性是比较主观的感受,但也可以通过遵循下面的规则进行一定程度上的量化。
    • 使用 "Arrange, Act, Assert" 或 "Given, When, Then" 来组织测试代码。
    • 每个测试块中专注于一项功能的测试,不要将太多断言混合在一起。
    • 避免使用魔术数字和字符串,使用含义更清晰的常量取代。
  4. 测试代码中不要重复实现源代码中的逻辑。例如,用于与实际代码返回的 Received 对比的 Expected 应该是一个简单的列举出来的字面量,而不是通过和源代码类似的逻辑动态生成的结果。
  5. 测试必须是稳定的。测试用例的成功或失败只取决于被测试的代码,测试用例不因其他用例、环境因素、外部依赖的变化而改变结果。
  6. 不要将测试与实现细节耦合。提高测试用例稳定性,避免因为代码中微小的改动而导致测试用例失败。如,使用 contain 代替 equal 检查内容;使用 getByRole 代替 querySelector。

参考资料

Loading script...
- + \ No newline at end of file diff --git a/book4/engineer-mfa.html b/book4/engineer-mfa.html index 28f7b7e..e518e25 100644 --- a/book4/engineer-mfa.html +++ b/book4/engineer-mfa.html @@ -10,13 +10,13 @@ - +

谈谈微前端

相关问题

  • 为什么要用微前端
  • 微前端的优缺点

回答关键点

独立开发 独立运行 独立部署 自治

微前端是一种架构理念,它将较大的前端应用拆分为若干个可以独立交付的前端应用。这样的好处是每个应用大小及复杂度相对可控。在合理拆分应用的前提下,微前端能降低应用之间的耦合度,提升每个团队的自治能力。

目前市面有各类不同的微前端方案,但没有完美的解决方案。微前端方案通常需要考虑:应用加载机制、通信机制、代码隔离机制等问题。

知识点深入

微前端使用场景

前端工程化中,一个前端项目常以组件或模块的粒度进行代码拆分,然后通过 script 标签、npm 包、submodules 或者动态加载(Dynamic import)等形式将代码集成到项目中。而微前端则是以更大的粒度对代码进行上下文划分,将较庞大的应用拆分成多个技术栈独立的应用,再通过技术手段将若干应用集成在一个容器内。

如果项目中存在以下问题,可参考微前端架构进行优化:

  • 存量系统如何渐进式地拥抱新技术:存量系统的技术栈老旧,重构和开发成本高。在做新的功能开发时可以考虑采用与老项目不同的技术栈,通过微前端的方案将新的功能与老系统进行集成。同时微前端架构也给老旧系统的技术升级和平滑迁移提供保障。
  • 大型系统的开发及沟通成本上升:通过分析业务功能,将系统拆分成多个独立子系统,使每个子系统能独立开发、运行及部署。将工程复杂度拆分并限制在子系统单元内。避免随需求迭代,项目维护成本增大,跨部门沟通困难导致效率低下等问题。

微前端部分核心能力

  1. 路由管理

    一般我们使用 Hash 或者 History 模式来对路由进行监听,如 hashchange 或 popstate 事件。

    目前常见的微前端解决方案主要是路由驱动的。在微前端的基座,进行子应用的路由注册,如 { path: '/microA/*' } ,基座根据路由匹配情况,按需挂载子应用。具体路由跳转规则由子应用接管响应。

  2. 隔离机制

    支持样式隔离和 JS 沙箱机制,以保证应用之间的样式或全局变量、事件等互不干扰。在应用卸载时,应当对子应用中产生的事件、全局变量、样式表等进行卸载。

    对于新的项目,做好样式隔离的方式包括采用 CSS Module、CSS in JS 或规范使用命名空间等。对于已有项目的 CSS 隔离,可以在打包阶段利用工具(如 postcss)自动对样式添加前缀。

    实现 JS 沙箱机制可以借助 Proxy 和 with 的能力,分别做对 Window 对象的访问进行拦截和修改子应用作用域的操作。不支持 Proxy 的宿主环境,可以采用快照的思路:对进入子应用前的 Window 对象进行快照,用于后续卸载子应用时还原 Window 对象;在卸载子应用时对 Window 对象进行快照,用于后续再次加载子应用时还原 Window 对象。

  3. 消息通信

    合理划分应用,可以避免频繁的跨应用通信。同时应当避免子应用之间直接通信。

    常见的消息通信机制可以通过原生 CustomEvent 类实现,子应用通过 dispatchEvent 和 addEventListener 来对自定义事件进行下发和监听。除此之外,借助 props 通过主应用向子应用传参,达到通信目的也是常见方法。

  4. 依赖管理

    常见的微前端框架中,基座应用统一对子应用的状态进行管理。根据路由和子应用状态,按需触发生命周期函数,做请求加载、渲染、卸载等动作。而多个子应用间可能存在一些公共库的依赖。

    为减少这类资源的重复加载,通常可以借助 webpack5 的 Module Federation 在构建时进行公共依赖的配置,实现运行时依赖共享的能力。除了使用打包工具的能力,也可以从代码层面通过实现类 external 功能对公共依赖进行管理。

关联技术

Web Components

Web Components 允许开发者不借助框架,实现一些可重用的自定义组件。构建一个 Web Components 通常使用到 customElements、Shadow DOM 的 API,和 templates、slot 标签。

基于 Web Components 开发,可以天然契合微前端的一些特性:技术栈无关,应用间隔离。但兼容性较差,不支持 IE。

iframe

iframe 常用于将应用嵌入另一个宿主应用中。这其实已经是一种微前端的思维。只使用 iframe 方案引入子应用的好处是浏览器兼容性强,接入成本低,样式及脚本天然隔离。但是由于 iframe 和宿主应用完全隔离,各自独立运行,导致了诸多限制,如:

  1. 资源无法共享;
  2. iframe 中的 UI 无法跨越 iframe 窗口边界;
  3. 刷新页面时 iframe 中的路由状态丢失;

目前腾讯提供了一个新的微前端实施思路:借助 ShadowRoot 渲染子应用的 DOM;iframe 负责运行子应用的 JavaScript 代码,从而实现 JS 沙箱和 CSS 隔离能力。另外,在保证子应用和主应用同源的前提下,将子应用的路由变化同步到主应用中,从而保证刷新页面后,路由地址正常。

webpack5 Module Federation

目前市面上的微前端方案中,有基于 Module Federation 的微前端框架实践。Module Federation 是 webpack 提供的一个插件,他支持通过配置以下核心参数,在打包构建阶段确定集成策略:

  1. exposes 参数,指定应用可以描述当自己作为被加载的远程模块时,可暴露给其他应用使用的模块路径。
  2. remotes 参数,指定应用可以从远端加载的远程模块地址。
  3. shared 参数,指定应用可以与其他远程模块共享的依赖(精确到版本)。

参考资料

  1. micro-frontends
  2. single-spa
  3. qiankun
  4. garfish
  5. micro-app
Loading script...
- + \ No newline at end of file diff --git a/book4/frame-react-event-mechanism.html b/book4/frame-react-event-mechanism.html index 8bef8a9..d160d4d 100644 --- a/book4/frame-react-event-mechanism.html +++ b/book4/frame-react-event-mechanism.html @@ -10,13 +10,13 @@ - +

React 事件机制原理

相关问题

  • React 合成事件与原生 DOM 事件的区别
  • React 如何注册和触发事件
  • React 事件如何解决浏览器兼容问题

回答关键点

React 的事件处理机制可以分为两个阶段:初始化渲染时在 root 节点上注册原生事件;原生事件触发时模拟捕获、目标和冒泡阶段派发合成事件。通过这种机制,冒泡的原生事件类型最多在 root 节点上注册一次,节省内存开销。且 React 为不同类型的事件定义了不同的处理优先级,从而让用户代码及时响应高优先级的用户交互,提升用户体验。

React 的事件机制中依赖合成事件这个核心概念。合成事件在符合 W3C 规范定义的前提下,抹平浏览器之间的差异化表现。并且简化事件逻辑,对关联事件进行合成。如每当表单类型组件的值发生改变时,都会触发 onChange 事件,而 onChange 事件由 change、click、input、keydown、keyup 等原生事件组成。

知识点深入

1. 原生事件和合成事件

JavaScript 通过事件可以和 DOM 进行交互。

1.1 原生事件

主流浏览器基于 DOM2、DOM3 规范,实现标准化 DOM 事件。基于 Event 实现了浏览器中常见的用户事件如 UIEvent、InputEvent、MouseEvent 等。

在事件发生时,相关信息会存储在 Event 的实例对象中,对象包含 currentTarget、detail、target、preventDefault()、stopPropagation() 等属性和方法。DOM 节点可以通过 addEventListener 和 removeEventListener 来添加或移除事件监听函数。

// Event 属性
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
void preventDefault()
void stopPropagation()
void stopImmediatePropagation()
DOMEventTarget target
number timeStamp
string type

1.2 React 合成事件

React 的事件机制中,在遵循规范的前提下,引入新的事件类型:合成事件(SyntheticEvent)。基于合成事件实现了浏览器中常见的用户事件,并对事件进行规范化处理,使它们在不同浏览器中具有一致的属性。

在事件发生时,相关信息会存储在 SyntheticEvent 的实例对象中,对象包含原生事件对象类似的属性。

// SyntheticEvent 属性
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

但是合成事件与原生事件不是一一映射的关系。比如 onMouseEnter 合成事件映射原生 mouseout、mouseover 事件。React 通过 registrationNameDependencies 来记录合成事件和原生事件的映射关系:

/**
* Mapping from registration name to event name
*/
export const registrationNameDependencies = {
onClick: ["click"],
onMouseEnter: ["mouseout", "mouseover"],
onChange: [
"change",
"click",
"focusin",
"focusout",
"input",
"keydown",
"keyup",
"selectionchange",
],
// ...
};

2. React 事件机制

2.1 React 事件的注册

使用 ReactDOM.createRoot 创建 Root 时,React 会调用 listenToAllSupportedEvents 方法对所有支持的原生事件进行监听:

  1. allNativeEvents 用于收集所有合成事件相关联的原生事件名。这个收集动作在事件插件初始化阶段完成;
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
  1. 对每个原生事件调用 addTrappedEventListener 函数。该函数最终使用 addEventListener 方法,对原生事件进行捕获或冒泡阶段的事件监听注册。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);

// ...

if (isCapturePhaseListener) {
addEventCaptureListener(targetContainer, domEventName, listener);
} else {
addEventBubbleListener(targetContainer, domEventName, listener);
}
}

基于以上流程可知,调用 ReactDOM.createRoot 方法时,就已经在 root 节点上初始化所有原生事件的监听回调函数。而不是在组件上写合成事件的监听时,才开始注册事件回调。

2.2 React 事件的触发

在注册事件阶段调用的 addTrappedEventListener 方法中,会使用 createEventListenerWrapperWithPriority 函数来创建事件回调。createEventListenerWrapperWithPriority 函数根据事件类型,划分出若干个不同优先级的 dispathEvent。事件回调最终都调用进 dispatchEvent 方法。

因此触发一个原生事件时,大致的执行流程如下:

  1. 原生事件触发后,进入 dispatchEvent 回调方法;
  2. attemptToDispatchEvent 方法根据该原生事件查找到当前原生 Dom 节点和映射的 Fiber 节点;
  3. 事件和 Fiber 等信息被派发给插件系统进行处理,插件系统调用各插件暴露的 extractEvents 方法;
  4. accumulateSinglePhaseListeners 方法向上收集 Fiber 树上监听相关事件的其他回调函数,构造合成事件并加入到派发队列 dispatchQueue 中;
  5. 调用 processDispatchQueue 方法,基于捕获或冒泡阶段的标识,按倒序或顺序执行 dispatchQueue 中的方法;

参考资料

  1. SyntheticEvent
  2. Handling Events
  3. UI Events
Loading script...
- + \ No newline at end of file diff --git a/book4/frame-react-vs-vue.html b/book4/frame-react-vs-vue.html index afeb6ab..64e61d2 100644 --- a/book4/frame-react-vs-vue.html +++ b/book4/frame-react-vs-vue.html @@ -10,13 +10,13 @@ - +

谈谈 React 和 Vue 的区别

回答关键点

推模型与拉模型 模板与 JSX

React 和 Vue 同为现代化的 Web 前端开发框架。相同之处都是采用数据驱动视图的思想,以虚拟 DOM 为基础,以组件化的方式组织应用,让开发者无需关心 DOM 细节,从更高的层次设计应用。不同之处在于,具体组件的编写方式(template vs jsx),数据响应模型以及具体的生态。

知识点深入

1. 开发体验的区别

学习曲线

  • Vue:旨在降低前端开发门槛,学习曲线平缓,对了解 HTML、CSS 及 JS 的传统模式的前端开发和后端开发人员更友好。
  • React:传播自身的概念和思想,需要了解 JSX 的相关知识,组件中的一切都可以通过 JavaScript 灵活控制,上手成本相较于 Vue 来说略高。

JSX 与模板语法

  • Vue:默认使用基于 HTML 的模板语法,将模板、样式及逻辑划分开来使关注点分离。也可以选配 JSX 支持。
  • React:默认使用 JSX 编写组件,将 HTML 和 CSS 组合到 JavaScript 中。在选用 TypeScript 作为开发语言时,可以更方便的整合相关工具链。
  • 关于模板语法与 JSX 对比的一些举例:
    • 定制化的模板语法糖能够使编码更加简洁,相应的是稍微增加一点记忆成本。
    • JSX 写起来可能稍微繁琐一点,但其中的逻辑都是原生 JavaScript 控制的,在理解代码上,无需引入额外的概念。
    • Vue 单文件组件在拆分模板时,需要新建组件。
    • React 组件在拆分 JSX 时,可以灵活存在于变量或当前文件的新的组件中。

社区生态差异

  • Vue:中文生态繁荣。偏向全家桶式的集成解决方案,官方提供了开箱即用的 Vuex,Vue Router 组件,使开发者可以不必纠结于基础组件的选择。
  • React:将选择权交给开发者,并没有提供默认的组合模板,开发者可以自行组合社区中成熟的组件来构建应用,如 React Router,Redux,Mobx 等。

2. 核心概念的区别

数据响应模型

  • Vue 是推模型,当数据改动时,界面会自动更新。
    • Vue 通过 defineProperty 监听数据的改动,可以做到在数据改变时,精准细粒度的更新对应的视图。
  • React 是拉模型,当数据改动时,需要手动调用 setState 更新界面。
    • React 倾向于函数式编程,view = f(state),鼓励使用不可变数据,当 state 发生改变时,react 默认会使用浅比较来对比状态的差异,更具对比结果决定是否更新视图。
  • 这两种模型不是互斥的,在 React 中可以使用像 MobX 之类的库实现推模型;在 Vue 中可以 freeze 数据,或调用组件的更新方法实现拉模型。

参考资料

  1. Vue
  2. React
Loading script...
- + \ No newline at end of file diff --git a/book4/js-ts-generics.html b/book4/js-ts-generics.html index 193c998..d3a6ac4 100644 --- a/book4/js-ts-generics.html +++ b/book4/js-ts-generics.html @@ -10,13 +10,13 @@ - +

什么是 TypeScript 泛型

相关问题

  • TypeScript 泛型的作用是什么

回答关键点

工具 使用时指定类型

TypeScript 泛型是一种工具。它能让开发者不在定义时指定类型,而在使用时指定类型。

知识点深入

1. 泛型类

类型参数在类名后面的尖括号中指定。泛型类可以具有泛型字段或方法。

class HZFEMember<T, U> {
private id!: T;
private name!: U;

setMember(id: T, name: U): void {
this.id = id;
this.name = name;
}

show(): void {
console.log(`ID: ${this.id}, Name: ${this.name}`);
}
}

const member1 = new HZFEMember<number, string>();
member1.setMember(1, "QingZhen");
member1.show(); // ID: 1, Name: QingZhen

const member2 = new HZFEMember<string, string>();
member2.setMember("02", "Aki");
member2.show(); // ID: 02, Name: Aki

2. 泛型接口

interface HZFEMember<T, U> {
id: T;
name: U;
}

const member1: HZFEMember<number, string> = {
id: 1,
name: "QingZhen",
};
console.log(`ID: ${member1.id}, Name: ${member1.name}`); // ID: 1, Name: QingZhen

const member2: HZFEMember<string, string> = {
id: "02",
name: "Aki",
};
console.log(`ID: ${member2.id}, Name: ${member2.name}`); // ID: 02, Name: Aki

函数类型的泛型接口

interface ShowHZFEMember<T, U> {
(id: T, name: U): void;
}

const showHZFEMember: ShowHZFEMember<number, string> = function (id, name) {
console.log(`ID: ${id}, Name: ${name}`);
};
showHZFEMember(1, "QingZhen"); // ID: 1, Name: QingZhen

const showHZFEMember2: ShowHZFEMember<string, string> = function (id, name) {
console.log(`ID: ${id}, Name: ${name}`);
};
showHZFEMember2("02", "Aki"); // ID: 02, Name: Aki

3. 泛型约束

在下面的例子中访问 member 的 id 属性时,因为编译器并不能证明 member 中有 id 属性,所以会报错。

function getHZFEMember<T>(member: T): T {
console.log(`ID: ${member.id}`); // Property 'id' does not exist on type 'T'.
return member;
}

如果我们想要限制函数只能处理带有 id 属性的类型,就需要列出对于 T 的约束要求。我们可以定义一个接口来描述约束条件,创建一个包含  id 属性的接口,使用这个接口和 extends 关键字来实现约束。

interface Member {
id: number;
}

function getHZFEMember<T extends Member>(member: T): T {
console.log(`ID: ${member.id}`);
return member;
}

getHZFEMember("QingZhen"); // Argument of type 'string' is not assignable to parameter of type 'Member'.
getHZFEMember({ id: 1, name: "QingZhen" }); // ID: 1

4. 内置的工具类型

TypeScript 提供了一些内置的工具类型,本质上也是通过范型来实现的。下面,我们通过几种常用的类型来看看它们是怎么实现的。

4.1 Partial<Type>

通过将 Type 中的所有属性都设置为可选来构造一个新的类型。

interface Member {
id: number;
name: string;
age: number;
}
const hzfer: Member = {
id: 1,
name: "Qingzhen",
}; // Property 'age' is missing in type '{ id: number; name: string; }' but required in type 'Member'.

type HZFEMember = Partial<Member>;
const hzfer2: HZFEMember = {
id: 1,
name: "Qingzhen",
}; // No errors

源码:

type Partial<T> = {
[P in keyof T]?: T[P];
};

4.2 Required<Type>

通过将 Type 中的所有属性都设置为必选来构造一个新的类型。和 Partial 相反。

interface Member {
id: number;
name: string;
age?: number;
}
const hzfer: Member = {
id: 1,
name: "Qingzhen",
}; // No errors

type HZFEMember = Required<Member>;
const hzfer2: HZFEMember = {
id: 1,
name: "Qingzhen",
}; // Property 'age' is missing in type '{ id: number; name: string; }' but required in type 'Required<Member>'

源码:

type Required<T> = {
[P in keyof T]-?: T[P];
};

4.3 Exclude<UnionType, ExcludedMembers>

从联合类型 UnionType 中排除 ExcludedMembers 中的所有联合成员来构造一个新的类型。

type HZFEMemberProps = Exclude<"id" | "name" | "age", "age">;

const hzferProp: HZFEMemberProps = "age"; // Type '"age"' is not assignable to type 'HZFEMemberProps'.

源码:

type Exclude<T, U> = T extends U ? never : T;

4.4 Pick<Type, Keys>

从一个已有的类型 Type 中选择一组属性 Keys 来构造一个新的类型。

interface Member {
id: number;
name: string;
age: number;
}

type HZFEMember = Pick<Member, "id" | "name">;

const hzfer: HZFEMember = {
id: 1,
name: "QingZhen",
age: 18, // Object literal may only specify known properties, and 'age' does not exist in type 'HZFEMember'.
};

源码:

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

4.5 Omit<Type, Keys>

从一个已有的类型 Type 中移除一组属性 Keys 来构造一个新的类型。

interface Member {
id: number;
name: string;
age: number;
}

type HZFEMember = Omit<Member, "id" | "age">;

const hzfer: HZFEMember = {
id: 1, // Object literal may only specify known properties, and 'id' does not exist in type 'HZFEMember'.
name: "QingZhen",
};

源码:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

4.6 ReturnType<Type>

构造一个由函数的返回值的类型 Type 组成的类型。

interface GetHZFEMember {
(id: number): {
id: number;
name: string;
age: number;
};
}

type HZFEMember = ReturnType<GetHZFEMember>; // type HZFEMember = { id: number; name: string; age: number; };

源码:

type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;

4.7 infer

infer 表示在 extends 条件语句中待推断的类型变量。

上文的 ReturnType 源码中的 infer R 就代表了待推断的函数的返回值类型。

借助这个能力,我们可以通过 infer 来实现 tuple 转 union:

// 如果泛型参数 T 满足约束条件 Array<infer I>,那么就返回这个类型变量
type TypeOfArrayItem<T> = T extends Array<infer I> ? I : never;

type MyTuple = [string, number];

type MyUnion = TypeOfArrayItem<MyTuple>; // string | number

参考资料

  1. Generics
Loading script...
- + \ No newline at end of file diff --git a/guide.html b/guide.html index 5f316e4..3c50d63 100644 --- a/guide.html +++ b/guide.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/index.html b/index.html index 789855e..8f56bd2 100644 --- a/index.html +++ b/index.html @@ -10,13 +10,13 @@ - +
-

前言

写作背景

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

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

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

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

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

  1. 面试时间总是有限的

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

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

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

面试痛点

image

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

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

  1. 复习什么

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

  2. 怎么复习

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

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

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

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

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

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

写作理念

image

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

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

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

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

  • 提炼面试回答要点

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

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

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

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

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

适合人群

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

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

互动与勘误

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

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

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

Loading script...
- +

前言

写作背景

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

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

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

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

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

  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 4b6b90d..c4d63a1 100644 --- a/readme.md +++ b/readme.md @@ -2,11 +2,6 @@ [阅读地址 1](https://febook.hzfe.org/awesome-interview/) [阅读地址 2](https://hzfe.github.io/awesome-interview/) -## 友情链接 - -[code996](https://github.com/hellodigua/code996) | 分析工具:统计 Git 项目的 commit 时间分布,推导项目编码强度 - - ## 互动与勘误 本书目前在 [GitHub](https://github.com/hzfe/awesome-interview) 中开源了第一版内容的部分题目,旨在接受广大开发者的检验和收集读者反馈后,能将本书打磨得更好。 @@ -23,8 +18,16 @@ - 短时间内参加面试的前端开发者,可借助本书快速了解面试高频的技术问题和相关解答。 - 前端面试官可参考本书的题型和题目,按岗位需求对候选人进行有梯度的考察。 +## Install App + + + ## CHANGELOG +#### 2023/2/19 + +- 剑指前端 Offer 网站添加 PWA 支持 + #### 2022/7/04 - 新增:本地存储方式及场景 diff --git a/search.html b/search.html index 041597c..e896955 100644 --- a/search.html +++ b/search.html @@ -10,13 +10,13 @@ - +

Search the documentation

- + \ No newline at end of file diff --git a/sw.js b/sw.js index f312ac3..ad2dd34 100644 --- a/sw.js +++ b/sw.js @@ -4635,7 +4635,7 @@ function getPossibleURLs(url) { (async () => { const params = parseSwParams(); // eslint-disable-next-line no-underscore-dangle - const precacheManifest = [{"revision":"59b23b15e8dcd99db0171a6839c77227","url":"404.html"},{"revision":"75b5daa9087185fbfb97e8f6a3d60662","url":"about.html"},{"revision":"776c9e86c39e24b547df01d1d4b36f9b","url":"assets/css/styles.0f62048e.css"},{"revision":"6f87439d38c2a566a95bfc97655041db","url":"assets/js/02fa4020.d80415df.js"},{"revision":"2f79ae91dbb07ab811642d93c70c1446","url":"assets/js/02fefe41.b3f9381e.js"},{"revision":"a385af6bb4054e71154e10b9c3dc2c39","url":"assets/js/0ba2ede9.56884b73.js"},{"revision":"6e998951c64db7d233ae83b5b97a4d68","url":"assets/js/1a4e3797.10deaa27.js"},{"revision":"edb94e524df6e60a51a6e2adf52f9a8f","url":"assets/js/1be78505.418dd524.js"},{"revision":"86a1a102148e6b6666de9aa84c27d4c5","url":"assets/js/1f391b9e.a00364fc.js"},{"revision":"828e172ac79ea1697b3f48f3dfeeee87","url":"assets/js/216712ef.2205d07e.js"},{"revision":"d982ab73c08ebb4090e6031cd4a2e142","url":"assets/js/223d151b.a2893301.js"},{"revision":"b19810e15a2f1aaa729ed9f2005584cf","url":"assets/js/230.34dddfc8.js"},{"revision":"61084057e6d23ecc8b1a27dbfca4d219","url":"assets/js/2384.4861d8b1.js"},{"revision":"558d8b0d50ee9329d3d64ce0061898d9","url":"assets/js/26d83c4c.edef56f4.js"},{"revision":"8fbfc81731128a26b15f8a827135250f","url":"assets/js/272.a34d203d.js"},{"revision":"06116d3f7ec91900bbbd9031cac9b478","url":"assets/js/2ad5369a.582e9919.js"},{"revision":"1e7c7d5002e1d9298ec742a7bb0a85a2","url":"assets/js/312ed758.c38443de.js"},{"revision":"e101ef4f79765a428593ac86080b1b7d","url":"assets/js/31bf44dd.fe411772.js"},{"revision":"1f3add7f57be1592666facb8936979d2","url":"assets/js/3bd79dc4.81d1364b.js"},{"revision":"6b154d24663e7c6cb4692f1189c89057","url":"assets/js/3d604b8e.a7165cd6.js"},{"revision":"83d66b8194923238c960299b55d1742a","url":"assets/js/4972.bea3865f.js"},{"revision":"34a94c91fc8fb6a45d80266dcffdcfc5","url":"assets/js/4f33924b.6c95a067.js"},{"revision":"0996414e2c5077df19d04d12a3db1a67","url":"assets/js/505fc875.611070a6.js"},{"revision":"3a79eb5d1a9a71af47b92d67081ec7e7","url":"assets/js/5178.6dd866d0.js"},{"revision":"596b28cc12cdb8e96c0a53a18ebf0112","url":"assets/js/520898ab.4d2ef29d.js"},{"revision":"ccbb72b9cbaf17bb6f89fcefe34062b8","url":"assets/js/5283.79fe536c.js"},{"revision":"5383f211d9a440836aa01c75de178dfd","url":"assets/js/57076a74.a9e996b1.js"},{"revision":"adcee72417145ca58531206968cb267f","url":"assets/js/5825b5f0.c4348d51.js"},{"revision":"e8d7ad3b6c0dc921f7de3019770f7af9","url":"assets/js/59da24a9.b2082b98.js"},{"revision":"be48e4f0613e3075d6b2d9eaaebc63a1","url":"assets/js/5ba709b9.3ab20d87.js"},{"revision":"95406e0c2dfd685ba542bb2c3c9214c9","url":"assets/js/5cddde15.3da6324e.js"},{"revision":"1ef35c7363ff769b6d2bcceaf8625b65","url":"assets/js/605371c8.71cb6ac4.js"},{"revision":"baa72c374dfbf546b41a31542f133e7b","url":"assets/js/6511.4f41f0a7.js"},{"revision":"360f7b30ec3d9297368edf586750616d","url":"assets/js/667a1a38.0def7602.js"},{"revision":"6ebb9861d4ab76802afdd516036c4d92","url":"assets/js/6b15a8e7.370eb108.js"},{"revision":"4d2b54b35c1097403bd76c8b6334a210","url":"assets/js/6b1ae1c5.73c2607f.js"},{"revision":"0a026e337ff2ea06091af62e6df0065d","url":"assets/js/7c39e10e.72217821.js"},{"revision":"3e77a5027aef961c975257d784b5a0ad","url":"assets/js/8584d295.ebf32f7e.js"},{"revision":"9c6bedb9d0e774791394ca0fc82e1488","url":"assets/js/8624.ef1f3195.js"},{"revision":"769f601644d503e971031074e9f3ca77","url":"assets/js/8894.446f680f.js"},{"revision":"e5a8e4d9e4b9e9559f93a6a04b80db4a","url":"assets/js/90b799f4.6601a4ef.js"},{"revision":"e74cc26b891cb9e2495d2779890f4373","url":"assets/js/92d10100.a6f541cd.js"},{"revision":"f3f2e1af90de4aa471ea8752bffda0a5","url":"assets/js/935f2afb.ae96f8c3.js"},{"revision":"82f77285b83da7da6fe77403e321ad22","url":"assets/js/972d49dd.e496a32b.js"},{"revision":"62f08caf3e53aa732d366dcdae60dd71","url":"assets/js/99c95826.de80d85d.js"},{"revision":"c2a5c0a83112a7aa0ab57a05f9d7081b","url":"assets/js/9e3afa9a.0b52dd30.js"},{"revision":"c391ee5a86241307f4d276654fe04108","url":"assets/js/9f1c36eb.f2d83d86.js"},{"revision":"4a45137797444ed6ff10e6fa748d41f7","url":"assets/js/a757db9b.62dd77de.js"},{"revision":"71a44e48e3fa20d59b7f1b9f75673189","url":"assets/js/ab21f6e2.e3bd364c.js"},{"revision":"e3af48e6d7ec23e3e11b1f7bb648507d","url":"assets/js/abf449ea.b1986ef7.js"},{"revision":"a72248a398ba44283bda5ce6c1b87a0d","url":"assets/js/b728f6fe.bba33834.js"},{"revision":"2be40b0e3a10ee5f687b2454983f2bff","url":"assets/js/b948ee85.51069a20.js"},{"revision":"4883afa26b0c32173de8ebb499bc490f","url":"assets/js/bfa6c7fa.1eac726c.js"},{"revision":"d7316b0fc65e451be8754322c311d77e","url":"assets/js/c48aeec7.94e91701.js"},{"revision":"abf084d9952e831c35af93772fa8145f","url":"assets/js/d04fa17f.2a5decdb.js"},{"revision":"f63015d5964152980d6790579ba8e27a","url":"assets/js/d4358da1.05e70cb8.js"},{"revision":"77647d01447df3220ae3ee790d8d475b","url":"assets/js/d5444868.acde29ec.js"},{"revision":"26d22022367f0dcf914f6cb72084d38b","url":"assets/js/d9cd0856.cc532560.js"},{"revision":"cc7d9defd7e8a08462417dea9a0a7ee2","url":"assets/js/d9d15992.54894bba.js"},{"revision":"55bec37dffd1c8e9a51d720e6e43defb","url":"assets/js/da95f3d6.ff213c15.js"},{"revision":"a11eba8db69b0b0c304973a0d4e67869","url":"assets/js/dd5deefd.039b46a5.js"},{"revision":"de334fda2763dabd727e522f77112ed5","url":"assets/js/dfd24482.a8c9edca.js"},{"revision":"f0a56040db25e20293d1bff86d0cae01","url":"assets/js/e2f5eafd.57ff7439.js"},{"revision":"af2391d45349e5dceeed3c1006e18443","url":"assets/js/e31563f4.3b2fb9de.js"},{"revision":"92dc99d1e7611b40e23d565b67c38213","url":"assets/js/e3741bf5.5a137d01.js"},{"revision":"957df6955f6894bef6f9b49cc498d4d6","url":"assets/js/e420c2e8.d885ff56.js"},{"revision":"c3bcdf044143c75017862559a1e7bc95","url":"assets/js/e9fc5b99.babb79cb.js"},{"revision":"b83af5b875b3f809e90d05b079b65fe6","url":"assets/js/f4f9ee34.40b1280a.js"},{"revision":"ea41e3b09be1da766bbf3f2e295142f2","url":"assets/js/f930e7e8.1501d559.js"},{"revision":"a360838c269114d49b73898b23bef399","url":"assets/js/main.a5e14537.js"},{"revision":"a30621b1c623ee6afc3700c5a57e4d24","url":"assets/js/runtime~main.e165dbd3.js"},{"revision":"d0670e845b443c1719d37cae365a76f5","url":"book1/algorithm-balanced-binary-trees.html"},{"revision":"d474995b0bfabc600aa4dd726a0088a3","url":"book1/browser-cross-origin.html"},{"revision":"95f09b0067d6f6c21c89ddaec8be48ce","url":"book1/browser-repain-reflow.html"},{"revision":"264ff66b4e570d3b99576a5f8aeae2f4","url":"book1/coding-promise.html"},{"revision":"06e3b55475bd1e7dae622feabf264472","url":"book1/css-bfc.html"},{"revision":"5eff05c51aa86a315262785ef025fd89","url":"book1/engineer-webpack-workflow.html"},{"revision":"15e74d4fad43028ce0fae61d5f503ec2","url":"book1/frame-vue-computed-watch.html"},{"revision":"09b12a018ca460c1fe696d09aab4d921","url":"book1/frame-vue-data-binding.html"},{"revision":"a304aff6d3d3c2dad45141a138cb0102","url":"book1/js-closures.html"},{"revision":"393904323f8552f769f95ed8494c68eb","url":"book1/js-module-specs.html"},{"revision":"1ac10de24d7b7d6bd870d4dc42e4e136","url":"book1/network-security.html"},{"revision":"3c5d9b3c6a30264bc0494185489e163b","url":"book1/topic-enter-url-display-xx.html"},{"revision":"0a4fc4dc2fbaf171aa815de0651b489a","url":"book2/algorithm-reverse-linked-list.html"},{"revision":"549934598ed9b6f3c22ad84abadf57d7","url":"book2/browser-garbage.html"},{"revision":"1430173461d52ccdf6a12739452046c4","url":"book2/browser-render-mechanism.html"},{"revision":"3cc0362c4937942e380e60dbc3ba76a1","url":"book2/coding-throttle-debounce.html"},{"revision":"53d84edcf5890d278a5b827b52fb0a1b","url":"book2/css-preprocessor.html"},{"revision":"bd11cfb5768949910eefde8b2b5699ca","url":"book2/engineer-babel.html"},{"revision":"2774d7a7f9ebc4207bbb665dfea0221b","url":"book2/frame-react-fiber.html"},{"revision":"d20abb1563d69a18a89529ec2590e7a0","url":"book2/frame-react-hoc-hooks.html"},{"revision":"1483fa7b2327da3e3e1f5ad76f9e7c03","url":"book2/js-inherite.html"},{"revision":"f53cbe636c1b6579f803821aaaf0b0c4","url":"book2/js-new.html"},{"revision":"61d1c60970b3acebc53683408190a6b5","url":"book2/network-http-cache.html"},{"revision":"eb5b69f6dceec522b82ae42933ff3f0f","url":"book2/topic-multi-pics-site-optimize.html"},{"revision":"93737ed2d31850d7bc163b237c2f4868","url":"book3/algorithm-binary-tree-k.html"},{"revision":"5e471947ccd1a6ed2b06f7061eef95cc","url":"book3/browser-event-loop.html"},{"revision":"40317b43a55e74ad7820f6102c76a640","url":"book3/browser-memory-leaks.html"},{"revision":"0586eba2f92ea824df45d9c037e6c90b","url":"book3/coding-arr-to-tree.html"},{"revision":"ae246b2ef975a43ba67c95cd4914f9b1","url":"book3/css-mobile-adaptive.html"},{"revision":"515e4952a698f99d49315d5b85df7ef0","url":"book3/engineer-webpack-loader.html"},{"revision":"d40d41541b6318b8e5a9d10b7d9375ac","url":"book3/frame-diff.html"},{"revision":"17beaccf04d6521b989772d0cbb01b95","url":"book3/frame-react-hooks.html"},{"revision":"ad9f970c3cf099d7f7688ab0801e2f53","url":"book3/js-async.html"},{"revision":"1770d03d602817181a68f0a25072c296","url":"book3/js-ts-interface-type.html"},{"revision":"5700c473b13dbbb76b65cd153acf335a","url":"book3/network-http-1-2.html"},{"revision":"2f155d87ae65b4de3a0e9d4aa86c0e13","url":"book3/topic-white-screen-optimization.html"},{"revision":"4125adf332f22002ab29fb6c9d5cf41c","url":"book4/array-repeat-number.html"},{"revision":"89d835f308991c4deacb3a1374590634","url":"book4/browser-local-storage.html"},{"revision":"ca3c96c34032abe216a8b3abe0bc3d7a","url":"book4/browser-router.html"},{"revision":"4d0c36a77b67582db9176d20b3987601","url":"book4/coding-apply-call-bind.html"},{"revision":"350e64667ac932d640530f091ccd1dad","url":"book4/css-vertical-horizontal-center.html"},{"revision":"5f64726866bc6be92ab3477617596d33","url":"book4/engineer-front-end-testing.html"},{"revision":"2e93d1e89a72f0fe0fd08a03b266ae18","url":"book4/engineer-mfa.html"},{"revision":"82aafd177d3c64e3e0a25a518598f97b","url":"book4/frame-react-event-mechanism.html"},{"revision":"1b4c636466496de91fc6a9cc773be302","url":"book4/frame-react-vs-vue.html"},{"revision":"6fa85219a78b3ff0045ca09a4cb74d55","url":"book4/js-ts-generics.html"},{"revision":"be1b99f962d4057f7a74f0a7304819f5","url":"guide.html"},{"revision":"a96c15e5b9475d07c9f178fb8c692d52","url":"index.html"},{"revision":"0b5d0a30a72f4f57af332297e57475ca","url":"manifest.json"},{"revision":"3aa282690425ba124fb1999ec4f7eb5f","url":"search.html"},{"revision":"0216538e39d4956a3cfb020333aad016","url":"src_sw_js.sw.js"},{"revision":"dc93c7184b0b39c1f57d820c4171152e","url":"img/arrow.svg"},{"revision":"d731e2242601d1fb02e261706792f96a","url":"img/badge-192.svg"},{"revision":"f06f79f3429a1da4a30749f75f886537","url":"img/badge-512.svg"},{"revision":"37f1a3340231f571423d3f19295e346f","url":"img/badge.png"},{"revision":"f6935ea4945b35d2b2ba8bed3b73adc0","url":"img/badge.svg"},{"revision":"bdda44c6c2115ed183c6110ea9e85101","url":"img/favicon.ico"}]; + const precacheManifest = [{"revision":"bb4fc2b92cc96bee4c526a9173270ae9","url":"404.html"},{"revision":"be36cd897f96b1cd3b25b00ba160b90e","url":"about.html"},{"revision":"776c9e86c39e24b547df01d1d4b36f9b","url":"assets/css/styles.0f62048e.css"},{"revision":"6f87439d38c2a566a95bfc97655041db","url":"assets/js/02fa4020.d80415df.js"},{"revision":"2f79ae91dbb07ab811642d93c70c1446","url":"assets/js/02fefe41.b3f9381e.js"},{"revision":"a385af6bb4054e71154e10b9c3dc2c39","url":"assets/js/0ba2ede9.56884b73.js"},{"revision":"6e998951c64db7d233ae83b5b97a4d68","url":"assets/js/1a4e3797.10deaa27.js"},{"revision":"edb94e524df6e60a51a6e2adf52f9a8f","url":"assets/js/1be78505.418dd524.js"},{"revision":"86a1a102148e6b6666de9aa84c27d4c5","url":"assets/js/1f391b9e.a00364fc.js"},{"revision":"828e172ac79ea1697b3f48f3dfeeee87","url":"assets/js/216712ef.2205d07e.js"},{"revision":"d982ab73c08ebb4090e6031cd4a2e142","url":"assets/js/223d151b.a2893301.js"},{"revision":"b19810e15a2f1aaa729ed9f2005584cf","url":"assets/js/230.34dddfc8.js"},{"revision":"61084057e6d23ecc8b1a27dbfca4d219","url":"assets/js/2384.4861d8b1.js"},{"revision":"558d8b0d50ee9329d3d64ce0061898d9","url":"assets/js/26d83c4c.edef56f4.js"},{"revision":"8fbfc81731128a26b15f8a827135250f","url":"assets/js/272.a34d203d.js"},{"revision":"06116d3f7ec91900bbbd9031cac9b478","url":"assets/js/2ad5369a.582e9919.js"},{"revision":"1e7c7d5002e1d9298ec742a7bb0a85a2","url":"assets/js/312ed758.c38443de.js"},{"revision":"e101ef4f79765a428593ac86080b1b7d","url":"assets/js/31bf44dd.fe411772.js"},{"revision":"1f3add7f57be1592666facb8936979d2","url":"assets/js/3bd79dc4.81d1364b.js"},{"revision":"6b154d24663e7c6cb4692f1189c89057","url":"assets/js/3d604b8e.a7165cd6.js"},{"revision":"83d66b8194923238c960299b55d1742a","url":"assets/js/4972.bea3865f.js"},{"revision":"34a94c91fc8fb6a45d80266dcffdcfc5","url":"assets/js/4f33924b.6c95a067.js"},{"revision":"0996414e2c5077df19d04d12a3db1a67","url":"assets/js/505fc875.611070a6.js"},{"revision":"3a79eb5d1a9a71af47b92d67081ec7e7","url":"assets/js/5178.6dd866d0.js"},{"revision":"596b28cc12cdb8e96c0a53a18ebf0112","url":"assets/js/520898ab.4d2ef29d.js"},{"revision":"ccbb72b9cbaf17bb6f89fcefe34062b8","url":"assets/js/5283.79fe536c.js"},{"revision":"5383f211d9a440836aa01c75de178dfd","url":"assets/js/57076a74.a9e996b1.js"},{"revision":"adcee72417145ca58531206968cb267f","url":"assets/js/5825b5f0.c4348d51.js"},{"revision":"e8d7ad3b6c0dc921f7de3019770f7af9","url":"assets/js/59da24a9.b2082b98.js"},{"revision":"be48e4f0613e3075d6b2d9eaaebc63a1","url":"assets/js/5ba709b9.3ab20d87.js"},{"revision":"95406e0c2dfd685ba542bb2c3c9214c9","url":"assets/js/5cddde15.3da6324e.js"},{"revision":"1ef35c7363ff769b6d2bcceaf8625b65","url":"assets/js/605371c8.71cb6ac4.js"},{"revision":"baa72c374dfbf546b41a31542f133e7b","url":"assets/js/6511.4f41f0a7.js"},{"revision":"360f7b30ec3d9297368edf586750616d","url":"assets/js/667a1a38.0def7602.js"},{"revision":"6ebb9861d4ab76802afdd516036c4d92","url":"assets/js/6b15a8e7.370eb108.js"},{"revision":"4d2b54b35c1097403bd76c8b6334a210","url":"assets/js/6b1ae1c5.73c2607f.js"},{"revision":"0a026e337ff2ea06091af62e6df0065d","url":"assets/js/7c39e10e.72217821.js"},{"revision":"3e77a5027aef961c975257d784b5a0ad","url":"assets/js/8584d295.ebf32f7e.js"},{"revision":"9c6bedb9d0e774791394ca0fc82e1488","url":"assets/js/8624.ef1f3195.js"},{"revision":"769f601644d503e971031074e9f3ca77","url":"assets/js/8894.446f680f.js"},{"revision":"e5a8e4d9e4b9e9559f93a6a04b80db4a","url":"assets/js/90b799f4.6601a4ef.js"},{"revision":"e74cc26b891cb9e2495d2779890f4373","url":"assets/js/92d10100.a6f541cd.js"},{"revision":"f3f2e1af90de4aa471ea8752bffda0a5","url":"assets/js/935f2afb.ae96f8c3.js"},{"revision":"82f77285b83da7da6fe77403e321ad22","url":"assets/js/972d49dd.e496a32b.js"},{"revision":"62f08caf3e53aa732d366dcdae60dd71","url":"assets/js/99c95826.de80d85d.js"},{"revision":"c2a5c0a83112a7aa0ab57a05f9d7081b","url":"assets/js/9e3afa9a.0b52dd30.js"},{"revision":"c391ee5a86241307f4d276654fe04108","url":"assets/js/9f1c36eb.f2d83d86.js"},{"revision":"4a45137797444ed6ff10e6fa748d41f7","url":"assets/js/a757db9b.62dd77de.js"},{"revision":"71a44e48e3fa20d59b7f1b9f75673189","url":"assets/js/ab21f6e2.e3bd364c.js"},{"revision":"e3af48e6d7ec23e3e11b1f7bb648507d","url":"assets/js/abf449ea.b1986ef7.js"},{"revision":"a72248a398ba44283bda5ce6c1b87a0d","url":"assets/js/b728f6fe.bba33834.js"},{"revision":"2be40b0e3a10ee5f687b2454983f2bff","url":"assets/js/b948ee85.51069a20.js"},{"revision":"4883afa26b0c32173de8ebb499bc490f","url":"assets/js/bfa6c7fa.1eac726c.js"},{"revision":"d7316b0fc65e451be8754322c311d77e","url":"assets/js/c48aeec7.94e91701.js"},{"revision":"abf084d9952e831c35af93772fa8145f","url":"assets/js/d04fa17f.2a5decdb.js"},{"revision":"f63015d5964152980d6790579ba8e27a","url":"assets/js/d4358da1.05e70cb8.js"},{"revision":"77647d01447df3220ae3ee790d8d475b","url":"assets/js/d5444868.acde29ec.js"},{"revision":"26d22022367f0dcf914f6cb72084d38b","url":"assets/js/d9cd0856.cc532560.js"},{"revision":"cc7d9defd7e8a08462417dea9a0a7ee2","url":"assets/js/d9d15992.54894bba.js"},{"revision":"55bec37dffd1c8e9a51d720e6e43defb","url":"assets/js/da95f3d6.ff213c15.js"},{"revision":"a11eba8db69b0b0c304973a0d4e67869","url":"assets/js/dd5deefd.039b46a5.js"},{"revision":"de334fda2763dabd727e522f77112ed5","url":"assets/js/dfd24482.a8c9edca.js"},{"revision":"f0a56040db25e20293d1bff86d0cae01","url":"assets/js/e2f5eafd.57ff7439.js"},{"revision":"5991729a76a4d24e674b64b23661e5e3","url":"assets/js/e31563f4.d05d9934.js"},{"revision":"92dc99d1e7611b40e23d565b67c38213","url":"assets/js/e3741bf5.5a137d01.js"},{"revision":"957df6955f6894bef6f9b49cc498d4d6","url":"assets/js/e420c2e8.d885ff56.js"},{"revision":"c3bcdf044143c75017862559a1e7bc95","url":"assets/js/e9fc5b99.babb79cb.js"},{"revision":"b83af5b875b3f809e90d05b079b65fe6","url":"assets/js/f4f9ee34.40b1280a.js"},{"revision":"ea41e3b09be1da766bbf3f2e295142f2","url":"assets/js/f930e7e8.1501d559.js"},{"revision":"a360838c269114d49b73898b23bef399","url":"assets/js/main.a5e14537.js"},{"revision":"160aaf995cafec0c6647e09cf18d58a9","url":"assets/js/runtime~main.a75952d5.js"},{"revision":"c187cc7e91f72fc5ca442b137e71633b","url":"book1/algorithm-balanced-binary-trees.html"},{"revision":"7d285a1bc36008e0188ab0b8f6906914","url":"book1/browser-cross-origin.html"},{"revision":"062aa7ef6138de914d7bec4914634219","url":"book1/browser-repain-reflow.html"},{"revision":"f5804985c2009bcf0e9e49f138a7aa00","url":"book1/coding-promise.html"},{"revision":"9e0445c73865535fad7e1bfdfcee1e3f","url":"book1/css-bfc.html"},{"revision":"8b7e1eac124b55c28531138050b09f35","url":"book1/engineer-webpack-workflow.html"},{"revision":"84a995d9befbd3ac5aa64f92e51b44d7","url":"book1/frame-vue-computed-watch.html"},{"revision":"fee6fab49124f057d6b29613b99d8037","url":"book1/frame-vue-data-binding.html"},{"revision":"0867a103c0d09186a7833d9e7365c4a1","url":"book1/js-closures.html"},{"revision":"5e86688ea4c7e8fe40a69136673cd0d2","url":"book1/js-module-specs.html"},{"revision":"69a487526c2e24ccbed90bf8ad751e6c","url":"book1/network-security.html"},{"revision":"e257346db351a3ffab989d8adbce637f","url":"book1/topic-enter-url-display-xx.html"},{"revision":"8fd000e1e9f0fef21c7d7dc64c9acc1f","url":"book2/algorithm-reverse-linked-list.html"},{"revision":"ea1086e224977e181fd1fe264e442525","url":"book2/browser-garbage.html"},{"revision":"ef0e8e536857510474d5b91a1bee2a78","url":"book2/browser-render-mechanism.html"},{"revision":"d9410ea53f9864c707cadf536bb97351","url":"book2/coding-throttle-debounce.html"},{"revision":"00d8affff5280a56c8390843da38ce2a","url":"book2/css-preprocessor.html"},{"revision":"23e2f69a72936e4d1858ee9c05681da8","url":"book2/engineer-babel.html"},{"revision":"a332b0a20e61064f108eb12c24f38248","url":"book2/frame-react-fiber.html"},{"revision":"3be7c2b6c974cfd6d32cf902980678a9","url":"book2/frame-react-hoc-hooks.html"},{"revision":"73a604641beccec4d873644e0d0bf1b8","url":"book2/js-inherite.html"},{"revision":"c0ce7b6877a60b2e987afb186669378f","url":"book2/js-new.html"},{"revision":"7a3190fea90487d82e24d9c2c649c8a5","url":"book2/network-http-cache.html"},{"revision":"c308aea6735e1a962ae1c0754d8191fc","url":"book2/topic-multi-pics-site-optimize.html"},{"revision":"f2717bf79edf7dd8396231db15f8496d","url":"book3/algorithm-binary-tree-k.html"},{"revision":"dce4c1c74323e40b9cc58f92b312c0c2","url":"book3/browser-event-loop.html"},{"revision":"1af2ec13cf747ceaacc854797921e23f","url":"book3/browser-memory-leaks.html"},{"revision":"7525571b60a6c6c0962086bfc0e65ee5","url":"book3/coding-arr-to-tree.html"},{"revision":"241f7b9710fd27f343cedb1d08abf509","url":"book3/css-mobile-adaptive.html"},{"revision":"5f1d0c217b8fdbaee80199cbe661de69","url":"book3/engineer-webpack-loader.html"},{"revision":"d7136cd3df877df74f8a43b012ef2eca","url":"book3/frame-diff.html"},{"revision":"6f0225dc458c1f667184c28a9dd84d4e","url":"book3/frame-react-hooks.html"},{"revision":"f8aaa338d1ca8d3d7b43fc1357d6f6cf","url":"book3/js-async.html"},{"revision":"baa57c384ce1727c07809e5ad79a6ebf","url":"book3/js-ts-interface-type.html"},{"revision":"35959dfbc68229fdb4690979d7c36a43","url":"book3/network-http-1-2.html"},{"revision":"933fadefe99b1a6ea4b93e9ea454e0c5","url":"book3/topic-white-screen-optimization.html"},{"revision":"98f68f7bea5bb8530ea9160447a01272","url":"book4/array-repeat-number.html"},{"revision":"373e137bc40941e48e279924e80ce8ca","url":"book4/browser-local-storage.html"},{"revision":"eb0dd73d4c5e69a27ab34fe0ab445cbe","url":"book4/browser-router.html"},{"revision":"03bc406bc2a466ec999ed57562621679","url":"book4/coding-apply-call-bind.html"},{"revision":"727d71f8f4aa4950ed63bfb92975fb24","url":"book4/css-vertical-horizontal-center.html"},{"revision":"a09587a15d5f5cb0912f7585327ab8fc","url":"book4/engineer-front-end-testing.html"},{"revision":"c9eb1f62eadfdd2bb8002c97361cb47b","url":"book4/engineer-mfa.html"},{"revision":"571b903ebd103e977b8365fd2465072d","url":"book4/frame-react-event-mechanism.html"},{"revision":"b8334ca10ff46196acf8df5b7f94aadb","url":"book4/frame-react-vs-vue.html"},{"revision":"e227363afb48d6c728118b3b683bd446","url":"book4/js-ts-generics.html"},{"revision":"a6a435007a519567706523959240c3cf","url":"guide.html"},{"revision":"dc55e395dfce0fc16c03d67291306a7b","url":"index.html"},{"revision":"0b5d0a30a72f4f57af332297e57475ca","url":"manifest.json"},{"revision":"75204b2a25b7bc08cc6aab02b9155f32","url":"search.html"},{"revision":"0216538e39d4956a3cfb020333aad016","url":"src_sw_js.sw.js"},{"revision":"dc93c7184b0b39c1f57d820c4171152e","url":"img/arrow.svg"},{"revision":"d731e2242601d1fb02e261706792f96a","url":"img/badge-192.svg"},{"revision":"f06f79f3429a1da4a30749f75f886537","url":"img/badge-512.svg"},{"revision":"37f1a3340231f571423d3f19295e346f","url":"img/badge.png"},{"revision":"f6935ea4945b35d2b2ba8bed3b73adc0","url":"img/badge.svg"},{"revision":"bdda44c6c2115ed183c6110ea9e85101","url":"img/favicon.ico"}]; const controller = new workbox_precaching__WEBPACK_IMPORTED_MODULE_0__.PrecacheController({ // Safer to turn this true? fallbackToNetwork: true,