2萬5千字大廠面經 | 掘金技術徵文

如下面試題來自騰訊、阿里、網易、餓了麼、美團、拼多多、百度等等大廠綜合起來常考的題目。javascript

如何寫一個漂亮的簡歷

簡歷不是一份記流水帳的東西,而是讓用人方瞭解你的亮點的。css

平時有在作一些修改簡歷的收費服務,也算看過蠻多簡歷了。不少簡歷都有以下特徵html

  • 喜歡說本身的特長、優勢,用人方真的不關注你的性格是否陽光等等
  • 我的技能可以佔半頁的篇幅,並且長得也都差很少
  • 項目經驗流水帳,好比我會用這個 API 實現了某某功能
  • 簡歷頁數過多,真心看不下去

以上相似簡歷能夠說用人方也看了無數份,徹底抓不到你的亮點。除非你呆過大廠或者教育背景不錯或者技術棧符合人家要求了,不然基本就是看運氣約面試了。前端

如下是我常常給別人修改簡歷的意見:vue

簡歷頁數控制在 2 頁如下java

  • 技術名詞注意大小寫
  • 突出我的亮點,擴充內容。好比在項目中如何找到 Bug,解決 Bug 的過程;好比如何發現的性能問題,如何解決性能問題,最終提高了多少性能;好比爲什麼如此選型,目的是什麼,較其餘有什麼優勢等等。整體思路就是不寫流水帳,突出你在項目中具備不錯的解決問題的能力和獨立思考的能力。
  • 斟酌熟悉、精通等字眼,不要給本身挖坑
  • 確保每個寫上去的技術點本身都能說出點什麼,杜絕面試官問你一個技術點,你只能答出會用 API 這種減分的狀況

作到以上內容,而後在投遞簡歷的過程當中加上一份求職信,對你的求職之路相信能幫上不少忙。node

JS 相關

談談變量提高?

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

接下來讓咱們看一個老生常談的例子,varc++

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}
複製代碼

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

在提高的過程當中,相同的函數會覆蓋上一個函數,而且函數優先於變量提高

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 提高了,在第一階段內存也已經爲他開闢好了空間,可是由於這個聲明的特性致使了並不能在聲明前使用。

bind、call、apply 區別

首先說下前二者的區別。

callapply 都是爲了解決改變 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 實現柯里化。

如何實現一個 bind 函數

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

  • 不傳入第一個參數,那麼默認爲 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))
  }
}
複製代碼

如何實現一個 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
}
複製代碼

如何實現一個 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
}
複製代碼

簡單說下原型鏈?

prototype

每一個函數都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。

每一個對象都有 __proto__ 屬性,指向了建立該對象的構造函數的原型。其實這個屬性指向了 [[prototype]],可是 [[prototype]] 是內部屬性,咱們並不能訪問到,因此使用 _proto_ 來訪問。

對象能夠經過 __proto__ 來尋找不屬於該對象的屬性,__proto__ 將對象鏈接起來組成了原型鏈。

若是你想更進一步的瞭解原型,能夠仔細閱讀 深度解析原型中的各個難點

怎麼判斷對象類型?

  • 能夠經過 Object.prototype.toString.call(xx)。這樣咱們就能夠得到相似 [object Type] 的字符串。
  • instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 prototype

箭頭函數的特色

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())
複製代碼

箭頭函數實際上是沒有 this 的,這個函數中的 this 只取決於他外面的第一個不是箭頭函數的函數的 this。在這個例子中,由於調用 a 符合前面代碼中的第一個狀況,因此 thiswindow。而且 this 一旦綁定了上下文,就不會被任何代碼改變。

This

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
複製代碼

async、await 優缺點

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

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

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 內部實現了 generatorsgenerators 會保留堆棧中東西,因此這時候 a = 0 被保存了下來
  • 由於 await 是異步操做,遇到await就會當即返回一個pending狀態的Promise對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,因此會先執行 console.log('1', a)
  • 這時候同步代碼執行完畢,開始執行異步代碼,將保存下來的值拿出來使用,這時候 a = 10
  • 而後後面就是常規執行代碼了

generator 原理

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

// 使用 * 表示這是一個 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();
      }
    }
  });
}
複製代碼

Promise

Promise 是 ES6 新增的語法,解決了回調地獄的問題。

能夠把 Promise 當作一個狀態機。初始是 pending 狀態,能夠經過函數 resolvereject ,將狀態轉變爲 resolved 或者 rejected 狀態,狀態一旦改變就不能再次變化。

then 函數會返回一個 Promise 實例,而且該返回值是一個新的實例而不是以前的實例。由於 Promise 規範規定除了 pending 狀態,其餘狀態是不能夠改變的,若是返回的是一個相同實例的話,多個 then 調用就失去意義了。

對於 then 來講,本質上能夠把它當作是 flatMap

如何實現一個 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);
  }
}
複製代碼

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

上圖中的 toPrimitive 就是對象轉基本類型。

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

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

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

垃圾回收

V8 實現了準確式 GC,GC 算法採用了分代式垃圾回收機制。所以,V8 將內存(堆)分爲新生代和老生代兩部分。

新生代算法

新生代中的對象通常存活時間較短,使用 Scavenge GC 算法。

在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,一定有一個空間是使用的,另外一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,若是有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

老生代算法

老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。

在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:

  • 新生代中的對象是否已經經歷過一次 Scavenge 算法,若是經歷過的話,會將對象重新生代空間移到老生代空間中。
  • To 空間的對象佔比大小超過 25 %。在這種狀況下,爲了避免影響到內存分配,會將對象重新生代空間移到老生代空間中。

老生代中的空間很複雜,有以下幾個空間

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不變的對象空間
  NEW_SPACE,   // 新生代用於 GC 複製算法的空間
  OLD_SPACE,   // 老生代常駐對象空間
  CODE_SPACE,  // 老生代代碼對象空間
  MAP_SPACE,   // 老生代 map 對象
  LO_SPACE,    // 老生代大空間對象
  NEW_LO_SPACE,  // 新生代大空間對象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
複製代碼

在老生代中,如下狀況會先啓動標記清除算法:

  • 某一個空間沒有分塊的時候
  • 空間中被對象超過必定限制
  • 空間不能保證新生代中的對象移動到老生代中

在這個階段中,會遍歷堆中全部的對象,而後標記活的對象,在標記完成後,銷燬全部沒有被標記的對象。在標記大型對內存時,可能須要幾百毫秒才能完成一次標記。這就會致使一些性能上的問題。爲了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工做分解爲更小的模塊,可讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓狀況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名爲併發標記。該技術可讓 GC 掃描和標記對象時,同時容許 JS 運行,你能夠點擊 該博客 詳細閱讀。

清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。

閉包

閉包的定義很簡單:函數 A 返回了一個函數 B,而且函數 B 中使用了函數 A 的變量,函數 B 就被稱爲閉包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
複製代碼

你是否會疑惑,爲何函數 A 已經彈出調用棧了,爲何函數 B 還能引用到函數 A 中的變量。由於函數 A 中的變量這時候是存儲在堆上的。如今的 JS 引擎能夠經過逃逸分析辨別出哪些變量須要存儲在堆上,哪些須要存儲在棧上。

經典面試題,循環中使用閉包解決 var 定義函數的問題

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
複製代碼

首先由於 setTimeout 是個異步函數,全部會先把循環所有執行完畢,這時候 i 就是 6 了,因此會輸出一堆 6。

解決辦法兩種,第一種使用閉包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}
複製代碼

第二種就是使用 setTimeout 的第三個參數

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}
複製代碼

第三種就是使用 let 定義 i

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
複製代碼

由於對於 let 來講,他會建立一個塊級做用域,至關於

