2021年前端面经
目前本人已经有了新的起点,之前在博客的求职贴也就作废了,这是我第一次写面经,写这个面经的目的希望大家能了解:
- 不要当作面试题背诵,因为题解仅仅是思路你会发现我的题解都比较浅甚至是有很多错误。
- 不要拿此题去面试其他人,因为好的面试官我认为不应该是行走的题库,应该考虑候选人是否符合团队技术栈和用人需求,不应该拘泥于业务题目,我认为多问问项目比问100道题解要有用的多。
- 分享出来也是记录自己的心路历程,希望大家对西安目前的招聘要求有一个大概的了解,如果你觉得我的题解有哪些错误,那就来打我吧(写评论区,我们探讨一下,我会改正的呢)
- 题目有些是我面试的时候必问的,有些是来自于沃趣,腾讯云,狼人杀团队的面试题,目前题主所在的公司问的题目不属于以下范畴,仅仅当作学习交流使用。
- 有些题目的题解我的博客也有文章(比如vue3和vite),所以大家可以去博客中看一下~
在mark一下我的博客和我现在在做的开源项目:
因卓诶-爱分享爱原创的技术博客 ~ 个人博客
最近在做的项目:
swordCodePractice/InternetQuestionBank
项目名字叫:剑指题解,业务题库app,帮助程序圈子的小伙伴们利用碎片化时间进行自我提升的小程序。
JS
事件循环模型,同异步,阻塞非阻塞,消息队列,调用栈,GUI渲染
JS虽然是一个单线程(主线程)的语言,但是它有很多工作线程,比如Ajax,定时器等等。那么同步和异步在分布式系统中来讲就是一种消息通讯机制罢了,关注的是是否能立即返回结果。它不能和阻塞和非阻塞概念混为一谈。阻塞指的是等待结果返回之前影响接下来的任务,非阻塞是不影响。当JS引擎执行异步任务的时候,会把这个任务注册成一个函数,通过事件循环模型通知对应的工作线程(比如Ajax),然后主线程会继续执行当下的任务,等到工作线程完成了任务,事件循环模型会将这个异步任务的回调放到消息队列中,当主线程的调用栈为空(有空闲了)就执行消息队列最前面的消息。那么主线程从消息队列中去取任务和执行任务回调的过程就是事件循环模型的特点。
HTML到DOM是由GUI线程控制的,GUI线程和JS线程是互斥的,GUI线程会在JS的调用栈为空的情况下才会执行,因此当JS中有大量的逻辑需要处理是会影响到GUI渲染的。
闭包,作用域(词法环境)
当我们在js创建一个函数的时候,会创建一个闭包,这个闭包像是一个气泡一样,包含了创建时作用域中的变量,闭包不会随着作用域的销毁而无效,闭包保存在内存中,我们可以任何时候可以访问到闭包中的变量(除非js认定它要销毁了才会销毁),我们可以使用闭包来做很多事情,比如实现私有变量,回调函数,包括实现柯里化;关于作用域,JS中有全局作用域和函数作用域以及块级作用域,其中全局作用域和函数作用域不难理解,就是直接写在window和脚本顶级位置的代码都处于全局,函数作用域就是在函数声明中的代码处于函数作用域,而块级作用域是es6新增的let const声明的代码(或者with语句以及trycatch),只能在{}这样的块级作用域中使用。
临时性锁区 TDZ
TDZ不仅仅是日常熟知的let,const情况下可能造成的问题,我们在很多地方都可能会出现TDZ。我们先来理解TDZ是什么:我们拿let和const举例子,我们在let声明之前去使用变量会得到ReferenceError这样的错误,而var不会报这样的错。因为es6规定,let声明的变量在他们的词法环境被实例化时候才会被创建,但是当它们词法绑定的时候才可以被访问(词法绑定换句话来说,没有对变量进行运算);如果在词法绑定之前就访问就会报错;那么从词法环境实例化之后到词法绑定完成这中间的过程就叫做TDZ临时锁区
变量提升是JS的变量最基本的特性,不管是let,const,var都有提升特性。let和const在词法绑定(运算)中,如果未赋值,则就是undefined;那么TDZ其他表现,会在typeof,包括函数参数预设值等场景出现。那么TDZ的作用就是让我们能够用正常的数据流去编写代码,所以我们在写代码应该要注意变量声明顺序,还有避免多个变量是同名情况。这些开发恶习是我们不提倡的。
原型链,原型
JS中的原型链是数据结构中链表的最佳体现,JS中的每个对象都有一个__proto__属性,JS中的函数中有一个原型属性prototype。
function foo(){};
foo.prototype.test = "this is test";
// a是foo的实例,a是一个对象,它有一个隐式原型指向了构造函数的原型对象
let a = new foo();
a.test; // this is test 隐式原型可以访问构造函数上的原型方法
Function也是对象,对象的隐式原型是什么呢?隐式原型指向的都是构造函数的原型,Funtion的构造函数还是Function,那么它的隐式原型就是Funtion.prototype;
Funtion.prototype中的__proto__指向了Object,Object的原型指向了Null,按照规定Null没有原型对象,所以Null是所有原型链的最终点,这个链表我们就称之为为原型链。当对象自身找不到属性的时候会去原型链上的每一个原型去找或者返回null;
总结:
1.对象有属性__proto__,指向该对象的构造函数的原型对象。
2.方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。
函数
函数是JS的第一等公民,函数的大部分核心内容都在以上的章节提到过,那么这个part我们将简单说一下IIFE。
IIFE指得就是立即执行函数,这个概念js的初学者都知道,我们会通常这么写:
(function(){
console.log("helllo world");
})()
IIFE起到了这样的作用,我们在匿名函数中声明的变量不会被外部访问即不会污染全局。
脚本异步加载
在HTML5之前我们在HTML中加载script脚本的时候,是遇到脚本然后会理解执行的,它是可以阻碍下面的DOM渲染的。其次呢就是在H5加入的2个属性,async指的就是,我们异步加载了这个脚本文件并且执行了它,这个过程是不会阻塞下面的DOM渲染的。defer也是同理,但是和async在执行时间上有区别,defer会在DOM渲染完毕执行。所以总结来说,defer更适合我们常理的加载JS的方式。
Promise
在JS的远古时代,我们假设要调用一个接口或者是做一个异步的操作,我们通常会使用回调函数来resolve结果,比如这样:
request(data, (res) => {
// 我们要通过res中的值进行下一步操作会这样写
request(res, (res1) => {
console.log("fuck is so hard!!")
})
})
当我们进行多个任务嵌套,这就造成了回调地狱,这是非常不优雅的写法,那么Promise就就解决这个问题,我们通过Promise来给返回值一个“诺言”,就像下面这样:
const a1 = new Promise((resolve, reject) => {
// resolve将会从then中获取
// reject将会从catch中获取
});
a1.then(res => {}).catch(err => {});
那么我们需要知道,promise有几个状态:
- fulfilled(resolve之后)
- reject(reject之后)
- pending(还未决定,还在等)
那么Promise有哪些特性呢?比如这个状态一改变就不能在改变了,同一时间只能存在一种状态。那么async和await和promise有什么区别呢,async是es7引入的语法糖,专门处理promise的对象,它在写法上更精简了,使得异步代码彻底的变成了同步代码的视觉样子,比我们promise.then这样链式调用更容易阅读和理解。
待补充:Promise A+规范有哪些?
this的理解
在绝大多数的情况下,函数的调用方式决定了this的值,es5里面通过apply, bind, call来执行函数,此时的this就是他们的第一参数。那么后续又引入了箭头函数,箭头函数不提供this绑定,所以当前如果在箭头函数上出现,那么this指向的就是创建函数的词法环境。
Proxy
Proxy是es6的新语法,熟悉Vue2的盆友都知道,vue2的响应式是基于
Object.defineProperty()
而vue3的响应式是用proxy的,但是并不是ref,computed都是用proxy实现的。那么proxy主要的作用有什么呢?proxy可以创建一个对象的代理,实现查找,拦截以及自定义的其他功能。
const target = {};
const proxy = new Proxy(target, {
get (target, key, receiver) {
return 567567;
}
});
console.log(proxy.getData);
proxy可以代理所有ProxyHandler定义的型位,也可以代理13种对象行为;
proxy和object.defineProperty的区别:
- 对于深层嵌套的对象,defineProperty需要遍历让每一层代理,这也就是vue2为什么watch的钩子中有一个deep的选项,开启deep就遍历。
- 对于数组,defineProperty不能监听关于它的操作,所以vue中.push一个数据到数组中,template不会变就是这个原因,我们必须要concat返回一个新的数组才能被访问到。
- defineProperty比proxy兼容性好。
数据类型/引用类型
基本数据类型有7种:number, string, obejct, null, undefined, boolean, symbol
引用数据类型:统称为object:
let a1 = {};
let a2 = {};
console.log(a1 == a2);
console.log(a1 === a2);
引用数据类型的比较是比较在堆内存中的引用;引用数据类型的真实数据是存在堆中,而基础数据类型存在于栈中。
那么这块就是老生常谈的就是==,===的区别,这个说了太多次了,==有隐式转换不严谨,我们在开发中应当使用===进行严格的判断,能够区分不同的数据类型;
我们需要注意的事情是,在堆中的对象是可变的,在栈中的变量是不可变的,我们如果更改了栈中的变量会重新在栈中划分一块区域。
深拷贝和浅拷贝
根据我们上面提到的数据类型相关知识,我们这里的深拷贝和浅拷贝只针对引用数据类型即object。我们知道引用数据类型虽然在栈中有自己的一个区域存储变量,但是同时又在堆中存储了栈中变量的引用,由于其特殊性,我们在对“对象”和“对象”进行浅拷贝操作的时候,仅仅会复制对象的指针,也就是他们还是指向同一个堆内存;但是深拷贝就不一样了,他会从内而外的复制一个新的对象,指向一个新的堆内存,2个对象互不干扰。这就是深拷贝和浅拷贝的区别。
那么浅拷贝和直接赋值有哪些区别呢?
首先赋值操作,肯定就是复制了栈中的对象,赋值之后此刻就是联动状态,a变b也变;浅拷贝唯一的区别就是第一层是否是基本数据类型。浅拷贝不会改变基础数据类型,但是如果对象其中有子对象(object,array)这个时候子对象变,那么浅拷贝之后的对象也会变。
常用的深拷贝和浅拷贝的操作?
浅拷贝就concat, splice, object.assign(这个深拷贝也可以用)等等
深拷贝就最简单的json.stringify或者写一个递归;
柯里化
柯里化的意思就是在计算机科学中,可以把一个函数多个参数的写法转换一系列一个参数的技术。
var curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg) => judge(...args, arg)
上面这个是高颜值写法,柯里化通常用于函数式编程,意指提高代码复用,更容易阅读;
模块化
ES6的模块化解决了很多以前JS的依赖管理和开发全局变量污染的情况,这个part将简述几个模块化标准的特点以及区别。
commonJS:
const m = require("./m.js");
m(); // m.***();
// 屈居于模块中的module的export对象导出的是什么
commonjs会遇到require语句会执行并且运行这个脚本,而这个脚本中的内容都会被缓存到内存中,模块可以被多次加载,但是只有第一次加载才会被缓存;NodeJS就是参照了CommonJS的规范,但是NodeJS自己加了一点东西,就是把module.exports指向了一个变量exports;
特点:
- 模块中的代码不会污染其他变量,是在模块作用域中。
- 模块加载会被缓存,使用模块中的内容会直接从缓存中取,如果需要获取再次获取模块,需要清除缓存。
- 模块加载顺序就是代码执行的顺序
AMD
commonjs非常好,但是它require之后需要立即执行这个特性不适合浏览器,所以AMD和CMD就是适合浏览器运行的模块解决方案,AMD是异步加载,加载完模块依赖的包之后,会有一个回调函数给开发者,我们在回调函数中才可以正常的使用模块中的方法,例如下面:
define({
add: function(x, y){
return x + y;
}
});
// 使用它
require("math", (a) => {
a.add(1, 2)
});
// 定义AMD模块其实有很多参数,我们只列举了最简单的写法
CMD
CMD的API和AMD很像,它支持AMD的所有API,但是CMD推荐的是:
define((require, exports, module) => {
// do something
})
CMD与AMD的区别就是:
- AMD提倡依赖前置,即依赖提前执行,CMD推崇依赖就近,依赖是延迟执行
UMD
那么既然CMD和AMD以及CommonJS这么好,我们可以灵活地使用这些模块,就需要UMD了,UMD是一个通用的模块标准,在UMD的实现中,通过判断环境来决定使用哪种模块化,如果环境中AMD的不存在(define)就寻找commonjs,如果commonjs都不支持,那么会把模块暴漏在window或者global中。
ESModule
我们最熟悉的ESmodule来了,其实在esmodule之前,那些规范都是社区定义的,但是esmodule是真正的官方语言层面提出的模块解决方案,那么esmodule对比commonjs有什么优势呢?commonjs和AMD它们一样,都是在运行时才能确定依赖,但是esmodule能做到编译时就能确定模块依赖。
ESmodule是使用,export 和 import这两个关键语句来进行模块引入和导出的,官方定义的模块就是不一样,支持别名,默认引入等等,写法也非常精简,所以esmodule是我们目前开发web浏览器端的代码时最常用的模块标准。
Vue
Vue2核心理念和Vue3核心理念
- Vue3的VDOM进行了重写,首先是体积更小了。
- 响应式系统的升级,用proxy来劫持数据,使得Vue2响应式系统的一些坑得到了完善(具体什么坑可以看defineProperty的缺点)
- 组件内不需要根节点了
- DIFF算法更快,标记了所有的静态节点,只需要DIFF动态的内容
- 使用组合API区别于传统的OptionAPI,原来的option将逻辑分散到各个地方不宜与维护和阅读,而组合API将同样逻辑的代码组合在一块,高内聚低耦合,阅读维护更加的方便。
style中的scoped原理
每个组件都会在postcss编译中产生一个Hash,在style中写scoped属性之后,在页面的表现就是使用css的选择器选择对应组件的hash。在这个选择器(组件)下的css是不会影响到外部的css的。
Vdom
VD本质就是一个JS对象,我们熟知的React和Vue都是使用VD进行页面渲染的,那么VD有哪些好处呢?我们都知道页面出现都要经过js运算,生成渲染树,绘制这几个阶段,那么组件中如果出现大量的变化是要经过重排/重绘的,为了避免这个情况,我们使用VDOM来表述DOM,它和DOM是一一对应的,然后经过在内存中我们Vdom和dom的比对,然后分批次的更新页面上的dom。那么页面渲染性能就可以得到很好的提升。
而且VD可以跨平台渲染,因为本质就是一个对象,不仅可以把对象编译成DOM,还可以编译成QQ,微信,ios,原生安卓。缺点也是显而易见的,我们需要创建额外的函数去做这个事情,但是也可以依赖工具比如vue-loader等。
domdiff是对比新旧虚拟dom一种算法,分两种情况:
- Components diff算法
- Element diff 算法
组件的diff则是查看组件的类型,类型不相同就新替换旧的,类型相同就更新组件属性,然后进入组件递归上述操作。
标签的diff则是查看标签名,如果标签名不同就直接替换,相同就只更新属性,之后进入标签内部递归上述操作。
MVVM(本来不想写)
如果要讨论MVVM,那么就要谈到MVC和MVP了,就简单的概述一下三种架构模式吧。
MVC
MVC是非常流行的架构模式,它分为view,model,controller层,它们都是单向的通信,由view去触发controller,controller再去触发model做数据处理,然后反方向返回给view层,在这个架构中controller层是最薄的,它只做了逻辑分发的作用,一个桥梁衔接。熟知的框架就是SpringMVC。
MVP
MVP是MVC的衍生的一种架构模式,它把controller变成了Presenter,各个部分的通信都是双向的,而这里的视图view则是被动的,在这个架构中,Presenter做了绝大部分工作,而view是最薄的。
MVVM
MVVM也是衍生的一种架构模式,它没有controller,新增了一个viewModel,它保证了view和数据的双向绑定,其中有一个绑定器去做这样的事情,view的变动会反应到viewModel,反之亦然。
谈谈Vite,为什么这么快,主要解决什么问题?
天下武功,唯快不破;Vite作为超现代的本地开发服务器而且是基于ESM的。那么现在很多开发服务器都是基于ESM的,比如SnowPack。Vite本地开发快主要使用ESM,对于TS和HMR使用的是ESbuild的插件,Esbuild是用GO语言原生编译的,所以Vite在TS和HMR都有出色的表现。vite在生产环境使用的是Rollup(和Vue-cli一样)。
Vite还有以下的特点:
- 开箱即用,TS,jSX,CSS等
- MPA应用或者库模式都是非常简单配置
- SSR服务端渲染
想要具体了解vite是如何运作的,可以了解我以前写的一篇浅析vite的文章:
从0开始理解Vite的主要新特性(一)
Vue如何做依赖收集
我们需要事先了解观察者模式,观察者模式是一个观察者和目标组成的,观察者直接订阅目标,当目标做出改变,观察者也要立即做出回应。那么发布订阅模式和观察者模式有些许不一样,发布者只需要做好发布的任务,不关心订阅者有没有订阅。
在Vue的组件中有一个watch去监听set值,监听到set值之后会去调用render从而实现视图上的变化。那么Vue如何知道哪些变量使用了呢?我们在template中访问了这个变量,势必会被getter监听,在getter我们就可以对这个变量进行标识,从而实现变量被使用之后才能被组件中的watcher监听到。这个步骤就是Vue的依赖收集简单原理。
那么上述就是,一个一对多的例子,一个数据变更了,多个用到这个数据的地方都要做出改变,这就是观察者模式要做到的东西。那么在这个里面目标是data数据,观察者就是计算属性,侦听器,视图等。
这个地方我们描述的是vue中内置的持久化状态的组件,而不是http里面的keep-alive,这个需要注意。如果熟悉vue的同学肯定写过这样的代码:
<keep-alive>
<App/>
</keep-alive>
(keep-alive下文简称ka), ka 包裹了app组件,就代表了app组件中所有状态都会被缓存,它们不会重走生命周期函数,即create和mounted只执行一次,但是请注意vue并不会销毁被缓存的组件,ka本身也不会去渲染任何元素,它是一个抽象组件。业务需求中我们也需要对缓存组件做一些额外初始化处理,但是生命周期只执行一次就很难受,vue给我们提供了activated这样的钩子,当缓存的组件再次渲染之后我们依然可以做一些初始化的工作。
mounted(){
// 第一次渲染被缓存的组件
},
activated(){
// 当缓存再次渲染后触发的钩子,但是此时不会触发mounted
},
deactivated(){
// 当缓存的组件被销毁,触发钩子
}
我们刚刚提到了ka是抽象组件的问题,vue内部是如何不渲染这个ka的呢,是因为在vue组件在初始化生命周期父子组件建立关系时,如果遇到abstract为true的组件则不渲染,而ka就是true;那vue内部实现中,keepalive是如何有缓存功能的?这一块我摘抄一篇文章提到的一段源码。
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
作者:wilton
链接:https://juejin.cn/post/6844903837770203144
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ka组件实例componentInstance是undefined但是keepAlive是true(所有被缓存过的组件keepAlive都是true),所以只执行到i(vnode, false / hydrating /)这一段代码,下一步的if没有执行。但是再次访问被缓存的组件的时候,componentInstance是上一次初始化的,所以我们能顺利的走完下一个if即insert(parentElm, vnode.elm, refElm);将缓存的内容插入到dom中。
我们之前提到的ka只会执行一次完整的生命周期,这一块的逻辑和上述一样,组件实例componentInstance不为undefined且keepAlive为true就不会执行mounted。
这就是keep-alive组件的源码实现,我这个题解描述的非常片面,在写这部分的时候,我自身对于ka的理解没有达到源码级别,所以看了这篇文章之后才有一个全新的认识,我把文章po一下:
彻底揭秘keep-alive原理
nextTick原理?怎么用
nextTick是Vue官方提供的一组API,用于获取在vue中下一次更新DOM结束的回调。由于Vue的DOM更新是异步的,所以我们更改DOM之后需要使用这个API来获取最新的DOM。
原理待补充
router原理?
路由这个概念其实是web层面是服务端的概念,但是前端为什么引入了路由概念,是由于spa应用的流行,很多库都有自己的router实现,那么前端路由中的URL和视图有哪些特性呢?
- 监听URL变化改变视图
- 不会刷新页面
我们使用hash和history来简单构建一下router基于我们的上面两个特性:
- hash指的就是URL中的锚点#之后的内容,锚点在HTML中常用于导航,在这个模式下,我们改变hash是不会刷新页面的。
window.addEventListener('hashchange', onHashChange);
function onHashChange(){
// 通过判断hash来对页面进行变换
}
- history模式在外表下和普通URL无异,但是在这个模式下要比hash做更多一层:popstate我们使用这个API来监听URL的变化,但是当直接改变URL或者a标签跳转是不会走popstate的,所以我们需要监听URL变化和a标签跳转:
window.addEventListener('popstate', onPopState)
// 我们通过拦截a标签的事件,自己绑定一个click事件,去写下面的代码
history.pushState(null, '', el.getAttribute('href'));
// 这样手动的更新URL,不会刷新页面
// 判断URL进行刷新试图
...
React
惭愧,react我只学了1-2天,而且直接接触的是hook,react这块只达到了能写的实战水平,但是水平完全够不到vue,因为知识有限,所以这块罗列的题目很少。
useState如何回调?
待补充
类组件 vs 函数组件
待补充
更新state是同步还是异步的?
待补充
hooks带来了什么?
待补充
Webpack/Vite/SnowPack/Babel
简单讲一下webpack的构建过程
- 寻找入口文件
- 通过入口文件,逐层识别模块依赖,commonjs,amd,esmodule都会被分析,然后获取代码中的依赖。
- 然后webpack就要做分析代码 转换代码 编译代码 输出代码这几个重要阶段,在这几个阶段中webpack中的loader和plugin都会起作用。
- 输出代码
常用的loader知道有哪些么?css-loader和style-loader是干嘛的?
loader就简单的用css-loader或者scss语法loader,那么题中的css-loader和style-loader都是出现率极高的loader,css-loader主要是分析语法中的@import语句的,还用于处理css和css文件的关联,style-loader会把css挂载到html中的style里面去。
常见的plugins有哪些知道么?
比如说:
- htmlwebpackplugins:生成一个html并且自动引入入口文件
- HMR plugins:热模块更新的插件
loader和plugins的区别?
loader主要是针对特定文件的分析和编译,而plugins是webpack生态的最重要支柱,它可以干loader干不了的事情,比如说在webpack执行中会广播很多事件,而plugin可以监听这些事件并且处理和改变输出结果。
简单讲一下sourceMap
“简单“讲一下,我也只能简单讲一下了,难的我也不会。首先我们通常前端在部署的时候,会将js,css这类文件进行压缩,通常这部分工作是打包器为我们做的。但是我们想要在生产环境中查看代码错误,而浏览器会将错误定位到我们压缩编译过后的js中,这对我们debug是不利的。所以我们可以使用sourcemap去解决这个问题,它本质是一个map文件,存储了编译过后的代码的位置信息,我们在编译过后的js中引入map文件即可,引入之后浏览器会根据map存储的位置进行反馈给控制台,我们就可以很快进行debug。那么在webpack中,sourcemap有几种的常见模式:
(省略7种常见的.....)我如果真的遇到问我哪7种模式的面试官我就会很无语,webpack成千上万的配置项我咋可能都能背出来,开发中就直接查文档就好了。eval构建速度很快,而且我们sourcemap不用存储很细致的位置信息,所以使用cheap就可以了,综合来看,一般都是下面这个配置:
开发环境:cheap-module-eval-source-map
生产环境: cheap-module-source-map
如何提升webpack打包速度呢?
Webpack: 从9个方面对打包速度提升(和Node.js的美好碰撞)
webpack的热模块更新怎么做的?
这个问题我说实话,就算不会webpack,也都知道热更新,就算不知道热更新这个词语也都知道我们通常在使用一些脚手架快速开发的时候,更改文件能在页面即时同步技术。那么webpack是如何做热更新的呢,webpack中有一个官方维护的plugin:
HotModuleReplacementPlugin | webpack
hmr之前有很多库比如我经常在vscode中用到的live server,能够监听文件的变化通知浏览器进行刷新,之所以hmr的出现风靡技术圈,原因就是以前的livereload机制太扯了。比如一个提交表单场景,你输了很多表单值,你准备进行测试,发现代码中有一个地方有错,你更改之后发现页面刷新,你之前输入的表单值全部没了。我对hmr工作原理理解非常浅,如果你要深入了解这个内容,我不妨推荐给你们一篇文章,我认为全网就这篇文章把hmr讲透彻了。
Webpack HMR 原理解析
简单说一下我的理解,我就是那种喜欢纸上谈兵的(狗头):
- 启动webpack时候有watch选项,即文件更改→重新打包→编译到内存中。
- webpack-dev-server会监听静态文件变化,然后通知浏览器livereload(重新加载非hmr)
- webpack-dev-server会和客户端建立websocket连接,推送给客户端不同类型的消息,比如上一步的静态文件信息,还有变化了的文件更改,还有新模块的hash,后续会根据hash值去和客户端本地文件做比对,达到热模块更新效果。
- HotModuleReplacement拿到上述从服务端给的hash之后,hmr插件通过一个runtime模块去请求所有要更新的文件hash列表,再发起请求拿到要更新的所有文件
- hmr插件将比对新旧模块区别,然后判断要不要替换,如果选择替换的话,将分析相关文件依赖,是要一起更新的,如果hmr热更新失败,那就触发livereload,刷新浏览器获取最新的文件。
我原本面试主要说的是建立websokect连接,balalalalala...,但是看了上面的分析文章之后,自己简单的概括了一下,收获非常大,hmr的难点应该是在于如何替换,其他倒是很容易理解。
webpack模块打包原理?
暂补充
webpack文件监听怎么做的?
—watch,webpack-dev-server是默认开启的,脱离webpack之后可以用nodemon做监听。
webpack代码分割本质,有什么意义?
应用越来越大,vendor也会变大,某个功能可能只依赖了vendor一点,但是也要把整个包加载,使用代码分割,可以将业务和业务隔离开,按需加载。避免了浏览器加载页面白屏或者加载功能模块性能的问题。
简单聊聊Babel原理吧
我对babel原理一窍不通,惭愧。babel我的理解就是js的翻译官,能够高效的把高级/实验性语法进行低版本兼容。
CSS
WebComponents
WC是现代组件化的一种技术,WC的核心理念就是:
- 自定义元素Custom Element
- ShadowDom
- 模板Template
我们可以用这些技术构建出一个自定义组件,而这个组件的css和js逻辑都是独立于组件本身。
在这以下几种情况,容器不能承载ShadowDom:
- 自身已经承载了Input,TextArea
- 承载了Img标签的容器
如何让页面元素消失?
这个是我面试必考题,能够考验候选人的CSS方面的想象力,下面简单列举一下我的答案:
- 宽高为0
- 背景为白色,让用户看不到
- 用posotion transform margin 一系列移动的方法把元素移出去看不到
- css缩小,旋转可以把元素处理到看不见
- 透明度和css可见,透明度可以用opacity和filter实现
- 把字体设置为白色让用户看不到
重排和重绘?如何写组件优化重绘重排带来的开销?
重排和重绘是浏览器页面渲染的必经之路,我们在开发中的代码操作可能引出重排和重绘,那么关于重排和重绘,我们只需要记住一句话:我们页面的布局发生了改变就会引起重排,而重新排会影响到重新绘制,更改颜色文字大小等会引起元素重新绘制但是不会进行重排。在我们开发中,浏览器已经做了很多渲染优化,我们只能在特殊的场景能够发现重排和重绘优化带来的红利,但是我们仍然需要关注渲染性能的优化。比如在我们开发组件的时候,可以使用css contain content,让组件变化仅仅组件内部进行重排重绘,而不是影响整个页面。
transform中的translate会引起重排么?
不会的,GUI线程会开启一个新的图层,不会影响普通文档的图层,那么让线程开启一个新的图层的操作也有很多,比如说透明度,过渡动画,3D等等。定位虽然脱离了普通文档流,但是它还是默认图层,所以还是会影响到普通文档流。创建新的图层是要内存消耗的,所以好的东西不要贪杯。
如何更改组件内部样式?(深度选择器)
这个我就暂且默认为Vue的组件吧,我们也在上面提到了Vue组件如果做样式隔离的原理。
- <<<选择器(记忆中,scss和less不支持)
- 曲线救国,再写一个style,把组件内部的样式写到没scoped的style中
- /deep/ 我一般用这个
css中如何获取html元素上的属性
这个题很简单啊,但是在开发中确实非常少用到,下面给大家上代码演示一下:
content:attr(href); // 必须在伪元素中写
有了解过calc么?
和上面那个题是一个智商的题,问出来的人其实我觉得水平不高,但是奈何我们的题解中预判它可能存在,就简单说一下:
width: calc(100px + 40vh - 100px); // 简单举个例子,计算各个属性的css值,运算符之间必须要空格隔开,要不然无效
CSS沙盒
css sandbox其实也是主要解决样式隔离的问题,那么样式隔离的问题,我们有以下几个方法去解决/设计:
- namespace在acticle下面用,但是这个场景一般用在H5制作器上,多个组件移动到同一个区域中展示。
- Iframe这个是真正意义上的沙盒,但是很多局限性,高度不能自适应等等,因为iframe是上古时代的产物,所以这里不太了解。
- Webcomponent的实现之一ShadowDOM可以解决样式隔离的问题,但是如果care兼容性的话还是用第二种iframe的办法吧。
简单聊聊BFC?如何解决BFC造成的问题?
BFC(块级格式化上下文),指的就是在一个如果一个盒子是BFC盒子的话,那么在盒子里面的任何css都不会影响外部,它更像是一个css"作用域",所以我们在css开发中遇到一些问题,包括BFC内的样式重叠等等,我们都可以用BFC把元素独立开,去解决这个问题。
如何触发BFC呢?(没见过没用过我不会写)
- body元素就是天然BFC的盒子
- overflow 除了auto其他的值都可以解决
- 定位 固定定位 绝对定位
- display inline-block flex
- 浮动 float
清除浮动
清除浮动这种题,我感觉没有脑血栓10年问不出来。我的面试风格就是挑有用的问,浮动都是很老的题目了,清除浮动这样的题以前很常见,但是2021我觉得没人会问了(校招笔试题可能会看到):
- overflow: hidden;
- 相邻的元素是clear:both
- 或者父子都浮动(尽管脑血栓20年才会这么干)
- 伪元素加clear:both(推荐)
HTML
有了解过PWA规范么?
pwa是一系列的技术/规范,是由谷歌提出的,我们都称之为渐进式应用(你也可以理解为国外小程序,个人觉得比我们国内小程序功能更实用,但是没有生态没有用户粘度),使用现代webAPI去构建原生一样的跨平台应用。那么这一块的内容有点多的,所以我们就简单讲解用的比较常见的几个:
- servicewoker 离线使用
- 像APP一样具体推送通知的功能
- 像APP一样添加到主屏幕
这一块其实有蛮多内容的,感兴趣的可以自己搜一下。
简单说一下可替换元素吧
在HTML中有一部分元素就叫做可替换元素,它们不受CSS引擎控制,我们往往看到可替换元素的时候,这个元素就本身自己具备了一些普通元素没有特性,比如Img,Input,textArea等等;
就拿Img来说,img是行内元素,行内元素的定义就是不独立一行且无法设置宽高,但是img作为行内可替换元素是可以设置宽高的。
单页应用和多页应用(spa和mpa)的区别?
这两个概念,尤其是spa这个概念是最近几年非常流行的。spa对应的就是mpa,即多页应用;我们传统的前端页面,都是由一个一个html组成的,通过各种link来连接一个一个页面,那么意味着我们每请求一次都要去请求一个新的html进行响应。这个方案在pc端还尚可,但是在移动端中,用户每切换一次页面都是一次巨大的成本,所以spa应用而生,spa也促成了很多前端路由方案,通过url和页面进行绑定,然后由JS去控制显示的视图,这样做的好处就是,切换速度很快,用户体验感好。
那么spa也有一些缺点,比如说是SEO搜索收录不太好,因为搜索爬虫爬取mpa页面会记录每一个HTML的路径,而爬虫遇到spa页面的时候将会大大受限。mpa在首次加载的速度又优于spa,但是切换速度就不如spa了。所以综合来讲,我们web常见的官网,论坛是适合用mpa来做;移动端适合用spa来做;
SSR服务端渲染是什么?
在了解SSR需要了解浏览器部分的页面解析过程(下文就有),SSR对应的是CSR,CSR即客户端渲染,在我们现在前端中,通常都是把HTML渲染,然后在渲染过程中去加载css和js,此时的HTML因为css和js可能没加载完会出现白屏,此时的HTML组装方式是在客户端。那SSR服务端渲染目前的模式是由服务端将css和js进行处理,然后纯粹的返回给客户端一个完整的html模板,此时HTML组装方式是在服务端。那么SSR和CSR各有什么优缺点呢?在我们了解spa的时候就讲到了SEO的例子,那么这边在延申一下,爬虫也分为高级和低级,目前百度谷歌等爬虫库里面存在着很多低级爬虫,低级爬虫只能爬解析url之后的HTML页面,而客户端渲染的spa应用返回的htm都是空的,所以只有高级爬虫才会对js和css文件进行加载然后爬取,所以SEO收录是spa的一个痛点。如果我们开启服务端渲染,那SEO的问题虽然会迎刃而解,但是要在服务端进行HTML的处理势必会造成大量的计算,开销/投入产出比都是我们应该考虑的。
等待补充,同构(即SSR和CSR共存)
浏览器和工程化
HTTP2带来了什么对于前端?
等待补充
输入一个URL到页面呈现中间经历了什么?
- 解析URL
- 查看浏览器中的缓存
- DNS解析
- 建立TCP/IP连接
- 发送HTTP请求
- 服务端处理请求返回报文
- 浏览器解析HTML成DOM树
- 异步解析css外链成css规则树
- 遇到script标签,同步执行(分async和defer进行异步加载)
- 将DOM树和css规则树构造成render树
- 渲染render树
你如何去架构一个新的前端项目?
等待补充
浏览器如何优化重排和重绘的?
等待补充
如何在浏览器中进行标签页面的通信呢?
postmessage,等待补充
跨域(其实这题应该不会问了)是怎么造成的
等待补充
有开发过浏览器插件么?
我答的很好,但是之后会补充这一块
有了解过微前端么?
我答的很好,但是之后会补充这一块
有了解过serverless么?
我答的很好,但是之后会补充这一块