以下面試題來自騰訊、阿里、網易、餓了么、美團、拼多多、百度等等大廠綜合起來常考的題目,
如何寫一個漂亮的簡歷簡歷不是一份記流水賬的東西,而是讓用人方了解你的亮點的,
平時有在做一些修改簡歷的收費服務,也算看過蠻多簡歷了,很多簡歷都有如下特征
喜歡說自己的特長、優點,用人方真的不關注你的性格是否陽光等等
個人技能能夠占半頁的篇幅,而且長得也都差不多
專案經驗流水賬,比如我會用這個 API 實作了某某功能
簡歷頁數過多,真心看不下去
以上類似簡歷可以說用人方也看了無數份,完全抓不到你的亮點,除非你呆過大廠或者教育背景不錯或者技術堆疊符合人家要求了,否則基本就是看運氣約面試了,
以下是我經常給別人修改簡歷的意見:
簡歷頁數控制在 2 頁以下
技術名詞注意大小寫
突出個人亮點,擴充內容,比如在專案中如何找到 Bug,解決 Bug 的程序;比如如何發現的性能問題,如何解決性能問題,最終提升了多少性能;比如為何如此選型,目的是什么,較其他有什么優點等等,總體思路就是不寫流水賬,突出你在專案中具有不錯的解決問題的能力和獨立思考的能力,
斟酌熟悉、精通等字眼,不要給自己挖坑
確保每一個寫上去的技術點自己都能說出點什么,杜絕面試官問你一個技術點,你只能答出會用 API 這種減分的情況
做到以上內容,然后在投遞簡歷的程序中加上一份求職信,對你的求職之路相信能幫上很多忙,
JS 相關
談談變數提升?
當執行 JS 代碼時,會生成執行環境,只要代碼不是寫在函式中的,就是在全域執行環境中,函式中的代碼會產生函式執行環境,只此兩種執行環境,
接下來讓我們看一個老生常談的例子,var
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的輸出大家肯定都已經明白了,這是因為函式和變數提升的原因,通常提升的解釋是說將宣告的代碼移動到了頂部,這其實沒有什么錯誤,便于大家理解,但是更準確的解釋應該是:在生成執行環境時,會有兩個階段,第一個階段是創建的階段,JS 解釋器會找出需要提升的變數和函式,并且給他們提前在記憶體中開辟好空間,函式的話會將整個函式存入記憶體中,變數只宣告并且賦值為 undefined,所以在第二個階段,也就是代碼執行階段,我們可以直接提前使用,
在提升的程序中,相同的函式會覆寫上一個函式,并且函式優先于變數提升
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 提升了,在第一階段記憶體也已經為他開辟好了空間,但是因為這個宣告的特性導致了并不能在宣告前使用,
bind、call、apply 區別
首先說下前兩者的區別,
call 和 apply 都是為了解決改變 this 的指向,作用都是相同的,只是傳參的方式不同,
除了第一個引數外,call 可以接收一個引數串列,apply 只接受一個引數陣列,
let a = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
bind 和其他兩個方法作用也是一致的,只是該方法會回傳一個函式,并且我們可以通過 bind 實作柯里化,
如何實作一個 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
}
如果有想一起學習web前端,想制作酷炫的網頁,可以來一下我的前端qq群:851231348,從最基礎的HTML+CSS+JavaScript【炫酷特效,游戲,插件封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理好友都會在里面交流 分享一些學習的方法和需要注意的小細節,每天也會準時的講一些前端的專案實戰,及免費前端直播課程學習
如何實作一個 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
}
簡單說下原型鏈
[圖片上傳失敗...(image-e8ccc5-1546698256133)]
每個函式都有 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
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 內部實作了 generators
,generators 會保留堆疊中東西,所以這時候 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 狀態,可以通過函式 resolve 和 reject ,將狀態轉變為 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);
}
}
== 和 === 區別,什么情況用 ==
[圖片上傳失敗...(image-e8a801-1546698256133)]
上圖中的 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.nextTick ,promise ,Object.observe ,MutationObserver
宏任務包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI 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 階段會執行 setTimeout 和 setInterval
一個 timer 指定的時間并不是準確時間,而是在達到這個時間后盡快執行回呼,可能會因為系統正在執行別的事務而延遲,
下限的時間有一個范圍:[1, 2147483647] ,如果設定的時間不在這個范圍,將被設定為1,
**I/O **
I/O 階段會執行除了 close 事件,定時器和 setImmediate 的回呼
idle, prepare
idle, prepare 階段內部實作
poll
poll 階段很重要,這一階段中,系統會做兩件事情
執行到點的定時器
執行 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()
}
}
}
這是一個簡單版的防抖,但是有缺陷,這個防抖只能在最后呼叫,一般的防抖會有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__
}
}
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,那么呼叫堆疊可能會很長
[圖片上傳失敗...(image-2744ce-1546698256133)]
呼叫堆疊過長,再加上中間進行了復雜的操作,就可能導致長時間阻塞主執行緒,帶來不好的用戶體驗,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 資料,
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 生命周期
生命周期函式就是組件在初始化或者資料更新時會觸發的鉤子函式,
[圖片上傳失敗...(image-3a5ab0-1546698256133)]
在初始化時,會呼叫以下代碼,生命周期就是通過 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 鉤子函式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/50006.html
標籤:JavaScript
下一篇:WEB前端——前端介紹
