目录
- 防抖和节流
- Set、Map、WeakSet 和 WeakMap
- DFS和BFS
- 利用DFS和BFS深拷贝对象
- class
- JS基础之异步
- event loop 和 任务队列
- 浏览器和Node事件循环的区别
- Promise原理
- Generator函数
- 数组扁平化去重
- 实现一个new
- http协议
- 介绍HTTPS握手过程
- HTTPS 中间人攻击和验证证书的合法性
- 谈谈你对TCP三次握手和四次挥手的理解
- npm install机制
- 数据类型
- 判断数组方法
- Repaint&Reflow
- 观察者模式和发布订阅模式的区别
- Redux 和 Vuex 的设计思想
- 前端中的模块化开发
- this指向
- const 和 let 的作用域
- xss与csrf
- react原理
- 对react setState异步更新的理解
- diff算法
- 以vue为例,如何实现一个mvvm(基于defineProperty)
- 实现一个mvvm(基于Proxy)
- vue的事件处理机制
- 数组的方法:forEach,map, filter,some, reduce
- virtual dom vs mvvm vs 原生dom
- IIFE下函数名为不可变量
- IIFE下的变量提升
- 浏览器的缓存机制
- BFC(Block Formatting Contexts
- vue如何限制子组件不能直接修改props?
- 响应式原理:Object.defineProperty VS Proxy
- div水平垂直居中
- 优先级与对象赋值
- array like
- 为什么数据埋点时通常使用1x1 像素的透明 gif 图片?
- 实现LazyMan类
- 箭头函数与普通函数的区别
- 如何实现token加密
- 如何设计实现无缝轮播
- requestAnimationFrame
- ES6 代码转成 ES5 代码的实现思路
- HMR原理
- BFC、IFC、GFC、FFC
- input处理中文输入
- currying与累加器
- react-router 里的 Link 标签和 a 标签有什么区别
- HTML5 history 和 hash模式
- list转tree
- 实现Promise相关类方法
- 时间复杂度为O(log(m+n))的取中位数算法
- 返回整数逆序后的字符串
- vuex数据无法刷新
- vue mpa
- react项目优化
- 前端工程化
- 重排(reflow)和重绘(repaint)
- 堆和栈
- React Hooks 原理
- 使用CSS实现各种形状
- <script>标签,async和defer属性
- css link 和 @import
- 浏览器输入URL之后发生了什么?
防抖和节流
防抖:只要输入频率持续高于阈值,函数就不触发,当最后一次触发频率低于阈值时执行,将多次频繁执行合并为一次,函数固定时间执行次数与输入频率有关
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
节流:输入频率持续高于阈值时,函数转换为低频率执行,即将函数由高频执行降为低频,函数固定时间执行次数与输入频率无关
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
参考
什么是防抖和节流?有什么区别?如何实现?
前端面试查漏补缺–(一) 防抖和节流
Set、Map、WeakSet 和 WeakMap
1.Set为不含重复值的类数组结构
2.WeakSet为存储对象的类数组结构;且垃圾回收机制不考虑WeakSet成员
3.Map为Value-Value的Hash结构,键名不限于字符串
4.WeakMap键值只能为对象;且垃圾回收机制不考虑WeakMap键值
DFS和BFS
数据:
let data = {
a: {
c: {
g: {
m: {
r: true,
s: true
}
},
h: {
n: {
t: true
},
o: true
}
},
d:{
i: true
}
},
b: {
e: {
j: true,
k: {
p: true,
q: true
}
},
f: {
l: true
}
}
}
深度遍历:
const depthFirstTraversing = (data)=>{
let traversingRes = [];
for(let key in data){
// console.log(key);
traversingRes.push(key);
if(data[key] !== true){
traversingRes = traversingRes.concat(depthFirstTraversing(data[key]))
}
}
return traversingRes;
}
console.log(depthFirstTraversing(data));
广度遍历
const breadthFirstTraversing = (data)=>{
let traversingRes = [];
let leveNodes = [];
for(let key in data){
leveNodes.push({key, data: data[key]});
}
while(leveNodes.length > 0){
let item = leveNodes.shift();
traversingRes.push(item.key);
for(let childKey in item.data){
leveNodes.push({key: childKey, data: item.data[childKey]})
};
}
return traversingRes;
}
console.log(breadthFirstTraversing(data))
利用DFS和BFS深拷贝对象
data同上题
利用DFS深拷贝对象
const deepCopyThroughDFS = (data)=>{
let copyRes = {};
for(let key in data){
// console.log(key);
if(data[key] !== true){
copyRes[key] = deepCopyThroughDFS(data[key]);
}else{
copyRes[key] = true;
}
}
return copyRes;
}
利用BFS深拷贝对象
const deepCopyThroughBFS = (data)=>{
let copyRes = {};
let leveNodes = [];
for(let key in data){
leveNodes.push({key, data: data[key], parentData: copyRes});
}
while(leveNodes.length > 0){
let item = leveNodes.shift();
if(item.data !== true){
item.parentData[item.key] = {};
for(let childKey in item.data){
leveNodes.push({key: childKey, data: item.data[childKey], parentData: item.parentData[item.key]})
};
}else{
item.parentData[item.key] = true;
}
}
return copyRes;
}
class
es6 class的实例方法,实例属性,原型链方法,静态方法(static)支持实现
es6 class的原型链属性,静态属性不支持实现,暂时用es5方式实现
JS基础之异步
参考
JS 基础之异步
event loop 和 任务队列
js执行机制:
- js为单线程语言,
主线程
从任务队列
取出任务放入执行栈执行;工作线程
将执行完毕的异步任务
不断放入任务队列
;主线程重复从任务队列取出任务执行(生产-消费模式) - js任务分为
宏任务(macrotasks)
和微任务(microtasks)
,macrotasks和microtasks分别放入macroqueue
和microqueue
- macrotasks,每次执行栈执行的代码就是一个宏任务,主要包含:script(整体代码)、setTimeout、setInterval、XHR/ajax callback、I/O、UI交互事件(DOM API event callback)、postMessage、MessageChannel、setImmediate(Node.js 环境)
- microtasks,当前 task 执行结束后立即执行的任务,主要包含:Promise.then、async/await(每个await表达式之后的代码)、MutaionObserver、process.nextTick(Node.js 环境)
- event loop:
5.1.主线程从macroqueue
取出一个macrotask
5.2.将macrotask
里的同步代码放入执行栈
执行
5.3.将macrotask
里的microtask
放入microqueue
5.4.将macrotask
里的macrotask
放入macroqueue
5.5.执行栈
对当前macrotask
的同步代码执行完毕之后,从microqueue
取出源自当前macrotask
的microtask
(5.3.)放入执行栈
执行
5.6.microtask
执行完毕后当前event loop
结束;主线程从macroqueue
取出一个macrotask
,重复5.1.开始新的event loop
- 每个
event loop
执行一个macrotask
后会清空当前macrotask
产生的microtasks
,然后进入下个event loop
参考:
js event loop 事件循环
浏览器的Tasks、microtasks、 queues 和 schedules
setTimeout、Promise、Async/Await 的区别
浏览器和Node事件循环的区别
1.Nodejs v11以下:
执行完第一个阶段的所有任务(包含macrotask与对应产生的microtasks),再执行完nextTick macrotasks队列里面的内容, 然后执行完微任务队列的内容
2.Nodejs v11及以上:
执行完每一个macrotask,再执行macrotask对应产生的microtasks,如此循环
参考:
浏览器和Node 事件循环的区别
Promise原理
- 在 then 函数执行时用一个属性this.callback保存回调函数 cb数组,然后在 resolve 执行时再将其执行
- 添加一个属性 isResolved,用来记录是否调用过 resolve 函数,避免多次执行resolve
- then返回Promise对象可支持链式调用
Promise 源码:实现一个简单的 Promise
Promise原理讲解 && 实现一个Promise对象 (遵循Promise/A+规范)
面试精选之Promise
Generator函数
以下generator函数代码输出什么?
function* demo() {
console.log('Hello ' + (yield 'a')); // OK
console.log('Hello ' + (yield 'b')); // OK
}
let de = demo()
let a = de.next('a')
console.log(a)
let b = de.next('b')
console.log(b)
let c = de.next('c')
console.log(c)
let d = de.next('d')
console.log(d)
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式
参考:
yield 表达式
数组扁平化去重
已知如下数组:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
const flattenData = (data)=>{
var newData = [];
data.forEach((item)=>{
if(typeof item === 'object'){
newData = newData.concat(flattenData(item))
}else{
newData.push(item)
}
})
return newData;
}
const sortAndDeduplication = (data)=>{
const sort = (a,b)=>{
return a - b;
}
var newData = [];
newData = data.sort(sort).reduce((acc, cVlue)=>{
if(cVlue !== acc[acc.length - 1]){
acc.push(cVlue);
}
return acc
}, [])
return newData;
}
const flatAndSortAndDeduplication = (data)=>{
// let flatData = flattenData(data);
let sortAndDeduplicatedData = sortAndDeduplication(flattenData(data));
return sortAndDeduplicatedData
}
console.log(flatAndSortAndDeduplication(arr))
实现一个new
const _new = function(){
const [klass, ...args] = arguments;
var newInstance = {};
klass.apply(newInstance, args);
newInstance.__proto__ = klass.prototype
return newInstance;
}
let classA = function(a, b){
this.a = a;
this.b = b;
}
classA.prototype = {
geta: function(){
console.log(this.a)
},
setb: function(b){
this.b = b
}
}
// 实例化classA
let aa = _new(classA, 1, 2)
http协议
1.请求报文格式
请求行(request line)、请求头部(header)、空行和请求数据
例:
2.响应报文格式
例:
3.状态码
分类 | 分类描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
参考:
一张图理清TCP/IP参考模型、OSI模型及HTTP/HTTPS的对应关系
HTTP和HTTPS协议,看一篇就够了
《图解HTTP与HTTPS》的干货1.2w字
介绍HTTPS握手过程
参考:
图文理清HTTPS及SSL/TLS加解密核心运行机制
SSL/TLS协议运行机制的概述
HTTP和HTTPS协议,看一篇就够了
介绍 HTTPS 握手过程
《图解HTTP与HTTPS》的干货1.2w字
《大前端进阶 安全》系列 HTTPS详解
一文看懂HTTPS、证书机构(CA)、证书、数字签名、私钥、公钥
HTTPS 中间人攻击和验证证书的合法性
中间人攻击
证书认证
CA颁发服务端证书
客户端验证证书
图文理清HTTPS及SSL/TLS加解密核心运行机制
大前端进阶 安全》系列 HTTPS详解
《图解HTTP与HTTPS》的干货1.2w字
一文看懂HTTPS、证书机构(CA)、证书、数字签名、私钥、公钥
HTTPS 握手过程中,客户端如何验证证书的合法性
谈谈你对TCP三次握手和四次挥手的理解
1. 三次握手
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。 进行三次握手:
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。 - 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。 - 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
在socket编程中,客户端执行connect()时,将触发三次握手。
1.1. 为什么需要三次握手?
- 建立TCP链接是为了保证稳定有序的收发数据,那么就要保证双发的发送和接收功能都是OK的。 三次握手三次是客户端和服务端都让 “对方” 知道自己发送和接收能力都OK的最小次数。 第一次:client—>server (服务端确认了客户端的发送功能) 第二次:setver—>client (客户端确认了服务端的发送功能和接收功能) 第三次:client—>server (服务端确认了客户端的接收功能); TCP连接建立。 两次无法保证,四次会多余。
- 确认ISN
1.2. 三次握手过程中可以携带数据吗?
第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。防止建立链接时遭受携带大量数据的攻击
1.3. SYN Flood攻击
一些恶意的人根据超时机制制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。常见的防御 SYN 攻击的方法有如下几种:
- 缩短超时(SYN Timeout)时间
- 增加最大半连接数
- 过滤网关防护
- SYN cookies技术
2. 四次挥手
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。
在socket编程中,任何一方执行close()操作即可产生挥手操作。
2.1. 挥手为什么需要四次
因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。挥手的时候,A说我要断开了,B还没发完最后的数据,因此需要先回应一下A,我收到你的断开的请求了,但是你要等我把最后的内容给你,所以这里分开了2步: (1)回应A; (2)发送自己的最后一个数据
2.2. 等待2MSL的意义
- TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL
- 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段
3. TCP重传机制?
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
遗留:为何不能跳着确认?
4. 快速重传机制
TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下:
参考:
面试官,不要再问我三次握手和四次挥手
TCP 的那些事儿(上)
作为前端的你了解多少tcp的内容
谈谈你对 TCP 三次握手和四次挥手的理解
TCP 请求头
npm install机制
关键点:扁平化安装,若存在多个不兼容版本则需嵌套安装
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/22
详解npm的模块安装机制
数据类型
原始类型:boolean、undefined、string、number、symbol
对象类型:null ,Array、function、Object
判断数组方法
1.Object.prototype.toString.call([]) // “[object Array]”
借用Object的toString方法,基本可以判断所有的数据类型
2.[] instanceof Array // true
判断[]原型链上是否能找到Array类型,仅对对象类型的数据有效,对原始类型无效
3.Array.isArray([]) // true
判断是否为数组,与2方法比较,该方法可以判断出iframe数组
4.typeof [] // “object”
注意:该方法不能判断对象类型,对象类型输出均为object,只能判断原始类型
Repaint&Reflow
参考:
介绍下重绘和回流(Repaint & Reflow),以及如何进行优化
观察者模式和发布订阅模式的区别
参考:
观察者模式和发布订阅模式的区别
介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景
Redux 和 Vuex 的设计思想
参考:
Vuex、Flux、Redux、Redux-saga、Dva、MobX
前端中的模块化开发
参考:
前端中的模块化开发
this指向
参考:
箭头函数
const 和 let 的作用域
1.const和let会创造block作用域,只要在同一个block内都可以拿到变量
2.顶部var变量挂在window下
示例:
如下创造了2个Block,分别对应变量e和f;1个Script(也可以理解为顶级Block),对应变量a和b,剩下的c和d作为全局变量
xss与csrf
1.xss:跨站脚本攻击,分为存储型 XSS,反射型 XSS和DOM 型 XSS。向被攻击网站注入恶意代码并执行
2.csrf:跨站请求伪造,利用用户在被攻击网站已经保存的信息,在第三方网站向被攻击网站发送请求,冒充用户执行操作
3.预防
xss:过滤用户输入;转义html
csrf:同源检测;添加csrf token
4.
遭受xss攻击的网站,cookie和token均可以被劫持
遭受csrf攻击的网站,cookie可以被劫持,token只要不放在cookie就不会被劫持,或者只要不直接使用cookie token作为键值,被劫持的token也为无效token
参考
前端安全系列(一):如何防止XSS攻击?
前端安全系列之二:如何防止CSRF攻击?
react原理
- 渲染机制及虚拟dom
- 更新机制
/**
* 更新
* @param {*} newState 新状态
*/
ReactClass.prototype.setState = function(newState) {
// 拿到ReactCompositeComponent的实例
// 在装载的时候保存
// 代码:this._reactInternalInstance = this
this._reactInternalInstance.receiveComponent(null, newState);
};
- 文本节点的 receiveComponent
文本节点的更新比较简单,拿到新的文本进行比较,不同则直接替换整个节点 - 自定义元素的 receiveComponent
- 合并 state
- 更新 state
- 然后看业务代码中是否实现生命周期方法 shouldComponentUpdate 有则调用,如果返回值为 false 则停止往下执行
- 然后是生命周期方法 componentWillUpdate
- 然后通过拿到新 state 的 instance 调用 render 方法拿到新的 element 和之旧的 element 进行比较
- 如果要更新就继续调用对应的 component 类对应的 receiveComponent 就好啦,其实就是直接当甩手掌柜,事情直接丢给手下去办了。当然还有种情况是,两次生成的 element 差别太大,就不是一个类型的,那好办直接重新生成一份新的代码重新渲染一次就 ok 了
-
基本元素的 receiveComponent
基础元素的更新包括两方面
-
属性的更新,包括对特殊属性比如事件的处理
-
子节点的更新
子节点的更新比较复杂,是提升效率的关键,所以需要处理以下问题: -
diff - 拿新的子节点树跟以前老的子节点树对比,找出他们之间的差别。
-
patch - 所有差别找出后,再一次性的去更新。
React 源码分析
React 源码剖析系列 - 不可思议的 react diff
从Preact了解一个类React的框架是怎么实现的(一): 元素创建
从Preact了解一个类React的框架是怎么实现的(二): 元素diff
从Preact了解一个类React的框架是怎么实现的(三): 组件
ARV 渲染实现比较总结
对react setState异步更新的理解
- 合成事件中,异步
- 生命周期函数中,异步
- 原生事件中,同步
- setTimeout中,同步
- 批量更新,合并相同的key
参考:
你真的理解setState吗?
diff算法
参考
简单版react原理分析(虚拟dom,节点渲染,数据更新,diff算法等)
React 源码剖析系列 - 不可思议的 react diff
详解vue的diff算法
VueDiff算法的简单分析和一些个人思考
解析vue2.0的diff算法
传统diff、react优化diff、vue优化diff
写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
协调
以vue为例,如何实现一个mvvm(基于defineProperty)
一个mvvm包含:
1.编译器(compiler,基础模板引擎)
提取指令,data等,渲染真实dom
2.数据劫持(observer,利用defineProperty)
核心方法
defineReactive(obj,key,value){
// 在获取某个值的适合 想弹个框
let that = this;
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ // 当取值时调用的方法
return value;
},
set(newValue){ // 当给data属性中设置值的适合 更改获取的属性的值
if(newValue!=value){
// 这里的this不是实例
that.observe(newValue);// 如果是设置的是对象继续劫持
value = newValue;
}
}
});
}
3.view观察者(watcher)
watcher在拥有data挂载的模板编译阶段实例化一次,确定回调更新函数,使view成为model更新的订阅者;watcher在内部的构造函数内将自身与dep关联并加入dep.subs队列
4.model发布主体(dep)
数据observer化时,在get方法内将关联watcher加入队列;在set方法发布订阅
5.数据代理(proxy)
将vm.data.foo代理到vm.foo
6.监听dom事件
在编译阶段查看是否涉及数据绑定事件监听,如v-model等,并设置回调函数更新vm.data
7.diff更新view(watcher执行订阅更新之后)
以上参考:
https://juejin.im/post/5af8eb55f265da0b814ba766
一种简化版MVVM的实现
实现一个mvvm(基于Proxy)
参照vue2.x(上例)的响应式设计模式,将数据劫持部分的Obejct.defineProperty替换为Proxy即可,其他部分,如compile,watcher,dep,事件监听等基本保持不变,简单实现代码如下:
class Watcher{
constructor(cb){
this.cb = cb;
}
update(){
this.cb()
}
}
class Dep{
constructor(){
this.subs = [];
}
publish(){
this.subs.forEach((item)=>{
item.update && item.update();
})
}
}
class MVVM{
constructor(data){
let that = this;
this.dep = new Dep();
this.data = new Proxy(data,{
get(obj, key, prox){
that.dep.target && that.dep.subs.push(that.dep.target);
return obj[key]
},
set(obj, key, value, prox){
obj[key] = value;
that.dep.publish();
return true;
}
})
this.compile();
}
compile(){
let divWatcher = new Watcher(()=>{
this.compileUtils().div();
})
this.dep.target = divWatcher;
this.compileUtils().div();
this.dep.target = null;
let inputWatcher = new Watcher(()=>{
this.compileUtils().input();
})
this.dep.target = inputWatcher;
this.compileUtils().input();
this.compileUtils().addListener();
this.dep.target = null;
}
compileUtils(){
let that = this;
return {
div(){
document.getElementById('foo').innerHTML = that.data.foo;
},
input(){
document.getElementById('bar').value = that.data.bar;
},
addListener(){
document.getElementById('bar').addEventListener('input', function(){
that.data.bar = this.value;
})
}
}
}
}
let mvvm = new MVVM({foo: 'foo233', bar: 'bar233'})
通过mvvm.data.foo
或者mvvm.data.bar
可以操作数据,可以观察到view做出了反应;在输入框改变输入值,也可以通过mvvm.data
观察到数据被触发改变
vue的事件处理机制
- 普通html元素和在组件上挂了
.native
修饰符的事件。最终EventTarget.addEventListener()
挂载事件
普通元素在compile阶段被解析为ast语法树,包含该元素监听的事件,事件在render阶段由add$1通过addEventListener挂载到真实元素上 - 组件上的,vue组件实例上的自定义事件(不包括
.native
)会调用原型上的$on,$emit
(包括一些其他api$off,$once
等等)
组件在编译阶段data.on被替换为.native事件,data.on与普通hml元素做一致处理;自定义事件被缓存为listener,利用实例$on
方法放入观察者队列,子组件调用$emit
方法可发布订阅
参考:
vue源码解析-事件机制
Compile - 源码版 之 generate 拼接绑定的事件
vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?
数组的方法:forEach,map, filter,some, reduce
1.reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
arr.reduce(callback(accumulator, currentValue[, index[, sourceArray]])[, initialValue])
2.filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
3.some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个Boolean类型的值。
arr.some(callback(element[, index[, array]])[, thisArg])
4.forEach() 方法对数组的每个元素执行一次提供的函数。没有办法中止或者跳出 forEach() 循环,除了抛出一个异常.
arr.forEach(callback(element[, index[, array]])[, thisArg]);
5.map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
virtual dom vs mvvm vs 原生dom
参考:
网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么
Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法
IIFE下函数名为不可变量
以下代码输出b函数
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
因为iife的函数表达式函数名为不可变量,在 strict 模式下会报错,非 strict 模式下静默失败
参考:
js的作用域问题?
下面的代码打印什么内容,为什么?
IIFE下的变量提升
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()
以上代码输出:
undefined -> 10 -> 20,因为IIFE作用域内a在后面声明时被提升了
如果去掉 var a = 20,则打印10->5->5,因为IIFE内部取到的是外部定义的变量a
浏览器的缓存机制
1.强制缓存
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。Cache-Control优先级高于Expires,Expires是http1.0的产物,Cache-Control是http1.1的产物
2.协商缓存
协商缓存可以在response header中添加 Last-Modified和Etag实现。Etag优先级高于 Last-Modified,Etag精度较好,Last-Modified性能较好
3.强缓存优先级高于协商缓存
4.用户行为对浏览器缓存的影响
- 地址栏输入url,检查是否有disk cache 可用
- 刷新页面,先检查memory cache,再检查disk cache
- 强制刷新 (Ctrl + F5),发送的请求头部均带有 Cache-control: no-cache,不使用缓存
参考:
深入理解浏览器的缓存机制
BFC(Block Formatting Contexts
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响
1.触发BFC的条件
- html 根元素
- float 浮动
- 绝对定位
- overflow 不为 visible
- display 为表格布局或者弹性布局
2.BFC的作用 - 清除浮动
- 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
- 实现文字环绕或左图右文的布局
参考:
介绍下 BFC 及其应用
https://github.com/glitchboyl/blog/issues/6
vue如何限制子组件不能直接修改props?
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/60
响应式原理:Object.defineProperty VS Proxy
1.Object.defineProperty不能检测数组下标变动,而Proxy可以代理整个数据
2.Object.defineProperty代理对象时需要遍历对象的每个属性(嵌套对象需要递归遍历),Proxy可以直接代理整个对象
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/90
div水平垂直居中
1.定宽高:
- 绝对定位+margin auto
- 绝对定位+transform
2.不定宽高:
- 绝对定位+transform
优先级与对象赋值
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
以上代码输出:
undefined->{n:2}
array like
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
以上代码输出:(push时array like对象length +1)
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]
判断类数组的方法
- 存在且是对象
- 对象上的splice 属性是函数类型
- 对象上有 length 属性且为正整数
为什么数据埋点时通常使用1x1 像素的透明 gif 图片?
- 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/87
实现LazyMan类
实现以下类:
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
关键点:
- 类原型方法返回实例
- 维护任务队列
- 利用setTimeout 0,实现队列任务全部添加完之后再执行任务
代码:
/*
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
*/
/*
实例属性 name
实例队列 queue
原型方法 eat, sleep, sleepFirst, doSth
sleep, sleepFirst 延迟x秒执行
sleepFirst 优先入队列
doSth 执行队列方法
*/
class LazyManClass {
constructor(name) {
this.name = name;
this.queue = [];
this.sayName();
setTimeout(() => {
this.doSth();
}, 0);
}
sayName() {
console.log(`Hi I am ${this.name}`);
}
eat(food) {
this.queue.push({ action: "doEat", sth: food });
return this;
}
doEat(food) {
console.log(`I am eating ${food}`);
}
sleep(time) {
this.queue.push({ action: "doSleep", sth: time });
return this;
}
sleepFirst(time) {
this.queue.unshift({ action: "doSleep", sth: time });
return this;
}
doSleep(time) {
setTimeout(() => {
console.log(`${time} sencond later...`);
this.doSth();
}, time * 1000);
}
doSth() {
while (this.queue.length) {
const { action, sth } = this.queue.shift();
this[action](sth);
if (action === "doSleep") {
break;
}
}
}
}
const LazyMan = function (name) {
return new LazyManClass(name);
};
export default LazyMan;
箭头函数与普通函数的区别
箭头函数没有自己的this,没有arguments,没有prototype,因此不能作为构造函数使用
如何实现token加密
- 需要一个secret(随机数)
- 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端
- 前端每次request在header中带上token
- 后端用同样的算法解密
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/106
如何设计实现无缝轮播
1.关键点:
- 元素处理:轮播周期结束后,将第一个li元素clone到尾部,然后移除第一个元素
- 利用requestAnimationFrame实现动画效果,在动画循环中通过时间判断位移,在轮播周期内重置开始时间
2.代码实现
const carousel = function(id, speed=1000){
this.speed = speed;
this.root = document.getElementById(id);
this.startTime = new Date();
this.req = null;
this.begin();
}
carousel.prototype = {
begin(){
this.render();
let timer = setInterval(()=>{
// cancelAnimationFrame(this.req);
// clearInterval(timer);
let lists = this.root.getElementsByTagName('li');
let length = lists.length;
let first = lists[0];
this.root.appendChild(first.cloneNode(true));
this.root.removeChild(first);
this.root.style.left = '0px';
this.startTime = new Date();
// this.render();
}, this.speed)
},
render(){
this.root.style.left = -(new Date() - this.startTime)/this.speed*100 + 'px'
this.req = requestAnimationFrame(this.render.bind(this));
}
}
let carouselIns = new carousel("carousel")
requestAnimationFrame
1.概念:
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
2.兼容:
window.requestAnimFrame = (function() {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
window.setTimeout(callback, 1000 / 60)
}
)
})()
3.优势:
- 浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果
- 一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力
ES6 代码转成 ES5 代码的实现思路
- 解析:解析代码字符串,生成 AST;
- 转换:按一定的规则转换、修改 AST;
- 生成:将修改后的 AST 转换成普通代码。
HMR原理
关键点
- webpack watch文件更新
- webpack-dev-server通过sockjs与浏览器建立长连接
- HMR runtime接收到更新信息后通过ajax获取文件,并对比更新
参考:
Webpack HMR 原理解析
介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面
BFC、IFC、GFC、FFC
input处理中文输入
使用compositionstart + compositionend判断,如过程不想触发input事件则加boolean值判断
currying与累加器
// 实现一个函数
add(1); // 1
add(1)(2); // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
currying版本
注意:该版本只实现了固定长度的参数调用
关键点:
- 判断待传入参数大于等于函数执行所需的参数长度,再触发执行
- 利用递归接收所有参数,传入
fn.bind(this, ...args)
保存所有参数
const originAdd = (x,y,z)=>{
return x+y+z
}
const currying = function(fn, length){
length = length || fn.length
return function(...args){
return args.length >= length ? fn.apply(this, args) : currying(fn.bind(this, ...args), length - args.length)
}
}
const add = currying(originAdd)
累加器版本
注意:该版本使用了toString方法,该方案在新版本的Chrome中似乎不起作用,待验证
关键点
- 维护闭包变量存储变量值
- 利用打印函数值调用toString方法,重写toString将所有参数累加
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
参考:
深入高阶函数应用之柯里化
请实现一个 add 函数,满足以下功能
详解JS函数柯里化
react-router 里的 Link 标签和 a 标签有什么区别
- link由react-router代理了a的默认事件,用event.preventDefault()阻止默认事件
- link点击事件查找是否有to或者replace,使用history替换
- 其他渲染行为
HTML5 history 和 hash模式
list转tree
以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:
// 原始 list 如下
let list =[
{id:1,name:'部门A',parentId:0},
{id:2,name:'部门B',parentId:0},
{id:3,name:'部门C',parentId:1},
{id:4,name:'部门D',parentId:1},
{id:5,name:'部门E',parentId:2},
{id:6,name:'部门F',parentId:3},
{id:7,name:'部门G',parentId:2},
{id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);
实现如下:
const convert = (list)=>{
let rs = [], listMap = {}, parent;
for(let i=0;i<list.length;i++){
let temp = list[i];
listMap[temp.id] = temp;
}
list.forEach((temp)=>{
if(temp.parentId === 0){
rs.push(temp);
}else{
parent = listMap[temp.parentId];
!parent.children && (parent.children = []);
parent.children.push(temp)
}
})
return rs;
}
let list = [
{id:3,name:'部门C',parentId:1},
{id:4,name:'部门D',parentId:1},
{id:5,name:'部门E',parentId:2},
{id:6,name:'部门F',parentId:3},
{id:7,name:'部门G',parentId:2},
{id:8,name:'部门H',parentId:4},
{id:1,name:'部门A',parentId:0},
{id:2,name:'部门B',parentId:0},
];
const result = convert(list);
console.log(result)
实现Promise相关类方法
Promise._all = function (promiseList) {
return new Promise((resolve, reject) => {
const totalCount = promiseList.length;
let count = 0;
let rs = [];
for (const [index, promiseItem] of promiseList.entries()) {
promiseItem.then((r) => {
count++;
rs[index] = r;
if (count === totalCount) {
resolve(rs);
}
}).catch(err=>{
reject(err)
})
}
});
};
Promise._race = function (promiseList) {
return new Promise((resolve, reject) => {
for (const promiseItem of promiseList) {
promiseItem.then((r) => {
resolve(r);
}).catch((err) => {
reject(err);
});
}
});
};
Promise._any = function (promiseList) {
return new Promise((resolve, reject) => {
const totalCount = promiseList.length;
let count = 0;
for (const promiseItem of promiseList) {
promiseItem.then((r) => {
resolve(r);
}).catch(err=>{
count++
if(count === totalCount){
reject('AggregateError: All promises were rejected')
}
})
}
});
};
Promise._allSettled = function (promiseList) {
return new Promise((resolve) => {
const totalCount = promiseList.length;
let count = 0;
let rs = [];
for (const [index, promiseItem] of promiseList.entries()) {
promiseItem
.then((r) => {
count++;
rs[index] = { status: "fulfilled", value: r };
if (count === totalCount) {
resolve(rs);
}
})
.catch((err) => {
count++;
rs[index] = { status: "rejected", reason: err };
if (count === totalCount) {
resolve(rs);
}
});
}
});
};
// Promise.finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
时间复杂度为O(log(m+n))的取中位数算法
返回整数逆序后的字符串
解法一
关键点
转字符串,二分,前后部分交换,递归
const reverseOrder = (num)=>{
let numStr = num.toString();
let length = numStr.length;
if(length == 1){
return numStr;
}else{
return length%2 === 0 ?
reverseOrder(numStr.slice(length/2,length)) + reverseOrder(numStr.slice(0,length/2)) :
reverseOrder(numStr.slice((length+1)/2,length)) + numStr[(length-1)/2] + reverseOrder(numStr.slice(0,(length-1)/2))
}
}
console.log(reverseOrder(12345678))
解法二
function fun(num){
let num1 = num / 10;
let num2 = num % 10;
if(num1<1){
return num;
}else{
num1 = Math.floor(num1)
return `${num2}${fun(num1)}`
}
}
vuex数据无法刷新
两个维度:
1.在生命周期钩子函数内将store.state.foo赋值给data,data无法刷新
解决:将store.state.foo绑定到计算属性
2.vuex无法持久化:
结合local/session storage,在set数据时写入缓存;在store.js读取缓存
vue mpa
- entry配置为多属性对象(或数组)
- 配置HtmlWebpackPlugin
参考:
vue-cli配置多入口多出口,实现一个项目两个访问地址,区分不同上线环境
搭建 vue2 vue-router2 webpack3 多入口工程
react项目优化
动态加载,treeshaking,exclude/include,state合并及合理管理,使用key,shouldComponentUpdate,PureComponent,事件非内联,虚拟列表
参考:
https://juejin.cn/post/6908895801116721160
前端工程化
- 技术选型
- 统一规范
- 测试
- 部署
- 监控
- 性能优化
- 重构
参考:
入门前端工程化
重排(reflow)和重绘(repaint)
页面生成的过程:
1 .HTML 被 HTML 解析器解析成 DOM 树;
2. CSS 被 CSS 解析器解析成 CSSOM 树;
3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
4. 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
5. 将布局绘制(paint)在屏幕上,显示出整个页面。
第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染。
重排(reflow):
当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
重排也叫回流,简单的说就是重新生成布局,重新排列元素。
重绘(Repaints):
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
重排优化建议:
- 减少重排范围
- 减少重排次数
2.1.样式集中改变
2.2.分离读写操作
2.3.将 DOM 离线
2.4.使用 absolute 或 fixed 脱离文档流
2.5.优化动画
堆和栈
参考:
深入理解js数据类型与堆栈内存
React Hooks 原理
参考:
React Hooks 原理
使用CSS实现各种形状
参考:
用 css 画三角形、梯形、扇形、箭头和椭圆几种基本形状
CSS 实现半圆环的两种方式
<script>标签,async和defer属性
<script>
标签用于引入js脚本文件,默认为同步加载和按顺序执行,会阻塞
HTML解析,async
和defer
可以优化<script>
的加载和执行行为
css link 和 @import
- link:浏览器会派发一个新的http线程加载资源文件,同时GUI渲染线程会继续向下渲染
- @import:GUI渲染线程会暂停止渲染,去服务器加载资源,资源文件没有返回前不会继续渲染
参考:
关于css @import url()总结
你真的理解@import和link引入样式的区别吗
浏览器输入URL之后发生了什么?
- 解析URL
- DNS域名解析
2.1. 递归查询
2.2. 迭代查询
2.3. DNS负载均衡 - TCP/IP协议
3.1. 三次握手
3.2.四次挥手 - 发送HTTP请求
请求行+请求头+请求内容 - 浏览器缓存
4.1. 强缓存
4.2. 协商缓存
4.3. 储存位置 - HTTPS
- 服务器处理请求并返回HTTP报文
响应行+响应头+响应内容 - 浏览器解析渲染页面