前端面試問題答案彙總--進階篇

轉載於https://github.com/poetries/FE-Interview-Questions,by poetriesjavascript

1、JS

#1 談談變量提高

當執行 JS 代碼時,會生成執行環境,只要代碼不是寫在函數中的,就是在全局執行環境中,函數中的代碼會產生函數執行環境,只此兩種執行環境。css

b() // call b console.log(a) // undefined var a = 'Hello world' function b() { console.log('call b') } 

想必以上的輸出你們確定都已經明白了,這是由於函數和變量提高的緣由。一般提高的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麼錯誤,便於你們理解。可是更準確的解釋應該是:在生成執行環境時,會有兩個階段。第一個階段是建立的階段,JS 解釋器會找出須要提高的變量和函數,而且給他們提早在內存中開闢好空間,函數的話會將整個函數存入內存中,變量只聲明而且賦值爲 undefined,因此在第二個階段,也就是代碼執行階段,咱們能夠直接提早使用html

  • 在提高的過程當中,相同的函數會覆蓋上一個函數,而且函數優先於變量提高
b() // call b second function b() { console.log('call b fist') } function b() { console.log('call b second') } var b = 'Hello world' 

var 會產生不少錯誤,因此在 ES6中引入了 letlet不能在聲明前使用,可是這並非常說的 let 不會提高,let提高了,在第一階段內存也已經爲他開闢好了空間,可是由於這個聲明的特性致使了並不能在聲明前使用前端

#2 bind、call、apply 區別

  • call 和 apply 都是爲了解決改變 this 的指向。做用都是相同的,只是傳參的方式不一樣。
  • 除了第一個參數外,call 能夠接收一個參數列表,apply 只接受一個參數數組
let a = { value: 1 } function getValue(name, age) { console.log(name) console.log(age) console.log(this.value) } getValue.call(a, 'yck', '24') getValue.apply(a, ['yck', '24']) 

bind 和其餘兩個方法做用也是一致的,只是該方法會返回一個函數。而且咱們能夠經過 bind 實現柯里化java

#3 如何實現一個 bind 函數

對於實現如下幾個函數,能夠從幾個方面思考node

  • 不傳入第一個參數,那麼默認爲 window
  • 改變了 this 指向,讓新的對象能夠執行該函數。那麼思路是否能夠變成給新的對象添加一個函數,而後在執行完之後刪除?
Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } var _this = this var args = [...arguments].slice(1) // 返回一個函數 return function F() { // 由於返回了一個函數,咱們能夠 new F(),因此須要判斷 if (this instanceof F) { return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } } 

#4 如何實現一個 call 函數

