前端面试-进阶篇
一、JS
#1 談談變量提升
當執(zhí)行?JS?代碼時,會生成執(zhí)行環(huán)境,只要代碼不是寫在函數中的,就是在全局執(zhí)行環(huán)境中,函數中的代碼會產生函數執(zhí)行環(huán)境,只此兩種執(zhí)行環(huán)境。
b() // call b console.log(a) // undefinedvar a = 'Hello world'function b() {console.log('call b') }想必以上的輸出大家肯定都已經明白了,這是因為函數和變量提升的原因。通常提升的解釋是說將聲明的代碼移動到了頂部,這其實沒有什么錯誤,便于大家理解。但是更準確的解釋應該是:在生成執(zhí)行環(huán)境時,會有兩個階段。第一個階段是創(chuàng)建的階段,JS?解釋器會找出需要提升的變量和函數,并且給他們提前在內存中開辟好空間,函數的話會將整個函數存入內存中,變量只聲明并且賦值為?undefined,所以在第二個階段,也就是代碼執(zhí)行階段,我們可以直接提前使用
- 在提升的過程中,相同的函數會覆蓋上一個函數,并且函數優(yōu)先于變量提升
var?會產生很多錯誤,所以在 ES6中引入了?let。let不能在聲明前使用,但是這并不是常說的?let?不會提升,let提升了,在第一階段內存也已經為他開辟好了空間,但是因為這個聲明的特性導致了并不能在聲明前使用
#2 bind、call、apply 區(qū)別
- call?和?apply?都是為了解決改變?this?的指向。作用都是相同的,只是傳參的方式不同。
- 除了第一個參數外,call?可以接收一個參數列表,apply?只接受一個參數數組
bind?和其他兩個方法作用也是一致的,只是該方法會返回一個函數。并且我們可以通過?bind?實現柯里化
#3 如何實現一個 bind 函數
對于實現以下幾個函數,可以從幾個方面思考
- 不傳入第一個參數,那么默認為?window
- 改變了?this?指向,讓新的對象可以執(zhí)行該函數。那么思路是否可以變成給新的對象添加一個函數,然后在執(zhí)行完以后刪除?
#4 如何實現一個 call 函數
Function.prototype.myCall = function (context) {var context = context || window// 給 context 添加一個屬性// getValue.call(a, 'yck', '24') => a.fn = getValuecontext.fn = this// 將 context 后面的參數取出來var args = [...arguments].slice(1)// getValue.call(a, 'yck', '24') => a.fn('yck', '24')var result = context.fn(...args)// 刪除 fndelete context.fnreturn result }#5 如何實現一個 apply 函數
Function.prototype.myApply = function (context) {var context = context || windowcontext.fn = thisvar result// 需要判斷是否存儲第二個參數// 如果存在,就將第二個參數展開if (arguments[1]) {result = context.fn(...arguments[1])} else {result = context.fn()}delete context.fnreturn result }#6 簡單說下原型鏈?
- 每個函數都有?prototype?屬性,除了?Function.prototype.bind(),該屬性指向原型。
- 每個對象都有?__proto__?屬性,指向了創(chuàng)建該對象的構造函數的原型。其實這個屬性指向了?[[prototype]],但是?[[prototype]]是內部屬性,我們并不能訪問到,所以使用?_proto_來訪問。
- 對象可以通過?__proto__?來尋找不屬于該對象的屬性,__proto__?將對象連接起來組成了原型鏈。
#7 怎么判斷對象類型
- 可以通過?Object.prototype.toString.call(xx)。這樣我們就可以獲得類似?[object Type]?的字符串。
- instanceof?可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的?prototype
#8 箭頭函數的特點
function a() {return () => {return () => {console.log(this)}} } console.log(a()()())箭頭函數其實是沒有?this?的,這個函數中的?this?只取決于他外面的第一個不是箭頭函數的函數的?this。在這個例子中,因為調用?a?符合前面代碼中的第一個情況,所以?this?是window。并且?this一旦綁定了上下文,就不會被任何代碼改變
#9 This
function foo() {console.log(this.a) } var a = 1 foo()var obj = {a: 2,foo: foo } obj.foo()// 以上兩者情況 `this` 只依賴于調用函數前的對象,優(yōu)先級是第二個情況大于第一個情況// 以下情況是優(yōu)先級最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a)// 還有種就是利用 call,apply,bind 改變 this,這個優(yōu)先級僅次于 new#10 async、await 優(yōu)缺點
async?和?await?相比直接使用?Promise?來說,優(yōu)勢在于處理 then 的調用鏈,能夠更清晰準確的寫出代碼。缺點在于濫用?await?可能會導致性能問題,因為?await?會阻塞代碼,也許之后的異步代碼并不依賴于前者,但仍然需要等待前者完成,導致代碼失去了并發(fā)性
下面來看一個使用?await?的代碼。
var a = 0 var b = async () => {a = a + await 10console.log('2', a) // -> '2' 10a = (await 10) + aconsole.log('3', a) // -> '3' 20 } b() a++ console.log('1', a) // -> '1' 1- 首先函數b?先執(zhí)行,在執(zhí)行到?await 10?之前變量?a?還是?0,因為在?await?內部實現了?generators?,generators?會保留堆棧中東西,所以這時候?a = 0?被保存了下來
- 因為?await?是異步操作,遇到await就會立即返回一個pending狀態(tài)的Promise對象,暫時返回執(zhí)行代碼的控制權,使得函數外的代碼得以繼續(xù)執(zhí)行,所以會先執(zhí)行?console.log('1', a)
- 這時候同步代碼執(zhí)行完畢,開始執(zhí)行異步代碼,將保存下來的值拿出來使用,這時候?a = 10
- 然后后面就是常規(guī)執(zhí)行代碼了
#11 generator 原理
Generator?是?ES6中新增的語法,和?Promise?一樣,都可以用來異步編程
// 使用 * 表示這是一個 Generator 函數 // 內部可以通過 yield 暫停代碼 // 通過調用 next 恢復執(zhí)行 function* test() {let a = 1 + 2;yield 2;yield 3; } let b = test(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true }從以上代碼可以發(fā)現,加上?*的函數執(zhí)行后擁有了?next?函數,也就是說函數執(zhí)行后返回了一個對象。每次調用?next?函數可以繼續(xù)執(zhí)行被暫停的代碼。以下是?Generator?函數的簡單實現
// cb 也就是編譯過的 test 函數 function generator(cb) {return (function() {var object = {next: 0,stop: function() {}};return {next: function() {var ret = cb(object);if (ret === undefined) return { value: undefined, done: true };return {value: ret,done: false};}};})(); } // 如果你使用 babel 編譯后可以發(fā)現 test 函數變成了這樣 function test() {var a;return generator(function(_context) {while (1) {switch ((_context.prev = _context.next)) {// 可以發(fā)現通過 yield 將代碼分割成幾塊// 每次執(zhí)行 next 函數就執(zhí)行一塊代碼// 并且表明下次需要執(zhí)行哪塊代碼case 0:a = 1 + 2;_context.next = 4;return 2;case 4:_context.next = 6;return 3;// 執(zhí)行完畢case 6:case "end":return _context.stop();}}}); }#12 Promise
- Promise?是?ES6?新增的語法,解決了回調地獄的問題。
- 可以把?Promise看成一個狀態(tài)機。初始是?pending?狀態(tài),可以通過函數?resolve?和?reject,將狀態(tài)轉變?yōu)?resolved?或者?rejected?狀態(tài),狀態(tài)一旦改變就不能再次變化。
- then?函數會返回一個?Promise?實例,并且該返回值是一個新的實例而不是之前的實例。因為?Promise?規(guī)范規(guī)定除了?pending?狀態(tài),其他狀態(tài)是不可以改變的,如果返回的是一個相同實例的話,多個?then?調用就失去意義了。 對于?then?來說,本質上可以把它看成是?flatMap
#13 如何實現一個 Promise
// 三種狀態(tài) const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; // promise 接收一個函數參數,該函數會立即執(zhí)行 function MyPromise(fn) {let _this = this;_this.currentState = PENDING;_this.value = undefined;// 用于保存 then 中的回調,只有當 promise// 狀態(tài)為 pending 時才會緩存,并且每個實例至多緩存一個_this.resolvedCallbacks = [];_this.rejectedCallbacks = [];_this.resolve = function (value) {if (value instanceof MyPromise) {// 如果 value 是個 Promise,遞歸執(zhí)行return value.then(_this.resolve, _this.reject)}setTimeout(() => { // 異步執(zhí)行,保證執(zhí)行順序if (_this.currentState === PENDING) {_this.currentState = RESOLVED;_this.value = value;_this.resolvedCallbacks.forEach(cb => cb());}})};_this.reject = function (reason) {setTimeout(() => { // 異步執(zhí)行,保證執(zhí)行順序if (_this.currentState === PENDING) {_this.currentState = REJECTED;_this.value = reason;_this.rejectedCallbacks.forEach(cb => cb());}})}// 用于解決以下問題// new Promise(() => throw Error('error))try {fn(_this.resolve, _this.reject);} catch (e) {_this.reject(e);} }MyPromise.prototype.then = function (onResolved, onRejected) {var self = this;// 規(guī)范 2.2.7,then 必須返回一個新的 promisevar promise2;// 規(guī)范 2.2.onResolved 和 onRejected 都為可選參數// 如果類型不是函數需要忽略,同時也實現了透傳// Promise.resolve(4).then().then((value) => console.log(value))onResolved = typeof onResolved === 'function' ? onResolved : v => v;onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;if (self.currentState === RESOLVED) {return (promise2 = new MyPromise(function (resolve, reject) {// 規(guī)范 2.2.4,保證 onFulfilled,onRjected 異步執(zhí)行// 所以用了 setTimeout 包裹下setTimeout(function () {try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === REJECTED) {return (promise2 = new MyPromise(function (resolve, reject) {setTimeout(function () {// 異步執(zhí)行onRejectedtry {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === PENDING) {return (promise2 = new MyPromise(function (resolve, reject) {self.resolvedCallbacks.push(function () {// 考慮到可能會有報錯,所以使用 try/catch 包裹try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});self.rejectedCallbacks.push(function () {try {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});}));} }; // 規(guī)范 2.3 function resolutionProcedure(promise2, x, resolve, reject) {// 規(guī)范 2.3.1,x 不能和 promise2 相同,避免循環(huán)引用if (promise2 === x) {return reject(new TypeError("Error"));}// 規(guī)范 2.3.2// 如果 x 為 Promise,狀態(tài)為 pending 需要繼續(xù)等待否則執(zhí)行if (x instanceof MyPromise) {if (x.currentState === PENDING) {x.then(function (value) {// 再次調用該函數是為了確認 x resolve 的// 參數是什么類型,如果是基本類型就再次 resolve// 把值傳給下個 thenresolutionProcedure(promise2, value, resolve, reject);}, reject);} else {x.then(resolve, reject);}return;}// 規(guī)范 2.3.3.3.3// reject 或者 resolve 其中一個執(zhí)行過得話,忽略其他的let called = false;// 規(guī)范 2.3.3,判斷 x 是否為對象或者函數if (x !== null && (typeof x === "object" || typeof x === "function")) {// 規(guī)范 2.3.3.2,如果不能取出 then,就 rejecttry {// 規(guī)范 2.3.3.1let then = x.then;// 如果 then 是函數,調用 x.thenif (typeof then === "function") {// 規(guī)范 2.3.3.3then.call(x,y => {if (called) return;called = true;// 規(guī)范 2.3.3.3.1resolutionProcedure(promise2, y, resolve, reject);},e => {if (called) return;called = true;reject(e);});} else {// 規(guī)范 2.3.3.4resolve(x);}} catch (e) {if (called) return;called = true;reject(e);}} else {// 規(guī)范 2.3.4,x 為基本類型resolve(x);} }#14 == 和 ===區(qū)別,什么情況用 ==
這里來解析一道題目?[] == ![] // -> true?,下面是這個表達式為何為?true?的步驟
// [] 轉成 true,然后取反變成 false [] == false // 根據第 8 條得出 [] == ToNumber(false) [] == 0 // 根據第 10 條得出 ToPrimitive([]) == 0 // [].toString() -> '' '' == 0 // 根據第 6 條得出 0 == 0 // -> true===用于判斷兩者類型和值是否相同。 在開發(fā)中,對于后端返回的?code,可以通過?==去判斷
#15 基本數據類型和引?類型在存儲上的差別
前者存儲在棧上,后者存儲在堆上
#16 瀏覽器 Eventloop 和 Node 中的有什么區(qū)別
眾所周知 JS 是門非阻塞單線程語言,因為在最初 JS 就是為了和瀏覽器交互而誕生的。如果 JS 是門多線程的語言話,我們在多個線程中處理 DOM 就可能會發(fā)生問題(一個線程中新加節(jié)點,另一個線程中刪除節(jié)點),當然可以引入讀寫鎖解決這個問題。
- JS?在執(zhí)行的過程中會產生執(zhí)行環(huán)境,這些執(zhí)行環(huán)境會被順序的加入到執(zhí)行棧中。如果遇到異步的代碼,會被掛起并加入到?Task(有多種?task) 隊列中。一旦執(zhí)行棧為空,Event Loop?就會從?Task?隊列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行,所以本質上來說?JS?中的異步還是同步行為
- 以上代碼雖然?setTimeout?延時為?0,其實還是異步。這是因為?HTML5?標準規(guī)定這個函數第二個參數不得小于?4?毫秒,不足會自動增加。所以?setTimeout還是會在?script end?之后打印。
- 不同的任務源會被分配到不同的?Task隊列中,任務源可以分為 微任務(microtask) 和 宏任務(macrotask)。在 ES6 規(guī)范中,microtask?稱為 jobs,macrotask?稱為?task。
- 以上代碼雖然?setTimeout?寫在?Promise?之前,但是因為?Promise?屬于微任務而?setTimeout屬于宏任務,所以會有以上的打印。
- 微任務包括?process.nextTick?,promise?,Object.observe,MutationObserver
- 宏任務包括?script?,?setTimeout?,setInterval,setImmediate?,I/O?,UI renderin
很多人有個誤區(qū),認為微任務快于宏任務,其實是錯誤的。因為宏任務中包括了?script?,瀏覽器會先執(zhí)行一個宏任務,接下來有異步代碼的話就先執(zhí)行微任務
所以正確的一次 Event loop 順序是這樣的
- 執(zhí)行同步代碼,這屬于宏任務
- 執(zhí)行棧為空,查詢是否有微任務需要執(zhí)行
- 執(zhí)行所有微任務
- 必要的話渲染?UI
- 然后開始下一輪?Event loop,執(zhí)行宏任務中的異步代碼
通過上述的?Event loop?順序可知,如果宏任務中的異步代碼有大量的計算并且需要操作?DOM?的話,為了更快的 界面響應,我們可以把操作?DOM?放入微任務中
#17 setTimeout 倒計時誤差
JS?是單線程的,所以?setTimeout?的誤差其實是無法被完全解決的,原因有很多,可能是回調中的,有可能是瀏覽器中的各種事件導致。這也是為什么頁面開久了,定時器會不準的原因,當然我們可以通過一定的辦法去減少這個誤差。
// 以下是一個相對準備的倒計時實現 var period = 60 * 1000 * 60 * 2 var startTime = new Date().getTime(); var count = 0 var end = new Date().getTime() + period var interval = 1000 var currentInterval = intervalfunction loop() {count++var offset = new Date().getTime() - (startTime + count * interval); // 代碼執(zhí)行所消耗的時間var diff = end - new Date().getTime()var h = Math.floor(diff / (60 * 1000 * 60))var hdiff = diff % (60 * 1000 * 60)var m = Math.floor(hdiff / (60 * 1000))var mdiff = hdiff % (60 * 1000)var s = mdiff / (1000)var sCeil = Math.ceil(s)var sFloor = Math.floor(s)currentInterval = interval - offset // 得到下一次循環(huán)所消耗的時間console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時間:'+offset, '下次循環(huán)間隔'+currentInterval) // 打印 時 分 秒 代碼執(zhí)行時間 下次循環(huán)間隔setTimeout(loop, currentInterval) }setTimeout(loop, currentInterval)#18 數組降維
[1, [2], 3].flatMap(v => v) // -> [1, 2, 3]如果想將一個多維數組徹底的降維,可以這樣實現
const flattenDeep = (arr) => Array.isArray(arr)? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , []): [arr]flattenDeep([1, [[2], [3, [4]], 5]])#19 深拷貝
這個問題通常可以通過?JSON.parse(JSON.stringify(object))?來解決
let a = {age: 1,jobs: {first: 'FE'} } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE但是該方法也是有局限性的:
- 會忽略?undefined
- 會忽略?symbol
- 不能序列化函數
- 不能解決循環(huán)引用的對象
在遇到函數、?undefined?或者?symbol?的時候,該對象也不能正常的序列化
let a = {age: undefined,sex: Symbol('male'),jobs: function() {},name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"}但是在通常情況下,復雜數據都是可以序列化的,所以這個函數可以解決大部分問題,并且該函數是內置函數中處理深拷貝性能最快的。當然如果你的數據中含有以上三種情況下,可以使用?lodash?的深拷貝函數
#20 typeof 于 instanceof 區(qū)別
typeof?對于基本類型,除了?null都可以顯示正確的類型
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒有聲明,但是還會顯示 undefinedtypeof?對于對象,除了函數都會顯示?object
typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'對于?null?來說,雖然它是基本類型,但是會顯示?object,這是一個存在很久了的?Bug
typeof null // 'object'instanceof?可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的?prototype
我們也可以試著實現一下 instanceof function instanceof(left, right) {// 獲得類型的原型let prototype = right.prototype// 獲得對象的原型left = left.__proto__// 判斷對象的類型是否等于類型的原型while (true) {if (left === null)return falseif (prototype === left)return trueleft = left.__proto__} }#二、瀏覽器
#1 cookie和localSrorage、session、indexDB 的區(qū)別
| 數據生命周期 | 一般由服務器生成,可以設置過期時間 | 除非被清理,否則一直存在 | 頁面關閉就清理 | 除非被清理,否則一直存在 |
| 數據存儲大小 | 4K | 5M | 5M | 無限 |
| 與服務端通信 | 每次都會攜帶在 header 中,對于請求性能影響 | 不參與 | 不參與 | 不參與 |
從上表可以看到,cookie?已經不建議用于存儲。如果沒有大量數據存儲需求的話,可以使用?localStorage和?sessionStorage?。對于不怎么改變的數據盡量使用?localStorage?存儲,否則可以用?sessionStorage?存儲。
對于?cookie,我們還需要注意安全性
| value | 如果用于保存用戶登錄態(tài),應該將該值加密,不能使用明文的用戶標識 |
| http-only | 不能通過?JS訪問?Cookie,減少?XSS攻擊 |
| secure | 只能在協(xié)議為?HTTPS?的請求中攜帶 |
| same-site | 規(guī)定瀏覽器不能在跨域請求中攜帶?Cookie,減少?CSRF?攻擊 |
#2 怎么判斷頁面是否加載完成?
- Load?事件觸發(fā)代表頁面中的?DOM,CSS,JS,圖片已經全部加載完畢。
- DOMContentLoaded?事件觸發(fā)代表初始的?HTML?被完全加載和解析,不需要等待?CSS,JS,圖片加載
#3 如何解決跨域
因為瀏覽器出于安全考慮,有同源策略。也就是說,如果協(xié)議、域名或者端口有一個不同就是跨域,Ajax請求會失敗。
我們可以通過以下幾種常用方法解決跨域的問題
JSONP
JSONP?的原理很簡單,就是利用?<script>標簽沒有跨域限制的漏洞。通過?<script>標簽指向一個需要訪問的地址并提供一個回調函數來接收數據當需要通訊時
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script>function jsonp(data) {console.log(data)} </script>JSONP?使用簡單且兼容性不錯,但是只限于?get?請求
- 在開發(fā)中可能會遇到多個?JSONP?請求的回調函數名是相同的,這時候就需要自己封裝一個?JSONP,以下是簡單實現
CORS
- ORS需要瀏覽器和后端同時支持。IE 8?和?9?需要通過?XDomainRequest?來實現。
- 瀏覽器會自動進行?CORS?通信,實現CORS通信的關鍵是后端。只要后端實現了?CORS,就實現了跨域。
- 服務端設置?Access-Control-Allow-Origin?就可以開啟?CORS。 該屬性表示哪些域名可以訪問資源,如果設置通配符則表示所有網站都可以訪問資源。
document.domain
- 該方式只能用于二級域名相同的情況下,比如?a.test.com?和?b.test.com?適用于該方式。
- 只需要給頁面添加?document.domain = 'test.com'?表示二級域名都相同就可以實現跨域
postMessage
這種方式通常用于獲取嵌入頁面中的第三方頁面數據。一個頁面發(fā)送消息,另一個頁面判斷來源并接收消息
// 發(fā)送消息端 window.parent.postMessage('message', 'http://test.com'); // 接收消息端 var mc = new MessageChannel(); mc.addEventListener('message', (event) => {var origin = event.origin || event.originalEvent.origin;if (origin === 'http://test.com') {console.log('驗證通過')} });#4 什么是事件代理
如果一個節(jié)點中的子節(jié)點是動態(tài)生成的,那么子節(jié)點需要注冊事件的話應該注冊在父節(jié)點上
<ul id="ul"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li> </ul> <script>let ul = document.querySelector('#ul')ul.addEventListener('click', (event) => {console.log(event.target);}) </script>- 事件代理的方式相對于直接給目標注冊事件來說,有以下優(yōu)點
- 節(jié)省內存
- 不需要給子節(jié)點注銷事件
#5 Service worker
service worker
Service workers?本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作為瀏覽器和網絡間的代理。它們旨在(除其他之外)使得能夠創(chuàng)建有效的離線體驗,攔截網絡請求并基于網絡是否可用以及更新的資源是否駐留在服務器上來采取適當的動作。他們還允許訪問推送通知和后臺同步API
目前該技術通常用來做緩存文件,提高首屏速度,可以試著來實現這個功能
// index.js if (navigator.serviceWorker) {navigator.serviceWorker.register("sw.js").then(function(registration) {console.log("service worker 注冊成功");}).catch(function(err) {console.log("servcie worker 注冊失敗");}); } // sw.js // 監(jiān)聽 `install` 事件,回調中緩存所需文件 self.addEventListener("install", e => {e.waitUntil(caches.open("my-cache").then(function(cache) {return cache.addAll(["./index.html", "./index.js"]);})); });// 攔截所有請求事件 // 如果緩存中已經有請求的數據就直接用緩存,否則去請求數據 self.addEventListener("fetch", e => {e.respondWith(caches.match(e.request).then(function(response) {if (response) {return response;}console.log("fetch source");})); });打開頁面,可以在開發(fā)者工具中的?Application?看到?Service Worker已經啟動了
#6 瀏覽器緩存
緩存對于前端性能優(yōu)化來說是個很重要的點,良好的緩存策略可以降低資源的重復加載提高網頁的整體加載速度。
- 通常瀏覽器緩存策略分為兩種:強緩存和協(xié)商緩存。
強緩存
實現強緩存可以通過兩種響應頭實現:Expires?和?Cache-Control?。強緩存表示在緩存期間不需要請求,state code?為?200
Expires: Wed, 22 Oct 2018 08:41:00 GMTExpires?是?HTTP / 1.0?的產物,表示資源會在Wed,22 Oct 2018 08:41:00 GMT?后過期,需要再次請求。并且?Expires?受限于本地時間,如果修改了本地時間,可能會造成緩存失效。
Cache-control: max-age=30- Cache-Control?出現于?HTTP / 1.1,優(yōu)先級高于?Expires?。該屬性表示資源會在?30?秒后過期,需要再次請求。
協(xié)商緩存
- 如果緩存過期了,我們就可以使用協(xié)商緩存來解決問題。協(xié)商緩存需要請求,如果緩存有效會返回?304。
- 協(xié)商緩存需要客戶端和服務端共同實現,和強緩存一樣,也有兩種實現方式
Last-Modified 和 If-Modified-Since
- Last-Modified表示本地文件最后修改日期,If-Modified-Since?會將?Last-Modified?的值發(fā)送給服務器,詢問服務器在該日期后資源是否有更新,有更新的話就會將新的資源發(fā)送回來。
- 但是如果在本地打開緩存文件,就會造成?Last-Modified被修改,所以在?HTTP / 1.1?出現了?ETag
ETag 和 If-None-Match
ETag?類似于文件指紋,If-None-Match?會將當前?ETag發(fā)送給服務器,詢問該資源?ETag?是否變動,有變動的話就將新的資源發(fā)送回來。并且?ETag?優(yōu)先級比?Last-Modified?高
選擇合適的緩存策略
對于大部分的場景都可以使用強緩存配合協(xié)商緩存解決,但是在一些特殊的地方可能需要選擇特殊的緩存策略
- 對于某些不需要緩存的資源,可以使用?Cache-control: no-store?,表示該資源不需要緩存
- 對于頻繁變動的資源,可以使用?Cache-Control: no-cache并配合?ETag?使用,表示該資源已被緩存,但是每次都會發(fā)送請求詢問資源是否更新。
- 對于代碼文件來說,通常使用?Cache-Control: max-age=31536000?并配合策略緩存使用,然后對文件進行指紋處理,一旦文件名變動就會立刻下載新的文件
#7 瀏覽器性能問題
重繪(Repaint)和回流(Reflow)
- 重繪和回流是渲染步驟中的一小節(jié),但是這兩個步驟對于性能影響很大。
- 重繪是當節(jié)點需要更改外觀而不會影響布局的,比如改變?color就叫稱為重繪
- 回流是布局或者幾何屬性需要改變就稱為回流。
- 回流必定會發(fā)生重繪,重繪不一定會引發(fā)回流?;亓魉璧某杀颈戎乩L高的多,改變深層次的節(jié)點很可能導致父節(jié)點的一系列回流。
所以以下幾個動作可能會導致性能問題:
- 改變?window?大小
- 改變字體
- 添加或刪除樣式
- 文字改變
- 定位或者浮動
- 盒模型
很多人不知道的是,重繪和回流其實和 Event loop 有關。
- 當?Event loop?執(zhí)行完?Microtasks后,會判斷?document?是否需要更新。- 因為瀏覽器是?60Hz?的刷新率,每?16ms才會更新一次。
- 然后判斷是否有resize?或者?scroll?,有的話會去觸發(fā)事件,所以?resize?和?scroll?事件也是至少 16ms 才會觸發(fā)一次,并且自帶節(jié)流功能。
- 判斷是否觸發(fā)了?media query
- 更新動畫并且發(fā)送事件
- 判斷是否有全屏操作事件
- 執(zhí)行?requestAnimationFrame回調
- 執(zhí)行?IntersectionObserver?回調,該方法用于判斷元素是否可見,可以用于懶加載上,但是兼容性不好
- 更新界面
- 以上就是一幀中可能會做的事情。如果在一幀中有空閑時間,就會去執(zhí)行?requestIdleCallback?回調。
減少重繪和回流
使用?translate?替代?top
<div class="test"></div> <style>.test {position: absolute;top: 10px;width: 100px;height: 100px;background: red;} </style> <script>setTimeout(() => {// 引起回流document.querySelector('.test').style.top = '100px'}, 1000) </script>- 使用?visibility?替換?display: none?,因為前者只會引起重繪,后者會引發(fā)回流(改變了布局)
- 把?DOM?離線后修改,比如:先把?DOM?給?display:none(有一次?Reflow),然后你修改100次,然后再把它顯示出來
- 不要把?DOM結點的屬性值放在一個循環(huán)里當成循環(huán)里的變量
- 不要使用?table?布局,可能很小的一個小改動會造成整個?table?的重新布局 動畫實現的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用?requestAnimationFrame
- CSS選擇符從右往左匹配查找,避免?DOM?深度過深
- 將頻繁運行的動畫變?yōu)閳D層,圖層能夠阻止該節(jié)點回流影響別的元素。比如對于?video?標簽,瀏覽器會自動將該節(jié)點變?yōu)閳D層。
CDN
靜態(tài)資源盡量使用?CDN?加載,由于瀏覽器對于單個域名有并發(fā)請求上限,可以考慮使用多個?CDN?域名。對于?CDN?加載靜態(tài)資源需要注意 CDN 域名要與主站不同,否則每次請求都會帶上主站的?Cookie
使用 Webpack 優(yōu)化項目
- 對于?Webpack4,打包項目使用?production?模式,這樣會自動開啟代碼壓縮
- 使用?ES6?模塊來開啟?tree shaking,這個技術可以移除沒有使用的代碼
- 優(yōu)化圖片,對于小圖可以使用?base64?的方式寫入文件中
- 按照路由拆分代碼,實現按需加載
#三、Webpack
?
#1 優(yōu)化打包速度
- 減少文件搜索范圍
- 比如通過別名
- loader?的?test,include & exclude
- Webpack4?默認壓縮并行
- Happypack?并發(fā)調用
- babel?也可以緩存編譯
#2 Babel 原理
- 本質就是編譯器,當代碼轉為字符串生成?AST,對?AST?進行轉變最后再生成新的代碼
- 分為三步:詞法分析生成?Token,語法分析生成?AST,遍歷?AST,根據插件變換相應的節(jié)點,最后把?AST轉換為代碼
#3 如何實現一個插件
- 調用插件?apply?函數傳入?compiler?對象
- 通過?compiler?對象監(jiān)聽事件
比如你想實現一個編譯結束退出命令的插件
apply (compiler) {const afterEmit = (compilation, cb) => {cb()setTimeout(function () {process.exit(0)}, 1000)}compiler.plugin('after-emit', afterEmit) } }module.exports = BuildEndPlugin總結
- 上一篇: JQuery 总结(8)Ajax 无刷新
- 下一篇: 前端-计算机基础