diff --git a/404.html b/404.html
index 9230cfb..888d31f 100644
--- a/404.html
+++ b/404.html
@@ -1 +1 @@
-
404: This page could not be found404
This page could not be found.
\ No newline at end of file
+404: This page could not be found404
This page could not be found.
\ No newline at end of file
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-algorithm.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-algorithm.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-algorithm.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-algorithm.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-engine.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-engine.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-engine.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-engine.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-handwritten.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-handwritten.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-handwritten.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-handwritten.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-javascript.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-javascript.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-javascript.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-javascript.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-mini.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-mini.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-mini.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-mini.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-mixture.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-mixture.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-mixture.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-mixture.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-node.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-node.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-node.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-node.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-project.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-project.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-project.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-project.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-react.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-react.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-react.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-react.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-typescript.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-typescript.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-typescript.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-typescript.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-visualization.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-visualization.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-visualization.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-visualization.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-vue.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-vue.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-vue.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-vue.json
diff --git a/_next/data/vXyg2l3A1ykgyWxKQNPrd/interview-webpack.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview-webpack.json
similarity index 100%
rename from _next/data/vXyg2l3A1ykgyWxKQNPrd/interview-webpack.json
rename to _next/data/gKYmpE7KIl6vlOO-j8XZx/interview-webpack.json
diff --git a/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview.json b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview.json
new file mode 100644
index 0000000..7ca537d
--- /dev/null
+++ b/_next/data/gKYmpE7KIl6vlOO-j8XZx/interview.json
@@ -0,0 +1 @@
+{"pageProps":{"post":{"content":"## Event Loop\n\nEvent Loop即事件循环,是指浏览器或者Nodejs解决javascript单线程运行时异步逻辑不会阻塞的一种机制。\n\nEvent Loop是一个执行模型,不同的运行环境有不同的实现,浏览器和nodejs基于不同的技术实现自己的event loop。\n\n- 浏览器的Event Loop是在HTML5规范中明确定义。\n- Nodejs的Event Loop是libuv实现的。\n- libuv已经对Event Loop作出了实现,HTML5规范中只是定义的浏览器中Event Loop的模型,具体的实现交给了浏览器厂商。\n\n### 宏队列和微队列\n\n在javascript中,任务被分为两种,一种为宏任务(macrotask),也称为task,一种为微任务(microtask),也称为jobs。\n\n宏任务主要包括:\n\n- script全部代码\n- setTimeout\n- setInterval\n- setImmediate (Nodejs独有,浏览器暂时不支持,只有IE10支持)\n- requestAnimationFrame (浏览器独有)\n- I/O\n- UI rendering (浏览器独有)\n\n微任务主要包括:\n\n- process.nextTick (Nodejs独有)\n- Promise\n- Object.observe (废弃)\n- MutationObserver\n\n### 浏览器中的Event Loop\n\nJavascript 有一个主线程 main thread 和 一个调用栈(执行栈) call-stack,所有任务都会被放到调用栈等待主线程的执行。\n\nJS调用栈采用的是后进先出的规则,当函数执行时,会被添加到调用栈的顶部,当执行栈执行完后,就会从栈顶移除,直到栈内被清空。\n\nJavascript单线程任务可以分为同步任务和异步任务,同步任务会在调用栈内按照顺序依次被主线程执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空的时候),被读取到调用栈内等待主线程的执行\n\n任务队列 Task Queue, 是先进先出的数据结构。\n\n![浏览器事件循环的进程模型](https://user-images.githubusercontent.com/8088864/124855609-c2904a00-dfdb-11eb-9138-df80150fa3a3.jpg)\n\n浏览器Event Loop的具体流程:\n\n1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句)\n2. 全局Javascript执行完毕后,调用栈call-stack会被清空\n3. 从微队列microtask queue中取出位于首部的回调函数,放入到调用栈call-stack中执行,执行完毕后从调用栈中删除,microtask queue的长度减1。\n4. 继续从微队列microtask queue的队首取出任务,放到调用栈中执行,依次循环往复,直至微任务队列microtask queue中的任务都被调用栈执行完毕。**特别注意,如果在执行微任务microtask过程中,又产生了微任务microtask,新产生的微任务也会追加到微任务队列microtask queue的尾部,新生成的微任务也会在当前周期中被执行完毕。**\n5. microtask queue中的任务都被执行完毕后,microtask queue为空队列,调用栈也处于空闲阶段\n6. 执行UI rendering\n7. 从宏队列macrotask queue的队首取出宏任务,放入调用栈中执行。\n8. 执行完后,调用栈为空闲状态\n9. 重复 3 - 8 的步骤,直至宏任务队列的任务都被执行完毕。\n...\n\n浏览器Event Loop的3个重点:\n\n1. 宏队列macrotask queue每次只从中取出一个任务放到调用栈中执行,执行完后去执行微任务队列中的所有任务\n2. 微任务队列中的所有任务都会依次取出来执行,只是微任务队列中的任务清空\n3. UI rendering 的执行节点在微任务队列执行完毕后,宏任务队列中取出任务执行之前执行\n\n### NodeJs中的Event Loop\n\nlibuv结构\n\n![libuv的事件循环模型](https://user-images.githubusercontent.com/8088864/125010304-d64db600-e098-11eb-824f-de433a12a095.png)\n\nNodeJs中的宏任务队列和微任务队列\n\nNodeJs的Event Loop中,执行宏任务队列的回调有6个阶段\n\n![NodeJS中的宏队列执行回调的6个阶段](https://user-images.githubusercontent.com/8088864/125010342-e9608600-e098-11eb-84e0-70a5bd5f5867.png)\n\nNode的Event Loop可以分为6个阶段,各个阶段执行的任务如下所示:\n\n- `timers`: 执行setTimeout和setInterval中到期的callback。\n- `I/O callbacks`: 执行几乎所有的回调,除了close callbacks以及timers调度的回调和setImmediate()调度的回调。\n- `idle, prepare`: 仅在内部使用。\n- `poll`: 最重要的阶段,检索新的I/O事件,在适当的情况下回阻塞在该阶段。\n- `check`: 执行setImmediate的callback(setImmediate()会将事件回调插入到事件队列的尾部,主线程和事件队列的任务执行完毕后会立即执行setImmediate中传入的回调函数)。\n- `close callbacks`: 执行`close`事件的callback,例如socket.on('close', fn)或则http.server.on('close', fn)等。\n\nNodeJs中的宏任务队列可以分为下列4个:\n\n 1. Timers Queue\n 2. I/O Callbacks Queue\n 3. Check Queue\n 4. Close Callbacks Queue\n\n在浏览器中只有一个宏任务队列,所有宏任务都会放入到宏任务队列中等待放入执行栈中被主线程执行,NodeJs中有4个宏任务队列,不同类型的宏任务会被放入到不同的宏任务队列中。\n\nNodeJs中的微任务队列可以分为下列2个:\n\n 1. `Next Tick Queue`: 放置process.nextTick(callback)的回调函数\n 2. `Other Micro Queue`: 其他microtask,例如Promise等\n\n在浏览器中只有一个微任务队列,所有微任务都会放入到微任务队列中等待放入执行栈中被主线程执行,NodeJs中有2个微任务队列,不同类型的微任务会被放入到不同的微任务队列中。\n\n![NodeJs事件循环](https://user-images.githubusercontent.com/8088864/125030923-71a55200-e0be-11eb-93be-95f1cbc456e3.png)\n\nNodeJs的Event Loop的具体流程:\n\n1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句)。\n2. 执行微任务队列中的微任务,先执行Next Tick Queue队列中的所有的所有任务,再执行Other Micro Queue队列中的所有任务。\n3. 开始执行宏任务队列中的任务,共6个阶段,从第1个阶段开始执行每个阶段对应宏任务队列中的所有任务,**注意,这里执行的是该阶段宏任务队列中的所有的任务,浏览器Event Loop每次只会中宏任务队列中取出队首的任务执行,执行完后开始执行微任务队列中的任务,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务后,才开始执行微任务队列中的任务,也就是步骤2**。\n4. Timers Queue -> 步骤2 -> I/O Callbacks Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue -> ......\n\n**特别注意:**\n\n- 上述的第三步,当 NodeJs 版本小于11时,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务\n- 当 NodeJS 版本大于等于11时,**在timer阶段的setTimeout,setInterval...和在check阶段的setImmediate都在node11里面都修改为一旦执行一个阶段里的一个任务就立刻执行微任务队列**。为了和浏览器更加趋同。\n\nNodeJs的Event Loop的microtask queue和macrotask queue的执行顺序详情\n\n![NodeJS中的微任务队列执行顺序](https://user-images.githubusercontent.com/8088864/125032436-8aaf0280-e0c0-11eb-926a-30be5bf116f9.png)\n\n![NodeJS中的宏任务队列执行顺序](https://user-images.githubusercontent.com/8088864/125032451-8f73b680-e0c0-11eb-8349-d6c5f20bd11a.png)\n\n当setTimeout(fn, 0)和setImmediate(fn)放在同一同步代码中执行时,可能会出现下面两种情况:\n\n1. **第一种情况**: 同步代码执行完后,timer还没到期,setImmediate中注册的回调函数先放入到Check Queue的宏任务队列中,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,由于在Timer Queue中未发现任何的回调函数,往下阶段走,直到Check Queue中发现setImmediate中注册的回调函数,先执行,然后timer到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,下一轮后再次执行到Timers Queue阶段时,才会再Timers Queue中发现了setTimeout注册的回调函数,于是执行该timer的回调,所以,**setImmediate(fn)注册的回调函数会早于setTimeout(fn, 0)注册的回调函数执行**。\n2. **第二种情况**: 同步代码执行完之前,timer已经到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,执行同步代码到setImmediate时,将其回调函数注册到Check Queue中,同步代码执行完后,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,在Timers Queue发现了timer中注册的回调函数,取出执行,往下阶段走,到Check Queue中发现setImmediate中注册的回调函数,又执行,所以这种情况时,**setTimeout(fn, 0)注册的回调函数会早于setImmediate(fn)注册的回调函数执行**。\n\n3. 在同步代码中同时调setTimeout(fn, 0)和setImmediate执行顺序情况是不确定的,但是如果把他们放在一个IO的回调,比如readFile('xx', function () {// ....})回调中,那么IO回调是在I/O Callbacks Queue中,setTimeout到期回调注册到Timers Queue,setImmediate回调注册到Check Queue,I/O Callbacks Queue执行完到Check Queue,Timers Queue得到下个循环周期,所以setImmediate回调这种情况下肯定比setTimeout(fn, 0)回调先执行。\n\n``` js\nsetImmediate(function A() {\n console.log(1);\n setImmediate(function B(){console.log(2);});\n});\n\nsetTimeout(function timeout() {\n console.log('TIMEOUT FIRED');\n}, 0);\n\n// 执行结果: 会存在下面两种情况\n// 第一种情况:\n// 1\n// TIMEOUT FIRED\n// 2\n\n// 第二种情况:\n// TIMEOUT FIRED\n// 1\n// 2\n```\n\n注:\n\n- setImmediate中如果又存在setImmediate语句,内部的setImmediate语句注册的回调函数会在下一个`check`阶段来执行,并不在当前的`check`阶段来执行。\n\npoll 阶段详解:\n\npoll 阶段主要又两个功能:\n\n1. 当timers到达指定的时间后,执行指定的timer的回调(Executing scripts for timers whose threshold has elapsed, then)。\n2. 处理poll队列的事件(Processing events in the poll queue)。\n\n当进入到poll阶段,并且没有timers被调用的时候,会出现下面的情况:\n\n- 如果poll队列不为空,Event Loop将同步执行poll queue中的任务,直到poll queue队列为空或者执行的callback达到上限。\n- 如果poll队列为空,会发生下面的情况:\n - 如果脚本执行过setImmediate代码,Event Loop会结束poll阶段,直接进入check阶段,执行Check Queue中调用setImmediate注册的回调函数。\n - 如果脚本没有执行过setImmediate代码,poll阶段将等待callback被添加到队列中,然后立即执行。\n\n当进入到poll阶段,并且调用了timers的话,会发生下面的情况:\n\n- 一旦poll queue为空,Event Loop会检测Timers Queue中是否存在任务,如果存在任务的话,Event Loop会回到timer阶段并执行Timers Queue中timers注册的回调函数。**执行完后是进入check阶段,还是又重新进入I/O callbacks阶段?**\n\nsetTimeout 对比 setImmediate\n\n- setTimeout(fn, 0)在timers阶段执行,并且是在poll阶段进行判断是否达到指定的timer时间才会执行\n- setImmediate(fn)在check阶段执行\n\n两者的执行顺序要根据当前的执行环境才能确定:\n\n- 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,顺序随机\n- 如果两者都不在主模块调用,即在一个I/O Circle中调用,那么setImmediate的回调永远先执行,因为会先到Check阶段\n\nsetImmediate 对比 process.nextTick\n\n- setImmediate(fn)的回调任务会插入到宏队列Check Queue中\n- process.nextTick(fn)的回调任务会插入到微队列Next Tick Queue中\n- process.nextTick(fn)调用深度有限制,上限是1000,而setImmediate则没有\n\n## Fetch API使用的常见问题及其解决办法\n\nXMLHttpRequest在发送web请求时需要开发者配置相关请求信息和成功后的回调,尽管开发者只关心请求成功后的业务处理,但是也要配置其他繁琐内容,导致配置和调用比较混乱,也不符合关注分离的原则;fetch的出现正是为了解决XHR存在的这些问题。\n\n**fetch是基于Promise设计的**,让开发者只关注请求成功后的业务逻辑处理,其他的不用关心,相当简单,FetchAPI的优点如下:\n\n- 语法简单,更加语义化\n- 基于标准的Promise实现,支持async/await\n- 使用isomorphic-fetch可以方便同构\n\n使用fetch来进行项目开发时,也是有一些常见问题的,下面就来说说fetch使用的常见问题。\n\n### Fetch 兼容性问题\n\nfetch是相对较新的技术,当然就会存在浏览器兼容性的问题,借用上面应用文章的一幅图加以说明fetch在各种浏览器的原生支持情况:\n\n![Fetch兼容性](https://user-images.githubusercontent.com/8088864/125045722-e03edb80-e0cf-11eb-9457-f56b13350846.png)\n\n从上图可以看出各个浏览器的低版本都不支持fetch技术。\n\n如何在所有浏览器中通用fetch呢,当然就要考虑fetch的polyfill了。\n\nfetch是基于Promise来实现的,所以在低版本浏览器中Promise可能也未被原生支持,所以还需要Promise的polyfill;大多数情况下,实现fetch的polyfill需要涉及到的:\n\n- promise的polyfill,例如es6-promise、babel-polyfill提供的promise实现。\n- fetch的polyfill实现,例如isomorphic-fetch和whatwg-fetch\n\nIE浏览器中IE8/9还比较特殊:IE8它使用的是ES3,而IE9则对ES5部分支持。这种情况下还需要ES5的polyfill es5-shim支持了。\n\n上述有关promise的polyfill实现,需要说明的是:\n\nbabel-runtime是不能作为Promise的polyfill的实现的,否则在IE8/9下使用fetch会报Promise未定义。为什么?我想大家猜到了,因为babel-runtime实现的polyfill是局部实现而不是全局实现,fetch底层实现用到Promise就是从全局中去取的,拿不到这报上述错误。\n\nfetch的polyfill实现思路:\n\n首先判断浏览器是否原生支持fetch,否则结合Promise使用XMLHttpRequest的方式来实现;这正是whatwg-fetch的实现思路,而同构应用中使用的isomorphic-fetch,其客户端fetch的实现是直接require(\"whatwg-fetch\")来实现的。\n\n### fetch默认不携带cookie\n\nfetch发送请求默认是不发送cookie的,不管是同域还是跨域;\n\n对于那些需要权限验证的请求就可能无法正常获取数据,可以配置其credentials项,其有3个值:\n\n- omit: 默认值,忽略cookie的发送\n- same-origin: 表示cookie只能同域发送,不能跨域发送\n- include: cookie既可以同域发送,也可以跨域发送\n\ncredentials所表达的含义,其实与XHR2中的withCredentials属性类似,表示请求是否携带cookie;\n\n若要fetch请求携带cookie信息,只需设置一下credentials选项即可,例如fetch(url, {credentials: 'include'});\n\nfetch默认对服务端通过Set-Cookie头设置的cookie也会忽略,若想选择接受来自服务端的cookie信息,也必须要配置credentials选项;\n\n### fetch请求对某些错误http状态不会reject\n\n主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;所以一般会对fetch请求做一层封装。\n\n``` js\nfunction checkStatus(response) {\n if (response.status >= 200 && response.status < 300) {\n return response;\n }\n const error = new Error(response.statusText);\n error.response = response;\n throw error;\n}\n\nfunction parseJSON(response) {\n return response.json();\n}\n\nexport default function request(url, options = {}) {\n return fetch(url, { credentials: 'include', ...options })\n .then(checkStatus)\n .then(parseJSON)\n .then((data) => data)\n .catch((err) => err);\n}\n```\n\n### fetch不支持超时timeout处理\n\nfetch不像大多数ajax库那样对请求设置超时timeout,它没有有关请求超时的功能,所以在fetch标准添加超时功能之前,都需要polyfill该特性。\n\n实际上,我们真正需要的是abort(), timeout可以通过timeout+abort方式来实现,起到真正超时丢弃当前的请求。\n\n目前的fetch指导规范中,fetch并不是一个具体实例,而只是一个方法;其返回的promise实例根据Promise指导规范标准是不能abort的,也不能手动改变promise实例的状态,只能由内部来根据请求结果来改变promise的状态。\n\n实现fetch的timeout功能,其思想就是新创建一个可以手动控制promise状态的实例,根据不同情况来对新promise实例进行resolve或者reject,从而达到实现timeout的功能;\n\n根据github上[timeout handling](https://github.com/github/fetch/issues/175)上的讨论,目前可以有两种不同的解决方法:\n\n方法一: 单纯setTimeout方法\n\n``` js\nvar fetchOrigin = fetch;\nwindow.fetch = function(url, options) {\n return new Promise(function(resolve, reject) {\n var timerId;\n if (options.timeout) {\n timerId = setTimeout(function() {\n reject(new Error('fetch timeout'));\n }, options.timeout);\n }\n\n fetchOrigin(url, option).then(function(response) {\n timerId && clearTimeout(timerId);\n resolve(response);\n }, function(error) {\n timerId && clearTimeout(timerId);\n reject(error);\n });\n });\n}\n```\n\n使用这种方式还可模拟XHR的abort方法\n\n``` js\nvar fetchOrigin = fetch;\nwindow.fetch = function(url, options) {\n return new Promise(function(resolve, reject) {\n var abort = function() {\n reject(new Error('fetch abort'));\n };\n\n const p = fetchOrigin(url, option).then(resolve, reject);\n p.abort = abort;\n\n return p;\n });\n}\n```\n\n方法二: 利用Promise.race方法\n\nPromise.race方法接受一个promise实例数组参数,表示多个promise实例中任何一个最先改变状态,那么race方法返回的promise实例状态就跟着改变\n\n``` js\nvar fetchOrigin = fetch;\nwindow.fetch = function(url, options) {\n var abortFn = null;\n var timeoutFn = null;\n\n var timeoutPromise = new Promise(function(resolve, reject) {\n timeoutFn = function () {\n reject(new Error('fetch timeout'));\n }\n });\n\n var abortPromise = new Promise(function(resolve, reject) {\n abortFn = function () {\n reject(new Error('fetch abort'));\n }\n });\n\n const fetchPromise = fetchOrigin(url, option);\n\n if (option.timeout) {\n setTimeout(timeoutFn, option.timeout);\n }\n\n const promise = Promise.race(\n timeoutPromise,\n abortPromise,\n fetchPromise,\n );\n\n promise.abort = abortFn;\n\n return promise;\n}\n```\n\n对fetch的timeout的上述实现方式补充几点:\n\n- timeout不是请求连接超时的含义,它表示发送请求到接收响应的时间,包括请求的连接、服务器处理及服务器响应回来的时间。\n- fetch的timeout即使超时发生了,本次请求也不会被abort丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已。\n\n### fetch不支持JSONP\n\nfetch是与服务器端进行异步交互的,而JSONP是外链一个javascript资源,是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题,并不是真正ajax,所以fetch与JSONP没有什么直接关联,当然至少目前是不支持JSONP的。\n\n这里我们把JSONP与fetch关联在一起有点差强人意,fetch只是一个ajax库,我们不可能使fetch支持JSONP;只是我们要实现一个JSONP,只不过这个JSONP的实现要与fetch的实现类似,即基于Promise来实现一个JSONP;而其外在表现给人感觉是fetch支持JSONP一样;\n\n目前比较成熟的开源JSONP实现[fetch-jsonp](https://github.com/camsong/fetch-jsonp)给我们提供了解决方案,想了解可以自行前往。不过再次想唠叨一下其JSONP的实现步骤,因为在本人面试的前端候选人中大部分人对JSONP的实现语焉不详;\n\n使用它非常简单,首先需要用npm安装fetch-jsonp\n\n``` shell\nnpm install fetch-jsonp --save-dev\n```\n\nfetch-jsonp源码如下所示:\n\n``` js\nconst defaultOptions = {\n timeout: 5000,\n jsonpCallback: 'callback',\n jsonpCallbackFunction: null,\n};\n\nfunction generateCallbackFunction() {\n return `jsonp_${Date.now()}_${Math.ceil(Math.random() * 100000)}`;\n}\n\nfunction clearFunction(functionName) {\n // IE8 throws an exception when you try to delete a property on window\n // http://stackoverflow.com/a/1824228/751089\n try {\n delete window[functionName];\n } catch (e) {\n window[functionName] = undefined;\n }\n}\n\nfunction removeScript(scriptId) {\n const script = document.getElementById(scriptId);\n if (script) {\n document.getElementsByTagName('head')[0].removeChild(script);\n }\n}\n\nfunction fetchJsonp(_url, options = {}) {\n // to avoid param reassign\n let url = _url;\n const timeout = options.timeout || defaultOptions.timeout;\n const jsonpCallback = options.jsonpCallback || defaultOptions.jsonpCallback;\n\n let timeoutId;\n\n return new Promise((resolve, reject) => {\n const callbackFunction = options.jsonpCallbackFunction || generateCallbackFunction();\n const scriptId = `${jsonpCallback}_${callbackFunction}`;\n\n window[callbackFunction] = (response) => {\n resolve({\n ok: true,\n // keep consistent with fetch API\n json: () => Promise.resolve(response),\n });\n\n if (timeoutId) clearTimeout(timeoutId);\n\n removeScript(scriptId);\n\n clearFunction(callbackFunction);\n };\n\n // Check if the user set their own params, and if not add a ? to start a list of params\n url += (url.indexOf('?') === -1) ? '?' : '&';\n\n const jsonpScript = document.createElement('script');\n jsonpScript.setAttribute('src', `${url}${jsonpCallback}=${callbackFunction}`);\n if (options.charset) {\n jsonpScript.setAttribute('charset', options.charset);\n }\n jsonpScript.id = scriptId;\n document.getElementsByTagName('head')[0].appendChild(jsonpScript);\n\n timeoutId = setTimeout(() => {\n reject(new Error(`JSONP request to ${_url} timed out`));\n\n removeScript(scriptId);\n\n clearFunction(callbackFunction);\n\n // 当前超时,请求并没有丢弃,请求完成的时候还是会调用该方法,如果直接干掉,会报错,修改函数体,回调过来时删除从全局上删除该函数\n window[callbackFunction] = () => {\n clearFunction(callbackFunction);\n };\n }, timeout);\n\n // Caught if got 404/500\n jsonpScript.onerror = () => {\n reject(new Error(`JSONP request to ${_url} failed`));\n\n clearFunction(callbackFunction);\n removeScript(scriptId);\n if (timeoutId) clearTimeout(timeoutId);\n };\n });\n}\n\nexport default fetchJsonp;\n```\n\n具体的使用方式:\n\n``` js\nfetchJsonp('/users.jsonp', {\n timeout: 3000,\n jsonpCallback: 'custom_callback'\n})\n.then(function(response) {\n return response.json()\n}).catch(function(ex) {\n console.log('parsing failed', ex)\n});\n```\n\n### fetch不支持progress事件\n\nXHR是原生支持progress事件的,例如下面代码这样:\n\n``` js\nvar xhr = new XMLHttpRequest();\nxhr.open('POST', '/uploads');\nxhr.onload = function() {}\nxhr.onerror = function() {}\nvar uploadProgress = function(event) {\n if (event.lengthComputable) {\n var percent = Math.round((event.loaded / event.total) * 100);\n console.log(percent);\n }\n};\n\n// 上传的progress事件\nxhr.upload.onprogress = uploadProgress;\n// 下载的progress事件\nxhr.onprogress = uploadProgress;\n```\n\n但是fetch是不支持有关progress事件的;不过可喜的是,根据fetch的指导规范标准,其内部设计实现了Request和Response类;其中Response封装一些方法和属性,通过Response实例可以访问这些方法和属性,例如response.json()、response.body等等;\n\n值得关注的地方是,response.body是一个可读字节流对象,其实现了一个getRender()方法,其具体作用是:\n\ngetRender()方法用于读取响应的原始字节流,该字节流是可以循环读取的,直至body内容传输完成;\n\n因此,利用到这点可以模拟出fetch的progress。\n\n代码实现如下:\n\n``` js\n// fetch() returns a promise that resolves once headers have been received\nfetch(url).then(response => {\n // response.body is a readable stream.\n // Calling getReader() gives us exclusive access to the stream's content\n var reader = response.body.getReader();\n var bytesReceived = 0;\n\n // read() returns a promise that resolves when a value has been received\n reader.read().then(function processResult(result) {\n // Result objects contain two properties:\n // done - true if the stream has already given you all its data.\n // value - some data. Always undefined when done is true.\n if (result.done) {\n console.log(\"Fetch complete\");\n return;\n }\n\n // result.value for fetch streams is a Uint8Array\n bytesReceived += result.value.length;\n console.log('Received', bytesReceived, 'bytes of data so far');\n\n // Read some more, and call this function again\n return reader.read().then(processResult);\n });\n});\n```\n\ngithub上也有使用Promise+XHR结合的方式实现类fetch的progress效果(当然这跟fetch完全不搭边)可以参考[这里](https://github.com/github/fetch/issues/89#issuecomment-256610849),具体代码如下:\n\n``` js\nfunction fetchProgress(url, opts={}, onProgress) {\n return new Promise((resolve, reject)=>{\n var xhr = new XMLHttpRequest();\n xhr.open(opts.method || 'get', url);\n\n for (var key in opts.headers||{}) {\n xhr.setRequestHeader(key, opts.headers[key]);\n }\n\n xhr.onload = function(event) {\n resolve(e.target.responseText)\n };\n\n xhr.onerror = reject;\n\n if (xhr.upload && onProgress) {\n xhr.upload.onprogress = onProgress; // event.loaded / event.total * 100 ; //event.lengthComputable\n }\n\n xhr.send(opts.body);\n });\n}\n\nfetchProgress('/').then(console.log)\n```\n\n### fetch跨域问题\n\n既然是ajax库,就不可避免与跨域扯上关系;XHR2是支持跨域请求的,只不过要满足浏览器端支持CORS,服务器通过Access-Control-Allow-Origin来允许指定的源进行跨域,仅此一种方式。\n\n与XHR2一样,fetch也是支持跨域请求的,只不过其跨域请求做法与XHR2一样,需要客户端与服务端支持;另外,fetch还支持一种跨域,不需要服务器支持的形式,具体可以通过其mode的配置项来说明。\n\nfetch的mode配置项有3个值,如下:\n\n- same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。\n- cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。\n- no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。\n\n针对跨域请求,cors模式是常见跨域请求实现,但是fetch自带的no-cors跨域请求模式则较为陌生,该模式有一个比较明显的特点:\n\n该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其response type为opaque不透明的原因。\n\n这与发送的请求类似,只是该模式不能访问响应的内容信息;但是它可以被其他APIs进行处理,例如ServiceWorker。另外,该模式返回的response可以在Cache API中被存储起来以便后续的对它的使用,这点对script、css和图片的CDN资源是非常合适的,因为这些资源响应头中都没有CORS头。\n\n总的来说,fetch的跨域请求是使用CORS方式,需要浏览器和服务端的支持。\n\n## 原型链和继承\n\nJavaScript是一门面向对象的设计语言,在JS里除了null和undefined,其余一切皆为对象。其中Array/Function/Date/RegExp是Object对象的特殊实例实现,Boolean/Number/String也都有对应的基本包装类型的对象(具有内置的方法)。传统语言是依靠class类来完成面向对象的继承和多态等特性,而JS使用原型链和构造器来实现继承,依靠参数arguments.length来实现多态。并且在ES6里也引入了class关键字来实现类。\n\n### 函数与对象的关系\n\n有时我们会好奇为什么能给一个函数添加属性,函数难道不应该就是一个执行过程的作用域吗?\n\n``` js\nvar name = 'Hank';\nfunction Person(name) {\n this.name = name;\n this.sayName = function() {\n alert(this.name);\n }\n}\nPerson.age = 10;\nconsole.log(Person.age); // 10\nconsole.log(Person);\n/* 输出函数体:\nƒ Person(name) {\n this.name = name;\n}\n*/\n```\n\n我们能够给函数赋一个属性值,当我们输出这个函数时这个属性却无影无踪了,这到底是怎么回事,这个属性又保存在哪里了呢?\n\n其实,在JS里,函数就是一个对象,这些属性自然就跟对象的属性一样被保存起来,函数名称指向这个对象的存储空间。\n\n函数调用过程没查到资料,个人理解为:这个对象内部拥有一个内部属性[\\[function]]保存有该函数体的字符串形式,当使用()来调用的时候,就会实时对其进行动态解析和执行,如同**eval()**一样。\n\n![内存栈和内存堆](https://user-images.githubusercontent.com/8088864/125233637-947b7480-e311-11eb-903e-73397c79b87e.png)\n\n上图是JS的具体内存分配方式,JS中分为值类型和引用类型,值类型的数据大小固定,我们将其分配在栈里,直接保存其数据。而引用类型是对象,会动态的增删属性,大小不固定,我们把它分配到内存堆里,并用一个指针指向这片地址,也就是Person其实保存的是一个指向这片地址的指针。这里的Person对象是个函数实例,所以拥有特殊的内部属性[\\[function]]用于调用。同时它也拥有内部属性arguments/this/name,因为不相关,这里我们没有绘出,而展示了我们为其添加的属性age。\n\n### 函数与原型的关系\n\n同时在JS里,我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个用于包含该函数所有实例的共享属性和方法的对象。而这个对象同时包含一个指针指向这个这个函数,这个指针就是**constructor**,这个函数也被成为构造函数。这样我们就完成了构造函数和原型对象的双向引用。\n\n而上面的代码实质也就是当我们创建了Person构造函数之后,同步开辟了一片空间创建了一个对象作为Person的原型对象,可以通过Person.prototype来访问这个对象,也可以通过Person.prototype.constructor来访问Person该构造函数。通过构造函数我们可以往实例对象里添加属性,如上面的例子里的name属性和sayName()方法。我们也可以通过prototype来添加原型属性,如:\n\n![函数原型](https://user-images.githubusercontent.com/8088864/125234076-7f531580-e312-11eb-9c55-3d760c70f5e7.png)\n\n要注意属性和原型属性不是同一个东西,也并不保存在同一个空间里:\n\n``` js\nPerson.age; // 10\nPerson.prototype.age; // 24\n```\n\n### 原型和实例的关系\n\n现在有了构造函数和原型对象,那我们接下来new一个实例出来,这样才能真正体现面向对象编程的思想,也就是**继承**:\n\n``` js\nvar person1 = new Person('Lee');\nvar person2 = new Person('Lucy');\n```\n\n我们新建了两个实例person1和person2,这些实例的内部都会包含一个指向其构造函数的原型对象的指针(内部属性),这个指针叫[\\[Prototype]],在ES5的标准上没有规定访问这个属性,但是大部分浏览器实现了**__proto__**的属性来访问它,成为了实际的通用属性,于是在ES6的附录里写进了该属性。__proto__前后的双下划线说明其本质上是一个内部属性,而不是对外访问的API,因此官方建议新的代码应当避免使用该属性,转而使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。\n\n这里的prototype我们称为显示原型,__proto__我们称为隐式原型。\n\n``` js\nObject.getPrototypeOf({}) === Object.prototype; // true\n```\n\n同时由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [\\[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [\\[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。\n\n此时它们的关系是(为了清晰,忽略函数属性的指向,用(function)代指):\n\n![构造函数实例的原型关系](https://user-images.githubusercontent.com/8088864/125234787-f89f3800-e313-11eb-8f2a-b1e346d904af.png)\n\n在这里我们可以看到两个实例指向了同一个原型对象,而在new的过程中调用了Person()方法,对每个实例分别初始化了name属性和sayName方法,属性值分别被保存,而方法作为引用对象也指向了不同的内存空间。\n\n我们可以用几种方法来验证实例的原型指针到底指向的是不是构造函数的原型对象:\n\n``` js\nperson1.__proto__ === Person.prototype // true\nPerson.prototype.isPrototypeOf(person1); // true\nObject.getPrototypeOf(person2) === Person.prototype; // true\nperson1 instanceof Person; // true\n```\n\n### 原型链\n\n现在我们访问实例person1的属性和方法了:\n\n``` js\nperson1.name; // Lee\nperson1.age; // 24\nperson1.toString(); // [object Object]\n```\n\n想下这个问题,我们的name值来自于person1的属性,那么age值来自于哪?toString( )方法又在哪定义的呢?\n\n这就是我们要说的原型链,原型链是实现继承的主要方法,其思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。如果我们让一个原型对象等于另一个类型的实例,那么该原型对象就会包含一个指向另一个原型的指针,而如果另一个原型对象又是另一个原型的实例,那么上述关系依然成立,层层递进,就构成了实例与原型的链条,这就是原型链的概念。\n\n上面代码的name来自于自身属性,age来自于原型属性,toString( )方法来自于Person原型对象的原型Object。当我们访问一个实例属性的时候,如果没有找到,我们就会继续搜索实例的原型,如果还没有找到,就递归搜索原型链直到原型链末端。我们可以来验证一下原型链的关系:\n\n``` js\nPerson.prototype.__proto__ === Object.prototype // true\n```\n\n同时让我们更加深入的验证一些东西:\n\n``` js\nPerson.__proto__ === Function.prototype // true\nFunction.prototype.__proto__ === Object.prototype // true\n```\n\n我们会发现Person是Function对象的实例,Function是Object对象的实例,Person原型是Object对象的实例。这证明了我们开篇的观点:JavaScript是一门面向对象的设计语言,在JS里除了null和undefined,其余一切皆为对象。\n\n下面祭出我们的原型链图:\n\n![原型链图](https://user-images.githubusercontent.com/8088864/125235100-7e22e800-e314-11eb-9dd0-bb6d0747ec99.jpg)\n\n根据我们上面讲述的关于prototype/constructor/__proto__的内容,我相信你可以完全看懂这张图的内容。需要注意两点:\n\n 1. 构造函数和对象原型一一对应,他们与实例一起作为三要素构成了三面这幅图。最左侧是实例,中间是构造函数,最右侧是对象原型。\n 2. 最最右侧的null告诉我们:Object.prototype.__proto__ = null,也就是Object.prototype是JS中一切对象的根源。其余的对象继承于它,并拥有自己的方法和属性。\n\n### 6种继承方法\n\n#### 第一种: 原型链继承\n\n利用原型链的特点进行继承\n\n``` js\nfunction Super(){\n this.name = 'web前端';\n this.type = ['JS','HTML','CSS'];\n}\nSuper.prototype.sayName=function(){\n return this.name;\n}\nfunction Sub(){};\nSub.prototype = new Super();\nSub.prototype.constructor = Sub;\nvar sub1 = new Sub();\nsub1.sayName();\n```\n\n优点:\n\n- 可以实现继承。\n\n缺点:\n\n- 子类的原型属性集成了父类实例化对象,所有子类的实例化对象都共享原型对象的属性和方法\n\n``` js\nvar sub1 = new Son();\nvar sub2 = new Son();\nsub1.type.push('VUE');\nconsole.log(sub1.type); // ['JS','HTML','CSS','VUE']\nconsole.log(sub2.type); // ['JS','HTML','CSS','VUE']\n```\n\n- 子类构造函数实例化对象时,无法传递参数给父类\n\n#### 第二种: 构造函数继承\n\n通过构造函数call方法实现继承。\n\n``` js\nfunction Super(){\n this.name = 'web前端';\n this.type = ['JS','HTML','CSS'];\n\n this.sayName = function() {\n return this.name;\n }\n}\nfunction Sub(){\n Super.call(this);\n}\nvar sub1 = new Sub();\nsub1.type.push('VUE');\nconsole.log(sub1.type); // ['JS','HTML','CSS','VUE']\nvar sub2 = new Sub();\nconsole.log(sub2.type); // ['JS','HTML','CSS']\n```\n\n优点:\n\n- 实现父类实例化对象的独立性\n\n- 还可以给父类实例化对象添加参数\n\n缺点:\n\n- 方法都在构造函数中定义,每次实例化对象都得创建一遍方法,基本无法实现函数复用\n\n- call方法仅仅调用了父级构造函数的属性及方法,没有办法访问父级构造函数原型对象的属性和方法\n\n#### 第三种: 组合继承\n\n利用原型链继承和构造函数继承的各自优势进行组合使用\n\n``` js\n\nfunction Super(name){\n this.name = name;\n this.type = ['JS','HTML','CSS'];\n}\n\nSuper.prototype.sayName=function(){\n return this.name;\n}\n\nfunction Sub(name){\n Super.call(this, name);\n}\n\nSub.prototype = new Super();\nsub1 = new Sub('张三');\nsub2 = new Sub('李四');\nsub1.type.push('VUE');\nsub2.type.push('PHP');\nconsole.log(sub1.type); // ['JS','HTML','CSS','VUE']\nconsole.log(sub2.type); // ['JS','HTML','CSS','PHP']\nsub1.sayName(); // 张三\nsub2.sayName(); // 李四\n```\n\n优点:\n\n- 利用原型链继承,实现原型对象方法的继承,允许访问父级构造函数原型对象属性和方法,实现方法复用\n\n- 利用构造函数继承,实现属性的继承,而且可以传递参数\n\n缺点:\n\n- 创建子类实例对象时,无论什么情况下,都会调用两次父级构造函数:一次是在创建子级原型的时候,另一次是在子级构造函数内部(call)\n\n#### 第四种: 原型式继承\n\n创建一个函数,将参数作为一个对象的原型对象。\n\n``` js\nfunction create(obj) {\n function Sub(){};\n Sub.prototype = obj;\n Sub.prototype.constructor = Sub;\n return new Sub();\n}\n\nvar parent = {\n name: '张三',\n type: ['JS','HTML','CSS'],\n};\n\nvar sub1 = create(parent);\nvar sub2 = create(parent);\n\nconsole.log(sub1.name); // 张三\nconsole.log(sub2.name); // 张三\n```\n\nES5规范化了这个原型继承,新增了Object.create()方法,接收两个参数,第一个为原型对象,第二个为要混合进新对象的属性,格式与Object.defineProperties()相同。\n\n``` js\nObject.create(null, {name: {value: 'Greg', enumerable: true}});\n\n// 相当于\nvar parent = {\n name: '张三',\n type: ['JS','HTML','CSS'],\n};\n\nvar sub1 = Object.create(parent);\nvar sub2 = Object.create(parent);\n\nconsole.log(sub1.name); // 张三\nconsole.log(sub2.name); // 张三\n```\n\n优缺点:\n\n- 跟原型链类似\n\n#### 第五种: 寄生继承\n\n在原型式继承的基础上,在函数内部丰富对象\n\n``` js\nfunction create(obj) {\n function Sub() {};\n Sub.prototype = obj;\n Sub.prototype.constructor = Sub;\n\n return new Sub();\n}\n\nfunction Parasitic(obj) {\n var clone = create(obj);\n clone.sayHi = function() {\n console.log('hi');\n };\n return clone;\n}\n\nvar parent = {\n name: '张三',\n type: ['JS','HTML','CSS'],\n};\n\nvar sub1 = Parasitic(parent);\nvar sub2 = Parasitic(parent);\n\nconsole.log(sub1.name); // 张三\nconsole.log(sub2.name); // 张三\n```\n\n如果使用ES5Object.create来代替create函数的话,可以简化成如下所示:\n\n``` js\nfunction Parasitic(obj) {\n var clone = Object.create(obj);\n clone.sayHi = function() {\n console.log('hi');\n };\n return clone;\n}\n\nvar parent = {\n name: '张三',\n type: ['JS','HTML','CSS'];\n};\n\nvar son1 = Parasitic(parent);\nvar son2 = Parasitic(parent);\n\nconsole.log(son1.name); // 张三\nconsole.log(son2.name); // 张三\nson1.sayHi();\nson2.sayHi();\n```\n\n优缺点:\n\n- 跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低\n\n#### 第六种: 寄生组合继承\n\n利用组合继承和寄生继承各自优势\n\n组合继承方法我们已经说了,它的缺点是两次调用父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部,那么我们只需要优化这个问题就行了,即减少一次调用父级构造函数,正好利用寄生继承的特性,继承父级构造函数的原型来创建子级原型。\n\n``` js\nfunction Super(name) {\n this.name = name;\n this.type = ['JS','HTML','CSS'];\n};\n\nSuper.prototype.sayName = function () {\n return this.name;\n};\n\nfunction Sub(name, age) {\n Super.call(this, name);\n this.age = age;\n}\n\n// 我们封装其继承过程\nfunction inheritPrototype(Sub, Super) {\n // 以该对象为原型创建一个新对象\n var prototype = Object.create(Super.prototype);\n prototype.constructor = Sub;\n Sub.prototype = prototype;\n}\n\ninheritPrototype(Sub, Super);\n\n// 必须定义在inheritPrototype方法之后\nSub.prototype.sayAge = function () {\n return this.age;\n}\n\nvar instance = new Sub('张三', 40);\ninstance.sayName(); // 张三\ninstance.sayAge(); // 40\n```\n\n这种方式只调用了一次父类构造函数,只在子类上创建一次对象,同时保持原型链,还可以使用instanceof和isPrototypeOf()来判断原型,是我们最理想的继承方式。\n\n#### 第七种: ES6 Class类和extends关键字\n\nES6引进了class关键字,用于创建类,这里的类是作为**ES5构造函数和原型对象的语法糖**存在的,其功能大部分都可以被ES5实现,不过在语言层面上ES6也提供了部分支持。新的写法不过让对象原型看起来更加清晰,更像面向对象的语法而已。\n\n``` js\n//定义类\nclass Point {\n constructor(x, y) {\n this.x = x;\n this.y = y;\n }\n\n toString() {\n return '(' + this.x + ', ' + this.y + ')';\n }\n}\n\nvar point = new Point(10, 10);\n```\n\n我们看到其中的constructor方法就是之前的构造函数,this就是之前的原型对象,toString()就是定义在原型上的方法,只能使用new关键字来新建实例。语法差别在于我们不需要function关键字和逗号分割符。其中,所有的方法都直接定义在原型上,注意所有的方法都不可枚举。类的内部使用严格模式,并且不存在变量提升,其中的this指向类的实例。\n\nnew是从构造函数生成实例的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。\n\n类存在静态方法,使用static关键字表示,其只能类和继承的子类来进行调用,不能被实例调用,也就是不能被实例继承,所以我们称它为静态方法。类不存在内部方法和内部属性。\n\n``` js\nclass Foo {\n static classMethod() {\n return 'hello';\n }\n}\n\nFoo.classMethod() // 'hello'\n\nvar foo = new Foo();\nfoo.classMethod()\n// TypeError: foo.classMethod is not a function\n```\n\n类通过extends关键字来实现继承,在继承的子类的构造函数里我们使用super关键字来表示对父类构造函数的引用;在静态方法里,super指向父类;在其它函数体内,super表示对父类原型属性的引用。其中super必须在子类的构造函数体内调用一次,因为我们需要调用时来绑定子类的元素对象,否则会报错。\n\n``` js\nclass ColorPoint extends Point {\n constructor(x, y, color) {\n super(x, y); // 调用父类的constructor(x, y)\n this.color = color;\n }\n\n toString() {\n return this.color + ' ' + super.toString(); // 调用父类的toString()\n }\n}\n```\n\n## 前端性能优化\n\n性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提升网站性能,坏的一面就是配置麻烦,或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景,需要谨慎使用。\n\n下面列出来了前端性能的24条建议:\n\n### 1. 减少 HTTP 请求\n\n一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。\n\n接下来看一个具体的例子帮助理解 HTTP :\n\n![http请求瀑布图](https://user-images.githubusercontent.com/8088864/125281253-957bc880-e348-11eb-97bf-464d4531ce8e.png)\n\n这是一个 HTTP 请求,请求的文件大小为 28.4KB。\n\n名词解释:\n\n- Queueing: 在请求队列中的时间。\n- Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。\n- Proxy negotiation: 与代理服务器连接进行协商所花费的时间。\n- DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。\n- Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手,重试和协商SSL。\n- SSL: 完成SSL握手所花费的时间。\n- Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。\n- Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。\n- Content Download: 接收响应数据所花费的时间。\n\n从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。\n\n### 2. 使用 HTTP2\n\nHTTP2 相比 HTTP1.1 有如下几个优点:\n\n#### 解析速度快\n\n服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。\n\n#### 多路复用\n\nHTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。\n\n在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。\n\n#### 首部压缩\n\nHTTP2 提供了首部压缩功能。\n\n例如有如下两个请求:\n\n```\n// 请求1\n:authority: unpkg.zhimg.com\n:method: GET\n:path: /za-js-sdk@2.16.0/dist/zap.js\n:scheme: https\naccept: */*\naccept-encoding: gzip, deflate, br\naccept-language: zh-CN,zh;q=0.9\ncache-control: no-cache\npragma: no-cache\nreferer: https://www.zhihu.com/\nsec-fetch-dest: script\nsec-fetch-mode: no-cors\nsec-fetch-site: cross-site\nuser-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36\n\n// 请求2\n:authority: zz.bdstatic.com\n:method: GET\n:path: /linksubmit/push.js\n:scheme: https\naccept: */*\naccept-encoding: gzip, deflate, br\naccept-language: zh-CN,zh;q=0.9\ncache-control: no-cache\npragma: no-cache\nreferer: https://www.zhihu.com/\nsec-fetch-dest: script\nsec-fetch-mode: no-cors\nsec-fetch-site: cross-site\nuser-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36\n```\n\n从上面两个请求可以看出来,有很多数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。\n\nHTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。\n\n下面再来看一个简化的例子,假设客户端按顺序发送如下请求首部:\n\n```\nHeader1:foo\nHeader2:bar\nHeader3:bat\n```\n\n当客户端发送请求时,它会根据首部值创建一张表:\n\n| 索引 | 首部名称 | 值 |\n| ---- | ---- | ---- |\n| 62 | Header1 | foo |\n| 63 | Header2 | bar |\n| 64 | Header3 | bar |\n\n如果服务器收到了请求,它会照样创建一张表。 当客户端发送下一个请求的时候,如果首部相同,它可以直接发送这样的首部块:\n\n```\n62 63 64\n```\n\n服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。\n\n#### 优先级\n\nHTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。\n\n#### 流量控制\n\n由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。\n\n#### 服务器推送\n\nHTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。\n\n例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL,来提前推送资源。\n\n现在有很多网站已经开始使用 HTTP2 了,例如知乎:\n\n![服务器推送](https://user-images.githubusercontent.com/8088864/125283274-d83ea000-e34a-11eb-95d5-7881c4af0403.jpg)\n\n其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。\n\n参考资料:\n\n- [半小时搞懂 HTTP、HTTPS和HTTP2](https://github.com/woai3c/Front-end-articles/blob/master/http-https-http2.md)\n\n### 3. 使用服务端渲染\n\n客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。\n\n服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。\n\n- 优点:首屏渲染快,SEO 好。\n- 缺点:配置麻烦,增加了服务器的计算压力。\n\n下面我用 Vue SSR 做示例,简单的描述一下 SSR 过程。\n\n#### 客户端渲染过程\n\n1. 访问客户端渲染的网站。\n2. 服务器返回一个包含了引入资源语句和 \\\\
的 HTML 文件。\n3. 客户端通过 HTTP 向服务器请求资源,当必要的资源都加载完毕后,执行 new Vue() 开始实例化并渲染页面。\n\n#### 服务端渲染过程\n\n1. 访问服务端渲染的网站。\n2. 服务器会查看当前路由组件需要哪些资源文件,然后将这些文件的内容填充到 HTML 文件。如果有 ajax 请求,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。\n3. 当客户端接收到这个 HTML 页面时,可以马上就开始渲染页面。与此同时,页面也会加载资源,当必要的资源都加载完毕后,开始执行 new Vue() 开始实例化并接管页面。\n\n从上述两个过程中可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。\n\n这样做的好处是什么?是更快的内容到达时间 (time-to-content)。\n\n假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。\n\n这样一算:客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染,总计大小为 4M(忽略 HTML 文件大小)。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染,总计大小为已经渲染完毕的 HTML 文件(这种文件不会太大,一般为几百K,我的个人博客网站(SSR)加载的 HTML 文件为 400K)。这就是服务端渲染更快的原因。\n\n参考资料:\n\n- [vue-ssr-demo](https://github.com/woai3c/vue-ssr-demo)\n- [Vue.js 服务器端渲染指南](https://ssr.vuejs.org/zh/)\n\n### 4. 静态资源使用 CDN\n\n内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。\n\n#### CDN 原理\n\n当用户访问一个网站时,如果没有 CDN,过程是这样的:\n\n1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。\n2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到网站服务器的 IP 地址。\n3. 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。\n\n![没有CDN的资源请求](https://user-images.githubusercontent.com/8088864/125375921-8171ae80-e3bc-11eb-9d66-adb57433b67a.jpg)\n\n如果用户访问的网站部署了 CDN,过程是这样的:\n\n1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。\n2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。\n3. 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。\n4. 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。\n5. SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。\n6. 浏览器再根据 SLB 发回的地址重定向到缓存服务器。\n7. 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。\n\n![有CDN的资源请求](https://user-images.githubusercontent.com/8088864/125376046-baaa1e80-e3bc-11eb-84ba-c86cd8d63a7f.jpg)\n\n参考资料:\n\n- [CDN是什么?使用CDN有什么优势?](https://www.zhihu.com/question/36514327/answer/193768864)\n- [CDN原理简析](https://juejin.cn/post/6844903873518239752)\n\n### 5. 将 CSS 放在文件头部,JavaScript 文件放在底部\n\n所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。\n\n那为什么 CSS 文件还要放在头部呢?\n\n因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。\n\n另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。\n\n### 6. 使用字体图标 iconfont 代替图片图标\n\n字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。\n\n#### 压缩字体文件\n\n使用 [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack) 插件对字体文件进行压缩。\n\n![fontmin-webpack](https://user-images.githubusercontent.com/8088864/125377089-efb77080-e3be-11eb-845b-d8992de47838.png)\n\n参考资料:\n\n- [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack)\n- [Iconfont-阿里巴巴矢量图标库](https://www.iconfont.cn/)\n\n### 7. 善用缓存,不重复加载相同的资源\n\n为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。\n\n不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?\n\n可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。\n\n具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用[数据摘要要算法](https://cloud.tencent.com/developer/article/1584742)对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。\n\n参考资料:\n\n- [webpack + express 实现文件精确缓存](https://github.com/woai3c/node-blog/blob/master/doc/node-blog7.md)\n- [webpack-缓存](https://www.webpackjs.com/guides/caching/)\n- [张云龙--大公司里怎样开发和部署前端代码?](https://www.zhihu.com/question/20790576/answer/32602154)\n\n### 8. 压缩文件\n\n压缩文件可以减少文件下载时间,让用户体验性更好。\n\n得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。\n\n在 webpack 可以使用如下插件进行压缩:\n\n- JavaScript:UglifyPlugin\n- CSS :MiniCssExtractPlugin\n- HTML:HtmlWebpackPlugin\n\n其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。\n\ngzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。\n\n附上 webpack 和 node 配置 gzip 的使用方法。\n\n下载插件\n\n``` shell\nnpm install compression-webpack-plugin --save-dev\nnpm install compression\n```\n\nwebpack 配置\n\n``` js\nconst CompressionPlugin = require('compression-webpack-plugin');\n\nmodule.exports = {\n plugins: [new CompressionPlugin()],\n}\n```\n\nnode 配置\n\n``` js\nconst compression = require('compression')\n// 在其他中间件前使用\napp.use(compression())\n```\n\n### 9. 图片优化\n\n#### (1). 图片延迟加载\n\n在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。\n\n首先可以将图片这样设置,在页面不可见时图片不会加载:\n\n``` html\n\n```\n\n等页面可见时,使用 JS 加载图片:\n\n``` js\nconst img = document.querySelector('img')\nimg.src = img.dataset.src\n```\n\n这样图片就加载出来了,完整的代码可以看一下参考资料。\n\n参考资料:\n\n- [web 前端图片懒加载实现原理](https://juejin.cn/post/6844903482164510734)\n\n#### (2). 响应式图片\n\n响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。\n\n通过 picture 实现\n\n``` html\n\n```\n\n通过 @media 实现\n\n``` css\n@media (min-width: 769px) {\n .bg {\n background-image: url(bg1080.jpg);\n }\n}\n@media (max-width: 768px) {\n .bg {\n background-image: url(bg768.jpg);\n }\n}\n```\n\n#### (3). 调整图片大小\n\n例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。\n\n所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。\n\n#### (4). 降低图片质量\n\n例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。\n\n压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。\n\n以下附上 webpack 插件 image-webpack-loader 的用法。\n\n``` shell\nnpm install --save-dev image-webpack-loader\n```\n\nwebpack 配置\n\n``` js\n{\n test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n use:[\n {\n loader: 'url-loader',\n options: {\n limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/\n name: utils.assetsPath('img/[name].[hash:7].[ext]')\n }\n },\n /*对图片进行压缩*/\n {\n loader: 'image-webpack-loader',\n options: {\n bypassOnDebug: true,\n }\n }\n ]\n}\n```\n\n参考资料:\n\n- [img图片在webpack中使用](https://juejin.cn/post/6844903816081457159)\n\n#### (5). 尽可能利用 CSS3 效果代替图片\n\n有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。\n\n#### (6). 使用 webp 格式的图片\n\nWebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。\n\n参考资料:\n\n- [WebP 相对于 PNG、JPG 有什么优势?](https://www.zhihu.com/question/27201061)\n\n### 10. 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码\n\n懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。\n\n#### 根据文件内容生成文件名,结合 import 动态引入组件实现按需加载\n\n通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash],它将根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 也会发生变化。\n\n``` js\n{\n output: {\n filename: '[name].[contenthash].js',\n chunkFilename: '[name].[contenthash].js',\n path: path.resolve(__dirname, '../dist'),\n },\n}\n```\n\n#### 提取第三方库\n\n由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。 这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项。\n\n``` js\noptimization: {\n runtimeChunk: {\n name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。\n },\n splitChunks: {\n cacheGroups: {\n vendor: {\n name: 'chunk-vendors',\n test: /[\\\\/]node_modules[\\\\/]/,\n priority: -10,\n chunks: 'initial'\n },\n common: {\n name: 'chunk-common',\n minChunks: 2,\n priority: -20,\n chunks: 'initial',\n reuseExistingChunk: true\n }\n },\n }\n},\n```\n\n- **test**: 过滤 modules,默认为所有的 modules,可匹配模块路径或 chunk 名字,当匹配到某个 chunk 的名字时,这个 chunk 里面引入的所有 module 都会选中。可以传递的值类型:RegExp、String和Function。\n- **priority**: 权重,数字越大表示优先级越高。一个 module 可能会满足多个 cacheGroups 的正则匹配,到底将哪个缓存组应用于这个 module,取决于优先级。\n- **reuseExistingChunk**: 表示是否使用已有的 chunk,true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的,即几个 chunk 复用被拆分出去的一个 module。\n- **minChunks**(默认是1): 在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)\n- **chunks**(默认是async): initial、async和all。chunks改为all,表示同时对静态加载(initial)和动态加载(async)起作用。\n- **name**(打包的chunks的名字): 字符串或者函数(函数可以根据条件自定义名字)\n\n#### 减少 ES6 转为 ES5 的冗余代码\n\nBabel 转化后的代码想要实现和原来代码一样的功能需要借助一些帮助函数,比如\n\n``` js\nclass Person {}\n```\n\n会被转换为:\n\n``` js\n\"use strict\";\n\nfunction _classCallCheck(instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n}\n\nvar Person = function Person() {\n _classCallCheck(this, Person);\n};\n```\n\n这里 `_classCallCheck` 就是一个 `helper` 函数,如果在很多文件里都声明了类,那么就会产生很多个这样的 `helper` 函数。\n\n这里的 `@babel/runtime` 包就声明了所有需要用到的帮助函数,而 `@babel/plugin-transform-runtime` 的作用就是将所有需要 `helper` 函数的文件,从 `@babel/runtime`包引进来:\n\n``` js\n\"use strict\";\n\nvar _classCallCheck2 = require(\"@babel/runtime/helpers/classCallCheck\");\n\nvar _classCallCheck3 = _interopRequireDefault(_classCallCheck2);\n\nfunction _interopRequireDefault(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\nvar Person = function Person() {\n (0, _classCallCheck3.default)(this, Person);\n};\n```\n\n这里就没有再编译出 helper 函数 classCallCheck 了,而是直接引用了 @babel/runtime 中的 helpers/classCallCheck。\n\n**安装**\n\n``` shell\nnpm install --save-dev @babel/plugin-transform-runtime @babel/runtime\n```\n\n**使用** 在 .babelrc 文件中\n\n``` json\n{\n \"plugins\": [\n \"@babel/plugin-transform-runtime\"\n ]\n}\n```\n\n参考资料:\n\n- [Babel 7.1介绍 transform-runtime polyfill env](https://www.jianshu.com/p/d078b5f3036a)\n- [webpack 懒加载](https://webpack.docschina.org/guides/lazy-loading/)\n- [Vue 路由懒加载](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD)\n- [webpack 缓存](https://webpack.docschina.org/guides/caching/)\n- [一步一步的了解webpack4的splitChunk插件](https://juejin.cn/post/6844903614759043079)\n\n### 11. 减少重绘重排\n\n浏览器渲染过程\n\n1. 解析HTML生成DOM树。\n2. 解析CSS生成CSSOM规则树。\n3. 将DOM树与CSSOM规则树合并在一起生成渲染树。\n4. 遍历渲染树开始布局,计算每个节点的位置大小信息。\n5. 将渲染树每个节点绘制到屏幕。\n\n![渲染树生成](https://user-images.githubusercontent.com/8088864/125440124-9cc83d52-c342-4959-af1e-dc67cfe7d312.png)\n\n#### 重排\n\n当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。\n\n#### 重绘\n\n当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排。\n\n重排和重绘这两个操作都是非常昂贵的,因为 **JavaScript** 引擎线程与 **GUI** 渲染线程是互斥,它们同时只能一个在工作。\n\n什么操作会导致重排?\n\n- 添加或删除可见的 **DOM** 元素\n- 元素位置改变\n- 元素尺寸改变\n- 内容改变\n- 浏览器窗口尺寸改变\n\n如何减少重排重绘?\n\n- 用 **JavaScript** 修改样式时,最好不要直接写样式,而是替换 **class** 来改变样式。\n- 如果要对 **DOM** 元素执行一系列操作,可以将 **DOM** 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(**display:none**)或文档碎片(**DocumentFragment**),都能很好的实现这个方案。\n\n### 12. 使用事件委托\n\n事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。\n\n``` html\n\n```\n\n``` js\n// good\ndocument.querySelector('ul').onclick = (event) => {\n const target = event.target;\n if (target.nodeName === 'LI') {\n console.log(target.innerHTML);\n }\n}\n\n// bad\ndocument.querySelectorAll('li').forEach((e) => {\n e.onclick = function() {\n console.log(this.innerHTML);\n }\n})\n```\n\n### 13. 注意程序的局部性\n\n一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。\n\n**局部性通常有两种不同的形式:**\n\n- 时间局部性: 在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。\n- 空间局部性: 在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。\n\n时间局部性示例\n\n``` js\nfunction sum(arry) {\n let i, sum = 0;\n let len = arry.length;\n\n for (i = 0; i < len; i++) {\n sum += arry[i];\n }\n\n return sum;\n}\n```\n\n在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性\n\n空间局部性示例\n\n**具有良好空间局部性的程序**\n\n``` js\n// 二维数组\nfunction sum1(arry, rows, cols) {\n let i, j, sum = 0;\n\n for (i = 0; i < rows; i++) {\n for (j = 0; j < cols; j++) {\n sum += arry[i][j];\n }\n }\n\n return sum;\n}\n```\n\n**空间局部性差的程序**\n\n``` js\n// 二维数组\nfunction sum2(arry, rows, cols) {\n let i, j, sum = 0;\n\n for (j = 0; j < cols; j++) {\n for (i = 0; i < rows; i++) {\n sum += arry[i][j];\n }\n }\n\n return sum;\n}\n```\n\n看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每个元素的方式,称为具有步长为1的引用模式。 如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。 一般而言,随着步长的增加,空间局部性下降。\n\n这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,马上就去扫下一行中的同一列元素。\n\n数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例得到了步长为 1 引用模式,具有良好的空间局部性;而另一个示例步长为 rows,空间局部性极差。\n\n**性能测试**\n\n运行环境:\n\n- cpu: i7-10510U\n- 浏览器: 83.0.4103.61\n\n对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下:\n\n``` js\nfunction sum2(arry, rows, cols) {\n let i, j, sum = 0;\n\n for (j = 0; j < cols; j++) {\n for (i = 0; i < rows; i++) {\n sum += arry[i][j];\n }\n }\n\n return sum;\n}\n\n// 二维数组\nfunction sum1(arry, rows, cols) {\n let i, j, sum = 0;\n\n for (i = 0; i < rows; i++) {\n for (j = 0; j < cols; j++) {\n sum += arry[i][j];\n }\n }\n\n return sum;\n}\n\nvar arry = new Array(9000).fill(new Array(9000).fill(1));\n\nlet ts = 0;\nfor (let i = 0; i < 10; i++) {\n const startTime = new Date().valueOf();\n sum1(arry, 9000, 9000);\n ts += (new Date().valueOf() - startTime);\n}\n\nconsole.log('sum1: ' + (ts / 10)); // 81.5ms\n\nlet ts2 = 0;\nfor (let i = 0; i < 10; i++) {\n const startTime = new Date().valueOf();\n sum2(arry, 9000, 9000);\n ts2 += (new Date().valueOf() - startTime);\n}\n\nconsole.log('sum2: ' + (ts2 / 10)); // 167.3ms\n```\n\n所用示例为上述两个空间局部性示例\n\n| 步长为1(sum1) | 步长为9000(sum2) |\n| ---- | ---- |\n| 81.5ms | 167.3ms |\n\n从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。\n\n总结:\n\n- 重复引用相同变量的程序具有良好的时间局部性\n- 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会很差\n\n参考资料:\n\n- [深入理解计算机系统](https://book.douban.com/subject/26912767/)\n\n### 14. if-else 对比 switch\n\n当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。\n\n``` js\nif (color == 'blue') {\n\n} else if (color == 'yellow') {\n\n} else if (color == 'white') {\n\n} else if (color == 'black') {\n\n} else if (color == 'green') {\n\n} else if (color == 'orange') {\n\n} else if (color == 'pink') {\n\n}\n\nswitch (color) {\n case 'blue':\n\n break;\n case 'yellow':\n\n break;\n case 'white':\n\n break;\n case 'black':\n\n break;\n case 'green':\n\n break;\n case 'orange':\n\n break;\n case 'pink':\n\n break;\n}\n```\n\n像以上这种情况,使用 switch 是最好的。假设 color 的值为 pink,则 if-else 语句要进行 7 次判断,switch 只需要进行一次判断。 从可读性来说,switch 语句也更好。\n\n从使用时机来说,当条件值大于两个的时候,使用 switch 更好。不过 if-else 也有 switch 无法做到的事情,例如有多个判断条件的情况下,无法使用 switch。\n\n### 15. 查找表\n\n当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。\n\n``` js\nswitch (index) {\n case '0':\n return result0;\n case '1':\n return result1;\n case '2':\n return result2;\n case '3':\n return result3;\n case '4':\n return result4;\n case '5':\n return result5;\n case '6':\n return result6;\n case '7':\n return result7;\n case '8':\n return result8;\n case '9':\n return result9;\n case '10':\n return result10;\n case '11':\n return result11;\n}\n```\n\n可以将这个 switch 语句转换为查找表\n\n``` js\nconst results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11];\n\nreturn results[index];\n```\n\n如果条件语句不是数值而是字符串,可以用对象来建立查找表\n\n``` js\nconst map = {\n red: result0,\n green: result1,\n};\n\nreturn map[color];\n```\n\n### 16. 避免页面卡顿\n\n**60fps 与设备刷新率**\n\n目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。 其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。\n\n![网页渲染流程](https://user-images.githubusercontent.com/8088864/125445172-29d132ea-e485-49c7-b32d-172956c4349b.jpeg)\n\n假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长,都会导致渲染这一帧的时间过长,平均帧率就会下降。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了一样。\n\n对于一些长时间运行的 JavaScript,我们可以使用定时器进行切分,延迟执行。\n\n``` js\nfor (let i = 0, len = arry.length; i < len; i++) {\n process(arry[i]);\n}\n```\n\n假设上面的循环结构由于 process() 复杂度过高或数组元素太多,甚至两者都有,可以尝试一下切分。\n\n``` js\nconst todo = arry.concat();\nsetTimeout(function(){\n process(todo.shift());\n if (todo.length) {\n setTimeout(arguments.callee, 25);\n } else {\n callback(arry);\n }\n}, 25);\n```\n\n如果有兴趣了解更多,可以查看一下高性能JavaScript第 6 章和[高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/)第 3 章。\n\n### 17. 使用 requestAnimationFrame 来实现视觉变化\n\n从第 16 点我们可以知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 `requestAnimationFrame`。\n\n``` js\n/**\n * If run as a requestAnimationFrame callback, this\n * will be run at the start of the frame.\n */\nfunction updateScreen(time) {\n // Make visual updates here.\n}\n\nrequestAnimationFrame(updateScreen);\n```\n\n如果采取 setTimeout 或 setInterval 来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。\n\n![requestAnimationFrame执行点](https://user-images.githubusercontent.com/8088864/125448006-c889aac7-f5d6-4a21-a4fe-b4c6c0cdf197.jpg)\n\n### 18. 使用 Web Workers\n\nWeb Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。\n\nWeb Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。\n\n创建一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js):\n\n``` js\nvar myWorker = new Worker('worker.js');\n// 你可以通过postMessage() 方法和onmessage事件向worker发送消息。\nfirst.onchange = function() {\n myWorker.postMessage([first.value,second.value]);\n console.log('Message posted to worker');\n}\n\nsecond.onchange = function() {\n myWorker.postMessage([first.value,second.value]);\n console.log('Message posted to worker');\n}\n```\n\n在 worker 中接收到消息后,我们可以写一个事件处理函数代码作为响应(worker.js):\n\n``` js\nonmessage = function(e) {\n console.log('Message received from main script');\n var workerResult = 'Result: ' + (e.data[0] * e.data[1]);\n console.log('Posting message back to main script');\n postMessage(workerResult);\n}\n```\n\nonmessage处理函数在接收到消息后马上执行,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。\n\n回到主线程,我们再次使用onmessage以响应worker回传的消息:\n\n``` js\nmyWorker.onmessage = function(e) {\n result.textContent = e.data;\n console.log('Message received from worker');\n}\n```\n\n在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。\n\n不过在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。\n\n参考资料:\n\n- [Web Workers](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers)\n\n### 19. 使用位操作\n\nJavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中,数字被转换为有符号的 32 位格式。即使需要转换,位操作也比其他数学运算和布尔操作快得多。\n\n#### 取模\n\n由于偶数的最低位为 0,奇数为 1,所以取模运算可以用位操作来代替。\n\n``` js\nif (value % 2) {\n // 奇数\n} else {\n // 偶数\n}\n// 位操作\nif (value & 1) {\n // 奇数\n} else {\n // 偶数\n}\n```\n\n#### 取整\n\n``` js\n~~10.12 // 10\n~~10 // 10\n~~'1.5' // 1\n~~undefined // 0\n~~null // 0\n```\n\n#### 位掩码\n\n``` js\nconst a = 1\nconst b = 2\nconst c = 4\nconst options = a | b | c\n```\n\n通过定义这些选项,可以用按位与操作来判断 a/b/c 是否在 options 中。\n\n``` js\n// 选项 b 是否在选项中\nif (b & options) {\n // ...\n}\n```\n\n### 20. 不要覆盖原生方法\n\n无论你的 JavaScript 代码如何优化,都比不上原生方法。因为原生方法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,特别是数学运算和 DOM 操作。\n\n### 21. 降低 CSS 选择器的复杂性\n\n#### (1). 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取\n\n看个示例\n\n``` css\n#block .text p {\n color: red;\n}\n```\n\n1. 查找所有 P 元素。\n2. 查找结果 1 中的元素是否有类名为 text 的父元素\n3. 查找结果 2 中的元素是否有 id 为 block 的父元素\n\n#### (2). CSS 选择器优先级\n\n```\n内联 > ID选择器 > 类选择器 > 标签选择器\n```\n\n根据以上两个信息可以得出结论。\n\n1. 选择器越短越好。\n2. 尽量使用高优先级的选择器,例如 ID 和类选择器。\n3. 避免使用通配符 *。\n\n最后要说一句,据我查找的资料所得,CSS 选择器没有优化的必要,因为最慢和慢快的选择器性能差别非常小。\n\n### 22. 使用 flexbox 而不是较早的布局模型\n\n在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在,我们有了新的布局方式 [flexbox](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox),它比起早期的布局方式来说有个优势,那就是性能比较好。\n\n下面的截图显示了在 1300 个框上使用浮动的布局开销:\n\n![float布局的元素](https://user-images.githubusercontent.com/8088864/125547454-c911b26b-4a1c-44d8-9044-e83a09dc618d.jpg)\n\n然后我们用 flexbox 来重现这个例子:\n\n![flexbox布局的元素](https://user-images.githubusercontent.com/8088864/125547509-ecf25fd0-a9ef-438c-827a-987ea0bb9ae5.jpg)\n\n现在,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。\n\n不过 flexbox 兼容性还是有点问题,不是所有浏览器都支持它,所以要谨慎使用。\n\n各浏览器兼容性:\n\n- Chrome 29+\n- Firefox 28+\n- Internet Explorer 11\n- Opera 17+\n- Safari 6.1+ (prefixed with -webkit-)\n- Android 4.4+\n- iOS 7.1+ (prefixed with -webkit-)\n\n但是在可能的情况下,至少应研究布局模型对网站性能的影响,并且采用最大程度减少网页执行开销的模型。\n\n在任何情况下,不管是否选择 Flexbox,都应当在应用的高压力点期间尝试完全避免触发布局!\n\n### 23. 使用 transform 和 opacity 属性更改来实现动画\n\n在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。\n\n![使用 transform 和 opacity 属性更改来实现动画](https://user-images.githubusercontent.com/8088864/125547800-ab61c27b-23fb-45bd-9d6a-2585df8d804e.jpeg)\n\n### 24. 合理使用规则,避免过度优化\n\n性能优化主要分为两类:\n\n1. 加载时优化\n2. 运行时优化\n\n上述 23 条建议中,属于加载时优化的是前面 10 条建议,属于运行时优化的是后面 13 条建议。通常来说,没有必要 23 条性能优化规则都用上,根据网站用户群体来做针对性的调整是最好的,节省精力,节省时间。\n\n在解决问题之前,得先找出问题,否则无从下手。所以在做性能优化之前,最好先调查一下网站的加载性能和运行性能。\n\n#### 检查加载性能\n\n一个网站加载性能如何主要看白屏时间和首屏时间。\n\n- 白屏时间:指从输入网址,到页面开始显示内容的时间。\n- 首屏时间:指从输入网址,到页面完全渲染的时间。\n\n将以下脚本放在 \\ 前面就能获取白屏时间。\n\n``` html\n\n```\n\n在 `window.onload` 事件里执行 `new Date() - performance.timing.navigationStart` 即可获取首屏时间。\n\n#### 检查运行性能\n\n配合 chrome 的开发者工具,我们可以查看网站在运行时的性能。\n\n打开网站,按 F12 选择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时可以模仿用户使用网站,在使用完毕后,点击 stop,然后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的情况;如果是绿色,则代表 FPS 很好。performance 的具体使用方法请用搜索引擎搜索一下,毕竟篇幅有限。\n\n通过检查加载和运行性能,相信你对网站性能已经有了大概了解。所以这时候要做的事情,就是使用上述 23 条建议尽情地去优化你的网站,加油!\n\n参考资料:\n\n- [performance.timing.navigationStart](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming/navigationStart)\n\n其他参考资料\n\n- 高性能网站建设指南\n- Web性能权威指南\n- 高性能JavaScript\n- [高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/)\n\n\n## 如何进行网站性能优化\n\n[雅虎 Best Practices for Speeding Up Your Web Site](https://developer.yahoo.com/performance/rules.html):\n\n- content 方面\n 1. 减少 HTTP 请求:合并文件、CSS 精灵、inline Image\n 2. 减少 DNS 查询:DNS 查询完成之前浏览器不能从这个主机下载任何任何文件。方法:DNS 缓存、将资源分布到恰当数量的主机名,平衡并行下载和 DNS 查询\n 3. 避免重定向:多余的中间访问\n 4. 使 Ajax 可缓存\n 5. 非必须组件延迟加载\n 6. 未来所需组件预加载\n 7. 减少 DOM 元素数量\n 8. 将资源放到不同的域下:浏览器同时从一个域下载资源的数目有限,增加域可以提高并行下载量\n 9. 减少 iframe 数量\n 10. 不要 404\n- Server 方面\n 1. 使用 CDN\n 2. 添加 Expires 或者 Cache-Control 响应头\n 3. 对组件使用 Gzip 压缩\n 4. 配置 ETag\n 5. Flush Buffer Early\n 6. Ajax 使用 GET 进行请求\n 7. 避免空 src 的 img 标签\n- Cookie 方面\n 1. 减小 cookie 大小\n 2. 引入资源的域名不要包含 cookie\n- css 方面\n 1. 将样式表放到页面顶部\n 2. 不使用 CSS 表达式\n 3. 使用``不使用@import\n 4. 不使用 IE 的 Filter\n- Javascript 方面\n 1. 将脚本放到页面底部\n 2. 将 javascript 和 css 从外部引入\n 3. 压缩 javascript 和 css\n 4. 删除不需要的脚本\n 5. 减少 DOM 访问\n 6. 合理设计事件监听器\n- 图片方面\n 1. 优化图片:根据实际颜色需要选择色深、压缩\n 2. 优化 css 精灵\n 3. 不要在 HTML 中拉伸图片\n 4. 保证 favicon.ico 小并且可缓存\n- 移动方面\n 1. 保证组件小于 25k\n 2. Pack Components into a Multipart Document\n\n## 强缓存与协商缓存\n\n### 浏览器缓存\n\n当浏览器去请求某个文件的时候,服务端就在response header里面对该文件做了缓存配置。缓存的时间、缓存类型都由服务端控制\n\n#### 缓存优点\n\n1. 减少不必要的数据传输,节省带宽\n2. 减少服务器的负担,提升网站性能\n3. 加快了客户端加载网页的速度,用户体验友好\n\n#### 缓存缺点\n\n资源如果有更改,会导致客户端不及时更新就会造成用户获取信息滞后\n\n#### 缓存流程\n\n浏览器第一次请求时\n\n![浏览器缓存第一次请求](https://user-images.githubusercontent.com/8088864/125554789-a7d7d647-b89f-4c84-a326-5af87e6782f6.png)\n\n浏览器后续在进行请求时\n\n![浏览器缓存再次请求](https://user-images.githubusercontent.com/8088864/125554810-255dcfd2-a1f0-4e09-a329-56bacdee6d22.png)\n\n从上图可以知道,浏览器缓存包括两种类型,即强缓存(本地缓存)和协商缓存,浏览器在第一次请求发生后,再次请求时\n\n- 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(`cache-control`和`expires`信息),若命中直接从缓存中获取资源信息,包括缓存header信息;本次请求根本就不会与服务器进行通信。\n\n请求头信息\n\n```\nAccept: xxx\nAccept-Encoding: gzip,deflate\nAccept-Language: zh-cn\nConnection: keep-alive\nHost: xxx\nReferer: xxx\nUser-Agent: xxx\n```\n\n来自缓存的响应头的信息\n\n```\nAccept-Ranges: bytes\nCache-Control: max-age= xxxx\nContent-Encoding: gzip\nContent-length: 3333\nContent-Type: application/javascript\nDate: xxx\nExpires: xxx\nLast-Modified: xxx\nServer: 服务器\n```\n\n- 如果没有命中强缓存,浏览器会发送请求到服务器,请求会携带第一次请求返回的有关缓存的header字段信息(`Last-Modified`/`If-Modified-Since`和`Etag`/`If-None-Match`),由服务器根据请求中的相关header信息来比对结果是否协商缓存命中;若命中,则服务器返回新的响应header信息更新缓存中的对应header信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容。\n\n强缓存与协商缓存的区别,可以用下表来进行描述:\n\n| | 获取资源形式 | 状态码 | 发送请求到服务器 |\n| ---- | ---- | ---- | ---- |\n| **强缓存** | 从缓存取 | 200(from cache) | 否,直接从缓存取 |\n| **协商缓存** | 从缓存取 | 304(not modified) | 是,正如其名,通过服务器来告知缓存是否可用 |\n\n### 强缓存相关的header字段\n\n强缓存上面已经介绍了,直接从缓存中获取资源而不经过服务器;与强缓存相关的header字段有两个:\n\n1. **expires**: 这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如**Mon, 10 Jun 2015 21:31:12 GMT**,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源。\n2. **cache-control:max-age=number**: 这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;cache-control除了该字段外,还有下面几个比较常用的设置值:\n - **no-cache**: 不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。\n - **no-store**: 直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。\n - **public**: 可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。\n - **private**: 只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。\n\n**注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires。**\n\n### 协商缓存相关的header字段\n\n协商缓存都是由服务器来确定缓存资源是否可用的,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(`Last-Modified`或者`Etag`),则后续请求则会带上对应的请求字段(`If-Modified-Since`或者`If-None-Match`),若响应头没有`Last-Modified`或者`Etag`字段,则请求头也不会有对应的字段。\n\n#### 1. Last-Modified/If-Modified-Since\n\n二者的值都是GMT格式的时间字符串,具体过程:\n\n- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header加上`Last-Modified`的header,这个header表示这个资源在服务器上的最后修改时间\n- 浏览器再次跟服务器请求这个资源时,在request的header上加上`If-Modified-Since`的header,这个header的值就是上一次请求时返回的Last-Modified的值\n- 服务器再次收到资源请求时,根据浏览器传过来`If-Modified-Since`和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回`304 Not Modified`,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回`304 Not Modified`的响应时,response header中不会再添加`Last-Modified`的header,因为既然资源没有变化,那么`Last-Modified`也就不会改变,这是服务器返回304时的response header\n- 浏览器收到304的响应后,就会从缓存中加载资源\n- 如果协商缓存没有命中,浏览器直接从服务器加载资源时,`Last-Modified`的Header在重新加载的时候会被更新,下次请求时,`If-Modified-Since`会启用上次返回的`Last-Modified`值\n\n#### 2. Etag/If-None-Match\n\n这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与**Last-Modified/If-Modified-Since**类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。\n\n### 既生Last-Modified何生Etag\n\n你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:\n\n- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;\n- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);\n- 某些服务器不能精确的得到文件的最后修改时间。\n\n这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。\n\n**注意: Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。**\n\n### 用户的行为对缓存的影响\n\n| 用户操作 | Expires/Cache-Control | Last-Modified/ETag |\n| ---- | ---- | ---- |\n| 地址栏回车 | 有效 | 有效 |\n| 页面链接条状 | 有效 | 有效 |\n| 新开窗口 | 有效 | 有效 |\n| 前进后退 | 有效 | 有效 |\n| F5刷新 | 无效 | 有效 |\n| Ctrl + F5强制刷新 | 无效 | 无效 |\n\n### 强缓存如何重新加载缓存缓存过的资源\n\n使用强缓存时,浏览器不会发送请求到服务端,根据设置的缓存时间浏览器一直从缓存中获取资源,在这期间若资源产生了变化,浏览器就在缓存期内就一直得不到最新的资源,那么如何防止这种事情发生呢?\n\n**通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。**\n\n``` html\n\n...\napp
\n```\n\n这样每次文件改变后就会生成新的query值,这样query值不同,也就是页面引用的资源路径不同了,之前缓存过的资源就被浏览器忽略了,因为资源请求的路径变了。\n\n## HTTP 各版本特点与区别\n\nHTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。\n\n| 版本 | 功能 | 备注 |\n| ---- | ---- | ---- |\n| HTTP 0.9 | 仅支持 Get
仅能访问 HTML 格式资源 | 简单单一 |\n| HTTP 1.0 | 新增POST,DELETE,PUT,HEADER等方式
增加请求头和响应头概念,指定协议版本号,携带其他元信息(状态码、权限、缓存、内容编码)
扩展传输内容格式(图片、音视频、二进制等都可以传输) | 存活时间短 |\n| HTTP 1.1 | 长连接:新增 Connection 字段,可以通过keep-alive保持长连接
管道化:一次连接就形成一次管道,管道内进行多次有序响应。允许向服务端发生多次请求,但是响应按序返回
缓存处理:新增 cache-control 和 etag 首部字段
断点续传
状态码增加 | 当前主流版本号
存在Header 重复问题 |\n| HTTP 2.0 | 二进制分帧:数据体和头信息可以都是二进制,统称帧
多路复用与数据流:能同时发送和响应多个请求,通过数据流来传输
头部压缩:对 Header 进行压缩,避免重复浪费
服务器推送:服务器可以向客户端主动发送资源 | 2005发布 |\n\n### 1、HTTP 0.9\n\nHTTP 0.9是第一个版本的HTTP协议,已过时。它的组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有协议头,造成了HTTP 0.9协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。\n\nHTTP 0.9具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。由此可见,HTTP协议的无状态特点在其第一个版本0.9中已经成型。一次HTTP 0.9的传输首先要建立一个由客户端到Web服务器的TCP连接,由客户端发起一个请求,然后由Web服务器返回页面内容,然后连接会关闭。如果请求的页面不存在,也不会返回任何错误码。\n\n### 2、HTTP 1.0\n\nHTTP协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用。相对于HTTP 0.9 增加了如下主要特性:\n\n- 请求与响应支持头域\n- 响应对象以一个响应状态行开始\n- 响应对象不只限于超文本\n- 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法\n- (短连接)每一个请求建立一个TCP连接,请求完成后立马断开连接。这将会导致2个问题:连接无法复用,队头阻塞(head of line blocking)。连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类请求影响较大。队头阻塞(head of line blocking)\n\n### 3、HTTP 1.1\n\nHTTP协议的第三个版本是HTTP 1.1,是目前使用最广泛的协议版本 。HTTP 1.1是目前主流的HTTP协议版本,因此这里就多花一些笔墨介绍一下HTTP 1.1的特性。\n\nHTTP 1.1引入了许多关键性能优化:keepalive连接,chunked编码传输,字节范围请求,请求流水线等\n\n#### Persistent Connection(keepalive连接)\n\n允许HTTP设备在事务处理结束之后将TCP连接保持在打开的状态,以便未来的HTTP请求重用现在的连接,直到客户端或服务器端决定将其关闭为止。在HTTP1.0中使用长连接需要添加请求头 `Connection: Keep-Alive`,而在HTTP 1.1 所有的连接默认都是长连接,除非特殊声明不支持( HTTP请求报文首部加上`Connection: close` )。服务器端按照FIFO原则来处理不同的Request。\n\n![长连接(keepalive连接)](https://user-images.githubusercontent.com/8088864/125572282-1b48362e-ed29-42a1-9882-3710ab106b76.jpg)\n\n#### chunked编码传输\n\n该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束,这在实体长度未知时特别有用(比如由数据库动态产生的数据)\n\n#### 字节范围请求\n\nHTTP1.1支持传送内容的一部分。比方说,当客户端已经有内容的一部分,为了节省带宽,可以只向服务器请求一部分。该功能通过在请求消息中引入了range头域来实现,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content)\n\n#### 断点续传\n\nHeader 字段\n\n服务端\n\nAccept-Ranges:表示服务器支持断点续传,并且数据传输以字节为单位\n\nEtag:资源的唯一 tag 后端自定义,验证文件是否修改过。修改过就重新重头传输\n\nLast-Modified:文件上次修改时间\n\nContent-Range:返回数据范围\n\n客户端\n\nIf-Range:服务器给的 Etag 值\n\nRange:请求的数据范围\n\nIf-Modified-Since: 将服务器响应的 Last-Modified 保存, 下次发送可以携带,后台接受判断文件是否修改,没有可以返回 304状态码,叫客户端使用缓存数据,避免重复发出资源。\n\n流程\n\n![断点续传](https://user-images.githubusercontent.com/8088864/125573335-f1eda73b-ad4f-470a-808f-caa393e38b2e.png)\n\n**注意:断点续传后台返回状态码为 206。**\n\n#### Pipelining(请求流水线)\n\n#### 其他特性\n\n另外,HTTP 1.1还新增了如下特性:\n\n- 请求消息和响应消息都支持Host头域:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。因此,Host头的引入就很有必要了。\n- 新增了一批Request method:HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT方法\n- 缓存处理:HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。\n\n### 4、HTTP 2.0\n\nHTTP 2.0是下一代HTTP协议。主要特点有:\n\n#### 二进制分帧\n\nHTTP 2.0最大的特点:不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。\n\n![二进制分帧](https://user-images.githubusercontent.com/8088864/125574741-7645e5f9-3476-44f3-94eb-4a3aaebce2ae.jpg)\n\n#### 多路复用\n\nHTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。\n\n通过单一的 HTTP2.0连接连续发起多重请求-响应消息,即客户端和服务器可以同时发送多个请求和响应,而不用顺序一一对应。\n\n每个数据流以HTTP消息的形式发送,HTTP消息被分为独立的帧,然后由一或多个帧组成,这些帧可以乱序发送,接收端根据这些帧的标识符号和首部将信息重新组装起来。\n\n默认什么情况下使用同一个连接\n\n- 同一个域名下的资源\n- 不同域名但是满足两个条件:1)解析到同一个 IP;2)使用同一个证书\n\n#### 头部压缩\n\n当一个客户端向相同服务器请求许多资源时,像来自同一个网页的图像,将会有大量的请求看上去几乎同样的,这就需要压缩技术对付这种几乎相同的信息。\n\n由于头信息使用文本,没有压缩,请求时候会来回重复传递,造成流量浪费。\n\n参考[HTTP2头部压缩技术介绍](https://imququ.com/post/header-compression-in-http2.html)\n\n头部压缩需要支持 HTTP2的浏览器和服务器之间:\n\n- 维护一份相同的静态字典(包含常见的头部名称,以及常见的头部名称与值的组合)\n- 维护一份相同的动态字典,动态添加内容(即实际的 Header 值)\n- 支持基于静态哈夫曼码表的哈夫曼编码(uffman Coding)\n\n原理图:\n\n![http头部压缩原理](https://user-images.githubusercontent.com/8088864/125578550-82fd62aa-eb21-4813-87d1-19904e1b42fc.png)\n\n总结: 通过映射表,传递对应编码和值来达到压缩。\n\n#### 随时复位\n\nHTTP1.1一个缺点是当HTTP信息有一定长度大小数据传输时,你不能方便地随时停止它,中断TCP连接的代价是昂贵的。使用HTTP2的RST_STREAM将能方便停止一个信息传输,启动新的信息,在不中断连接的情况下提高带宽利用效率。\n\n#### 服务器端推流\n\nServer Push。客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。\n\n#### 优先权和依赖\n\n每个流都有自己的优先级别,会表明哪个流是最重要的,客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。\n\n## 队头阻塞以及解决办法\n\n### 前言\n\n通常我们提到队头阻塞,指的可能是TCP协议中的队头阻塞,但是HTTP1.1中也有一个类似TCP队头阻塞的问题,下面各自介绍一下。\n\n### TCP队头阻塞\n\n队头阻塞(head-of-line blocking)发生在一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止。该后续分节的延迟递送确保接收应用进程能够按照发送端的发送顺序接收数据。这种为了达到完全有序而引入的延迟机制,非常有用,但也有不利之处。\n\n假设在单个TCP连接上发送语义独立的消息,比如说服务器可能发送3幅不同的图像供Web浏览器显示。为了营造这几幅图像在用户屏幕上并行显示的效果,服务器先发送第一幅图像的一个断片,再发送第二幅图像的一个断片,然后再发送第三幅图像的一个断片;服务器重复这个过程,直到这3幅图像全部成功地发送到浏览器为止。\n\n要是第一幅图像的某个断片内容的TCP分节丢失了,客户端将保持已到达的不按序的所有数据,直到丢失的分节重传成功。这样不仅延缓了第一幅图像数据的递送,也延缓了第二幅和第三幅图像数据的递送。\n\n### HTTP队头阻塞\n\n上面用浏览器请求图片资源举例子,但实际上HTTP自身也有类似TCP队头阻塞的情况。要介绍HTTP队头阻塞,就需要先讲讲HTTP的管道化(pipelining)。\n\n#### HTTP管道化是什么\n\nHTTP1.1 允许在持久连接上可选的使用请求管道。这是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列,当第一条请求发往服务器的时候,第二第三条请求也可以开始发送了,在高延时网络条件下,这样做可以降低网络的环回时间,提高性能。\n\n非管道化与管道化的区别示意图\n\n![HTTP非管道化与管道化](https://user-images.githubusercontent.com/8088864/125586316-36604fa7-fcc1-453b-9ae3-4c84b39690bd.png)\n\n#### HTTP管道化产生的背景\n\n在一般情况下,HTTP遵守“请求-响应”的模式,也就是客户端每次发送一个请求到服务端,服务端返回响应。这种模式非常容易理解,但是效率并不是那么高,为了提高速度和效率,人们做了很多尝试:\n\n- 最简单的情况下,服务端一旦返回响应后就会把对应的连接关闭,客户端的多个请求实际上是串行发送的。\n- 除此之外,客户端可以选择同时创建多个连接,在多个连接上并行的发送不同请求。但是创建更多连接也带来了更多的消耗,当前大部分浏览器都会限制对同一个域名的连接数。\n- 从HTTP1.0开始增加了持久连接的概念(HTTP1.0的Keep-Alive和HTTP1.1的persistent),可以使HTTP能够复用已经创建好的连接。客户端在收到服务端响应后,可以复用上次的连接发送下一个请求,而不用重新建立连接。\n- 现代浏览器大多采用并行连接与持久连接共用的方式提高访问速度,对每个域名建立并行地少量持久连接。\n- 而在持久连接的基础上,HTTP1.1进一步地支持在持久连接上使用管道化(pipelining)特性。管道化允许客户端在已发送的请求收到服务端的响应之前发送下一个请求,借此来减少等待时间提高吞吐;如果多个请求能在同一个TCP分节发送的话,还能提高网络利用率。但是因为HTTP管道化本身可能会导致队头阻塞的问题,以及一些其他的原因,现代浏览器默认都关闭了管道化。\n\n#### HTTP管道化的限制\n\n1. 管道化要求服务端按照请求发送的顺序返回响应(FIFO),原因很简单,HTTP请求和响应并没有序号标识,无法将乱序的响应与请求关联起来。\n2. 客户端需要保持未收到响应的请求,当连接意外中断时,需要重新发送这部分请求。\n3. 只有幂等的请求才能进行管道化,也就是只有GET和HEAD请求才能管道化,否则可能会出现意料之外的结果\n\n#### HTTP管道化引起的请求队头阻塞\n\n前面提到HTTP管道化要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达。\n\n### 如何解决队头阻塞\n\n#### 如何解决HTTP队头阻塞\n\n对于HTTP1.1中管道化导致的请求/响应级别的队头阻塞,可以使用HTTP2解决。HTTP2不使用管道化的方式,而是引入了帧、消息和数据流等概念,每个请求/响应被称为消息,每个消息都被拆分成若干个帧进行传输,每个帧都分配一个序号。每个帧在传输是属于一个数据流,而一个连接上可以存在多个流,各个帧在流和连接上独立传输,到达之后在组装成消息,这样就避免了请求/响应阻塞。\n\n当然,即使使用HTTP2,如果HTTP2底层使用的是TCP协议,仍可能出现TCP队头阻塞。\n\n#### 如何解决TCP队头阻塞\n\nTCP中的队头阻塞的产生是由TCP自身的实现机制决定的,无法避免。想要在应用程序当中避免TCP队头阻塞带来的影响,只有舍弃TCP协议。\n\n比如google推出的QUIC协议,在某种程度上可以说避免了TCP中的队头阻塞,因为它根本不使用TCP协议,而是在UDP协议的基础上实现了可靠传输。而UDP是面向数据报的协议,数据报之间不会有阻塞约束。\n\n此外还有一个SCTP(流控制传输协议),它是和TCP、UDP在同一层次的传输协议。SCTP的多流特性也可以尽可能的避免队头阻塞的情况。\n\n### 总结\n\n从TCP队头阻塞和HTTP队头阻塞的原因我们可以看到,出现队头阻塞的原因有两个:\n\n 1. 独立的消息数据都在一个链路上传输,也就是有一个“队列”。比如TCP只有一个流,多个HTTP请求共用一个TCP连接\n 2. 队列上传输的数据有严格的顺序约束。比如TCP要求数据严格按照序号顺序,HTTP管道化要求响应严格按照请求顺序返回\n\n所以要避免队头阻塞,就需要从以上两个方面出发,比如quic协议不使用TCP协议而是使用UDP协议,SCTP协议支持一个连接上存在多个数据流等等。\n\n## QUIC\n\nQUIC(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP传输层协议,同时兼具TCP、TLS、HTTP/2等协议的可靠性与安全性,可以有效减少连接与传输延迟,更好地应对当前传输层与应用层的挑战。\n\n### QUIC的由来:为什么是UDP而非TCP?\n\nUDP和TCP都属于传输层协议。TCP是面向连接的,更强调的是传输的可靠性,通过TCP连接传送的数据,无差错,不丢失,不重复,按序到达,但是因为TCP在传递数据之前会有三次握手来建立连接,所以效率低、占用系统的CPU、内存等硬件资源较高;而UDP的无连接的(即发送数据之前不需要建立连接),只需要知道对方地址即可发送数据,具有较好的实时性,工作效率比TCP高,占用系统资源比TCP少,但是在数据传递时,如果网络质量不好,就会很容易丢包。\n\n我们知道,大部分Web平台的数据传输都基于TCP协议。实际上,TCP在设计之初,网络环境复杂、丢包率高、网速差,所以TCP可以完美解决可靠性的问题。而如今的网络环境和网速都已经取得了巨大的改善,网络传输可靠性已经不再是棘手的问题。另外,TCP还有一个很大的问题是更新非常困难。这是因为:TCP网络协议栈的实现依赖于系统内核更新,一旦系统内核更新,终端设备、中间设备的系统更新都会非常缓慢,迭代需要花费几年甚至十几年的时间,这显然跟不上当今互联网的发展速度。所以现在解法就是,抛弃TCP而使用UDP,来实现低延迟的传输需求。\n\n![QUIC is very similar to TCP TLS HTTP 2 0 implemented on UDP](https://user-images.githubusercontent.com/8088864/125581409-742f54c2-93aa-4d3a-919e-d3710b318361.jpg)\n\n为了结合两者优点,谷歌公司推出了QUIC,它的升级不依赖于系统内核,只需要Client和Server端更新到指定版本。如此一来,基于UDP的QUIC就能月更甚至周更,很好的解决了TCP部署和更新的困难,更灵活地实现部署和更新。\n\n### 为什么要用QUIC?\n\n#### 1. 建连延迟低\n\n网民传统TCP三次握手+TLS1`~`2RTT握手+http数据,基于TCP的HTTPS一次建连至少需要2`~`3个RTT,而QUIC基于UDP,完整握手只需要1RTT乃至0RTT,可以显著降低延迟。\n\n![QUIC握手](https://user-images.githubusercontent.com/8088864/125584078-81044014-9ed7-47ba-93a4-24623b716b07.jpg)\n\n#### 2. 安全又可靠\n\nQUIC具备TCP、TLS、HTTPS/2等协议的安全、可靠性的特点,通过提供安全功能(如身份验证和加密)来实现加密传输,这些功能由传输协议本身的更高层协议(如TLS)来实现。\n\n#### 3. 改造灵活\n\nQUIC在应用程序层面就能实现不同的拥塞控制算法,不需要操作系统和内核支持,这相比于传统的TCP协议改造灵活性更好。\n\n#### 4. 改进的拥塞控制\n\nQUIC主要实现了TCP的慢启动、拥塞避免、快重传、快恢复。在这些拥塞控制算法的基础上改进,例如单调递增的 Packet Number,解决了重传的二义性,确保RTT准确性,减少重传次数。\n\n#### 5. 无队头阻塞的多路复用\n\nHTTP2实现了多路复用,可以在一条TCP流上并发多个HTTP请求,但基于TCP的HTTP2在传输层却有个问题,TCP无法识别不同的HTTP2流,实际收数据仍是一个队列,当后发的流先收到时,会因前面的流未到达而被阻塞。QUIC一个connection可以复用传输多个stream,每个stream之间都是独立的,一个stream的丢包不会影响到其他stream的接收和处理。\n\n![QUIC特点](https://user-images.githubusercontent.com/8088864/125585210-a874fcb0-87ab-46a5-b254-825c78034943.jpg)\n\n综上所述,QUIC具有众多优点,它融合了UDP协议的速度、性能与TCP的安全与可靠,大大优化了互联网传输体验。\n\n作为提升终端用户访问效率的CDN服务,其节点之间存在大量数据互通,节点之间的网络连接、传输架构等因素都会对CDN服务质量产生影响。而将QUIC应用在CDN系统中,CDN用户开启QUIC功能后,系统将遵循QUIC协议进行用户IP请求处理,既能满足安全传输的需求,也能提升传输效率。\n\n### QUIC对客户端的要求\n\n- 如果您使用Chrome浏览器,则只支持QUIC协议Q43版本。当前阿里云CDN的QUIC协议是Q39版本,不支持直接对阿里云CDN发起QUIC请求。\n- 如果您使用自研App,则App必须集成支持QUIC协议的网络库,例如:lsquic-client或cronet网络库。\n\n### QUIC应用场景\n\n1. 图片小文件:明显降低文件下载总耗时,提升效率\n2. 视频点播:提升首屏秒开率,降低卡顿率,提升用户观看体验\n3. 动态请求:适用于动态请求,提升访问速度,如网页登录、交易等交互体验提升\n4. 弱网环境:在丢包和网络延迟严重的情况下仍可提供可用的服务,并优化卡顿率、请求失败率、秒开率、提高连接成功率等传输指标\n5. 大并发连接:连接可靠性强,支持页面资源数较多、并发连接数较多情况下的访问速率提升\n6. 加密连接:具备安全、可靠的传输性能\n\n\n## HTTP协议\n\n一面中,如果有笔试,考HTTP协议的可能性较大。\n\n### 1. 前言\n\n一面要讲的内容:\n\n- `HTTP`协议的主要特点\n- `HTTP`报文的组成部分\n- `HTTP`方法\n- `get` 和 `post`的区别\n- `HTTP`状态码\n- 什么是持久连接\n- 什么是管线化\n\n二面要讲的内容;\n\n- 缓存\n- `CSRF`攻击\n- TLS 协商\n\n### 2. HTTP协议的主要特点\n\n- 简单快速\n- 灵活\n- **无连接**\n- **无状态**\n\n> 通常我们要答出以上四个内容。如果实在记不住,一定要记得后面的两个:**无连接、无状态**。\n\n\n我们分别来解释一下。\n\n\n#### 2.1 简单快速\n\n> **简单**:每个资源(比如图片、页面)都通过 url 来定位。这都是固定的,在`http`协议中,处理起来也比较简单,想访问什么资源,直接输入url即可。\n\n\n#### 2.2 灵活\n\n> `http`协议的头部有一个`数据类型`,通过`http`协议,就可以完成不同数据类型的传输。\n\n#### 2.3 无连接\n\n> 连接一次,就会断开,不会继续保持连接。\n\n#### 2.4 无状态\n\n> 客户端和服务器端是两种身份。第一次请求结束后,就断开了,第二次请求时,**服务器端并没有记住之前的状态**,也就是说,服务器端无法区分客户端是否为同一个人、同一个身份。\n\n> 有的时候,我们访问网站时,网站能记住我们的账号,这个是通过其他的手段(比如 `session`)做到的,并不是`http`协议能做到的。\n\n\n### 3 HTTP报文的组成部分\n\n![](https://github.com/hankliu62/interview/assets/8088864/04027cd7-2135-4442-a1f5-90e9bb637664)\n\n> 在回答此问题时,我们要按照顺序回答:\n\n- 先回答的是,`http`报文包括:**请求报文**和**响应报文**。\n- 再回答的是,每个报文包含什么部分。\n- 最后回答,每个部分的内容是什么\n\n#### 3.1 请求报文包括:\n\n![](https://github.com/hankliu62/interview/assets/8088864/9872cd7e-0d2a-48f1-b9aa-7068eed8603c)\n\n- 请求行:包括请求方法、请求的`url`、`http`协议及版本。\n- 请求头:一大堆的键值对。\n- **空行**指的是:当服务器在解析请求头的时候,如果遇到了空行,则表明,后面的内容是请求体\n- 请求体:数据部分。\n\n#### 3.2 响应报文包括:\n\n![](https://github.com/hankliu62/interview/assets/8088864/a19f9ded-ea43-49c6-9b0d-74edb48843a6)\n\n\n- 状态行:`http`协议及版本、状态码及状态描述。\n- 响应头\n- 空行\n- 响应体\n\n\n### 4 HTTP方法\n\n包括:\n\n- `GET`:获取资源\n- `POST`:传输资源\n- `put`:更新资源\n- `DELETE`:删除资源\n- `HEAD`:获得报文首部\n\n> `HTTP`方法有很多,但是上面这五个方法,要求在面试时全部说出来,不要漏掉。\n\n- `get` `和 `post` 比较常见。\n- `put` 和 `delete` 在实际应用中用的很少。况且,业务中,一般不删除服务器端的资源。\n- `head` 可能偶尔用的到。\n\n\n### 5 get 和 post的区别\n\n![](https://github.com/hankliu62/interview/assets/8088864/48572bc6-d23e-4757-aa83-68fbec4f08d2)\n\n- 区别有很多,如果记不住,面试时,至少要任意答出其中的三四条。\n- 有一点要强调,**get是相对不隐私的,而post是相对隐私的**。\n\n> 我们大概要记住以下几点:\n\n1. 浏览器在回退时,`get` **不会重新请求**,但是`post`会重新请求。【重要】\n2. `get`请求会被浏览器**主动缓存**,而`post`不会。【重要】\n3. `get`请求的参数,会报**保留**在浏览器的**历史记录**里,而`post`不会。做业务时要注意。为了防止`CSRF`攻击,很多公司把`get`统一改成了`post`。\n4. `get`请求在`url`中`传递的参数有大小限制,基本是`2kb`,不同的浏览器略有不同。而post没有注意。\n5. `get`的参数是直接暴露在`url`上的,相对不安全。而`post`是放在请求体中的。\n\n\n### 6 http状态码\n\n> `http`状态码分类:\n\n![](https://github.com/hankliu62/interview/assets/8088864/b085af71-e367-4699-8c96-0be02cd4d13f)\n\n> 常见的`http`状态码:\n\n![](https://github.com/hankliu62/interview/assets/8088864/10e64fd5-3a09-4141-8496-e9b1c4be25ac)\n\n\n**部分解释**:\n\n- `206`的应用:`range`指的是请求的范围,客户端只请求某个大文件里的一部分内容。比如说,如果播放视频地址或音频地址的前面一部分,可以用到`206`。\n- `301`:重定向(永久)。\n- `302`:重定向(临时)。\n- `304`:我这个服务器告诉客户端,你已经有缓存了,不需要从我这里取了。\n\n![](https://github.com/hankliu62/interview/assets/8088864/8cc31f2e-f801-45c5-a603-a009a44a602a)\n\n- `400`和`401`用的不多,未授权。`403`指的是请求被拒绝。`404`指的是资源不存在。\n\n### 7 持久链接/http长连接\n\n> 如果你能答出持久链接,这是面试官很想知道的一个点。\n\n- **轮询**:`http1.0`中,客户端每隔很短的时间,都会对服务器发出请求,查看是否有新的消息,只要轮询速度足够快,例如`1`秒,就能给人造成交互是实时进行的印象。这种做法是无奈之举,实际上对服务器、客户端双方都造成了大量的性能浪费。\n- **长连接**:`HTTP1.1`中,通过使用`Connection:keep-alive`进行长连接,。客户端只请求一次,但是服务器会将继续保持连接,当再次请求时,避免了重新建立连接。\n\n> 注意,`HTTP 1.1`默认进行持久连接。在一次 `TCP` 连接中可以完成多个 `HTTP` 请求,但是对**每个请求仍然要单独发 header**,`Keep-Alive`不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如`Apache`)中设定这个时间。\n\n\n### 8 长连接中的管线化\n\n> 如果能答出**管线化**,则属于加分项。\n\n#### 8.1 管线化的原理\n\n> 长连接时,**默认**的请求这样的:\n\n```\n 请求1 --> 响应1 -->请求2 --> 响应2 --> 请求3 --> 响应3\n```\n\n\n> 管线化就是,我把现在的请求打包,一次性发过去,你也给我一次响应回来。\n\n\n#### 8.2 管线化的注意事项\n\n> 面试时,不会深究管线化。如果真要问你,就回答:“我没怎么研究过,准备回去看看~”\n\n### 9 TLS 协商\n\nTransport Layer Security (TLS) 是一个为计算机网络提供通信安全的加密协议。它广泛应用于大量应用程序,其中之一即浏览网页。网站可以使用 TLS 来保证服务器和网页浏览器之间的所有通信安全。\n\n整个 TLS 握手过程包含以下几个步骤:\n\n- 客户端向服务器发送 『Client hello』 信息,附带着客户端随机值(random_C)和支持的加密算法组合。\n- 服务器返回给客户端 『Server hello』信息,附带着服务器随机值(random_S),以及选择一个客户端发送过来加密算法。\n- 服务器返回给客户端认证证书及或许要求客户端返回一个类似的证书,认证证书里面携带服务端的公钥信息。\n- 服务器返回『Server hello done』信息。\n- 如果服务器要求客户端发送一个证书,客户端进行发送。\n- 客户端创建一个随机的 Pre-Master 密钥然后使用服务器证书中的公钥来进行加密,向服务器发送加密过的 Pre-Master 密钥。\n- 服务器收到 Pre-Master 密钥。服务器和客户端各自生成基于 Pre-Master 密钥的主密钥和会话密钥。两个明文随机数 random_C 和 random_S 与自己计算产生的 pre-master,计算得到协商密钥enc_key=Fuc(random_C, random_S, pre-master)\n- 客户端给服务器发送一个 『Change cipher spec』的通知,表明客户端将会开始使用协商密钥和加密算法进行加密通信。\n- 客户端也发送了一个 『Client finished』的消息。\n- 服务器接收到『Change cipher spec』的通知然后使用协商密钥和加密算法进行加密通信。\n- 服务器返回客户端一个 『Server finished』消息。\n- 客户端和服务器现在可以通过建立的安全通道来交换程序数据。所有客户端和服务器之间发送的信息都会使用会话密钥进行加密。\n\n每当发生任何验证失败的时候,用户会收到警告。比如服务器使用自签名的证书。\n\n## WebRTC的优缺点\n\nWebRTC,即网页即时通信(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的API。\n\n目前几乎所有主流浏览器都支持了 WebRTC,越来越多的公司正在使用 WebRTC 并且将其加到自己的应用程序中。在浏览器端,依赖于浏览器获取音视频的能力,以及强大的网页上的渲染能力,就能够为高清的通信体验打下基础。同时,相比移动端来说,屏幕比较大,视窗选择也比较灵活。\n\n### 优点\n\n1. 方便。对于用户来说,在WebRTC出现之前想要进行实时通信就需要安装插件和客户端,但是对于很多用户来说,插件的下载、软件的安装和更新这些操作是复杂而且容易出现问题的,现在WebRTC技术内置于浏览器中,用户不需要使用任何插件或者软件就能通过浏览器来实现实时通信。对于开发者来说,在Google将WebRTC开源之前,浏览器之间实现通信的技术是掌握在大企业手中,这项技术的开发是一个很困难的任务,现在开发者使用简单的HTML标签和JavaScript API就能够实现Web音/视频通信的功能。\n2. 免费。虽然WebRTC技术已经较为成熟,其集成了最佳的音/视频引擎,十分先进的codec,但是Google对于这些技术不收取任何费用。\n3. 强大的打洞能力。WebRTC技术包含了使用STUN、ICE、TURN、RTP-over-TCP的关键NAT和防火墙穿透技术,并支持代理。\n\n### 缺点\n\n1. 缺乏服务器方案的设计和部署。\n2. 传输质量难以保证。WebRTC的传输设计基于P2P,难以保障传输质量,优化手段也有限,只能做一些端到端的优化,难以应对复杂的互联网环境。比如对跨地区、跨运营商、低带宽、高丢包等场景下的传输质量基本是靠天吃饭,而这恰恰是国内互联网应用的典型场景。\n3. WebRTC比较适合一对一的单聊,虽然功能上可以扩展实现群聊,但是没有针对群聊,特别是超大群聊进行任何优化。\n4. 设备端适配,如回声、录音失败等问题层出不穷。这一点在安卓设备上尤为突出。由于安卓设备厂商众多,每个厂商都会在标准的安卓框架上进行定制化,导致很多可用性问题(访问麦克风失败)和质量问题(如回声、啸叫)。\n5. 对Native开发支持不够。WebRTC顾名思义,主要面向Web应用,虽然也可以用于Native开发,但是由于涉及到的领域知识(音视频采集、处理、编解码、实时传输等)较多,整个框架设计比较复杂,API粒度也比较细,导致连工程项目的编译都不是一件容易的事。\n\n## EventSource和轮询的优缺点\n\n### EventSource\n\n#### 简介\n\nEventSource 是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。\n\n一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。\n\n与 WebSockets,不同的是,服务端推送是单向的。数据信息被单向从服务端到客户端分发. 当不需要以消息形式将数据从客户端发送到服务器时,这使它们成为绝佳的选择。例如,对于处理社交媒体状态更新,新闻提要或将数据传递到客户端存储机制(如IndexedDB或Web存储)之类的,EventSource无疑是一个有效方案。\n\n- `EventSource`(Server-sent events)简称SSE用于向服务端发送事件,它是基于http协议的单向通讯技术,以`text/event-stream`格式接受事件,如果不关闭会一直处于连接状态,直到调用`EventSource.close()`方法才能关闭连接;\n- `EvenSource`本质上也就是`XHR-streaming`只不过浏览器给它提供了标准的API封装和协议。\n- 由于`EventSource`是单向通讯,所以只能用来实现像股票报价、新闻推送、实时天气这些只需要服务器发送消息给客户端场景中。\n- `EventSource`虽然不支持双向通讯,但是在功能设计上他也有一些优点比如可以自动重连接,event IDs,以及发送随机事件的等功能\n\n`EventSource`案例浏览器端代码如下所示:\n\n``` js\n// 实例化 EventSource 参数是服务端监听的路由\nvar source = new EventSource('http://localhost:3000');\n\nsource.onopen = function (event) { // 与服务器连接成功回调\n console.log('成功与服务器连接');\n}\n\n// 监听从服务器发送来的所有没有指定事件类型的消息(没有event字段的消息)\nsource.onmessage = function (event) { // 监听未命名事件\n console.log('未命名事件', event.data);\n}\n\nsource.onerror = function (error) { // 监听错误\n console.log('错误');\n}\n\n// 监听指定类型的事件(可以监听多个)\nsource.addEventListener(\"ping\", function (event) {\n console.log(\"ping\", event.data)\n})\n```\n\n服务器端\n\n``` js\nconst http = require('http');\n\nhttp.createServer((req, res) => {\n res.writeHead(200, {\n 'Content-Type' :'text/event-stream',\n 'Access-Control-Allow-Origin':'*'\n });\n\n let i = 0;\n const timer = setInterval(()=>{\n const date = {date:new Date()}\n var content ='event: ping\\n'+\"data:\"+JSON.stringify(date)+\"\" +\"\\n\\n\";\n res.write(content);\n },1000)\n\n res.connection.on(\"close\", function(){\n res.end();\n clearInterval(timer);\n console.log(\"Client closed connection. Aborting.\");\n });\n\n}).listen(3000);\nconsole.log('server is run http://localhost:3000');\n```\n\n#### EventSource规范字段\n\n- **event**: 事件类型,如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的字段值,你可以使用addEventListener()方法在当前EventSource对象上监听任意类型的命名事件,如果该条消息没有event字段,则会触发onmessage属性上的事件处理函数。\n- **data**: 消息的数据字段,如果该消息包含多个data字段,则客户端会用换行符把他们连接成一个字符串来处理\n- **id**: 事件ID,会成为当前EventSource对象的内部属性“最后一个事件ID”的属性值;\n- **retry**: 一个整数值,指定了重新连接的时间(单位为毫秒),如果该字段不是整数,则会被忽略。\n\n#### EventSource属性\n\n- **EventSource.onerror**: 是一个 EventHandler,当发生错误时被调用,并且在此对象上派发 error 事件。\n- **EventSource.onmessage**: 是一个 EventHandler,当收到一个 message事件,即消息来自源头时被调用。\n- **EventSource.onopen**: 是一个 EventHandler,当收到一个 open 事件,即连接刚打开时被调用。\n- **EventSource.readyState**(只读): 一个 unsigned short 值,代表连接状态。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。\n- **EventSource.url**(只读): 一个DOMString,代表源头的URL。\n\n#### EventSource 通讯过程\n\n![EventSource通讯过程](https://user-images.githubusercontent.com/8088864/125590756-ffd10207-83de-4166-a8b5-9fc848c191cc.png)\n\n#### 缺点\n\n1. 因为是服务器->客户端的,所以它不能处理客户端请求流\n2. 因为是明确指定用于传输UTF-8数据的,所以对于传输二进制流是低效率的,即使你转为base64的话,反而增加带宽的负载,得不偿失。\n\n### 轮询\n\n#### 短轮询(Polling)\n\n是一种简单粗暴,同样也是一种效率低下的实现“实时”通讯方案,这种方案的原理就是定期向服务器发送请求,主动拉取最新的消息队列。\n\n客户端代码:\n\n``` js\nfunction Polling() {\n fetch(url).then(data => {\n // somthing\n }).catch(err => {\n console.error(err);\n });\n}\n\n//每5s执行一次\nsetInterval(polling, 5000);\n```\n\n![短轮询流程](https://user-images.githubusercontent.com/8088864/125591641-814c4239-47e3-41da-ad9e-a0c7e64dfe72.png)\n\n这种轮询方式比较适合服务器信息定期更新的场景,如天气预报股票行情等,每隔一段时间会进行更新,且轮询间隔的服务器更新频率保持一致是比较理想的方式,但很多多时候会因网络或者服务器出现阻塞早场事件间隔不一致。\n\n优点:\n\n- 可以看到实现非常简单,它的兼容性也比较好的只要支持http协议就可以用这种方式实现\n\n缺点:\n\n- 资源浪费: 比如轮询的间隔小于服务器信息跟新频率,会浪费很多HTTP请求,消耗宝贵的CPU时间和带宽。\n- 容易导致请求轰炸: 例如当服务器负载比较高时,第一个请求还没有处理完,这时第三、第四个请求接踵而来,无用的额外请求对服务器端进行了轰炸。\n\n#### 长轮询(Long Polling)\n\n这是一种优化的轮询方式,称为长轮询,sockjs就是使用的这种轮询方式,长轮询值的是浏览器发送一个请求到服务器,服务器只有在有可用的新数据时才会响应。\n\n客户端代码:\n\n``` js\nfunction LongPolling() {\n fetch(url).then(data => {\n LongPolling();\n }).catch(err => {\n LongPolling();\n console.log(err);\n });\n}\nLongPolling();\n```\n\n![长轮询流程](https://user-images.githubusercontent.com/8088864/125592542-e5c7fb6b-18b8-434f-a4ee-f986684dcbbf.png)\n\n客户端向服务器发送一个消息获取请求时,服务器会将当前的消息队列返回给客户端,然后关闭连接。当消息队列为空的时,服务器不会立即关闭连接,而是等待指定的时间间隔,如果在这个时间间隔内没有新的消息,则由客户端主动超时关闭连接。\n\n相比Polling,客户端的轮询请求只有在上一个请求连接关闭后才会重新发起。这就解决了Polling的请求轰炸问题。服务器可以控制的请求时序,因为在服务器未响应之前,客户端不会发送额为的请求。\n\n优点:\n\n- 长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。\n\n缺点:\n\n- 连接挂起也会导致资源的浪费。\n\n### EventSource VS 轮询\n\n| | 轮询(Polling) | 长轮询(Long-Polling) | EventSource |\n| ---- | ---- | ---- | ---- |\n| 通信协议 | http | http | http |\n| 触发方式 | client(客户端) | client(客户端) | client、server(客户端、服务端) |\n| 优点 | 兼容性好容错性强,实现简单 | 比短轮询节约服务器资源 | 实现简便,开发成本低 |\n| 缺点 | 安全性差,占较多的内存资源与请求数量,容易对服务器造成压力,请求时间间隔容易导致不一致 | 安全性差,占较多的内存资源与请求数,请求时间间隔容易导致不一致 | 只适用高级浏览器,老版本的浏览器不兼容 |\n| 延迟 | 非实时,延迟取决于请求间隔 | 非实时,延迟取决于请求间隔 | 非实时,默认3秒延迟,延迟可自定义 |\n\n### 总结\n\n通过对上面两种对通讯技术比较,可以从不同的角度考虑;\n\n- 兼容性: 短轮询 > 长轮询 > EventSource\n- 性能: EvenSource > 长轮询 > 短轮询\n- 服务端推送: EventSource > 长连接 (短轮询基本不考虑)\n\n## WebSocket 是什么原理?为什么可以实现持久连接?\n\n### WebSocket 机制\n\n以下简要介绍一下WebSocket的原理及运行机制。\n\nWebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:\n\n- WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;\n- WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。\n\n传统HTTP客户端与服务器请求响应模式如下图所示:\n\n![传统HTTP客户端与服务器请求响应模型](https://user-images.githubusercontent.com/8088864/125600810-db0eaedf-6a66-4d71-b9c6-1a5d891a7b86.jpg)\n\nWebSocket模式客户端与服务器请求响应模式如下图:\n\n![WebSocket模式客户端与服务器请求响应模式](https://user-images.githubusercontent.com/8088864/125600954-0e796b1d-dd3a-482c-ab83-0d43f1abf610.jpg)\n\n上图对比可以看出,相对于传统HTTP每次请求-响应都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。\n\n相比HTTP长连接,WebSocket有以下特点:\n\n- 是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。\n- HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。\n\n### WebSocket协议的原理\n\n与http协议一样,WebSocket协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过http协议建立通道,然后在此基础上用真正的WebSocket协议进行通信,所以WebSocket协议和http协议是有一定的交叉关系的。\n\n![WebSocket协议原理流程图](https://user-images.githubusercontent.com/8088864/125603352-ba55e8bd-f554-4ef1-8c0c-add611f63023.jpg)\n\n下面是WebSocket协议请求头:\n\n![WebSocket协议请求头](https://user-images.githubusercontent.com/8088864/125603469-ef8dfb8e-988a-4bc6-a041-487f697cb72a.jpg)\n\n其中请求头中重要的字段:\n\n``` request header\nConnection:Upgrade\n\nUpgrade:websocket\n\nSec-WebSocket-Extensions:permessage-deflate; client_max_window_bits\n\nSec-WebSocket-Key:mg8LvEqrB2vLpyCNnCJV3Q==\n\nSec-WebSocket-Version:13\n```\n\n1. Connection和Upgrade字段告诉服务器,客户端发起的是WebSocket协议请求\n2. Sec-WebSocket-Extensions表示客户端想要表达的协议级的扩展\n3. Sec-WebSocket-Key是一个Base64编码值,由浏览器随机生成\n4. Sec-WebSocket-Version表明客户端所使用的协议版本\n\n而得到的响应头中重要的字段:\n\n``` response header\nConnection:Upgrade\n\nUpgrade:websocket\n\nSec-WebSocket-Accept:AYtwtwampsFjE0lu3kFQrmOCzLQ=\n```\n\n1. Connection和Upgrade字段与请求头中的作用相同\n2. Sec-WebSocket-Accept表明服务器接受了客户端的请求\n\n``` response header\nStatus Code:101 Switching Protocols\n```\n\n并且http请求完成后响应的状态码为101,表示切换了协议,说明WebSocket协议通过http协议来建立运输层的TCP连接,之后便与http协议无关了。\n\n### WebSocket协议的优缺点\n\n优点:\n\n- WebSocket协议一旦建议后,互相沟通所消耗的请求头是很小的\n- 服务器可以向客户端推送消息了\n\n缺点:\n\n- 少部分浏览器不支持,浏览器支持的程度与方式有区别\n\nWebSocket协议的应用场景\n\n- 即时聊天通信\n- 多玩家游戏\n- 在线协同编辑/编辑\n- 实时数据流的拉取与推送\n- 体育/游戏实况\n- 实时地图位置\n\n一个使用WebSocket应用于视频的业务思路如下:\n\n- 使用心跳维护websocket链路,探测客户端端的网红/主播是否在线\n- 设置负载均衡7层的proxy_read_timeout默认为60s\n- 设置心跳为50s,即可长期保持Websocket不断开\n\n## 网络相关\n\n### 1.1 DNS 预解析\n\n- DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP\n\n```html\n\n```\n\n### 1.2 缓存\n\n- 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度\n- 通常浏览器缓存策略分为两种:强缓存和协商缓存\n\n**强缓存**\n\n> 实现强缓存可以通过两种响应头实现:`Expires `和 `Cache-Control` 。强缓存表示在缓存期间不需要请求,`state code `为 `200`\n\n```\nExpires: Wed, 22 Oct 2018 08:41:00 GMT\n```\n\n> `Expires` 是 `HTTP / 1.0` 的产物,表示资源会在 `Wed, 22 Oct 2018 08:41:00 GMT` 后过期,需要再次请求。并且 `Expires` 受限于本地时间,如果修改了本地时间,可能会造成缓存失效\n\n```\nCache-control: max-age=30\n```\n\n> `Cache-Control` 出现于 `HTTP / 1.1`,优先级高于 `Expires` 。该属性表示资源会在 `30` 秒后过期,需要再次请求\n\n**协商缓存**\n\n- 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304\n- 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式\n\nLast-Modified 和 If-Modified-Since\n\n- `Last-Modified` 表示本地文件最后修改日期,`If-Modified-Since` 会将 `Last-Modified `的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来\n- 但是如果在本地打开缓存文件,就会造成 `Last-Modified` 被修改,所以在 `HTTP / 1.1` 出现了 `ETag`\n\nETag 和 If-None-Match\n\n- `ETag` 类似于文件指纹,`If-None-Match` 会将当前 `ETag` 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 `ETag` 优先级比 `Last-Modified` 高\n\n**选择合适的缓存策略**\n\n> 对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略\n\n- 对于某些不需要缓存的资源,可以使用 `Cache-control: no-store` ,表示该资源不需要缓存\n- 对于频繁变动的资源,可以使用 `Cache-Control: no-cache` 并配合 `ETag` 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。\n- 对于代码文件来说,通常使用 `Cache-Control: max-age=31536000` 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件\n\n### 1.3 使用 HTTP / 2.0\n\n- 因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间\n- 在 HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小\n\n\n### 1.4 预加载\n\n- 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载\n- 预加载其实是声明式的 `fetch` ,强制浏览器请求资源,并且不会阻塞 `onload` 事件,可以使用以下代码开启预加载\n\n```html\n\n```\n\n> 预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好\n\n\n### 1.5 预渲染\n\n> 可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染\n\n```html\n\n```\n\n- 预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染\n\n## 优化渲染过程\n\n### 2.1 懒执行\n\n- 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒\n\n### 2.2 懒加载\n\n- 懒加载就是将不关键的资源延后加载\n\n> 懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载\n\n- 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等\n\n\n## 三栏弹性布局的5种方法(绝对定位、圣杯、双飞翼、flex、grid)\n\n### 需求\n\n用css实现三栏布局,html结构代码如下,顺序不能变(main优先渲染),可以适当加元素,同时要求left宽度200px,right宽度300px,main宽度自适应。\n\n``` html\n\n
main 宽度自适应
\n
left 宽200px
\n
right 宽300px
\n
\n```\n\n![三栏布局](https://user-images.githubusercontent.com/8088864/125612523-d7b144ff-a0a3-4522-ad8b-c2a7179198c2.gif)\n\n### 5种具体实现和优缺点比较\n\n#### 1. 绝对定位布局\n\n原始的布局方法\n\n- 原理:container为相对定位并设置左右padding为left和right的宽度,left\\right绝对定位在左右两侧,main不用设置。\n- 优点:兼容好、原理简单\n- 缺点:left和right都为绝对定位,高度不能撑开container\n\n``` html\n\n\n \n 绝对定位布局\n\n\n\n \n
main 宽度自适应
\n
left 宽200px
\n
right 宽300px
\n
\n\n