共计 12967 个字符,预计需要花费 33 分钟才能阅读完成。
过程之前的通信形式
(1)管道通信
管道是一种最根本的过程间通信机制。管道就是操作系统在内核中开拓的一段缓冲区,过程 1 能够将须要交互的数据拷贝到这段缓冲区,过程 2 就能够读取了。
管道的特点:
- 只能单向通信
- 只能血缘关系的过程进行通信
- 依赖于文件系统
- 生命周期随过程
- 面向字节流的服务
- 管道外部提供了同步机制
(2)音讯队列通信
音讯队列就是一个音讯的列表。用户能够在音讯队列中增加音讯、读取音讯等。音讯队列提供了一种从一个过程向另一个过程发送一个数据块的办法。每个数据块都被认为含有一个类型,接管过程能够独立地接管含有不同类型的数据结构。能够通过发送音讯来防止命名管道的同步和阻塞问题。然而音讯队列与命名管道一样,每个数据块都有一个最大长度的限度。
应用音讯队列进行过程间通信,可能会收到数据块最大长度的限度束缚等,这也是这种通信形式的毛病。如果频繁的产生过程间的通信行为,那么过程须要频繁地读取队列中的数据到内存,相当于间接地从一个过程拷贝到另一个过程,这须要破费工夫。
(3)信号量通信
共享内存最大的问题就是多过程竞争内存的问题,就像相似于线程平安问题。咱们能够应用信号量来解决这个问题。信号量的实质就是一个计数器,用来实现过程之间的互斥与同步。例如信号量的初始值是 1,而后 a 过程来拜访内存 1 的时候,咱们就把信号量的值设为 0,而后过程 b 也要来拜访内存 1 的时候,看到信号量的值为 0 就晓得曾经有过程在拜访内存 1 了,这个时候过程 b 就会拜访不了内存 1。所以说,信号量也是过程之间的一种通信形式。
(4)信号通信
信号(Signals)是 Unix 零碎中应用的最古老的过程间通信的办法之一。操作系统通过信号来告诉过程零碎中产生了某种预先规定好的事件(一组事件中的一个),它也是用户过程之间通信和同步的一种原始机制。
(5)共享内存通信
共享内存就是映射一段能被其余过程所拜访的内存,这段共享内存由一个过程创立,但多个过程都能够拜访(使多个过程能够拜访同一块内存空间)。共享内存是最快的 IPC 形式,它是针对其余过程间通信形式运行效率低而专门设计的。它往往与其余通信机制,如信号量,配合应用,来实现过程间的同步和通信。
(6)套接字通信
下面说的共享内存、管道、信号量、音讯队列,他们都是多个过程在一台主机之间的通信,那两个相隔几千里的过程可能进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如咱们平时通过浏览器发动一个 http 申请,而后服务器给你返回对应的数据,这种就是采纳 Socket 的通信形式了。
基于 Localstorage 设计一个 1M 的缓存零碎,须要实现缓存淘汰机制
设计思路如下:
- 存储的每个对象须要增加两个属性:别离是过期工夫和存储工夫。
- 利用一个属性保留零碎中目前所占空间大小,每次存储都减少该属性。当该属性值大于 1M 时,须要依照工夫排序零碎中的数据,删除一定量的数据保障可能存储下目前须要存储的数据。
- 每次取数据时,须要判断该缓存数据是否过期,如果过期就删除。
以下是代码实现,实现了思路,然而可能会存在 Bug,然而这种设计题个别是给出设计思路和局部代码,不会须要写出一个无问题的代码
class Store {constructor() {let store = localStorage.getItem('cache')
if (!store) {
store = {
maxSize: 1024 * 1024,
size: 0
}
this.store = store
} else {this.store = JSON.parse(store)
}
}
set(key, value, expire) {this.store[key] = {date: Date.now(),
expire,
value
}
let size = this.sizeOf(JSON.stringify(this.store[key]))
if (this.store.maxSize < size + this.store.size) {console.log('超了 -----------');
var keys = Object.keys(this.store);
// 工夫排序
keys = keys.sort((a, b) => {let item1 = this.store[a], item2 = this.store[b];
return item2.date - item1.date;
});
while (size + this.store.size > this.store.maxSize) {let index = keys[keys.length - 1]
this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
delete this.store[index]
}
}
this.store.size += size
localStorage.setItem('cache', JSON.stringify(this.store))
}
get(key) {let d = this.store[key]
if (!d) {console.log('找不到该属性');
return
}
if (d.expire > Date.now) {console.log('过期删除');
delete this.store[key]
localStorage.setItem('cache', JSON.stringify(this.store))
} else {return d.value}
}
sizeOf(str, charset) {
var total = 0,
charCode,
i,
len;
charset = charset ? charset.toLowerCase() : '';
if (charset === 'utf-16' || charset === 'utf16') {for (i = 0, len = str.length; i < len; i++) {charCode = str.charCodeAt(i);
if (charCode <= 0xffff) {total += 2;} else {total += 4;}
}
} else {for (i = 0, len = str.length; i < len; i++) {charCode = str.charCodeAt(i);
if (charCode <= 0x007f) {total += 1;} else if (charCode <= 0x07ff) {total += 2;} else if (charCode <= 0xffff) {total += 3;} else {total += 4;}
}
}
return total;
}
}
箭头函数和一般函数有啥区别?箭头函数能当构造函数吗?
- 一般函数通过 function 关键字定义,this 无奈联合词法作用域应用,在运行时绑定,只取决于函数的调用形式,在哪里被调用,调用地位。(取决于调用者,和是否独立运行)
-
箭头函数应用被称为“胖箭头”的操作
=>
定义,箭头函数不利用一般函数 this 绑定的四种规定,而是依据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无奈被批改(new 也不行)。- 箭头函数罕用于回调函数中,包含事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
-
不能通过 new 关键字调用
- 一个函数外部有两个办法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 办法,创立一个实例对象,而后再执行这个函数体,将函数的 this 绑定在这个实例对象上
- 当间接调用时,执行 [[Call]] 办法,间接执行函数体
- 箭头函数没有 [[Construct]] 办法,不能被用作结构函数调用,当应用 new 进行函数调用时会报错。
function foo() {return (a) => {console.log(this.a);
}
}
var obj1 = {a: 2}
var obj2 = {a: 3}
var bar = foo.call(obj1);
bar.call(obj2);
对作用域、作用域链的了解
1)全局作用域和函数作用域
(1)全局作用域
- 最外层函数和最外层函数里面定义的变量领有全局作用域
- 所有未定义间接赋值的变量主动申明为全局作用域
- 所有 window 对象的属性领有全局作用域
- 全局作用域有很大的弊病,过多的全局作用域变量会净化全局命名空间,容易引起命名抵触。
(2)函数作用域
- 函数作用域申明在函数外部的变零,个别只有固定的代码片段能够拜访到
- 作用域是分层的,内层作用域能够拜访外层作用域,反之不行
2)块级作用域
- 应用 ES6 中新增的 let 和 const 指令能够申明块级作用域,块级作用域能够在函数中创立也能够在一个代码块中的创立(由
{}
包裹的代码片段) - let 和 const 申明的变量不会有变量晋升,也不能够反复申明
- 在循环中比拟适宜绑定块级作用域,这样就能够把申明的计数器变量限度在循环外部。
作用域链: 在以后作用域中查找所需变量,然而该作用域没有这个变量,那这个变量就是自在变量。如果在本人作用域找不到该变量就去父级作用域查找,顺次向下级作用域查找,直到拜访到 window 对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是 保障对执行环境有权拜访的所有变量和函数的有序拜访,通过作用域链,能够拜访到外层环境的变量和函数。
作用域链的实质上是一个指向变量对象的指针列表。变量对象是一个蕴含了执行环境中所有变量和函数的对象。作用域链的前端始终都是以后执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最初一个对象。
当查找一个变量时,如果以后执行环境中没有找到,能够沿着作用域链向后查找。
数组扁平化
数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。应用 Array.prototype.flat 能够间接将多层数组拍平成一层:
[1, [2, [3]]].flat(2) // [1, 2, 3]
当初就是要实现 flat 这种成果。
ES5 实现:递归。
function flatten(arr) {var result = [];
for (var i = 0, len = arr.length; i < len; i++) {if (Array.isArray(arr[i])) {result = result.concat(flatten(arr[i]))
} else {result.push(arr[i])
}
}
return result;
}
ES6 实现:
function flatten(arr) {while (arr.some(item => Array.isArray(item))) {arr = [].concat(...arr);
}
return arr;
}
代码输入后果
const promise = new Promise((resolve, reject) => {console.log(1);
console.log(2);
});
promise.then(() => {console.log(3);
});
console.log(4);
输入后果如下:
1
2
4
promise.then 是微工作,它会在所有的宏工作执行完之后才会执行,同时须要 promise 外部的状态发生变化,因为这里外部没有发生变化,始终处于 pending 状态,所以不输入 3。
instance 如何应用
右边能够是任意值,左边只能是函数
'hello tuture' instanceof String // false
数组去重
ES5 实现:
function unique(arr) {var res = arr.filter(function(item, index, array) {return array.indexOf(item) === index
})
return res
}
ES6 实现:
var unique = arr => [...new Set(arr)]
函数节流
触发高频事件,且 N 秒内只执行一次。
简略版:应用工夫戳来实现,立刻执行一次,而后每 N 秒执行一次。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {func.apply(context, args);
previous = now;
}
}
}
最终版:反对勾销节流;另外通过传入第三个参数,options.leading 来示意是否能够立刻执行一次,opitons.trailing 示意完结调用的时候是否还要执行一次,默认都是 true。
留神设置的时候不能同时将 leading 或 trailing 设置为 false。
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {if (timeout) {clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
节流的应用就不拿代码举例了,参考防抖的写就行。
解析 URL 参数为对象
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
let [key, val] = param.split('='); // 宰割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创立 key 并设置值
paramsObj[key] = val;
}
} else { // 解决没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
对节流与防抖的了解
- 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。
- 函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。
防抖函数的利用场景:
- 按钮提交场景:防⽌屡次提交按钮,只执⾏最初提交的⼀次
- 服务端验证场景:表单验证须要服务端配合,只执⾏⼀段间断的输⼊事件的最初⼀次,还有搜寻联想词性能相似⽣存环境请⽤ lodash.debounce
节流函数的适⽤场景:
- 拖拽场景:固定工夫内只执⾏⼀次,防⽌超⾼频次触发地位变动
- 缩放场景:监控浏览器 resize
- 动画场景:防止短时间内屡次触发动画引起性能问题
Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行程序?
Node 中的 Event Loop 和浏览器中的是齐全不雷同的货色。
Node 的 Event Loop 分为 6 个阶段,它们会依照 程序 重复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量达到零碎设定的阈值,就会进入下一阶段。
(1)Timers(计时器阶段):首次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(蕴含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行结束后,如果回调中触发了相应的微工作,会接着执行所有微工作,执行完微工作后再进入 Pending callbacks 阶段。
(2)Pending callbacks:执行推延到下一个循环迭代的 I / O 回调(零碎调用相干的回调)。
(3)Idle/Prepare:仅供外部应用。
(4)Poll(轮询阶段):
- 当回调队列不为空时:会执行回调,若回调中触发了相应的微工作,这里的微工作执行机会和其余中央有所不同,不会等到所有回调执行结束后才执行,而是针对每一个回调执行结束后,就执行相应微工作。执行完所有的回调后,变为上面的状况。
- 当回调队列为空时(没有回调或所有回调执行结束):但如果存在有计时器(setTimeout、setInterval 和 setImmediate)没有执行,会完结轮询阶段,进入 Check 阶段。否则会阻塞并期待任何正在执行的 I / O 操作实现,并马上执行相应的回调,直到所有回调执行结束。
(5)Check(查问阶段):会查看是否存在 setImmediate 相干的回调,如果存在则执行所有回调,执行结束后,如果回调中触发了相应的微工作,会接着执行所有微工作,执行完微工作后再进入 Close callbacks 阶段。
(6)Close callbacks:执行一些敞开回调,比方 socket.on(‘close’, …)等。
上面来看一个例子,首先在有些状况下,定时器的执行程序其实是 随机 的
setTimeout(() => { console.log('setTimeout')}, 0)setImmediate(() => { console.log('setImmediate')})
对于以上代码来说,setTimeout
可能执行在前,也可能执行在后
- 首先
setTimeout(fn, 0) === setTimeout(fn, 1)
,这是由源码决定的 - 进入事件循环也是须要老本的,如果在筹备时候破费了大于 1ms 的工夫,那么在 timer 阶段就会间接执行
setTimeout
回调 - 那么如果筹备工夫破费小于 1ms,那么就是
setImmediate
回调先执行了
当然在某些状况下,他们的执行程序肯定是固定的,比方以下代码:
const fs = require('fs')
fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');
}, 0)
setImmediate(() => {console.log('immediate')
})
})
在上述代码中,setImmediate
永远 先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行结束后队列为空,发现存在 setImmediate
回调,所以就间接跳转到 check 阶段去执行回调了。
下面都是 macrotask 的执行状况,对于 microtask 来说,它会在以上每个阶段实现前 清空 microtask 队列,
setTimeout(() => {console.log('timer21')
}, 0)
Promise.resolve().then(function() {console.log('promise1')
})
对于以上代码来说,其实和浏览器中的输入是一样的,microtask 永远执行在 macrotask 后面。
最初来看 Node 中的 process.nextTick
,这个函数其实是独立于 Event Loop 之外的,它有一个本人的队列,当每个阶段实现后,如果存在 nextTick 队列,就会 清空队列中的所有回调函数,并且优先于其余 microtask 执行。
setTimeout(() => {console.log('timer1')
Promise.resolve().then(function() {console.log('promise1')
})
}, 0)
process.nextTick(() => {console.log('nextTick')
process.nextTick(() => {console.log('nextTick')
process.nextTick(() => {console.log('nextTick')
process.nextTick(() => {console.log('nextTick')
})
})
})
})
对于以上代码,永远都是先把 nextTick 全副打印进去。
代码输入后果
function runAsync (x) {const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result:', res))
.catch(err => console.log(err))
输入后果如下:
1
'result:' 1
2
3
then 只会捕捉第一个胜利的办法,其余的函数尽管还会继续执行,然而不是被 then 捕捉了。
偏函数
什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,残余参数(n – x)将在下次调用全副传入。举个例子:
function add(a, b, c) {return a + b + c}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
发现没有,其实偏函数和函数柯里化有点像,所以依据函数柯里化的实现,可能能很快写出偏函数的实现:
function partial(fn, ...args) {return (...arg) => {return fn(...args, ...arg)
}
}
如上这个性能比较简单,当初咱们心愿偏函数能和柯里化一样能实现占位性能,比方:
function clg(a, b, c) {console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3) // 顺次打印:1, 2, 3
_
占的位其实就是 1 的地位。相当于:partial(clg, 1, 2),而后 partialClg(3)。明确了原理,咱们就来写实现:
function partial(fn, ...args) {return (...arg) => {args[index] =
return fn(...args, ...arg)
}
}
代码输入后果
console.log('1');
setTimeout(function() {console.log('2');
process.nextTick(function() {console.log('3');
})
new Promise(function(resolve) {console.log('4');
resolve();}).then(function() {console.log('5')
})
})
process.nextTick(function() {console.log('6');
})
new Promise(function(resolve) {console.log('7');
resolve();}).then(function() {console.log('8')
})
setTimeout(function() {console.log('9');
process.nextTick(function() {console.log('10');
})
new Promise(function(resolve) {console.log('11');
resolve();}).then(function() {console.log('12')
})
})
输入后果如下:
1
7
6
8
2
4
3
5
9
11
10
12
(1)第一轮事件循环流程剖析如下:
- 整体 script 作为第一个宏工作进入主线程,遇到
console.log
,输入 1。 - 遇到
setTimeout
,其回调函数被散发到宏工作 Event Queue 中。暂且记为setTimeout1
。 - 遇到
process.nextTick()
,其回调函数被散发到微工作 Event Queue 中。记为process1
。 - 遇到
Promise
,new Promise
间接执行,输入 7。then
被散发到微工作 Event Queue 中。记为then1
。 - 又遇到了
setTimeout
,其回调函数被散发到宏工作 Event Queue 中,记为setTimeout2
。
宏工作 Event Queue | 微工作 Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
上表是第一轮事件循环宏工作完结时各 Event Queue 的状况,此时曾经输入了 1 和 7。发现了 process1
和then1
两个微工作:
- 执行
process1
,输入 6。 - 执行
then1
,输入 8。
第一轮事件循环正式完结,这一轮的后果是输入 1,7,6,8。
(2)第二轮工夫循环从 **setTimeout1**
宏工作开始:
- 首先输入 2。接下来遇到了
process.nextTick()
,同样将其散发到微工作 Event Queue 中,记为process2
。 new Promise
立刻执行输入 4,then
也散发到微工作 Event Queue 中,记为then2
。
宏工作 Event Queue | 微工作 Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
第二轮事件循环宏工作完结,发现有 process2
和then2
两个微工作能够执行:
- 输入 3。
- 输入 5。
第二轮事件循环完结,第二轮输入 2,4,3,5。
(3)第三轮事件循环开始,此时只剩 setTimeout2 了,执行。
- 间接输入 9。
- 将
process.nextTick()
散发到微工作 Event Queue 中。记为process3
。 - 间接执行
new Promise
,输入 11。 - 将
then
散发到微工作 Event Queue 中,记为then3
。
宏工作 Event Queue | 微工作 Event Queue |
---|---|
process3 | |
then3 |
第三轮事件循环宏工作执行完结,执行两个微工作 process3
和then3
:
- 输入 10。
- 输入 12。
第三轮事件循环完结,第三轮输入 9,11,10,12。
整段代码,共进行了三次事件循环,残缺的输入为 1,7,6,8,2,4,3,5,9,11,10,12。
手写题:数组扁平化
function flatten(arr) {let result = [];
for (let i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {result = result.concat(flatten(arr[i]));
} else {result = result.concat(arr[i]);
}
}
return result;
}
const a = [1, [2, [3, 4]]];
console.log(flatten(a));
如何优化要害渲染门路?
为尽快实现首次渲染,咱们须要最大限度减小以下三种可变因素:
(1)要害资源的数量。
(2)要害门路长度。
(3)关键字节的数量。
要害资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其余资源的占用也就越少。同样,要害门路长度受所有要害资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后能力开始下载,并且资源越大,下载所需的往返次数就越多。最初,浏览器须要下载的关键字节越少,解决内容并让其呈现在屏幕上的速度就越快。要缩小字节数,咱们能够缩小资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化要害渲染门路的惯例步骤如下:
(1)对要害门路进行剖析和个性形容:资源数、字节数、长度。
(2)最大限度缩小要害资源的数量:删除它们,提早它们的下载,将它们标记为异步等。
(3)优化要害字节数以缩短下载工夫(往返次数)。
(4)优化其余要害资源的加载程序:您须要尽早下载所有要害资产,以缩短要害门路长度
代码输入后果
function SuperType(){this.property = true;}
SuperType.prototype.getSuperValue = function(){return this.property;};
function SubType(){this.subproperty = false;}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){return this.subproperty;};
var instance = new SubType();
console.log(instance.getSuperValue());
输入后果:true
实际上,这段代码就是在实现原型链继承,SubType 继承了 SuperType,实质是重写了 SubType 的原型对象,代之以一个新类型的实例。SubType 的原型被重写了,所以 instance.constructor 指向的是 SuperType。具体如下:
事件循环机制(Event Loop)
事件循环机制从整体上通知了咱们 JavaScript 代码的执行程序 Event Loop
即事件循环,是指浏览器或 Node
的一种解决 javaScript
单线程运行时不会阻塞的一种机制,也就是咱们常常应用 异步 的原理。
先执行 Script 脚本,而后清空微工作队列,而后开始下一轮事件循环,持续先执行宏工作,再清空微工作队列,如此往返。
- 宏工作:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
- 微工作:process.nextTick()/Promise
上诉的 setTimeout 和 setInterval 等都是工作源,真正进入工作队列的是他们散发的工作。
优先级
- setTimeout = setInterval 一个队列
- setTimeout > setImmediate
- process.nextTick > Promise
for (const macroTask of macroTaskQueue) {handleMacroTask();
for (const microTask of microTaskQueue) {handleMicroTask(microTask);
}
}