金三银四招聘季,身边很多朋友也在各种面试。就着大家的反馈,总结一些前端在面试中遇到的问题。
1.javascript的垃圾回收机制
有2种,标记清除和引用计数。
- 标记清除:从根出发,遍历本地函数内局部变量、全局变量、调用链上的函数的变量和参数等给所有的引用进行标记,并级联标记子孙引用。最后,所有未被标记的变量等都将被清除。
- 引用计数: 很好理解,每个变量都会有一个计数,用于统计被引用次数。在注销或者删除一个引用时计数会减1,在绑定引用时则加1。每到一个垃圾回收周期会将所有引用计数为0的变量清除。
2.JavaScript的事件循环
js的异步任务有两种:
- 宏任务:setTimeout, setInterval, I/O 等。
- 微任务:Promise,process.nextTick(node独有)
其它为同步任务,同步任务会立即执行。 PS:new Promise((resolve,reject)=>{console.log(‘hello’)})为同步任务。
执行顺序:
1)执行完所有的同步任务后,此时执行栈为空。
2)执行所有微任务栈中的任务,执行期间新的微任务也会在此过程中执行
3)从宏任务中去一个任务到执行栈
4)回到第1)步
3.import和require有哪些区别
老问题,不展开写了。
require野生,import标准
require可以动态,import静态
import有default,require没有
兼容性不同
require有很多实践,比如amd/cmd/umd
4.webpack的loader和plugin有什么区别
loader即为文件加载器,操作的是文件,将文件A通过loader转换成文件B,是一个单纯的文件转化过程。
plugin即为插件,是一个扩展器,丰富webpack本身,增强功能 ,针对的是在loader结束之后,webpack打包的整个过程,他并不直接操作文件,而是基于事件机制工作,监听webpack打包过程中的某些节点,执行广泛的任务。
5.怎么写一个webpack plugin
官网:https://webpack.js.org/contribute/writing-a-plugin/
文章:https://segmentfault.com/a/1190000019010101
几代webpack,plugin的写法基本没有变化,主要就是通过webpack提供的hook,在合适的时刻做一些自定义的事情。
- 编写一个JavaScript命名函数。
- 在它的原型上定义一个apply方法。
- 指定挂载的webpack事件钩子。
- 处理webpack内部实例的特定数据。
- 功能完成后调用webpack提供的回调。
编写插件之前要理解compiler和compilation两个对象。
webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
compiler负责编译, compilation负责创建bundles,二者都是Tapable的实例。
Tapable暴露出挂载plugin的方法,使我们能 将plugin控制在webapack事件流上运行。
apply属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。通过Function.prototype.apply方法,你可以把任意函数作为插件传递(this 将指向compiler)。我们可以在配置中使用这样的方式来内联自定义插件。
// tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
// 实例化一个钩子
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
// 绑定事件到webapck事件流 apAsync/tapPromise/tap
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
// 执行绑定的事件 callAsync/promise
hook1.call(1,2,3)
6.babel插件怎么编写
babel的本质是操作AST(abstract syntax tree)来完成代码的转译。
所以说babel其实是一个编译器。而编译器的工作过程无非3个部分:
- parse:把源代码转换成抽象的表示,比如AST
- transform:根据编译器的功能对AST做一些特殊处理
- generate:将第二步经过transform的AST生成新的代码。
所以,写babel插件,就是在这些过程中,主要是transform过程中做一些自定义的操作,即操作AST。
AST由一个又一个的节点构成,操作AST就是操作这些节点,我们可以对这些节点进行增删改等操作。
一个常见的identifier节点:
{
type:'Identifier',
name:'btn'
}
更多的节点规范见: https//github.com/estree/estree
AST是一棵树,对节点进行操作,自然需要遍历。不过这里我们不需要自己写遍历。只需要通过Babel提供的Visitor对象来进行操作即可。Visitor上挂载所有以type命名的方法,Babel会遍历AST,节点会根据自己的type进入不同的方法执行。比如箭头函数的type为ArrowFunction。那么Babel解析的代码可能是:
const visitor = {
ArrowFunction(path){
path.replaceWith(t.FunctionDeclaration(id.params,body))
}
}
所以,将源代码和目标代码都解析成AST,观察它们,找找看如何增删改AST可以达到目的。
可以在这里完成此工作。
7.浏览器渲染过程
浏览器拿到HTML之后的渲染过程:(不同内核实现不一样,但大体差不多)
- 解析HTML,构建DOM tree。
- 解析CSS,构建CSSOM tree。
- 合并DOM tree和CSSOM tree,生成render tree。
- 布局(layout/reflow),计算各元素尺寸、位置。
- 绘制(paint/repaint),绘制页面像素信息。
- 浏览器将各层的信息发送给GPU,GPU将各层合成,显示在屏幕上。
当修改了DOM或CSSOM,上述过程中的一些步骤就会重复执行。
构建OM:要经过Bytes → characters → tokens → nodes → object model这个过程。
TIPS:
解析HTML遇到外部CSS立即请求 —-CSS文件合并,减少HTTP请求;
新的CSS style修改CSSOM,会重新渲染页面 —-CSS文件应放在头部,缩短首次渲染时间
遇到<img>
会发出请求,但不会阻塞,服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;(最好图片都设置尺寸,避免重新渲染)
遇到<script>
标签,会立即执行js代码,阻塞渲染。(script最好放置页面最下面)
js修改DOM会重新渲染。 (页面初始化样式不要使用js控制)
8.回流reflow和重绘repaint
回流reflow
当某个部分发生了变化影响了布局,需要倒回去重新渲染, 该过程称为reflow(回流)。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。
改变窗囗大小,改变文字大小,添加/删除样式表,内容的改变,如用户在输入框中敲字,激活伪类,如:hover (IE里是一个兄弟结点的伪类被激活),操作class属性,脚本操作DOM,计算offsetWidth和offsetHeight,设置style属性都会导致回流发生。
repaint重绘
如果只是改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,将只会引起浏览器 repaint(重绘)。repaint 的速度明显快于 reflow(在IE下需要换一下说法,reflow 要比 repaint 更缓慢)。
reflow一定引起repaint,而repaint不一定要reflow。reflow的成本比repaint高很多,DOM tree里每个结点的reflow很可能触发其子结点、祖先结点、兄弟结点的reflow。reflow(回流)是导致DOM脚本执行低效的关键因素之一。
现代浏览器会对回流做优化,它会等到足够数量的变化发生,再做一次批处理回流。
优化,尽量避免reflow:
优化回流:
尽可能限制reflow的影响范围,修改DOM层级较低的结点。不要通过父级元素影响子元素样式。最好直接加在子元素上。改变子元素样式尽可能不要影响父元素和兄弟元素的尺寸。
不要一条一条的修改DOM的style,最好通过设置class的方式。 避免触发多次reflow和repaint。
经常reflow的元素,比如动画,position设为fixed或absolute,使其脱离文档流,不影响其它元素的布局。
权衡速度的平滑。比如实现一个动画,以1个像素为单位移动这样最平滑,但reflow就会过于频繁,CPU很快就会被完全占用。如果以3个像素为单位移动就会好很多。
不要用tables布局。tables中某个元素一旦触发reflow就会导致table里所有的其它元素reflow。在适合用table的场合,可以设置table-layout为auto或fixed,这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
避免使用css expression(每次都会重新计算)。
减少不必要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会导致所有层级的改变,上至根部,下至被改变节点的子节点。这导致大量时间耗费在执行 reflow 上面。
避免不必要的复杂的 CSS 选择器,尤其是后代选择器(descendant selectors),因为为了匹配选择器将耗费更多的 CPU。
尽量不要频繁的增加、修改、删除元素,可以先把DOM节点抽离到内存中进行复杂的操作然后再display到页面上。(display:none的节点不会被加入render tree,而visibility:hidden会;display:none会触发reflow,而visibility:hidden只会触发repaint,因为layout没有变化)。
让要进行复杂操作的元素进行“离线处理”,处理完后一起更新。比如使用DocumentFragment,DocumentFragment节点不属于文档树,继承的parentNode属性总是null。即将元素添加到DocumentFragment中,再将DocumentFragment添加到页面
使用display:none,先隐藏后显示,只会引起两次reflow和repaint。因display:none的元素不在render tree,对其操作不会引起其他元素的reflow和repaint。
使用cloneNode和replaceChild,引发一次reflow和repaint。
9.设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
这个题主要利用js的Map对象在迭代时会根据对象中元素的插入顺序来进行的特点。因此不需要我们自己记录每个key插入和被使用的时间。
// 新添加的元素会被插入到map的末尾,整个栈倒序查看
class LRUCache {
constructor(capacity) {
this.secretKey = new Map();
this.capacity = capacity;
}
get(key) {
if (this.secretKey.has(key)) {
let tempValue = this.secretKey.get(key);
this.secretKey.delete(key);
this.secretKey.set(key, tempValue);
return tempValue;
}
else return -1;
}
put(key, value) {
// key存在,仅修改值
if (this.secretKey.has(key)) {
this.secretKey.delete(key);
this.secretKey.set(key, value);
}
// key不存在,cache未满
else if(this.secretKey.size<this.capacity){
this.secretKey.set(key, value);
}
// 添加新key,删除旧key
else{
this.secretKey.set(key,value);
// 删除map的第一个元素,即为最长未使用的
this.secretKey.delete(this.secretKey.keys().next().value);
}
}
}
10.防抖和节流函数
//防抖
function deVabrint(func, interval = 100) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, interval);
};
}
//节流
function bottoleFunc(func, interval) {
let start = new Date() - 0;
return function () {
let now = new Date() - 0;
if (now - start > interval) {
func.apply(this, arguments);
start = new Date() - 0;
}
};
}
11.grpc的优缺点
RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。如下图所示就是一个典型的RPC结构图。
gRPC vs. Restful API
gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:
- gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
- 另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
- gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)
使用场景
- 需要对接口进行严格约束的情况
- 对于性能有更高的要求时。
但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。
12.http2.0的相关特性
HTTP/2 的首要目标是通过完全的请求,响应多路复用,头部的压缩头部域来减小头部的体积,添加了请求优先级,服务端推送.
为了支持这些特性,他需要大量的协议增加头部字段来支持,例如新的流量控制,差错处理,升级机制.而这些是每个web开发者都应该在他们的应用中用到的.
https://www.cnblogs.com/yixiaogo/p/11932966.html
二进制帧层:它指HTTP消息在客户端和服务端如何封装和传输.
流,消息,帧
流:已经建立的连接之间双向流动的字节,它能携带一个至多个消息。
消息:一个完整的帧序列,它映射到逻辑的请求和响应消息。
帧:在HTTP/2通信的最小单元。每个桢包括一个帧头,里面有个很小标志,来区别是属于哪个流。请求和响应的多路复用:在HTTP/2中,新的二进制帧层,解除了这个限制.使得所有的请求和响应多路复用.通过允许客户端和服务端把HTTP消息分解成独立的帧,交错传输,然后在另一端组装.
流的优先级:为了能方便流的传输顺序,HTTP/2.0提出,使每个流都有一个权重(1-256)和依赖.
每个源一个连接
流量控制
服务端推送:服务器为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应之外,服务器还可以向客户端推送额外的资源(图12-5),而不需要客户端明确请求每一个资源!
头部压缩
13.viewport 和移动端布局方案
TODO
14.vue的依赖收集原理
早期使用Object.defineProperty实现,后改造成Proxy,但原理相同。
主要基于Dep.target,Dep和dep.addSub
首先会遍历整个state数据结构,给每个数据都加上一个get监控和set监控,在某个数据被get的时候就会进行依赖收集。
function defineReactive(obj, key) {
var dep = new Dep();
var val = obj[key]
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
// 收集依赖
dep.addSub(Dep.target)
}
return val
}
});
}
Dep.target会永远指向当前正被解析的watcher,watch的watcher,页面的watcher等等。简单想,指向哪个watcher,那么就是那个 watcher 正在使用数据,数据就要收集这个watcher。
Dep 是一个构造函数,用于创建实例,并带有很多方法
于是,收集流程大概是这样
1、页面的渲染函数执行, name 被读取
2、触发 name的 Object.defineProperty.get 方法
3、于是,页面的 watcher 就会被收集到 name 专属的闭包dep 的 subs 中
PS:
基础数据类型,只使用 【闭包dep】 来存储依赖
引用数据类型,使用 【闭包dep】 和 【 ob.dep】 两种来存储依赖
15.怎么给一个购物车做架构设计
16.手写一个Promise
两个版本
简单版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
进阶版:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let that = this; // 缓存当前promise实例对象
that.status = PENDING; // 初始状态
that.value = undefined; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数
function resolve(value) { // value成功态时接收的终值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用resolve 回调对应onFulfilled函数
if (that.status === PENDING) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
function reject(reason) { // reason失败态时接收的拒因
setTimeout(() => {
// 调用reject 回调对应onRejected函数
if (that.status === PENDING) {
// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}
// 捕获在excutor执行器中抛出的异常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function" ? onRejected : reason => {
throw reason;
};
if (that.status === FULFILLED) { // 成功态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try{
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
} catch(e) {
reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
}
});
})
}
if (that.status === REJECTED) { // 失败态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
if (that.status === PENDING) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
that.onRejectedCallbacks.push((reason) => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
};
JavaScript的new 做了什么
比如 const p = new Persion('xxx')
1.添加一个{}对象,将函数的this指向这个对象。然后将对象返回。
2.将Person函数prototype原型也指向这个对象的proto。 即 p.__proto = Person.prototype
。
react 内部如何识别 class 组件和 function 组件
直接区分是不是class是不行的,因为在被babel等工具转译之后二者都是function。
好像可以使用原型机制。 如果所有的class都继承自React.Component,通过 XXX.prototype instanceof React.Component
可以达到效果。
但有时候我们检查的组件可能是继承至别的React组件的React.Component副本。这个时候instanceof就抓瞎了。
所以,react在React.Component的prototype上加了一个属性:isReactComponent
:
class Component {}
Component.prototype.isReactComponent = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes
React 事件机制
- 当我们在组件上设置事件处理器时,React并不会在改DOM元素上直接绑定事件处理器,而是在react内部自定义一套事件系统,在这个系统上进行统一的事件订阅和分发
- react利用事件委托机制在Document上统一监听DOM事件,在根据触发的target将事件分发到具体的组件实例,实际我们在事件里面拿到的event其实并不是原始的DOM事件对象,而是一个合成事件对象
为什么需要
1 .抹平浏览器之间的兼容性差异,react还会通过其他事件来模拟一些低版本不兼容的事件
2 .事件合成,自定义高级事件,比如onChange事件,为表单元素定义了统一的值来变动事件
3 .React打算更多优化。比如利用事件委托,大部分事件最总绑定了Document,而不是dom节点本身,这样简化了dom事件处理逻辑,减少了内存的开销。react自己实现了一套模拟事件冒泡的机制
4 .react干预了事件的分发。Fiber架构,优化了用户的交互体验,干预事件的分发,不同的事件有不同的优先级。高的优先级事件可以中断渲染,让用户代码即使响应用户交互
https://www.jianshu.com/p/440f0fd43c8f
css扇形(一个元素)
.fan{
border-radius:50%;
border:100px solid transparent;
width:0;
border-top-color: red;
}
Sleep 函数(手写)
- 用while循环,强阻断
- setTimeout 回调
- Promise + setTimeout + async/await
es5 实现 es6 extend
TODO
二叉树所有根到叶子路径组成的数字之和
略。
Form JSON schema
TODO
怎么实现动画,js动画的缺陷
TODO
未完待续…
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!