{ // 造成塊級做用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}
複製代碼

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

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

瀏覽器 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.nextTickpromiseObject.observeMutationObserver

宏任務包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

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

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

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

經過上述的 Event loop 順序可知,若是宏任務中的異步代碼有大量的計算而且須要操做 DOM 的話,爲了更快的 界面響應,咱們能夠把操做 DOM 放入微任務中。

Node 中的 Event loop

Node 中的 Event loop 和瀏覽器中的不相同。

Node 的 Event loop 分爲6個階段,它們會按照順序反覆運行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
複製代碼

timer

timers 階段會執行 setTimeoutsetInterval

一個 timer 指定的時間並非準確時間,而是在達到這個時間後儘快執行回調,可能會由於系統正在執行別的事務而延遲。

下限的時間有一個範圍:[1, 2147483647] ,若是設定的時間不在這個範圍,將被設置爲1。

**I/O **

I/O 階段會執行除了 close 事件,定時器和 setImmediate 的回調

idle, prepare

idle, prepare 階段內部實現

poll

poll 階段很重要,這一階段中,系統會作兩件事情

  1. 執行到點的定時器
  2. 執行 poll 隊列中的事件

而且當 poll 中沒有定時器的狀況下,會發現如下兩件事情

  • 若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者系統限制
  • 若是 poll 隊列爲空,會有兩件事發生
    • 若是有 setImmediate 須要執行,poll 階段會中止而且進入到 check 階段執行 setImmediate
    • 若是沒有 setImmediate 須要執行,會等待回調被加入到隊列中並當即執行回調

若是有別的定時器須要被執行,會回到 timer 階段執行回調。

check

check 階段執行 setImmediate

close callbacks

close callbacks 階段執行 close 事件

而且在 Node 中,有些狀況下的定時器執行順序是隨機的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 這裏可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於性能
// 由於可能進入 event loop 用了不到 1 毫秒,這時候會執行 setImmediate
// 不然會執行 setTimeout
複製代碼

固然在這種狀況下,執行順序是相同的

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 由於 readFile 的回調在 poll 中執行
// 發現有 setImmediate ,因此會當即跳到 check 階段執行回調
// 再去 timer 階段執行 setTimeout
// 因此以上輸出必定是 setImmediate,setTimeout
複製代碼

上面介紹的都是 macrotask 的執行狀況,microtask 會在以上每一個階段完成後當即執行。

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代碼在瀏覽器和 node 中打印狀況是不一樣的
// 瀏覽器中必定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
複製代碼

Node 中的 process.nextTick 會先於其餘 microtask 執行。

setTimeout(() => {
  console.log("timer1");

  Promise.resolve().then(function() {
    console.log("promise1");
  });
}, 0);

process.nextTick(() => {
  console.log("nextTick");
});
// nextTick, timer1, promise1
複製代碼

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)
複製代碼

防抖

你是否在平常開發中遇到一個問題,在滾動事件中須要作個複雜計算或者實現一個按鈕的防二次點擊操做。

這些需求均可以經過函數防抖動來實現。尤爲是第一個需求,若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做。

PS:防抖和節流的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於wait,防抖的狀況下只會調用一次,而節流的 狀況會每隔必定時間(參數wait)調用函數。

咱們先來看一個袖珍版的防抖理解一下防抖的實現:

// func是用戶傳入須要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這裏返回的函數是每次用戶實際調用的防抖函數
  // 若是已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出若是用戶調用該函數的間隔小於wait的狀況下,上一次的時間還未到就被清除了,並不會執行函數
複製代碼

這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:

  • 例如在搜索引擎搜索問題的時候,咱們固然是但願用戶輸入完最後一個字才調用查詢接口,這個時候適用延遲執行的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。
  • 例如用戶給interviewMap點star的時候,咱們但願用戶點第一下的時候就去調用接口,而且成功以後改變star按鈕的樣子,用戶就能夠立馬獲得反饋是否star成功了,這個狀況適用當即執行的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。

