如下面試題來自騰訊、阿里、網易、餓了麼、美團、拼多多、百度等等大廠綜合起來常考的題目。javascript
簡歷不是一份記流水帳的東西,而是讓用人方瞭解你的亮點的。css
平時有在作一些修改簡歷的收費服務,也算看過蠻多簡歷了。不少簡歷都有以下特徵html
以上相似簡歷能夠說用人方也看了無數份,徹底抓不到你的亮點。除非你呆過大廠或者教育背景不錯或者技術棧符合人家要求了,不然基本就是看運氣約面試了。前端
如下是我常常給別人修改簡歷的意見:vue
簡歷頁數控制在 2 頁如下java
作到以上內容,而後在投遞簡歷的過程當中加上一份求職信,對你的求職之路相信能幫上不少忙。node
當執行 JS 代碼時,會生成執行環境,只要代碼不是寫在函數中的,就是在全局執行環境中,函數中的代碼會產生函數執行環境,只此兩種執行環境。react
接下來讓咱們看一個老生常談的例子,var
c++
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中引入了 let
。let
不能在聲明前使用,可是這並非常說的 let
不會提高,let
提高了,在第一階段內存也已經爲他開闢好了空間,可是由於這個聲明的特性致使了並不能在聲明前使用。
首先說下前二者的區別。
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
實現柯里化。
對於實現如下幾個函數,能夠從幾個方面思考
window
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))
}
}
複製代碼
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
}
複製代碼
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
屬性,除了 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
符合前面代碼中的第一個狀況,因此 this
是 window
。而且 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
相比直接使用 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
內部實現了 generators
,generators
會保留堆棧中東西,因此這時候 a = 0
被保存了下來await
是異步操做,遇到await
就會當即返回一個pending
狀態的Promise
對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,因此會先執行 console.log('1', a)
a = 10
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 是 ES6 新增的語法,解決了回調地獄的問題。
能夠把 Promise 當作一個狀態機。初始是 pending
狀態,能夠經過函數 resolve
和 reject
,將狀態轉變爲 resolved
或者 rejected
狀態,狀態一旦改變就不能再次變化。
then
函數會返回一個 Promise 實例,而且該返回值是一個新的實例而不是以前的實例。由於 Promise 規範規定除了 pending
狀態,其餘狀態是不能夠改變的,若是返回的是一個相同實例的話,多個 then
調用就失去意義了。
對於 then
來講,本質上能夠把它當作是 flatMap
// 三種狀態
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 就結束了。
老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。
在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:
老生代中的空間很複雜,有以下幾個空間
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
}
...
}
複製代碼
前者存儲在棧上,後者存儲在堆上
衆所周知 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.observe
,MutationObserver
宏任務包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
不少人有個誤區,認爲微任務快於宏任務,實際上是錯誤的。由於宏任務中包括了 script
,瀏覽器會先執行一個宏任務,接下來有異步代碼的話就先執行微任務。
因此正確的一次 Event loop 順序是這樣的
經過上述的 Event loop 順序可知,若是宏任務中的異步代碼有大量的計算而且須要操做 DOM 的話,爲了更快的 界面響應,咱們能夠把操做 DOM 放入微任務中。
Node 中的 Event loop 和瀏覽器中的不相同。
Node 的 Event loop 分爲6個階段,它們會按照順序反覆運行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
timer
timers 階段會執行 setTimeout
和 setInterval
一個 timer
指定的時間並非準確時間,而是在達到這個時間後儘快執行回調,可能會由於系統正在執行別的事務而延遲。
下限的時間有一個範圍:[1, 2147483647]
,若是設定的時間不在這個範圍,將被設置爲1。
**I/O **
I/O 階段會執行除了 close 事件,定時器和 setImmediate
的回調
idle, prepare
idle, prepare 階段內部實現
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
複製代碼
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的)函數觸發以後調用。當即執行
的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於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
,就能夠再次點擊了。[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
對於基本類型,除了 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 | 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 攻擊 |
Load 事件觸發表明頁面中的 DOM,CSS,JS,圖片已經所有加載完畢。
DOMContentLoaded 事件觸發表明初始的 HTML 被徹底加載和解析,不須要等待 CSS,JS,圖片加載。
由於瀏覽器出於安全考慮,有同源策略。也就是說,若是協議、域名或者端口有一個不一樣就是跨域,Ajax 請求會失敗。
咱們能夠經過如下幾種經常使用方法解決跨域的問題
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
請求。
在開發中可能會遇到多個 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須要瀏覽器和後端同時支持。IE 8 和 9 須要經過 XDomainRequest
來實現。
瀏覽器會自動進行 CORS 通訊,實現CORS通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。
服務端設置 Access-Control-Allow-Origin
就能夠開啓 CORS。 該屬性表示哪些域名能夠訪問資源,若是設置通配符則表示全部網站均可以訪問資源。
該方式只能用於二級域名相同的狀況下,好比 a.test.com
和 b.test.com
適用於該方式。
只須要給頁面添加 document.domain = 'test.com'
表示二級域名都相同就能夠實現跨域
這種方式一般用於獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另外一個頁面判斷來源並接收消息
// 發送消息端
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 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 已經啓動了
緩存對於前端性能優化來講是個很重要的點,良好的緩存策略能夠下降資源的重複加載提升網頁的總體加載速度。
一般瀏覽器緩存策略分爲兩種:強緩存和協商緩存。
實現強緩存能夠經過兩種響應頭實現: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
的值發送給服務器,詢問服務器在該日期後資源是否有更新,有更新的話就會將新的資源發送回來。
可是若是在本地打開緩存文件,就會形成 Last-Modified
被修改,因此在 HTTP / 1.1 出現了 ETag
。
ETag
相似於文件指紋,If-None-Match
會將當前 ETag
發送給服務器,詢問該資源 ETag
是否變更,有變更的話就將新的資源發送回來。而且 ETag
優先級比 Last-Modified
高。
對於大部分的場景均可以使用強緩存配合協商緩存解決,可是在一些特殊的地方可能須要選擇特殊的緩存策略
Cache-control: no-store
,表示該資源不須要緩存Cache-Control: no-cache
並配合 ETag
使用,表示該資源已被緩存,可是每次都會發送請求詢問資源是否更新。Cache-Control: max-age=31536000
並配合策略緩存使用,而後對文件進行指紋處理,一旦文件名變更就會馬上下載新的文件。重繪和迴流是渲染步驟中的一小節,可是這兩個步驟對於性能影響很大。
color
就叫稱爲重繪迴流一定會發生重繪,重繪不必定會引起迴流。迴流所需的成本比重繪高的多,改變深層次的節點極可能致使父節點的一系列迴流。
因此如下幾個動做可能會致使性能問題:
不少人不知道的是,重繪和迴流其實和 Event loop 有關。
resize
或者 scroll
,有的話會去觸發事件,因此 resize
和 scroll
事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。requestAnimationFrame
回調IntersectionObserver
回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差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 個思路了:
head
中script
標籤放在 body
底部,由於 JS 文件執行會阻塞渲染。固然也能夠把 script
標籤放在任意位置而後加上 defer
,表示該文件會並行下載,可是會放到 HTML 解析完成後順序執行。對於沒有任何依賴的 JS 文件能夠加上 async
,表示加載和渲染後續文檔元素的過程將和 JS 文件的加載與執行並行無序進行。Webworker
。Webworker
可讓咱們另開一個線程執行腳本而不影響渲染。靜態資源儘可能使用 CDN 加載,因爲瀏覽器對於單個域名有併發請求上限,能夠考慮使用多個 CDN 域名。對於 CDN 加載靜態資源須要注意 CDN 域名要與主站不一樣,不然每次請求都會帶上主站的 Cookie。
本質就是編譯器,當代碼轉爲字符串生成 AST,對 AST 進行轉變最後再生成新的代碼
好比你想實現一個編譯結束退出命令的插件
class BuildEndPlugin {
apply (compiler) {
const afterEmit = (compilation, cb) => {
cb()
setTimeout(function () {
process.exit(0)
}, 1000)
}
compiler.plugin('after-emit', afterEmit)
}
}
module.exports = BuildEndPlugin
複製代碼
在 V16 版本中引入了 Fiber 機制。這個機制必定程度上的影響了部分生命週期的調用,而且也引入了新的 2 個 API 來解決問題。
在以前的版本中,若是你擁有一個很複雜的複合組件,而後改動了最上層組件的 state
,那麼調用棧可能會很長
調用棧過長,再加上中間進行了複雜的操做,就可能致使長時間阻塞主線程,帶來很差的用戶體驗。Fiber 就是爲了解決該問題而生。
Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。
對於如何區別優先級,React 有本身的一套邏輯。對於動畫這種實時性很高的東西,也就是 16 ms 必須渲染一次保證不卡頓的狀況下,React 會每 16 ms(之內) 暫停一下更新,返回來繼續渲染動畫。
對於異步渲染,如今渲染有兩個階段:reconciliation
和 commit
。前者過程是能夠打斷的,後者不能暫停,會一直更新界面直到完成。
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 數據。
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
在 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)
})
}
複製代碼
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
})
}
}
複製代碼
生命週期函數就是組件在初始化或者數據更新時會觸發的鉤子函數。
在初始化時,會調用如下代碼,生命週期就是經過 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')
}
}
}
複製代碼
上圖還有兩個生命週期沒有說,分別爲 activated
和 deactivated
,這兩個鉤子函數是 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
鉤子函數。
v-model
實現父傳子,子傳父。由於 v-model 默認解析成 :value 和 :inputprops
$children
訪問子組件數組,注意該數組亂序v-bind={$attrs}
,經過對象的方式篩選出父組件中傳入但子組件不須要的 props.native
修飾器的) v-on
事件監聽器。$emit
觸發props
$parent
訪問父組件前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,而後匹配路由規則,顯示相應的頁面,而且無須刷新。目前單頁面使用的路由就只有兩種實現方式
www.test.com/#/
就是 Hash URL,當 #
後面的哈希值發生變化時,不會向服務器請求數據,能夠經過 hashchange
事件來監聽到 URL 的變化,從而進行跳轉頁面。
History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀
MVVM 由如下三個內容組成
在 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()
來實現雙向綁定,經過這個函數能夠監聽到 set
和 get
的事件。
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
}
})
}
複製代碼
以上代碼簡單的實現瞭如何監聽數據的 set
和 get
的事件,可是僅僅如此是不夠的,還須要在適當的時候給屬性添加發布訂閱
<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 來實現發佈訂閱的添加。
Object.defineProperty
雖然已經可以實現雙向綁定了,可是他仍是有缺陷的。
雖然 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 涉及的內容不少,具體能夠參考我以前 寫的文章
在 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 攻擊。遇到這種狀況你能夠選擇調低重試次數或者乾脆在不能處理的狀況下拒絕請求。
擁塞處理和流量控制不一樣,後者是做用於接收方,保證接收方來得及接受數據。而前者是做用於網絡,防止過多的數據擁塞網絡,避免出現網絡負載過大的狀況。
擁塞處理包括了四個算法,分別爲:慢開始,擁塞避免,快速重傳,快速恢復。
慢開始算法,顧名思義,就是在傳輸開始時將發送窗口慢慢指數級擴大,從而避免一開始就傳輸大量數據致使網絡擁塞。
慢開始算法步驟具體以下
擁塞避免算法相比簡單點,每過一個 RTT 窗口大小隻加一,這樣可以避免指數級增加致使網絡擁塞,慢慢將大小調整到最佳值。
在傳輸過程當中可能定時器超時的狀況,這時候 TCP 會認爲網絡擁塞了,會立刻進行如下步驟:
快速重傳通常和快恢復一塊兒出現。一旦接收端收到的報文出現失序的狀況,接收端只會回覆最後一個順序正確的報文序號(沒有 Sack 的狀況下)。若是收到三個重複的 ACK,無需等待定時器超時再重發而是啓動快速重傳。具體算法分爲兩種:
TCP Taho 實現以下
TCP Reno 實現以下
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 仍是經過了 HTTP 來傳輸信息,可是信息經過 TLS 協議進行了加密。
TLS 協議位於傳輸層之上,應用層之下。首次進行 TLS 協議傳輸須要兩個 RTT ,接下來能夠經過 Session Resumption 減小到一個 RTT。
在 TLS 中使用了兩種加密技術,分別爲:對稱加密和非對稱加密。
對稱加密:
對稱加密就是兩邊擁有相同的祕鑰,兩邊都知道如何將密文加密解密。
非對稱加密:
有公鑰私鑰之分,公鑰全部人均可以知道,能夠將數據用公鑰加密,可是將數據解密必須使用私鑰解密,私鑰只有分發公鑰的一方纔知道。
TLS 握手過程以下圖:
經過以上步驟可知,在 TLS 握手階段,兩端使用非對稱加密的方式來通訊,可是由於非對稱加密損耗的性能比對稱加密大,因此在正式傳輸數據時,兩端使用對稱加密的方式通訊。
PS:以上說明的都是 TLS 1.2 協議的握手狀況,在 1.3 協議中,首次創建鏈接只須要一個 RTT,後面恢復鏈接不須要 RTT 了。
script
標籤的話,會判斷是否存在 async
或者 defer
,前者會並行進行下載並執行 JS,後者會先下載文件,而後等待 HTML 解析完成後順序執行,若是以上都沒有,就會阻塞住渲染流程直到 JS 執行完畢。遇到文件下載的會去下載文件,這裏若是使用 HTTP 2.0 協議的話會極大的提升多圖的下載效率。DOMContentLoaded
事件2XX 成功
3XX 重定向
4XX 客戶端錯誤
5XX 服務器錯誤
如下兩個函數是排序中會用到的通用函數,就不一一寫了
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
。
如下是實現該算法的代碼
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 的文字的,其實不少面試題均可以在個人萬星項目中找到答案,如下是 項目地址
若是你想學習到更多的前端知識、面試技巧或者一些我我的的感悟,能夠關注個人公衆號一塊兒學習
瞭解掘金秋招求職徵文活動更多信息👉秋招求職時,寫文就有好禮相送 | 掘金技術徵文