金三银四招聘季,身边很多朋友也在各种面试。就着大家的反馈,总结一些前端在面试中遇到的问题。

1.javascript的垃圾回收机制

有2种,标记清除和引用计数。

  1. 标记清除:从根出发,遍历本地函数内局部变量、全局变量、调用链上的函数的变量和参数等给所有的引用进行标记,并级联标记子孙引用。最后,所有未被标记的变量等都将被清除。
  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,在合适的时刻做一些自定义的事情。

  1. 编写一个JavaScript命名函数。
  2. 在它的原型上定义一个apply方法。
  3. 指定挂载的webpack事件钩子。
  4. 处理webpack内部实例的特定数据。
  5. 功能完成后调用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之后的渲染过程:(不同内核实现不一样,但大体差不多)

  1. 解析HTML,构建DOM tree。
  2. 解析CSS,构建CSSOM tree。
  3. 合并DOM tree和CSSOM tree,生成render tree。
  4. 布局(layout/reflow),计算各元素尺寸、位置。
  5. 绘制(paint/repaint),绘制页面像素信息。
  6. 浏览器将各层的信息发送给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.怎么给一个购物车做架构设计

https://www.v2ex.com/t/669861

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 事件机制

  1. 当我们在组件上设置事件处理器时,React并不会在改DOM元素上直接绑定事件处理器,而是在react内部自定义一套事件系统,在这个系统上进行统一的事件订阅和分发
  2. 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 函数(手写)

  1. 用while循环,强阻断
  2. setTimeout 回调
  3. Promise + setTimeout + async/await

es5 实现 es6 extend

TODO

二叉树所有根到叶子路径组成的数字之和

略。

Form JSON schema

TODO

怎么实现动画,js动画的缺陷

TODO

未完待续…


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

袁老的宿舍 上一篇
常用的git命令介绍和简单的git工作流 下一篇