目录
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 : "");
}