下面咱們來實現一個帶有當即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/** * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行 * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {boolean} immediate 設置爲ture時,是否當即調用函數 * @return {function} 返回客戶調用函數 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的狀況下,函數會在延遲函數中執行
    // 使用到以前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這裏返回的函數是每次實際調用的函數
  return function(...params) {
    // 若是沒有建立延遲執行函數(later),就建立一個
    if (!timer) {
      timer = later()
      // 若是是當即執行,調用函數
      // 不然緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
    // 這樣作延遲函數會從新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
複製代碼

總體函數實現的不難,總結一下。

  • 對於按鈕防點擊來講的實現:若是函數是當即執行的,就當即調用,若是函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都從新計時。一旦你點累了,定時器時間到,定時器重置爲 null,就能夠再次點擊了。
  • 對於延時執行函數來講的實現:清除定時器ID,若是是延遲調用就調用函數

數組降維

[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]])
複製代碼

深拷貝

這個問題一般能夠經過 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"}
複製代碼

你會發如今上述狀況中,該方法會忽略掉函數和 undefined

可是在一般狀況下,複雜數據都是能夠序列化的,因此這個函數能夠解決大部分問題,而且該函數是內置函數中處理深拷貝性能最快的。固然若是你的數據中含有以上三種狀況下,能夠使用 lodash 的深拷貝函數

若是你所需拷貝的對象含有內置類型而且不包含函數,能夠使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 注意該方法是異步的
// 能夠處理 undefined 和循環引用對象
(async () => {
  const clone = await structuralClone(obj)
})()
複製代碼

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'
複製代碼

PS:爲何會出現這種狀況呢?由於在 JS 的最第一版本中,使用的是 32 位系統,爲了性能考慮使用低位存儲了變量的類型信息,000 開頭表明是對象,然而 null 表示爲全零,因此將它錯誤的判斷爲 object 。雖然如今的內部類型判斷代碼已經改變了,可是對於這個 Bug 倒是一直流傳下來。

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

咱們也能夠試着實現一下 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__
    }
}
複製代碼

瀏覽器相關

cookie和localSrorage、session、indexDB 的區別

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

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

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

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

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

Load 事件觸發表明頁面中的 DOM,CSS,JS,圖片已經所有加載完畢。

DOMContentLoaded 事件觸發表明初始的 HTML 被徹底加載和解析,不須要等待 CSS,JS,圖片加載。

如何解決跨域

由於瀏覽器出於安全考慮,有同源策略。也就是說,若是協議、域名或者端口有一個不一樣就是跨域,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

CORS須要瀏覽器和後端同時支持。IE 8 和 9 須要經過 XDomainRequest 來實現。

瀏覽器會自動進行 CORS 通訊,實現CORS通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。

服務端設置 Access-Control-Allow-Origin 就能夠開啓 CORS。 該屬性表示哪些域名能夠訪問資源,若是設置通配符則表示全部網站均可以訪問資源。

document.domain

該方式只能用於二級域名相同的狀況下,好比 a.test.comb.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('驗證經過')
    }
});
複製代碼

什麼是事件代理

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

<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>
複製代碼

事件代理的方式相對於直接給目標註冊事件來講,有如下優勢

  • 節省內存
  • 不須要給子節點註銷事件

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 已經啓動了

瀏覽器緩存

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

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

強緩存

實現強緩存能夠經過兩種響應頭實現:ExpiresCache-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 並配合策略緩存使用,而後對文件進行指紋處理,一旦文件名變更就會馬上下載新的文件。

瀏覽器性能問題

重繪(Repaint)和迴流(Reflow)

重繪和迴流是渲染步驟中的一小節,可是這兩個步驟對於性能影響很大。

  • 重繪是當節點須要更改外觀而不會影響佈局的,好比改變 color 就叫稱爲重繪
  • 迴流是佈局或者幾何屬性須要改變就稱爲迴流。

迴流一定會發生重繪,重繪不必定會引起迴流。迴流所需的成本比重繪高的多,改變深層次的節點極可能致使父節點的一系列迴流。

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

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

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

  1. 當 Event loop 執行完 Microtasks 後,會判斷 document 是否須要更新。由於瀏覽器是 60Hz 的刷新率,每 16ms 纔會更新一次。
  2. 而後判斷是否有 resize 或者 scroll ,有的話會去觸發事件,因此 resizescroll 事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。
  3. 判斷是否觸發了 media query
  4. 更新動畫而且發送事件
  5. 判斷是否有全屏操做事件
  6. 執行 requestAnimationFrame 回調
  7. 執行 IntersectionObserver 回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差
  8. 更新界面
  9. 以上就是一幀中可能會作的事情。若是在一幀中有空閒時間,就會去執行 requestIdleCallback 回調。

以上內容來自於 HTML 文檔

減小重繪和迴流
  • 使用 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 標籤,瀏覽器會自動將該節點變爲圖層。

圖片優化

計算圖片大小

對於一張 100 * 100 像素的圖片來講,圖像上有 10000 個像素點,若是每一個像素的值是 RGBA 存儲的話,那麼也就是說每一個像素有 4 個通道,每一個通道 1 個字節(8 位 = 1個字節),因此該圖片大小大概爲 39KB(10000 * 1 * 4 / 1024)。

可是在實際項目中,一張圖片可能並不須要使用那麼多顏色去顯示,咱們能夠經過減小每一個像素的調色板來相應縮小圖片的大小。

瞭解瞭如何計算圖片大小的知識,那麼對於如何優化圖片,想必你們已經有 2 個思路了:

  • 減小像素點
  • 減小每一個像素點可以顯示的顏色
圖片加載優化
  1. 不用圖片。不少時候會使用到不少修飾類圖片,其實這類修飾圖片徹底能夠用 CSS 去代替。
  2. 對於移動端來講,屏幕寬度就那麼點,徹底沒有必要去加載原圖浪費帶寬。通常圖片都用 CDN 加載,能夠計算出適配屏幕的寬度,而後去請求相應裁剪好的圖片。
  3. 小圖使用 base64 格式
  4. 將多個圖標文件整合到一張圖片中(雪碧圖)
  5. 選擇正確的圖片格式:
    • 對於可以顯示 WebP 格式的瀏覽器儘可能使用 WebP 格式。由於 WebP 格式具備更好的圖像數據壓縮算法,能帶來更小的圖片體積,並且擁有肉眼識別無差別的圖像質量,缺點就是兼容性並很差
    • 小圖使用 PNG,其實對於大部分圖標這類圖片,徹底能夠使用 SVG 代替
    • 照片使用 JPEG

其餘文件優化

  • CSS 文件放在 head
  • 服務端開啓文件壓縮功能
  • script 標籤放在 body 底部,由於 JS 文件執行會阻塞渲染。固然也能夠把 script 標籤放在任意位置而後加上 defer ,表示該文件會並行下載,可是會放到 HTML 解析完成後順序執行。對於沒有任何依賴的 JS 文件能夠加上 async ,表示加載和渲染後續文檔元素的過程將和 JS 文件的加載與執行並行無序進行。
  • 執行 JS 代碼過長會卡住渲染,對於須要不少時間計算的代碼能夠考慮使用 WebworkerWebworker 可讓咱們另開一個線程執行腳本而不影響渲染。

CDN

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

使用 Webpack 優化項目

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

Webpack

優化打包速度

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

Babel 原理

本質就是編譯器,當代碼轉爲字符串生成 AST,對 AST 進行轉變最後再生成新的代碼

  • 分爲三步:詞法分析生成 Token,語法分析生成 AST,遍歷 AST,根據插件變換相應的節點,最後把 AST 轉換爲代碼

如何實現一個插件

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

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

class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }

    compiler.plugin('after-emit', afterEmit)
  }
}

module.exports = BuildEndPlugin
複製代碼

框架

React 生命週期

在 V16 版本中引入了 Fiber 機制。這個機制必定程度上的影響了部分生命週期的調用,而且也引入了新的 2 個 API 來解決問題。

在以前的版本中,若是你擁有一個很複雜的複合組件,而後改動了最上層組件的 state,那麼調用棧可能會很長

調用棧過長,再加上中間進行了複雜的操做,就可能致使長時間阻塞主線程,帶來很差的用戶體驗。Fiber 就是爲了解決該問題而生。

Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。

對於如何區別優先級,React 有本身的一套邏輯。對於動畫這種實時性很高的東西,也就是 16 ms 必須渲染一次保證不卡頓的狀況下,React 會每 16 ms(之內) 暫停一下更新,返回來繼續渲染動畫。

對於異步渲染,如今渲染有兩個階段:reconciliationcommit 。前者過程是能夠打斷的,後者不能暫停,會一直更新界面直到完成。

Reconciliation 階段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 階段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由於 reconciliation 階段是能夠被打斷的,因此 reconciliation 階段會執行的生命週期函數就可能會出現調用屢次的狀況,從而引發 Bug。因此對於 reconciliation 階段調用的幾個函數,除了 shouldComponentUpdate 之外,其餘都應該避免去使用,而且 V16 中也引入了新的 API 來解決這個問題。

getDerivedStateFromProps 用於替換 componentWillReceiveProps ,該函數會在初始化和 update 時被調用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}
複製代碼

getSnapshotBeforeUpdate 用於替換 componentWillUpdate ,該函數會在 update 後 DOM 更新前被調用,用於讀取最新的 DOM 數據。

V16 生命週期函數用法建議

class ExampleComponent extends React.Component {
  // 用於初始化 state
  constructor() {}
  // 用於替換 `componentWillReceiveProps` ,該函數會在初始化和 `update` 時被調用
  // 由於該函數是靜態函數,因此取不到 `this`
  // 若是須要對比 `prevProps` 須要單獨在 `state` 中維護
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判斷是否須要更新組件,多用於組件性能優化
  shouldComponentUpdate(nextProps, nextState) {}
  // 組件掛載後調用
  // 能夠在該函數中進行請求或者訂閱
  componentDidMount() {}
  // 用於得到最新的 DOM 數據
  getSnapshotBeforeUpdate() {}
  // 組件即將銷燬
  // 能夠在此處移除訂閱,定時器等等
  componentWillUnmount() {}
  // 組件銷燬後調用
  componentDidUnMount() {}
  // 組件更新後調用
  componentDidUpdate() {}
  // 渲染組件函數
  render() {}
  // 如下函數不建議使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}
複製代碼

setState

setState 在 React 中是常用的一個 API,可是它存在一些問題,可能會致使犯錯,核心緣由就是由於這個 API 是異步的。

首先 setState 的調用並不會立刻引發 state 的改變,而且若是你一次調用了多個 setState ,那麼結果可能並不如你期待的同樣。

handle() {
  // 初始化 `count` 爲 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}
複製代碼

第一,兩次的打印都爲 0,由於 setState 是個異步 API,只有同步代碼運行完畢纔會執行。setState 異步的緣由我認爲在於,setState 可能會致使 DOM 的重繪,若是調用一次就立刻去進行重繪,那麼調用屢次就會形成沒必要要的性能損失。設計成異步的話,就能夠將屢次調用放入一個隊列中,在恰當的時候統一進行更新過程。

第二,雖然調用了三次 setState ,可是 count 的值仍是爲 1。由於屢次調用會合併爲一次,只有當更新結束後 state 纔會改變,三次調用等同於以下代碼

Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)
複製代碼

固然你也能夠經過如下方式來實現調用三次 setState 使得 count 爲 3

handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}
複製代碼

若是你想在每次調用 setState 後得到正確的 state ,能夠經過以下代碼實現

handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}
複製代碼

Vue的 nextTick 原理

nextTick 可讓咱們在下次 DOM 更新循環結束以後執行延遲迴調,用於得到更新後的 DOM。

在 Vue 2.4 以前都是使用的 microtasks,可是 microtasks 的優先級太高,在某些狀況下可能會出現比事件冒泡更快的狀況,但若是都使用 macrotasks 又可能會出現渲染的性能問題。因此在新版本中,會默認使用 microtasks,但在特殊狀況下會使用 macrotasks,好比 v-on。

對於實現 macrotasks ,會先判斷是否能使用 setImmediate ,不能的話降級爲 MessageChannel ,以上都不行的話就使用 setTimeout

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (
  typeof MessageChannel !== 'undefined' &&
  (isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
複製代碼

nextTick 同時也支持 Promise 的使用,會判斷是否實現了 Promise

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // 將回調函數整合進一個數組中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // 判斷是否能夠使用 Promise 
  // 能夠的話給 _resolve 賦值
  // 這樣回調函數就能以 promise 的方式調用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
複製代碼

Vue 生命週期

生命週期函數就是組件在初始化或者數據更新時會觸發的鉤子函數。

在初始化時,會調用如下代碼,生命週期就是經過 callHook 調用的

Vue.prototype._init = function(options) {
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 拿不到 props data
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
}
複製代碼

能夠發如今以上代碼中,beforeCreate 調用的時候,是獲取不到 props 或者 data 中的數據的,由於這些數據的初始化都在 initState 中。

接下來會執行掛載函數

export function mountComponent {
    callHook(vm, 'beforeMount')
    // ...
    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
}
複製代碼

beforeMount 就是在掛載前執行的,而後開始建立 VDOM 並替換成真實 DOM,最後執行 mounted 鉤子。這裏會有個判斷邏輯,若是是外部 new Vue({}) 的話,不會存在 $vnode ,因此直接執行 mounted 鉤子了。若是有子組件的話,會遞歸掛載子組件,只有當全部子組件所有掛載完畢,纔會執行根組件的掛載鉤子。

接下來是數據更新時會調用的鉤子函數

function flushSchedulerQueue() {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 調用 beforeUpdate
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        )
        break
      }
    }
  }
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks(queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}
複製代碼

上圖還有兩個生命週期沒有說,分別爲 activateddeactivated ,這兩個鉤子函數是 keep-alive 組件獨有的。用 keep-alive 包裹的組件在切換時不會進行銷燬,而是緩存到內存中並執行 deactivated 鉤子函數,命中緩存渲染後會執行 actived 鉤子函數。

最後就是銷燬組件的鉤子函數了

Vue.prototype.$destroy = function() {
  // ...
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}
複製代碼

在執行銷燬操做前會調用 beforeDestroy 鉤子函數,而後進行一系列的銷燬操做,若是有子組件的話,也會遞歸銷燬子組件,全部子組件都銷燬完畢後纔會執行根組件的 destroyed 鉤子函數。

Vue 雙向綁定

  • 在初始化 data props 時,遞歸對象,給每個屬性雙向綁定,對於數組而言,會拿到原型重寫函數,實現手動派發更新。由於函數不能監聽到數據的變更,和 proxy 比較一下。
  • 除了以上數組函數,經過索引改變數組數據或者給對象添加新屬性也不能觸發,須要使用自帶的set 函數,這個函數內部也是手動派發更新
  • 在組件掛載時,會實例化渲染觀察者,傳入組件更新的回調。在實例化過程當中,會對模板中的值對象進行求值,觸發依賴收集。在觸發依賴以前,會保存當前的渲染觀察者,用於組件含有子組件的時候,恢復父組件的觀察者。觸發依賴收集後,會清理掉不須要的依賴,性能優化,防止不須要的地方去重複渲染。
  • 改變值會觸發依賴更新,會將收集到的全部依賴所有拿出來,放入 nextTick 中統一執行。執行過程當中,會先對觀察者進行排序,渲染的最後執行。先執行 beforeupdate 鉤子函數,而後執行觀察者的回調。在執行回調的過程當中,可能 watch 會再次 push 進來,由於存在在回調中再次賦值,判斷無限循環。

v-model原理

  • v:model 在模板編譯的時候轉換代碼
  • v-model 本質是 :value 和 v-on,可是略微有點區別。在輸入控件下,有兩個事件監聽,輸入中文時只有當輸出中文才觸發數據賦值
  • v-model 和:bind 同時使用,前者優先級更高,若是 :value 會出現衝突
  • v-model 由於語法糖的緣由,還能夠用於父子通訊

watch 和 computed 的區別和運用的場景

  • 前者是計算屬性,依賴其餘屬性計算值。而且 computer 的值有緩存,只有當計算值變化才變化觸發渲染。後者監聽到值得變化就會執行回調
  • computer 就是簡單計算一下,適用於渲染頁面。watch 適合作一些複雜業務邏輯
  • 前者有依賴兩個 watcher,computer watcher 和渲染 watcher。判斷計算出的值變化後渲染 watcher 派發更新觸發渲染

Vue 的父子通訊

  • 使用 v-model 實現父傳子,子傳父。由於 v-model 默認解析成 :value 和 :input
  • 父傳子
    • 經過 props
    • 經過 $children 訪問子組件數組,注意該數組亂序
    • 對於多級父傳子,能夠使用 v-bind={$attrs} ,經過對象的方式篩選出父組件中傳入但子組件不須要的 props
    • $listens 包含了父做用域中的 (不含 .native 修飾器的) v-on 事件監聽器。
  • 子傳父
    • 父組件傳遞函數給子組件,子組件經過 $emit 觸發
    • 修改父組件的 props
    • 經過 $parent 訪問父組件
    • .sync
  • 平行組件
    • EventBus
  • Vuex 解決一切

路由原理

前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,而後匹配路由規則,顯示相應的頁面,而且無須刷新。目前單頁面使用的路由就只有兩種實現方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,當 # 後面的哈希值發生變化時,不會向服務器請求數據,能夠經過 hashchange 事件來監聽到 URL 的變化,從而進行跳轉頁面。

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀

MVVM

MVVM 由如下三個內容組成

  • View:界面
  • Model:數據模型
  • ViewModel:做爲橋樑負責溝通 View 和 Model

在 JQuery 時期,若是須要刷新 UI 時,須要先取到對應的 DOM 再更新 UI,這樣數據和業務的邏輯就和頁面有強耦合。

在 MVVM 中,UI 是經過數據驅動的,數據一旦改變就會相應的刷新對應的 UI,UI 若是改變,也會改變對應的數據。這種方式就能夠在業務處理中只關心數據的流轉,而無需直接和頁面打交道。ViewModel 只關心數據和業務的處理,不關心 View 如何處理數據,在這種狀況下,View 和 Model 均可以獨立出來,任何一方改變了也不必定須要改變另外一方,而且能夠將一些可複用的邏輯放在一個 ViewModel 中,讓多個 View 複用這個 ViewModel。

在 MVVM 中,最核心的也就是數據雙向綁定,例如 Angluar 的髒數據檢測,Vue 中的數據劫持。

髒數據檢測

當觸發了指定事件後會進入髒數據檢測,這時會調用 $digest 循環遍歷全部的數據觀察者,判斷當前值是否和先前的值有區別,若是檢測到變化的話,會調用 $watch 函數,而後再次調用 $digest 循環直到發現沒有變化。循環至少爲二次 ,至多爲十次。

髒數據檢測雖然存在低效的問題,可是不關心數據是經過什麼方式改變的,均可以完成任務,可是這在 Vue 中的雙向綁定是存在問題的。而且髒數據檢測能夠實現批量檢測出更新的值,再去統一更新 UI,大大減小了操做 DOM 的次數。因此低效也是相對的,這就仁者見仁智者見智了。

數據劫持

Vue 內部使用了 Object.defineProperty() 來實現雙向綁定,經過這個函數能夠監聽到 setget 的事件。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判斷類型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 遞歸子屬性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}
複製代碼

以上代碼簡單的實現瞭如何監聽數據的 setget 的事件,可是僅僅如此是不夠的,還須要在適當的時候給屬性添加發布訂閱

<div>
    {{name}}
</div>
複製代碼

在解析如上模板代碼時,遇到 {{name}} 就會給屬性 name 添加發布訂閱。

// 經過 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 實例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局屬性,經過該屬性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 將 Dep.target 指向本身
    // 而後觸發屬性的 getter 添加監聽
    // 最後將 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 得到新值
    this.value = this.obj[this.key]
    // 調用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模擬解析到 `{{name}}` 觸發的操做
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 
複製代碼

接下來,對 defineReactive 函數進行改造

function defineReactive(obj, key, val) {
  // 遞歸子屬性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 將 Watcher 添加到訂閱
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 執行 watcher 的 update 方法
      dp.notify()
    }
  })
}
複製代碼

以上實現了一個簡易的雙向綁定,核心思路就是手動觸發一次屬性的 getter 來實現發佈訂閱的添加。

Proxy 與 Object.defineProperty 對比

Object.defineProperty 雖然已經可以實現雙向綁定了,可是他仍是有缺陷的。

  1. 只能對屬性進行數據劫持,因此須要深度遍歷整個對象
  2. 對於數組不能監聽到數據的變化

雖然 Vue 中確實能檢測到數組數據的變化,可是實際上是使用了 hack 的辦法,而且也是有缺陷的。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 如下幾個函數
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 得到原生函數
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 調用原生函數
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 觸發更新
    ob.dep.notify()
    return result
  })
})
複製代碼

反觀 Proxy 就沒以上的問題,原生支持監聽數組變化,而且能夠直接對整個對象進行攔截,因此 Vue 也將在下個大版本中使用 Proxy 替換 Object.defineProperty

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
複製代碼

虛擬 DOM

虛擬 DOM 涉及的內容不少,具體能夠參考我以前 寫的文章

路由鑑權

  • 登陸頁和其餘頁面分開,登陸之後實例化 Vue 而且初始化須要的路由
  • 動態路由,經過 addRoute 實現

Vue 和 React 區別

  • Vue 表單支持雙向綁定開發更方便
  • 改變數據方式不一樣,setState 有使用坑
  • props Vue 可變,React 不可變
  • 判斷是否須要更新 React 能夠經過鉤子函數判斷,Vue 使用依賴追蹤,修改了什麼才渲染什麼
  • React 16之後 有些鉤子函數會執行屢次
  • React 須要使用 JSX,須要 Babel 編譯。Vue 雖然能夠使用模板,可是也能夠經過直接編寫 render 函數不須要編譯就能運行。
  • 生態 React 相對較好

網絡

TCP 3次握手

在 TCP 協議中,主動發起請求的一端爲客戶端,被動鏈接的一端稱爲服務端。無論是客戶端仍是服務端,TCP 鏈接創建完後都能發送和接收數據,因此 TCP 也是一個全雙工的協議。

起初,兩端都爲 CLOSED 狀態。在通訊開始前,雙方都會建立 TCB。 服務器建立完 TCB 後遍進入 LISTEN 狀態,此時開始等待客戶端發送數據。

第一次握手

客戶端向服務端發送鏈接請求報文段。該報文段中包含自身的數據通信初始序號。請求發送後,客戶端便進入 SYN-SENT 狀態,x 表示客戶端的數據通訊初始序號。

第二次握手

服務端收到鏈接請求報文段後,若是贊成鏈接,則會發送一個應答,該應答中也會包含自身的數據通信初始序號,發送完成後便進入 SYN-RECEIVED 狀態。

第三次握手

當客戶端收到鏈接贊成的應答後,還要向服務端發送一個確認報文。客戶端發完這個報文段後便進入ESTABLISHED 狀態,服務端收到這個應答後也進入 ESTABLISHED 狀態,此時鏈接創建成功。

PS:第三次握手能夠包含數據,經過 TCP 快速打開(TFO)技術。其實只要涉及到握手的協議,均可以使用相似 TFO 的方式,客戶端和服務端存儲相同 cookie,下次握手時發出 cookie 達到減小 RTT 的目的。

你是否有疑惑明明兩次握手就能夠創建起鏈接,爲何還須要第三次應答?

由於這是爲了防止失效的鏈接請求報文段被服務端接收,從而產生錯誤。

能夠想象以下場景。客戶端發送了一個鏈接請求 A,可是由於網絡緣由形成了超時,這時 TCP 會啓動超時重傳的機制再次發送一個鏈接請求 B。此時請求順利到達服務端,服務端應答完就創建了請求。若是鏈接請求 A 在兩端關閉後終於抵達了服務端,那麼這時服務端會認爲客戶端又須要創建 TCP 鏈接,從而應答了該請求並進入 ESTABLISHED 狀態。此時客戶端實際上是 CLOSED 狀態,那麼就會致使服務端一直等待,形成資源的浪費。

PS:在創建鏈接中,任意一端掉線,TCP 都會重發 SYN 包,通常會重試五次,在創建鏈接中可能會遇到 SYN FLOOD 攻擊。遇到這種狀況你能夠選擇調低重試次數或者乾脆在不能處理的狀況下拒絕請求。

TCP 擁塞控制

擁塞處理和流量控制不一樣,後者是做用於接收方,保證接收方來得及接受數據。而前者是做用於網絡,防止過多的數據擁塞網絡,避免出現網絡負載過大的狀況。

擁塞處理包括了四個算法,分別爲:慢開始,擁塞避免,快速重傳,快速恢復。

慢開始算法

慢開始算法,顧名思義,就是在傳輸開始時將發送窗口慢慢指數級擴大,從而避免一開始就傳輸大量數據致使網絡擁塞。

慢開始算法步驟具體以下

  1. 鏈接初始設置擁塞窗口(Congestion Window) 爲 1 MSS(一個分段的最大數據量)
  2. 每過一個 RTT 就將窗口大小乘二
  3. 指數級增加確定不能沒有限制的,因此有一個閾值限制,當窗口大小大於閾值時就會啓動擁塞避免算法。

擁塞避免算法

擁塞避免算法相比簡單點,每過一個 RTT 窗口大小隻加一,這樣可以避免指數級增加致使網絡擁塞,慢慢將大小調整到最佳值。

在傳輸過程當中可能定時器超時的狀況,這時候 TCP 會認爲網絡擁塞了,會立刻進行如下步驟:

  • 將閾值設爲當前擁塞窗口的一半
  • 將擁塞窗口設爲 1 MSS
  • 啓動擁塞避免算法

快速重傳

快速重傳通常和快恢復一塊兒出現。一旦接收端收到的報文出現失序的狀況,接收端只會回覆最後一個順序正確的報文序號(沒有 Sack 的狀況下)。若是收到三個重複的 ACK,無需等待定時器超時再重發而是啓動快速重傳。具體算法分爲兩種:

TCP Taho 實現以下

  • 將閾值設爲當前擁塞窗口的一半
  • 將擁塞窗口設爲 1 MSS
  • 從新開始慢開始算法

TCP Reno 實現以下

  • 擁塞窗口減半
  • 將閾值設爲當前擁塞窗口
  • 進入快恢復階段(重發對端須要的包,一旦收到一個新的 ACK 答覆就退出該階段)
  • 使用擁塞避免算法

TCP New Ren 改進後的快恢復

TCP New Reno 算法改進了以前 TCP Reno 算法的缺陷。在以前,快恢復中只要收到一個新的 ACK 包,就會退出快恢復。

TCP New Reno 中,TCP 發送方先記下三個重複 ACK 的分段的最大序號。

假如我有一個分段數據是 1 ~ 10 這十個序號的報文,其中丟失了序號爲 3 和 7 的報文,那麼該分段的最大序號就是 10。發送端只會收到 ACK 序號爲 3 的應答。這時候重發序號爲 3 的報文,接收方順利接收並會發送 ACK 序號爲 7 的應答。這時候 TCP 知道對端是有多個包未收到,會繼續發送序號爲 7 的報文,接收方順利接收並會發送 ACK 序號爲 11 的應答,這時發送端認爲這個分段接收端已經順利接收,接下來會退出快恢復階段。

HTTPS 握手

HTTPS 仍是經過了 HTTP 來傳輸信息,可是信息經過 TLS 協議進行了加密。

TLS

TLS 協議位於傳輸層之上,應用層之下。首次進行 TLS 協議傳輸須要兩個 RTT ,接下來能夠經過 Session Resumption 減小到一個 RTT。

在 TLS 中使用了兩種加密技術,分別爲:對稱加密和非對稱加密。

對稱加密

對稱加密就是兩邊擁有相同的祕鑰,兩邊都知道如何將密文加密解密。

非對稱加密

有公鑰私鑰之分,公鑰全部人均可以知道,能夠將數據用公鑰加密,可是將數據解密必須使用私鑰解密,私鑰只有分發公鑰的一方纔知道。

TLS 握手過程以下圖:

  1. 客戶端發送一個隨機值,須要的協議和加密方式
  2. 服務端收到客戶端的隨機值,本身也產生一個隨機值,並根據客戶端需求的協議和加密方式來使用對應的方式,發送本身的證書(若是須要驗證客戶端證書須要說明)
  3. 客戶端收到服務端的證書並驗證是否有效,驗證經過會再生成一個隨機值,經過服務端證書的公鑰去加密這個隨機值併發送給服務端,若是服務端須要驗證客戶端證書的話會附帶證書
  4. 服務端收到加密過的隨機值並使用私鑰解密得到第三個隨機值,這時候兩端都擁有了三個隨機值,能夠經過這三個隨機值按照以前約定的加密方式生成密鑰,接下來的通訊就能夠經過該密鑰來加密解密

經過以上步驟可知,在 TLS 握手階段,兩端使用非對稱加密的方式來通訊,可是由於非對稱加密損耗的性能比對稱加密大,因此在正式傳輸數據時,兩端使用對稱加密的方式通訊。

PS:以上說明的都是 TLS 1.2 協議的握手狀況,在 1.3 協議中,首次創建鏈接只須要一個 RTT,後面恢復鏈接不須要 RTT 了。

從輸入 URL 到頁面加載全過程

  1. 首先作 DNS 查詢,若是這一步作了智能 DNS 解析的話,會提供訪問速度最快的 IP 地址回來
  2. 接下來是 TCP 握手,應用層會下發數據給傳輸層,這裏 TCP 協議會指明兩端的端口號,而後下發給網絡層。網絡層中的 IP 協議會肯定 IP 地址,而且指示了數據傳輸中如何跳轉路由器。而後包會再被封裝到數據鏈路層的數據幀結構中,最後就是物理層面的傳輸了
  3. TCP 握手結束後會進行 TLS 握手,而後就開始正式的傳輸數據
  4. 數據在進入服務端以前,可能還會先通過負責負載均衡的服務器,它的做用就是將請求合理的分發到多臺服務器上,這時假設服務端會響應一個 HTML 文件
  5. 首先瀏覽器會判斷狀態碼是什麼,若是是 200 那就繼續解析,若是 400 或 500 的話就會報錯,若是 300 的話會進行重定向,這裏會有個重定向計數器,避免過屢次的重定向,超過次數也會報錯
  6. 瀏覽器開始解析文件,若是是 gzip 格式的話會先解壓一下,而後經過文件的編碼格式知道該如何去解碼文件
  7. 文件解碼成功後會正式開始渲染流程,先會根據 HTML 構建 DOM 樹,有 CSS 的話會去構建 CSSOM 樹。若是遇到 script 標籤的話,會判斷是否存在 async 或者 defer ,前者會並行進行下載並執行 JS,後者會先下載文件,而後等待 HTML 解析完成後順序執行,若是以上都沒有,就會阻塞住渲染流程直到 JS 執行完畢。遇到文件下載的會去下載文件,這裏若是使用 HTTP 2.0 協議的話會極大的提升多圖的下載效率。
  8. 初始的 HTML 被徹底加載和解析後會觸發 DOMContentLoaded 事件
  9. CSSOM 樹和 DOM 樹構建完成後會開始生成 Render 樹,這一步就是肯定頁面元素的佈局、樣式等等諸多方面的東西
  10. 在生成 Render 樹的過程當中,瀏覽器就開始調用 GPU 繪製,合成圖層,將內容顯示在屏幕上了

HTTP 經常使用返回碼

2XX 成功

  • 200 OK,表示從客戶端發來的請求在服務器端被正確處理
  • 204 No content,表示請求成功,但響應報文不含實體的主體部分
  • 205 Reset Content,表示請求成功,但響應報文不含實體的主體部分,可是與 204 響應不一樣在於要求請求方重置內容
  • 206 Partial Content,進行範圍請求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示資源已被分配了新的 URL
  • 302 found,臨時性重定向,表示資源臨時被分配了新的 URL
  • 303 see other,表示資源存在着另外一個 URL,應使用 GET 方法獲取資源
  • 304 not modified,表示服務器容許訪問資源,但因發生請求未知足條件的狀況
  • 307 temporary redirect,臨時重定向,和302含義相似,可是指望客戶端保持請求方法不變向新的地址發出請求

4XX 客戶端錯誤

  • 400 bad request,請求報文存在語法錯誤
  • 401 unauthorized,表示發送的請求須要有經過 HTTP 認證的認證信息
  • 403 forbidden,表示對請求資源的訪問被服務器拒絕
  • 404 not found,表示在服務器上沒有找到請求的資源

5XX 服務器錯誤

  • 500 internal sever error,表示服務器端在執行請求時發生了錯誤
  • 501 Not Implemented,表示服務器不支持當前請求所須要的某個功能
  • 503 service unavailable,代表服務器暫時處於超負載或正在停機維護,沒法處理請求

數據結構算法

常見排序

如下兩個函數是排序中會用到的通用函數,就不一一寫了

function checkArray(array) {
    if (!array || array.length <= 2) return
}
function swap(array, left, right) {
    let rightValue = array[right]
    array[right] = array[left]
    array[left] = rightValue
}
複製代碼

冒泡排序

冒泡排序的原理以下,從第一個元素開始,把當前元素和下一個索引元素進行比較。若是當前元素大,那麼就交換位置,重複操做直到比較到最後一個元素,那麼此時最後一個元素就是該數組中最大的數。下一輪重複以上操做,可是此時最後一個元素已是最大數了,因此不須要再比較最後一個元素,只須要比較到 length - 1 的位置。

如下是實現該算法的代碼

function bubble(array) {
  checkArray(array);
  for (let i = array.length - 1; i > 0; i--) {
    // 從 0 到 `length - 1` 遍歷
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) swap(array, j, j + 1)
    }
  }
  return array;
}
複製代碼

該算法的操做次數是一個等差數列 n + (n - 1) + (n - 2) + 1 ,去掉常數項之後得出時間複雜度是 O(n * n)

插入排序

插入排序的原理以下。第一個元素默認是已排序元素,取出下一個元素和當前元素比較,若是當前元素大就交換位置。那麼此時第一個元素就是當前的最小數,因此下次取出操做從第三個元素開始,向前對比,重複以前的操做。

如下是實現該算法的代碼

function insertion(array) {
  checkArray(array);
  for (let i = 1; i < array.length; i++) {
    for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)
      swap(array, j, j + 1);
  }
  return array;
}
複製代碼

該算法的操做次數是一個等差數列 n + (n - 1) + (n - 2) + 1 ,去掉常數項之後得出時間複雜度是 O(n * n)

選擇排序

選擇排序的原理以下。遍歷數組,設置最小值的索引爲 0,若是取出的值比當前最小值小,就替換最小值索引,遍歷完成後,將第一個元素和最小值索引上的值交換。如上操做後,第一個元素就是數組中的最小值,下次遍歷就能夠從索引 1 開始重複上述操做。

如下是實現該算法的代碼

function selection(array) {
  checkArray(array);
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      minIndex = array[j] < array[minIndex] ? j : minIndex;
    }
    swap(array, i, minIndex);
  }
  return array;
}
複製代碼

該算法的操做次數是一個等差數列 n + (n - 1) + (n - 2) + 1 ,去掉常數項之後得出時間複雜度是 O(n * n)

歸併排序

歸併排序的原理以下。遞歸的將數組兩兩分開直到最多包含兩個元素,而後將數組排序合併,最終合併爲排序好的數組。假設我有一組數組 [3, 1, 2, 8, 9, 7, 6],中間數索引是 3,先排序數組 [3, 1, 2, 8] 。在這個左邊數組上,繼續拆分直到變成數組包含兩個元素(若是數組長度是奇數的話,會有一個拆分數組只包含一個元素)。而後排序數組 [3, 1][2, 8] ,而後再排序數組 [1, 3, 2, 8] ,這樣左邊數組就排序完成,而後按照以上思路排序右邊數組,最後將數組 [1, 2, 3, 8][6, 7, 9] 排序。

如下是實現該算法的代碼

function sort(array) {
  checkArray(array);
  mergeSort(array, 0, array.length - 1);
  return array;
}

function mergeSort(array, left, right) {
  // 左右索引相同說明已經只有一個數
  if (left === right) return;
  // 等同於 `left + (right - left) / 2`
  // 相比 `(left + right) / 2` 來講更加安全,不會溢出
  // 使用位運算是由於位運算比四則運算快
  let mid = parseInt(left + ((right - left) >> 1));
  mergeSort(array, left, mid);
  mergeSort(array, mid + 1, right);

  let help = [];
  let i = 0;
  let p1 = left;
  let p2 = mid + 1;
  while (p1 <= mid && p2 <= right) {
    help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
  }
  while (p1 <= mid) {
    help[i++] = array[p1++];
  }
  while (p2 <= right) {
    help[i++] = array[p2++];
  }
  for (let i = 0; i < help.length; i++) {
    array[left + i] = help[i];
  }
  return array;
}
複製代碼

以上算法使用了遞歸的思想。遞歸的本質就是壓棧,每遞歸執行一次函數,就將該函數的信息(好比參數,內部的變量,執行到的行數)壓棧,直到遇到終止條件,而後出棧並繼續執行函數。對於以上遞歸函數的調用軌跡以下

mergeSort(data, 0, 6) // mid = 3
  mergeSort(data, 0, 3) // mid = 1
    mergeSort(data, 0, 1) // mid = 0
      mergeSort(data, 0, 0) // 遇到終止,回退到上一步
    mergeSort(data, 1, 1) // 遇到終止,回退到上一步
    // 排序 p1 = 0, p2 = mid + 1 = 1
    // 回退到 `mergeSort(data, 0, 3)` 執行下一個遞歸
  mergeSort(2, 3) // mid = 2
    mergeSort(3, 3) // 遇到終止,回退到上一步
  // 排序 p1 = 2, p2 = mid + 1 = 3
  // 回退到 `mergeSort(data, 0, 3)` 執行合併邏輯
  // 排序 p1 = 0, p2 = mid + 1 = 2
  // 執行完畢回退
  // 左邊數組排序完畢,右邊也是如上軌跡
複製代碼

該算法的操做次數是能夠這樣計算:遞歸了兩次,每次數據量是數組的一半,而且最後把整個數組迭代了一次,因此得出表達式 2T(N / 2) + T(N) (T 表明時間,N 表明數據量)。根據該表達式能夠套用 該公式 得出時間複雜度爲 O(N * logN)

快排

快排的原理以下。隨機選取一個數組中的值做爲基準值,從左至右取值與基準值對比大小。比基準值小的放數組左邊,大的放右邊,對比完成後將基準值和第一個比基準值大的值交換位置。而後將數組以基準值的位置分爲兩部分,繼續遞歸以上操做。

如下是實現該算法的代碼

function sort(array) {
  checkArray(array);
  quickSort(array, 0, array.length - 1);
  return array;
}

function quickSort(array, left, right) {
  if (left < right) {
    swap(array, , right)
    // 隨機取值,而後和末尾交換,這樣作比固定取一個位置的複雜度略低
    let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);
    quickSort(array, left, indexs[0]);
    quickSort(array, indexs[1] + 1, right);
  }
}
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
      // 當前值比基準值小,`less` 和 `left` 都加一
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      // 當前值比基準值大,將當前值和右邊的值交換
      // 而且不改變 `left`,由於當前換過來的值尚未判斷過大小
      swap(array, --more, left);
    } else {
      // 和基準值相同,只移動下標
      left++;
    }
  }
  // 將基準值和比基準值大的第一個值交換位置
  // 這樣數組就變成 `[比基準值小, 基準值, 比基準值大]`
  swap(array, right, more);
  return [less, more];
}
複製代碼

該算法的複雜度和歸併排序是相同的,可是額外空間複雜度比歸併排序少,只需 O(logN),而且相比歸併排序來講,所需的常數時間也更少。

面試題

Sort Colors:該題目來自 LeetCode,題目須要咱們將 [2,0,2,1,1,0] 排序成 [0,0,1,1,2,2] ,這個問題就能夠使用三路快排的思想。

如下是代碼實現

var sortColors = function(nums) {
  let left = -1;
  let right = nums.length;
  let i = 0;
  // 下標若是遇到 right,說明已經排序完成
  while (i < right) {
    if (nums[i] == 0) {
      swap(nums, i++, ++left);
    } else if (nums[i] == 1) {
      i++;
    } else {
      swap(nums, i, --right);
    }
  }
};
複製代碼

Kth Largest Element in an Array:該題目來自 LeetCode,題目須要找出數組中第 K 大的元素,這問題也能夠使用快排的思路。而且由於是找出第 K 大元素,因此在分離數組的過程當中,能夠找出須要的元素在哪邊,而後只須要排序相應的一邊數組就好。

如下是代碼實現

var findKthLargest = function(nums, k) {
  let l = 0
  let r = nums.length - 1
  // 得出第 K 大元素的索引位置
  k = nums.length - k
  while (l < r) {
    // 分離數組後得到比基準樹大的第一個元素索引
    let index = part(nums, l, r)
    // 判斷該索引和 k 的大小
    if (index < k) {
      l = index + 1
    } else if (index > k) {
      r = index - 1
    } else {
      break
    }
  }
  return nums[k]
};
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      swap(array, --more, left);
    } else {
      left++;
    }
  }
  swap(array, right, more);
  return more;
}
複製代碼

堆排序

堆排序利用了二叉堆的特性來作,二叉堆一般用數組表示,而且二叉堆是一顆徹底二叉樹(全部葉節點(最底層的節點)都是從左往右順序排序,而且其餘層的節點都是滿的)。二叉堆又分爲大根堆與小根堆。

  • 大根堆是某個節點的全部子節點的值都比他小
  • 小根堆是某個節點的全部子節點的值都比他大

堆排序的原理就是組成一個大根堆或者小根堆。以小根堆爲例,某個節點的左邊子節點索引是 i * 2 + 1,右邊是 i * 2 + 2,父節點是 (i - 1) /2

  1. 首先遍歷數組,判斷該節點的父節點是否比他小,若是小就交換位置並繼續判斷,直到他的父節點比他大
  2. 從新以上操做 1,直到數組首位是最大值
  3. 而後將首位和末尾交換位置並將數組長度減一,表示數組末尾已經是最大值,不須要再比較大小
  4. 對比左右節點哪一個大,而後記住大的節點的索引而且和父節點對比大小,若是子節點大就交換位置
  5. 重複以上操做 3 - 4 直到整個數組都是大根堆。

如下是實現該算法的代碼

function heap(array) {
  checkArray(array);
  // 將最大值交換到首位
  for (let i = 0; i < array.length; i++) {
    heapInsert(array, i);
  }
  let size = array.length;
  // 交換首位和末尾
  swap(array, 0, --size);
  while (size > 0) {
    heapify(array, 0, size);
    swap(array, 0, --size);
  }
  return array;
}

function heapInsert(array, index) {
  // 若是當前節點比父節點大,就交換
  while (array[index] > array[parseInt((index - 1) / 2)]) {
    swap(array, index, parseInt((index - 1) / 2));
    // 將索引變成父節點
    index = parseInt((index - 1) / 2);
  }
}
function heapify(array, index, size) {
  let left = index * 2 + 1;
  while (left < size) {
    // 判斷左右節點大小
    let largest =
      left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
    // 判斷子節點和父節點大小
    largest = array[index] < array[largest] ? largest : index;
    if (largest === index) break;
    swap(array, index, largest);
    index = largest;
    left = index * 2 + 1;
  }
}
複製代碼

以上代碼實現了小根堆,若是須要實現大根堆,只須要把節點對比反一下就好。

該算法的複雜度是 O(logN)

系統自帶排序實現

每一個語言的排序內部實現都是不一樣的。

對於 JS 來講,數組長度大於 10 會採用快排,不然使用插入排序 源碼實現 。選擇插入排序是由於雖然時間複雜度不好,可是在數據量很小的狀況下和 O(N * logN)相差無幾,然而插入排序須要的常數時間很小,因此相對別的排序來講更快。

對於 Java 來講,還會考慮內部的元素的類型。對於存儲對象的數組來講,會採用穩定性好的算法。穩定性的意思就是對於相同值來講,相對順序不能改變。

搜索二叉樹

其餘

介紹下設計模式

工廠模式

工廠模式分爲好幾種,這裏就不一一講解了,如下是一個簡單工廠模式的例子

class Man {
  constructor(name) {
    this.name = name
  }
  alertName() {
    alert(this.name)
  }
}

class Factory {
  static create(name) {
    return new Man(name)
  }
}

Factory.create('yck').alertName()
複製代碼

固然工廠模式並不只僅是用來 new 出實例

能夠想象一個場景。假設有一份很複雜的代碼須要用戶去調用,可是用戶並不關心這些複雜的代碼,只須要你提供給我一個接口去調用,用戶只負責傳遞須要的參數,至於這些參數怎麼使用,內部有什麼邏輯是不關心的,只須要你最後返回我一個實例。這個構造過程就是工廠。

工廠起到的做用就是隱藏了建立實例的複雜度,只須要提供一個接口,簡單清晰。

在 Vue 源碼中,你也能夠看到工廠模式的使用,好比建立異步組件

export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
    
    // 邏輯處理...
  
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
複製代碼

在上述代碼中,咱們能夠看到咱們只須要調用 createComponent 傳入參數就能建立一個組件實例,可是建立這個實例是很複雜的一個過程,工廠幫助咱們隱藏了這個複雜的過程,只須要一句代碼調用就能實現功能。

單例模式

單例模式很經常使用,好比全局緩存、全局狀態管理等等這些只須要一個對象,就能夠使用單例模式。

單例模式的核心就是保證全局只有一個對象能夠訪問。由於 JS 是門無類的語言,因此別的語言實現單例的方式並不能套入 JS 中,咱們只須要用一個變量確保實例只建立一次就行,如下是如何實現單例模式的例子

class Singleton {
  constructor() {}
}

Singleton.getInstance = (function() {
  let instance
  return function() {
    if (!instance) {
      instance = new Singleton()
    }
    return instance
  }
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
複製代碼

在 Vuex 源碼中,你也能夠看到單例模式的使用,雖然它的實現方式不大同樣,經過一個外部變量來控制只安裝一次 Vuex

let Vue // bind on install

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    // ...
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
複製代碼

適配器模式

適配器用來解決兩個接口不兼容的狀況,不須要改變已有的接口,經過包裝一層的方式實現兩個接口的正常協做。

如下是如何實現適配器模式的例子

class Plug {
  getName() {
    return '港版插頭'
  }
}

class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 適配器轉二腳插頭'
  }
}

let target = new Target()
target.getName() // 港版插頭 適配器轉二腳插頭
複製代碼

在 Vue 中,咱們其實常用到適配器模式。好比父組件傳遞給子組件一個時間戳屬性,組件內部須要將時間戳轉爲正常的日期顯示,通常會使用 computed 來作轉換這件事情,這個過程就使用到了適配器模式。

裝飾模式

裝飾模式不須要改變已有的接口,做用是給對象添加功能。就像咱們常常須要給手機戴個保護套防摔同樣,不改變手機自身,給手機添加了保護套提供防摔功能。

如下是如何實現裝飾模式的例子,使用了 ES7 中的裝飾器語法

function readonly(target, key, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Test {
  @readonly
  name = 'yck'
}

let t = new Test()

t.yck = '111' // 不可修改
複製代碼

在 React 中,裝飾模式其實隨處可見

import { connect } from 'react-redux'
class MyComponent extends React.Component {
    // ...
}
export default connect(mapStateToProps)(MyComponent)
複製代碼

代理模式

代理是爲了控制對對象的訪問,不讓外部直接訪問到對象。在現實生活中,也有不少代理的場景。好比你須要買一件國外的產品,這時候你能夠經過代購來購買產品。

在實際代碼中其實代理的場景不少,也就不舉框架中的例子了,好比事件代理就用到了代理模式。

<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>
複製代碼

由於存在太多的 li,不可能每一個都去綁定事件。這時候能夠經過給父節點綁定一個事件,讓父節點做爲代理去拿到真實點擊的節點。

發佈-訂閱模式

發佈-訂閱模式也叫作觀察者模式。經過一對一或者一對多的依賴關係,當對象發生改變時,訂閱方都會收到通知。在現實生活中,也有不少相似場景,好比我須要在購物網站上購買一個產品,可是發現該產品目前處於缺貨狀態,這時候我能夠點擊有貨通知的按鈕,讓網站在產品有貨的時候經過短信通知我。

在實際代碼中其實發布-訂閱模式也很常見,好比咱們點擊一個按鈕觸發了點擊事件就是使用了該模式

<ul id="ul"></ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>
複製代碼

在 Vue 中,如何實現響應式也是使用了該模式。對於須要實現響應式的對象來講,在 get 的時候會進行依賴收集,當改變了對象的屬性時,就會觸發派發更新。

若是你對於如何實現響應式還有疑問,能夠閱讀我以前的文章 深度解析 Vue 響應式原理

最後

若是你認爲還有什麼好的題目能夠貢獻,也能夠在評論中提出

你可能會疑問我怎麼寫出 25K 的文字的,其實不少面試題均可以在個人萬星項目中找到答案,如下是 項目地址

若是你想學習到更多的前端知識、面試技巧或者一些我我的的感悟,能夠關注個人公衆號一塊兒學習

瞭解掘金秋招求職徵文活動更多信息👉秋招求職時,寫文就有好禮相送 | 掘金技術徵文

相關文章
相關標籤/搜索