Function.prototype.myCall = function (context) { var context = context || window // 給 context 添加一個屬性 // getValue.call(a, 'yck', '24') => a.fn = getValue context.fn = this // 將 context 後面的參數取出來 var args = [...arguments].slice(1) // getValue.call(a, 'yck', '24') => a.fn('yck', '24') var result = context.fn(...args) // 刪除 fn delete context.fn return result } 

#5 如何實現一個 apply 函數

Function.prototype.myApply = function (context) { var context = context || window context.fn = this var result // 須要判斷是否存儲第二個參數 // 若是存在,就將第二個參數展開 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result } 

#6 簡單說下原型鏈?

  • 每一個函數都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。
  • 每一個對象都有 __proto__ 屬性,指向了建立該對象的構造函數的原型。其實這個屬性指向了 [[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一旦綁定了上下文,就不會被任何代碼改變webpack

#9 This

function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上二者狀況 `this` 只依賴於調用函數前的對象,優先級是第二個狀況大於第一個狀況 // 如下狀況是優先級最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) // 還有種就是利用 call,apply,bind 改變 this,這個優先級僅次於 new 

#10 async、await 優缺點

async 和 await 相比直接使用 Promise 來講,優點在於處理 then 的調用鏈,可以更清晰準確的寫出代碼。缺點在於濫用 await 可能會致使性能問題,由於 await 會阻塞代碼,也許以後的異步代碼並不依賴於前者,但仍然須要等待前者完成,致使代碼失去了併發性git

下面來看一個使用 await 的代碼。github

var a = 0 var b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 a = (await 10) + a console.log('3', a) // -> '3' 20 } b() a++ console.log('1', a) // -> '1' 1 
  • 首先函數b 先執行,在執行到 await 10 以前變量 a 仍是 0,由於在 await 內部實現了 generators ,generators 會保留堆棧中東西,因此這時候 a = 0 被保存了下來
  • 由於 await 是異步操做,遇到await就會當即返回一個pending狀態的Promise對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,因此會先執行 console.log('1', a)
  • 這時候同步代碼執行完畢,開始執行異步代碼,將保存下來的值拿出來使用,這時候 a = 10
  • 而後後面就是常規執行代碼了

#11 generator 原理

Generator 是 ES6中新增的語法,和 Promise 同樣,均可以用來異步編程web

// 使用 * 表示這是一個 Generator 函數 // 內部能夠經過 yield 暫停代碼 // 經過調用 next 恢復執行 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 } 

從以上代碼能夠發現,加上 *的函數執行後擁有了 next 函數,也就是說函數執行後返回了一個對象。每次調用 next 函數能夠繼續執行被暫停的代碼。如下是 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 編譯後能夠發現 test 函數變成了這樣 function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 能夠發現經過 yield 將代碼分割成幾塊 // 每次執行 next 函數就執行一塊代碼 // 而且代表下次須要執行哪塊代碼 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 執行完畢 case 6: case "end": return _context.stop(); } } }); } 

#12 Promise

  • Promise 是 ES6 新增的語法,解決了回調地獄的問題。
  • 能夠把 Promise當作一個狀態機。初始是 pending 狀態,能夠經過函數 resolve 和 reject,將狀態轉變爲 resolved 或者 rejected 狀態,狀態一旦改變就不能再次變化。
  • then 函數會返回一個 Promise 實例,而且該返回值是一個新的實例而不是以前的實例。由於 Promise 規範規定除了 pending 狀態,其餘狀態是不能夠改變的,若是返回的是一個相同實例的話,多個 then 調用就失去意義了。 對於 then 來講,本質上能夠把它當作是 flatMap

#13 如何實現一個 Promise

// 三種狀態 const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; // promise 接收一個函數參數,該函數會當即執行 function MyPromise(fn) { let _this = this; _this.currentState = PENDING; _this.value = undefined; // 用於保存 then 中的回調,只有當 promise // 狀態爲 pending 時纔會緩存,而且每一個實例至多緩存一個 _this.resolvedCallbacks = []; _this.rejectedCallbacks = []; _this.resolve = function (value) { if (value instanceof MyPromise) { // 若是 value 是個 Promise,遞歸執行 return value.then(_this.resolve, _this.reject) } setTimeout(() => { // 異步執行,保證執行順序 if (_this.currentState === PENDING) { _this.currentState = RESOLVED; _this.value = value; _this.resolvedCallbacks.forEach(cb => cb()); } }) }; _this.reject = function (reason) { setTimeout(() => { // 異步執行,保證執行順序 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; // 規範 2.2.7,then 必須返回一個新的 promise var promise2; // 規範 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) { // 規範 2.2.4,保證 onFulfilled,onRjected 異步執行 // 因此用了 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 () { // 異步執行onRejected try { 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); } }); })); } }; // 規範 2.3 function resolutionProcedure(promise2, x, resolve, reject) { // 規範 2.3.1,x 不能和 promise2 相同,避免循環引用 if (promise2 === x) { return reject(new TypeError("Error")); } // 規範 2.3.2 // 若是 x 爲 Promise,狀態爲 pending 須要繼續等待不然執行 if (x instanceof MyPromise) { if (x.currentState === PENDING) { x.then(function (value) { // 再次調用該函數是爲了確認 x resolve 的 // 參數是什麼類型,若是是基本類型就再次 resolve // 把值傳給下個 then resolutionProcedure(promise2, value, resolve, reject); }, reject); } else { x.then(resolve, reject); } return; } // 規範 2.3.3.3.3 // reject 或者 resolve 其中一個執行過得話,忽略其餘的 let called = false; // 規範 2.3.3,判斷 x 是否爲對象或者函數 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 規範 2.3.3.2,若是不能取出 then,就 reject try { // 規範 2.3.3.1 let then = x.then; // 若是 then 是函數,調用 x.then if (typeof then === "function") { // 規範 2.3.3.3 then.call( x, y => { if (called) return; called = true; // 規範 2.3.3.3.1 resolutionProcedure(promise2, y, resolve, reject); }, e => { if (called) return; called = true; reject(e); } ); } else { // 規範 2.3.3.4 resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { // 規範 2.3.4,x 爲基本類型 resolve(x); } } 

#14 == 和 ===區別,什麼狀況用 ==

這裏來解析一道題目 [] == ![] // -> true ,下面是這個表達式爲什麼爲 true 的步驟

// [] 轉成 true,而後取反變成 false [] == false // 根據第 8 條得出 [] == ToNumber(false) [] == 0 // 根據第 10 條得出 ToPrimitive([]) == 0 // [].toString() -> '' '' == 0 // 根據第 6 條得出 0 == 0 // -> true 

===用於判斷二者類型和值是否相同。 在開發中,對於後端返回的 code,能夠經過 ==去判斷

#15 基本數據類型和引⽤類型在存儲上的差異

前者存儲在棧上,後者存儲在堆上

#16 瀏覽器 Eventloop 和 Node 中的有什麼區別

衆所周知 JS 是門非阻塞單線程語言,由於在最初 JS 就是爲了和瀏覽器交互而誕生的。若是 JS 是門多線程的語言話,咱們在多個線程中處理 DOM 就可能會發生問題(一個線程中新加節點,另外一個線程中刪除節點),固然能夠引入讀寫鎖解決這個問題。

  • JS 在執行的過程當中會產生執行環境,這些執行環境會被順序的加入到執行棧中。若是遇到異步的代碼,會被掛起並加入到 Task(有多種 task) 隊列中。一旦執行棧爲空,Event Loop 就會從 Task 隊列中拿出須要執行的代碼並放入執行棧中執行,因此本質上來講 JS 中的異步仍是同步行爲
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); console.log('script end'); 
  • 以上代碼雖然 setTimeout 延時爲 0,其實仍是異步。這是由於 HTML5 標準規定這個函數第二個參數不得小於 4 毫秒,不足會自動增長。因此 setTimeout仍是會在 script end 以後打印。
  • 不一樣的任務源會被分配到不一樣的 Task隊列中,任務源能夠分爲 微任務(microtask) 和 宏任務(macrotask)。在 ES6 規範中,microtask 稱爲 jobs,macrotask 稱爲 task
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise((resolve) => { console.log('Promise') resolve() }).then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // script start => Promise => script end => promise1 => promise2 => setTimeout 
  • 以上代碼雖然 setTimeout 寫在 Promise 以前,可是由於 Promise 屬於微任務而 setTimeout屬於宏任務,因此會有以上的打印。
  • 微任務包括 process.nextTick ,promise ,Object.observeMutationObserver
  • 宏任務包括 script , setTimeout ,setIntervalsetImmediate ,I/O ,UI renderin

不少人有個誤區,認爲微任務快於宏任務,實際上是錯誤的。由於宏任務中包括了 script,瀏覽器會先執行一個宏任務,接下來有異步代碼的話就先執行微任務

因此正確的一次 Event loop 順序是這樣的

  • 執行同步代碼,這屬於宏任務
  • 執行棧爲空,查詢是否有微任務須要執行
  • 執行全部微任務
  • 必要的話渲染 UI
  • 而後開始下一輪 Event loop,執行宏任務中的異步代碼

經過上述的 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 = interval function loop() { count++ var offset = new Date().getTime() - (startTime + count * interval); // 代碼執行所消耗的時間 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 // 獲得下一次循環所消耗的時間 console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執行時間:'+offset, '下次循環間隔'+currentInterval) // 打印 時 分 秒 代碼執行時間 下次循環間隔 setTimeout(loop, currentInterval) } setTimeout(loop, currentInterval) 

#18 數組降維

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4] 

若是想將一個多維數組完全的降維,能夠這樣實現

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
  • 不能序列化函數
  • 不能解決循環引用的對象
let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj) 復 

在遇到函數、 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 區別

typeof 對於基本類型,除了 null均可以顯示正確的類型

typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒有聲明,可是還會顯示 undefined 

typeof 對於對象,除了函數都會顯示 object

typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function' 

對於 null 來講,雖然它是基本類型,可是會顯示 object,這是一個存在好久了的 Bug

typeof null // 'object'

instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 iprototype

咱們也能夠試着實現一下 instanceof function instanceof(left, right) { // 得到類型的原型 let prototype = right.prototype // 得到對象的原型 left = left.__proto__ // 判斷對象的類型是否等於類型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ } } 

#2、瀏覽器

#1 cookie和localSrorage、session、indexDB 的區別

特性 cookie localStorage sessionStorage indexDB
數據生命週期 通常由服務器生成,能夠設置過時時間 除非被清理,不然一直存在 頁面關閉就清理 除非被清理,不然一直存在
數據存儲大小 4K 5M 5M 無限
與服務端通訊 每次都會攜帶在 header 中,對於請求性能影響 不參與 不參與 不參與

從上表能夠看到,cookie 已經不建議用於存儲。若是沒有大量數據存儲需求的話,可使用 localStorage和 sessionStorage 。對於不怎麼改變的數據儘可能使用 localStorage 存儲,不然能夠用 sessionStorage 存儲。

對於 cookie,咱們還須要注意安全性

屬性 做用
value 若是用於保存用戶登陸態,應該將該值加密,不能使用明文的用戶標識
http-only 不能經過 JS訪問 Cookie,減小 XSS攻擊
secure 只能在協議爲 HTTPS 的請求中攜帶
same-site 規定瀏覽器不能在跨域請求中攜帶 Cookie,減小 CSRF 攻擊

#2 怎麼判斷頁面是否加載完成?

  • Load 事件觸發表明頁面中的 DOMCSSJS,圖片已經所有加載完畢。
  • DOMContentLoaded 事件觸發表明初始的 HTML 被徹底加載和解析,不須要等待 CSSJS,圖片加載

#3 如何解決跨域

由於瀏覽器出於安全考慮,有同源策略。也就是說,若是協議、域名或者端口有一個不一樣就是跨域,Ajax請求會失敗。

咱們能夠經過如下幾種經常使用方法解決跨域的問題

JSONP

JSONP 的原理很簡單,就是利用 <script>標籤沒有跨域限制的漏洞。經過 <script>標籤指向一個須要訪問的地址並提供一個回調函數來接收數據當須要通信時

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script> <script> function jsonp(data) { console.log(data) } </script> 

JSONP 使用簡單且兼容性不錯,可是隻限於 get 請求

  • 在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,這時候就須要本身封裝一個 JSONP,如下是簡單實現
function jsonp(url, jsonpCallback, success) { let script = document.createElement("script"); script.src = url; script.async = true; script.type = "text/javascript"; window[jsonpCallback] = function(data) { success && success(data); }; document.body.appendChild(script); } jsonp( "http://xxx", "callback", function(value) { console.log(value); } ); 

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

這種方式一般用於獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另外一個頁面判斷來源並接收消息

// 發送消息端 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 什麼是事件代理

若是一個節點中的子節點是動態生成的,那麼子節點須要註冊事件的話應該註冊在父節點上

<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> 
  • 事件代理的方式相對於直接給目標註冊事件來講,有如下優勢
    • 節省內存
    • 不須要給子節點註銷事件

#5 Service worker

service worker

Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。他們還容許訪問推送通知和後臺同步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 // 監聽 `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"); }) ); }); 

打開頁面,能夠在開發者工具中的 Application 看到 Service Worker已經啓動了

#6 瀏覽器緩存

緩存對於前端性能優化來講是個很重要的點,良好的緩存策略能夠下降資源的重複加載提升網頁的總體加載速度。

  • 一般瀏覽器緩存策略分爲兩種:強緩存和協商緩存。

強緩存

實現強緩存能夠經過兩種響應頭實現:Expires 和 Cache-Control 。強緩存表示在緩存期間不須要請求,state code 爲 200

Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expires 是 HTTP / 1.0 的產物,表示資源會在Wed,22 Oct 2018 08:41:00 GMT 後過時,須要再次請求。而且 Expires 受限於本地時間,若是修改了本地時間,可能會形成緩存失效。

Cache-control: max-age=30
  • Cache-Control 出現於 HTTP / 1.1,優先級高於 Expires 。該屬性表示資源會在 30 秒後過時,須要再次請求。

協商緩存

  • 若是緩存過時了,咱們就可使用協商緩存來解決問題。協商緩存須要請求,若是緩存有效會返回 304
  • 協商緩存須要客戶端和服務端共同實現,和強緩存同樣,也有兩種實現方式

Last-Modified 和 If-Modified-Since

  • Last-Modified表示本地文件最後修改日期,If-Modified-Since 會將 Last-Modified 的值發送給服務器,詢問服務器在該日期後資源是否有更新,有更新的話就會將新的資源發送回來。
  • 可是若是在本地打開緩存文件,就會形成 Last-Modified被修改,因此在 HTTP / 1.1 出現了 ETag

ETag 和 If-None-Match

ETag 相似於文件指紋,If-None-Match 會將當前 ETag發送給服務器,詢問該資源 ETag 是否變更,有變更的話就將新的資源發送回來。而且 ETag 優先級比 Last-Modified 高

選擇合適的緩存策略

對於大部分的場景均可以使用強緩存配合協商緩存解決,可是在一些特殊的地方可能須要選擇特殊的緩存策略

  • 對於某些不須要緩存的資源,可使用 Cache-control: no-store ,表示該資源不須要緩存
  • 對於頻繁變更的資源,可使用 Cache-Control: no-cache並配合 ETag 使用,表示該資源已被緩存,可是每次都會發送請求詢問資源是否更新。
  • 對於代碼文件來講,一般使用 Cache-Control: max-age=31536000 並配合策略緩存使用,而後對文件進行指紋處理,一旦文件名變更就會馬上下載新的文件

#7 瀏覽器性能問題

重繪(Repaint)和迴流(Reflow)

  • 重繪和迴流是渲染步驟中的一小節,可是這兩個步驟對於性能影響很大。
  • 重繪是當節點須要更改外觀而不會影響佈局的,好比改變 color就叫稱爲重繪
  • 迴流是佈局或者幾何屬性須要改變就稱爲迴流。
  • 迴流一定會發生重繪,重繪不必定會引起迴流。迴流所需的成本比重繪高的多,改變深層次的節點極可能致使父節點的一系列迴流。

因此如下幾個動做可能會致使性能問題:

  • 改變 window 大小
  • 改變字體
  • 添加或刪除樣式
  • 文字改變
  • 定位或者浮動
  • 盒模型

不少人不知道的是,重繪和迴流其實和 Event loop 有關。

  • 當 Event loop 執行完 Microtasks後,會判斷 document 是否須要更新。- 由於瀏覽器是 60Hz 的刷新率,每 16ms纔會更新一次。
  • 而後判斷是否有resize 或者 scroll ,有的話會去觸發事件,因此 resize 和 scroll 事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。
  • 判斷是否觸發了 media query
  • 更新動畫而且發送事件
  • 判斷是否有全屏操做事件
  • 執行 requestAnimationFrame回調
  • 執行 IntersectionObserver 回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差
  • 更新界面
  • 以上就是一幀中可能會作的事情。若是在一幀中有空閒時間,就會去執行 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 ,由於前者只會引發重繪,後者會引起迴流(改變了佈局)
  • 把 DOM 離線後修改,好比:先把 DOM 給 display:none(有一次 Reflow),而後你修改100次,而後再把它顯示出來
  • 不要把 DOM結點的屬性值放在一個循環裏當成循環裏的變量
for(let i = 0; i < 1000; i++) { // 獲取 offsetTop 會致使迴流,由於須要去獲取正確的值 console.log(document.querySelector('.test').style.offsetTop) } 
  • 不要使用 table 佈局,可能很小的一個小改動會形成整個 table 的從新佈局 動畫實現的速度的選擇,動畫速度越快,迴流次數越多,也能夠選擇使用 requestAnimationFrame
  • CSS選擇符從右往左匹配查找,避免 DOM 深度過深
  • 將頻繁運行的動畫變爲圖層,圖層可以阻止該節點回流影響別的元素。好比對於 video 標籤,瀏覽器會自動將該節點變爲圖層。

CDN

靜態資源儘可能使用 CDN 加載,因爲瀏覽器對於單個域名有併發請求上限,能夠考慮使用多個 CDN 域名。對於 CDN 加載靜態資源須要注意 CDN 域名要與主站不一樣,不然每次請求都會帶上主站的 Cookie

使用 Webpack 優化項目

  • 對於 Webpack4,打包項目使用 production 模式,這樣會自動開啓代碼壓縮
  • 使用 ES6 模塊來開啓 tree shaking,這個技術能夠移除沒有使用的代碼
  • 優化圖片,對於小圖可使用 base64 的方式寫入文件中
  • 按照路由拆分代碼,實現按需加載

#3、Webpack



#1 優化打包速度

  • 減小文件搜索範圍
    • 好比經過別名
    • loader 的 testinclude & exclude
  • Webpack4 默認壓縮並行
  • Happypack 併發調用
  • babel 也能夠緩存編譯

#2 Babel 原理

  • 本質就是編譯器,當代碼轉爲字符串生成 AST,對 AST 進行轉變最後再生成新的代碼
  • 分爲三步:詞法分析生成 Token,語法分析生成 AST,遍歷 AST,根據插件變換相應的節點,最後把 AST轉換爲代碼

#3 如何實現一個插件

  • 調用插件 apply 函數傳入 compiler 對象
  • 經過 compiler 對象監聽事件

好比你想實現一個編譯結束退出命令的插件

apply (compiler) { const afterEmit = (compilation, cb) => { cb() setTimeout(function () { process.exit(0) }, 1000) } compiler.plugin('after-emit', afterEmit) } } module.exports = BuildEndPlugin
相關文章
相關標籤/搜索