目錄
JS
Es6
Node
效能優化
網路 / 瀏覽器
演算法
說說js中的詞法作用域
js中只有詞法作用域,也就是說在定義時而不是執行時確定作用域。例如:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();<br>//1
複製程式碼
注意: with和eval可以修改詞法作用域
什麼是閉包
《深入淺出nodejs》中對閉包的定義:
在js中,實現外部作用域訪問內部作用域中變數的方法叫做“閉包”。
說說js的垃圾回收(GC)
v8的垃圾回收策略主要基於分代式垃圾回收機制。將記憶體分為新生代和老生代,分別採用不同的演算法。
新生代採用Scavenge演算法
Scavenge為新生代採用的演算法,是一種採用複製的方式實現的垃圾回收演算法。它將記憶體分為from和to兩個空間。每次gc,會將from空間的存活物件複製到to空間。然後兩個空間角色對換(又稱反轉)。
該演算法是犧牲空間換時間,所以適合新生代,因為它的物件生存週期較短。
老生代採用Mark-Sweep 和 Mark-Compact
老生代中物件存活時間較長,不適合Scavenge演算法。
Mark-Sweep是標記清除的意思。Scavenge是隻複製存活物件,而Mark-Sweep是隻清除死亡物件。該演算法分為兩個步驟:
- 遍歷堆中所有物件並標記活著的物件
- 清除沒有標記的物件
Mark-Sweep存在一個問題,清除死亡物件後會造成記憶體空間不連續,如果這時候再分配一個大物件,所有的空間碎片都無法完成此次分配,就會造成提前觸發gc。這時候v8會使用Mark-Compact演算法。
Mark-Copact是標記整理的意思。它會在標記完成之後將活著的物件往一端移動,移動完成後直接清理掉邊界外的記憶體。因為存在整理過程,所以它的速度慢於Mark-Sweep,node中主要採用Mark-Sweep。
Incremental Marking
為了避免出現Javascript應用邏輯與垃圾回收器看到的情況不一致,垃圾回收時應用邏輯會停下來。這種行為被成為全停頓(stop-the-world)。這對老生代影響較大。
Incremental Marking稱為增量標記,也就是拆分為許多小的“步進”,每次做完一“步進”,就讓Javascript執行一會兒,垃圾回收與應用邏輯交替執行。
採用Incremental Marking後,gc的最大停頓時間較少到原來的 1 / 6 左右。
v8的記憶體限制
- 64位系統最大約為1.4G
- 32位系統最大約為0.7G
node中檢視記憶體使用量
➜ ~ node
> process.memoryUsage() //node程式記憶體使用
{ rss: 27054080, // 程式常駐記憶體
heapTotal: 7684096, // 已申請到的堆記憶體
heapUsed: 4850344, // 當前使用的堆記憶體
external: 9978 // 堆外記憶體(不是通過v8分配的記憶體)
> os.totalmem() //系統總記憶體
17179869184
> os.freemem() //系統閒置記憶體
3239858176
複製程式碼
說說你瞭解的設計模式
釋出訂閱模式
在js中事件模型就相當於傳統的釋出訂閱模式,具體實現參考實現一個node中的EventEmiter
策略模式
定義: 定義一系列演算法,把它們一個個封裝起來,並且使它們可以相互替換。
策略模式實現表單校驗
const strategies = {
isNoEmpty: function(value, errorMsg){
if(value.trim() === ''){
return errorMsg
}
},
maxLength: function(value, errorMsg, len) {
if(value.trim() > len) {
return errorMsg
}
}
}
class Validator {
constructor() {
this.catch = [];
}
add(value, rule, errorMsg, ...others) {
this.catch.push(function() {
return strategies[rule].apply(this, [value, errorMsg, ...others]);
});
}
start() {
for (let i = 0, validatorFunc; (validatorFunc = this.catch[i++]); ) {
let msg = validatorFunc();
if (msg) {
return msg;
}
}
}
}
//使用
const validatorFunc = function() {
const validator = new Validator();
validator.add(username, 'isNoEmpty', '使用者名稱不能為空');
validator.add(password, 'isNoEmpty', '密碼不能為空');
const USERNAME_LEN = PASSWORD_LEN = 10;
validator.add(username, 'maxLength', `使用者名稱不能超過${USERNAME_LEN}個字`, USERNAME_LEN);
validator.add(password, 'isNoEmpty', `密碼不能為空${PASSWORD_LEN}個字`, PASSWORD_LEN);
let msg = validator.start();
if(msg) {
return msg;
}
}
複製程式碼
命令模式
應用場景: 有時候我們要向某些物件傳送請求,但不知道請求的接收者是誰,也不知道請求的操作是什麼,此時希望以一種鬆耦合的方式來設計軟體,使得請求的傳送者和接收者能夠消除彼此的耦合關係。
命令模式實現動畫
class MoveCommand {
constructor(reciever, pos) {
this.reciever = reciever;
this.pos = pos;
this.oldPos = null;
}
excute() {
this.reciever.start("left", this.pos, 1000);
this.reciever.getPos();
}
undo() {
this.reciever.start("left", this.oldPos, 1000);
}
}
複製程式碼
ES6 模組與 CommonJS 模組的差異
-
CommonJS輸出的是值的拷貝,ES6模組輸出的是值的引用。
也就是說CommonJS引用後改變模組內變數的值,其他引用模組不會改變,而ES6模組會改變。 -
CommonJS是執行時載入,ES6模組是編譯時輸出介面。
之所以Webpack的Tree Shaking是基於ES6的,就是因為ES6在編譯的時候就能確定依賴。因為使用babel-preset-2015這個預設預設是會把ES6模組編譯為CommonJS的,所以想使用Tree Shaking還需要手動修改這個預設。
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [['babel-preset-es2015', {modules: false}]],
}
}
}
]
}
複製程式碼
async函式實現原理
async函式是基於generator實現,所以涉及到generator相關知識。在沒有async函式之前,通常使用co庫來執行generator,所以通過co我們也能模擬async的實現。
function Asyncfn() {
return co(function*() {
//.....
});
}
function co(gen) {
return new Promise((resolve, reject) => {
const fn = gen();
function next(data) {
let { value, done } = fn.next(data);
if (done) return resolve(value);
Promise.resolve(value).then(res => {
next(res);
}, reject);
}
next();
});
}
複製程式碼
說說瀏覽器和node中的事件迴圈(EventLoop)
瀏覽器
如圖:瀏覽器中相對簡單,共有兩個事件佇列,當主執行緒空閒時會清空Microtask queue(微任務佇列)依次執行Task Queue(巨集任務佇列)中的回撥函式,每執行完一個之後再清空Microtask queue。
“當前執行棧” -> “micro-task” -> “task queue中取一個回撥” -> “micro-task” -> ... (不斷消費task queue) -> “micro-task”
nodejs
node中機制和瀏覽器有一些差異。node中的task queue是分為幾個階段,清空micro-task是在一個階段結束之後(瀏覽器中是每一個任務結束之後),各個階段如下:
┌───────────────────────┐ ┌─>│ timers │<————— 執行 setTimeout()、setInterval() 的回撥 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回撥 │ ┌──────────┴────────────┐ │ │ pending callbacks │<————— 執行由上一個 Tick 延遲下來的 I/O 回撥(待完善,可忽略) │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回撥 │ ┌──────────┴────────────┐ │ │ idle, prepare │<————— 內部呼叫(可忽略) │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回撥 | | ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ - (執行幾乎所有的回撥,除了 close callbacks 以及 timers 排程的回撥和 setImmediate() 排程的回撥,在恰當的時機將會阻塞在此階段) │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ | | | | | └───────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回撥 | ┌──────────┴────────────┐ │ │ check │<————— setImmediate() 的回撥將會在這個階段執行 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回撥 │ ┌──────────┴────────────┐ └──┤ close callbacks │<————— socket.on('close', ...) └───────────────────────┘
這裡我們主要關注其中的3個階段:timer、poll和check,其中poll佇列相對複雜:
輪詢 階段有兩個重要的功能:
1、計算應該阻塞和輪詢 I/O 的時間。
2、然後,處理 輪詢 佇列裡的事件。
當事件迴圈進入 輪詢 階段且 沒有計劃計時器時 ,將發生以下兩種情況之一:
1、如果輪詢佇列不是空的,事件迴圈將迴圈訪問其回撥佇列並同步執行它們,直到佇列已用盡,或者達到了與系統相關的硬限制。
2、如果輪詢佇列是空的,還有兩件事發生:
a、如果指令碼已按 setImmediate() 排定,則事件迴圈將結束 輪詢 階段,並繼續 check階段以執行這些計劃指令碼。
b、如果指令碼 尚未 按 setImmediate()排定,則事件迴圈將等待回撥新增到佇列中,然後立即執行。
一旦輪詢佇列為空,事件迴圈將檢查已達到時間閾值的計時器。如果一個或多個計時器已準備就緒,則事件迴圈將繞回計時器階段以執行這些計時器的回撥。
細節請參考The Node.js Event Loop, Timers, and process.nextTick()
中文:Node.js 事件迴圈,定時器和 process.nextTick()
通過程式理解瀏覽器和node中的差異
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function() {
console.log("promise2");
});
}, 0);
複製程式碼
在瀏覽器中的順序是:timer1 -> promise1 -> timer2 -> pormise2
node中順序是: timer1 -> timer2 -> promise1 -> promise2
這道題目很好的說明了node中的micro-task是在一個階段的任務執行完之後才清空的。
實現一個node中的EventEmiter
簡單實現:
class EventsEmiter {
constructor() {
this.events = {};
}
on(type, fn) {
const events = this.events;
if (!events[type]) {
events[type] = [fn];
} else {
events[type].push(fn);
}
}
emit(type, ...res) {
const events = this.events;
if (events[type]) {
events[type].forEach(fn => fn.apply(this, res));
}
}
remove(type, fn) {
const events = this.events;
if (events[type]) {
events[type] = events[type].filer(lisener => lisener !== fn);
}
}
}
複製程式碼
實現一個node中util模組的promisify方法
let fs = require("fs");
let read = fs.readFile;
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
};
}
// 回撥用法
// read("./test.json", (err, data) => {
// if (err) {
// console.error("err", err);
// }
// console.log("data", data.toString());
// });
// promise用法
let readPromise = promisify(read);
readPromise("./test.json").then(res => {
console.log("data", res.toString());
});
複製程式碼
如何實現一個自定義流
根據所建立的流型別,新的流類必須實現一個或多個特定的方法,如下圖所示:
用例 | 類 | 需實現的方法 |
---|---|---|
只讀流 | Readable | _read |
只寫流 | Writable | _write , _writev , _final |
可讀可寫流 | Duplex | _read , _write , _writev , _final |
對寫入的資料進行操作,然後讀取結果 | Transform | _transform , _flush , _final |
以雙工流為例:
const { Duplex } = require('stream');
class Myduplex extends Duplex {
constructor(arr, opt) {
super(opt);
this.arr = arr
this.index = 0
}
//實現可讀流部分
_read(size) {
this.index++
if(this.index === 3) {
this.push(null)
} else {
this.push(this.index.toString())
}
}
//實現可寫流
_write(chunk, encoding, callback) {
this.arr.push(chunk.toString())
callback()
}
}
複製程式碼
更多內容可以參考我的另一篇文章:說說node中可讀流和可寫流 和 nodejs官網
效能優化之dns-prefetch、prefetch、preload、defer、async
dns-prefetch
域名轉化為ip是一個比較耗時的過程,dns-prefetch能讓瀏覽器空閒的時候幫你做這件事。尤其大型網站會使用多域名,這時候更加需要dns預取。
//來自百度首頁
<link rel="dns-prefetch" href="//m.baidu.com">
複製程式碼
prefetch
prefetch一般用來預載入可能使用的資源,一般是對使用者行為的一種判斷,瀏覽器會在空閒的時候載入prefetch的資源。
<link rel="prefetch" href="http://www.example.com/">
複製程式碼
preload
和prefetch不同,prefecth通常是載入接下來可能用到的頁面資源,而preload是載入當前頁面要用的指令碼、樣式、字型、圖片等資源。所以preload不是空閒時載入,它的優先順序更強,並且會佔用http請求數量。
<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"
複製程式碼
as值包括
- "script"
- "style"
- "image"
- "media"
- "document" onload方法是資源載入完成的回撥函式
defer和async
//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
複製程式碼
defer和async都是非同步(並行)載入資源,不同點是async是載入完立即執行,而defer是載入完不執行,等到所有元素解析完再執行,也就是DOMContentLoaded事件觸發之前。
因為async載入的資源是載入完執行,所以它比不能保證順序,而defer會按順序執行指令碼。
說說react效能優化
shouldComponentUpdate
舉例:下面是antd-design-mobile的Modal元件中對的內部蒙層元件的處理
import * as React from "react";
export interface lazyRenderProps {
style: {};
visible?: boolean;
className?: string;
}
export default class LazyRender extends React.Component<lazyRenderProps, any> {
shouldComponentUpdate(nextProps: lazyRenderProps) {
return !!nextProps.visible;
}
render() {
const props: any = { ...this.props };
delete props.visible;
return <div {...props} />;
}
}
複製程式碼
immutable
像上面這種只比較了一個visible屬性,並且它是string型別,如果是一個object型別那麼就不能直接比較了,這時候使用immutable庫更好一些。
immutable優勢:
- 效能更好
- 更加安全 immutable劣勢:
- 庫比較大(壓縮後大約16k)
- api和js不相容
解決方案:seamless-immutable seamless-immutable這個庫沒有完整實現Persistent Data Structure,而是使用了Object.defineProperty擴充套件了JS的Object和Array物件,所以保持了相同的Api,同時庫的程式碼量更少,壓縮後大約2k
基於key的優化
文件中已經強調,key需要保證在當前的作用域中唯一,不要使用當前迴圈的index(尤其在長列表中)。
參考 reactjs.org/docs/reconc…
說說瀏覽器渲染流程
瀏覽器的主程式:Browser程式
- 負責下載資源
- 建立銷燬renderer程式
- 負責將renderer程式生成的點陣圖渲染到頁面上
- 與使用者互動
瀏覽器核心:renderer程式
js引擎執行緒
由一個主執行緒和多個web worder執行緒組成,web worker執行緒不能操作dom
GUI執行緒
用於解析html生成DOM樹,解析css生成CSSOM,佈局layout、繪製paint。迴流和重繪依賴該執行緒
事件執行緒
當事件觸發時,該執行緒將事件的回撥函式放入callback queue(任務佇列)中,等待js引擎執行緒處理
定時觸發執行緒
setTimeout和setInterval由該執行緒來記時,記時結束,將回撥函式放入任務佇列
http請求執行緒
每有一個http請求就開一個該執行緒,每當檢測到狀態變更就會產生一個狀態變更事件,如果這個事件由對應的回掉函式,將這個函式放入任務佇列
任務佇列輪詢執行緒
用於輪詢監聽任務佇列
流程
- 獲取html檔案
- 從上到下解析html
- 並行請求資源(css資源不會阻塞html解析,但是會阻塞頁面渲染。js資源會組織html解析)
- 生成DOM tree 和 style rules
- 構建render tree
- 執行佈局過程(layout、也叫回流),確定元素在螢幕上的具體座標
- 繪製到螢幕上(paint)
事件
DOMContentLoaded
當初始的HTML文件被完全載入和解析完成(script指令碼執行完,所屬的script指令碼之前的樣式表載入解析完成)之後,DOMContentLoaded事件被觸發
onload
所有資源載入完成觸發window的onload事件
參考流程圖:www.processon.com/view/5a6861…
說說http2.0
http2.0是對SPDY協議的一個升級版。和http1.0相比主要有以下特性:
- 二進位制分幀
- 首部壓縮
- 多路複用
- 請求優先順序
- 服務端推送(server push)
詳細可參考: HTTP----HTTP2.0新特性
實現一個reduce方法
注意邊界條件:1、陣列長度為0,並且reduce沒有傳入初始引數時,丟擲錯誤。2、reduce有返回值。
Array.prototype.myReduce = function(fn, initial) {
if (this.length === 0 && !initial) {
throw new Error("no initial and array is empty");
}
let start = 1;
let pre = this[0];
if (initial) {
start = 0;
pre = initial;
}
for (let i = start; i < this.length; i++) {
let current = this[i];
pre = fn.call(this, pre, current, i);
}
return pre;
};
複製程式碼
實現一個promise.all方法,要求保留錯誤並且併發數為3
標準的all方法是遇到錯誤會立即將promise置為失敗態,並觸發error回撥。保留錯誤的定義為:promise遇到錯誤儲存在返回的結果中。
function promiseall(promises) {
return new Promise(resolve => {
let result = [];
let flag = 0;
let taskQueue = promises.slice(0, 3); //任務佇列,初始為最大併發數3
let others = promises.slice(3); //排隊的任務
taskQueue.forEach((promise, i) => {
singleTaskRun(promise, i);
});
let i = 3; //新的任務從索引3開始
function next() {
if (others.length === 0) {
return;
}
const newTask = others.shift();
singleTaskRun(newTask, i++);
}
function singleTaskRun(promise, i) {
promise
.then(res => {
check();
result[i] = res;
next();
})
.catch(err => {
check();
result[i] = err;
next();
});
}
function check() {
flag++;
if (flag === promises.length) {
resolve(result);
}
}
});
}
複製程式碼
測試程式碼:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("1");
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("2");
}, 1500);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("3");
}, 2000);
});
let p4 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("4");
}, 2500);
});
let p_e = new Promise((resolve, reject) => {
// throw new Error("出錯");
reject("錯誤");
});
let p5 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("5");
}, 5000);
});
let all = promiseall([p_e, p1, p3, p2, p4, p5]);
all.then(
data => {
console.log("data", data); // [ '錯誤', '1', '3', '2', '4', '5' ]
}
);
複製程式碼
不用遞迴函式求一個二叉樹的高度
先看一下遞迴的實現(二叉樹的深度優先遍歷):
function getBinaryTreeHeigth(node) {
let maxDeep = 0;
function next(n, deep) {
deep++;
if (n.l) {
let newDeep = next(n.l, deep);
if (newDeep > maxDeep) {
maxDeep = newDeep;
}
}
if (n.r) {
let newDeep = next(n.r, deep);
if (newDeep > maxDeep) {
maxDeep = newDeep;
}
}
return deep;
}
next(node, 0);
return maxDeep;
}
function Node(v, l, r) {
this.v = v;
this.l = l;
this.r = r;
}
複製程式碼
非遞迴的實現(二叉樹的廣度優先遍歷):
function getBinaryTreeHeigth(node) {
if (!node) {
return 0;
}
const queue = [node];
let deep = 0;
while (queue.length) {
deep++;
for (let i = 0; i < queue.length; i++) {
const cur = queue.pop();
if (cur.l) {
queue.unshift(cur.l);
}
if (cur.r) {
queue.unshift(cur.r);
}
}
}
return deep;
}
function Node(v, l, r) {
this.v = v;
this.l = l;
this.r = r;
}
複製程式碼
js中求兩個大數相加
給定兩個以字串形式表示的非負整數 num1 和 num2,返回它們的和,仍用字串表示。
輸入:num1 = '1234', num2 = '987'
輸出:'2221'
function bigIntAdd(str1, str2) {
let result = [];
let ary1 = str1.split("");
let ary2 = str2.split("");
let flag = false; //是否進位
while (ary1.length || ary2.length) {
let result_c = sigle_pos_add(ary1.pop(), ary2.pop());
if (flag) {
result_c = result_c + 1;
}
result.unshift(result_c % 10);
if (result_c >= 10) {
flag = true;
} else {
flag = false;
}
}
if(flag) {
result.unshift('1');
}
return result.join("");
}
function sigle_pos_add(str1_c, str2_c) {
let l = (r = 0);
if (str1_c) {
l = Number(str1_c);
}
if (str2_c) {
r = Number(str2_c);
}
return l + r;
}
複製程式碼
測試程式碼:
const str1 = "1234";
const str2 = "987654321";
const str3 = "4566786445677555";
const str4 = "987";
console.log(bigIntAdd(str1, str4)) //'2221'
console.log(bigIntAdd(str2, str3)) //'4566787433331876'
複製程式碼
實現一個陣列隨機打亂演算法
function disOrder(ary) {
for (let i = 0; i < ary.length; i++) {
let randomIndex = Math.floor(Math.random() * ary.length);
swap(ary, i, randomIndex);
}
}
function swap(ary, a, b) {
let temp = ary[a];
ary[a] = ary[b];
ary[b] = temp;
}
let ary = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
disOrder(ary);
console.log(ary);
複製程式碼
給數字增加“逗號”分隔
輸入: '"123456789.012"' 輸出:123,456,789.012
正則解法:
function parseNumber(num) {
if (!num) return "";
return num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,");
}
複製程式碼
非正則:
function formatNumber(num) {
if (!num) return "";
let [int, float] = num.split(".");
let intArr = int.split("");
let result = [];
let i = 0;
while (intArr.length) {
if (i !== 0 && i % 3 === 0) {
result.unshift(intArr.pop() + ",");
} else {
result.unshift(intArr.pop());
}
i++;
}
return result.join("") + "." + (float ? float : "");
}
複製程式碼