本文主要给大家带来一些我面试的经历和经验,希望对正在求职的同学有所帮助。我先大致说下面试之前的个人情况:2017年7月正式入职海康威视数字技术股份有限公司,使用Vue.js技术栈。
我写的篇幅可能有点长,如果只想看成功的面试请直接从阿里企业智能事业部(一面)开始,大家见谅哈。
Hi,大家好,我们是阿里巴巴新成立的BU,目前还有大量的Web前端职位空缺,机会难得,希望正在找工作的同学们可以来试试:
目前Web前端急缺P6和P7(阿里的很多BU都只招P7了)
新的BU你进来即是元老????????????
前端技术体系大部分需要一起重新开拓,可以学习到更多的新内容
主要负责PC端、客户端、钉钉E应用以及支付宝小程序的开发(我本人完全不会小程序,不用担心????????????)
技术栈是React(如果你是Vue技术栈完全不用担心,因为我也是????????????)
其他BU面试可能有五轮,我们这边只有4轮面试
真的机会难得哦,如果想更多了解我们BU以及找我内推的同事加我钉钉或者微信(纯粹找我了解或者沟通技术也行,啊哈哈):18768107826
我的简历只是简单的用MD做了一份,大致包含了以下几个部分:
基本资料
专业技能
工作经历
实习经历(可选)
项目经历
小提示:在基本资料里一定要填写正确的邮箱地址,我在前期面试的时候都没有打开邮箱查看面试情况,导致一些面试的时间点和面试结果都不清楚(一直以为会发短信通知)。
如果去现场面试,一定要记得带上笔和简历。一方面你给面试官的简历必定是最新的(在不断面试的过程中你必定会修改简历),另一方面这也会给面试官一种非常舒心的感觉。
对于简历这里提一点,在写自己的专业技能和项目经历时尽量不要给自己挖坑,这里展示一下我的专业技能(我会的不多):
熟悉嵌入式C、JavaScript、Node.js
熟悉Vue.js框架
切忌写一大堆让人感觉花里胡哨的技能,尤其是一些很浅显的技能(基本技能除外)。如果你有一些别人很难替代的技能,那这些技能就是亮点了,我这里就没什么亮点技能。有些技能你会但是不熟练,你可以适当的在你的项目经历中体现出来。对于项目经历尽量挑自己觉得非常有技术含量的项目进行说明(宁缺毋滥),对于自己参加过但不是特别熟悉的项目尽量不要填写,防止给自己挖坑。
小提示:这里附上的我的面试简历供大家参考。感谢jsliang的文章2019 面试系列 - 简历,大家制作简历时也可以参考这篇文章。
在投递简历时大家千万不要被招聘信息中的要求吓到,记得有一次投递简历时我对招聘者说自身不太符合要求,招聘者当时说要求都是唬人的,觉得有兴趣就投,有些招聘要求可能正是你未来学习或者深入的领域。
简历制作完后我大概投了四家公司:有赞、滴滴、51信用卡和阿里。其中有赞挂在二面,滴滴挂在一面,51信用卡挂在一面,阿里两个部门挂在一面,一个部门面试成功。很多面试者的经历可能都是像我这样,在一次次的面试失败中不断的总结进步,最终拿到理想的Offer。
小提示:建议大家在投递简历时可以先投递一些试水的小公司,先检验一下自己是不是可以胜任这些公司的面试。同时在每一次面试完后记得把面试官提问的问题记录下来,对于没有答上来的问题还是要好好搞懂或者实践一下,因为很有可能下一家的面试官会问同样的问题。
在面试的过程中,这里我给出几点意见:
心态放平稳,假设第一题你答不上来很正常,面试官不会因为第一题你不会就PASS你
不会的题目一定不要瞎猜,往往面试官给你挖的坑就是希望你往错的方向猜,一定要答不知道
不要说太多跟当前面试题无关的内容,问你什么问题尽量就答什么问题,除非面试官指定你发散一下思维
如果没有听懂面试题可以试着询问面试官,您要问的是关于xxx的问题么
对于某些问题一定要自己先提前精炼一下(例如作用域链、继承以及原型链等问题)
如果面试官问的某项技术自己在某些场景使用过或看到别的场景有使用,可结合这些场景进行讲解(让面试官知道你不仅仅理解它,你还会很好的使用它)
如果是Vue技术栈希望可以深入源码或者至少理解一些别人的源码分析
如果面试阿里那么面试之前一定要好好准备这样一个问题:你觉得你最擅长什么
面试一定要真诚,切勿投机取巧
面试态度一定要谦虚
接下来我会按照面试顺序给出面试题以及自己理解的一些答案:
大部分答案都是借鉴别人的博客
有些答案不一定合理
有些答案写的很零散
有些答案会举一反三
有些题目太基础或者重复了就没有写答案
有些题目太宏观或者不知道怎么回答合理,希望大家可以在评论中补充答案供更多的人受益
!important
内联样式(1000)
ID选择器(0100)
类选择器/属性选择器/伪类选择器(0010)
元素选择器/关系选择器/伪元素选择器(0001)
通配符选择器(0000)
小提示:这个问题重点是BFC是什么,BFC触发的条件有哪些,BFC可以干什么。这里我试着讲解了一下Boostrap的清除浮动(display:table创建匿名table-cell间接触发BFC),如果有看到别的场景使用或者自身有使用的场景可以尝试讲解一下使用技巧。这样可以让面试官觉得你不仅仅知道他问的东西是什么,你还能很好的使用它。
BFC 全称为块级格式化上下文 (Block Formatting Context) 。BFC是 W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行定位以及与其他元素的关系和相互作用,当涉及到可视化布局的时候,Block Formatting Context提供了一个环境,HTML元素在这个环境中按照一定规则进行布局。一个环境中的元素不会影响到其它环境中的布局。比如浮动元素会形成BFC,浮动元素内部子元素的主要受该浮动元素影响,两个浮动元素之间是互不影响的。这里有点类似一个BFC就是一个独立的行政单位的意思。可以说BFC就是一个作用范围,把它理解成是一个独立的容器,并且这个容器里box的布局与这个容器外的box毫不相干。
根元素或其它包含它的元素
浮动元素 (元素的 float
不是 none
)
绝对定位元素 (元素具有 position
为 absolute
或 fixed
)
内联块 (元素具有 display: inline-block
)
表格单元格 (元素具有 display: table-cell
,HTML表格单元格默认属性)
表格标题 (元素具有 display: table-caption
, HTML表格标题默认属性)
具有overflow
且值不是 visible
的块元素
弹性盒(flex
或inline-flex
)
display: flow-root
column-span: all
内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流)
处于同一个BFC中的元素相互影响,可能会发生外边距重叠
每个元素的margin box的左边,与容器块border box的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然
计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算
浮动盒区域不叠加到BFC上
垂直外边距重叠问题
去除浮动
自适用两列布局(float
+ overflow
)
包括内容区域、内边距区域、边框区域和外边距区域。box-sizing: content-box
(W3C盒子模型):元素的宽高大小表现为内容的大小。box-sizing: border-box
(IE盒子模型):元素的宽高表现为内容 + 内边距 + 边框的大小。背景会延伸到边框的外沿。
IE5.x和IE6在怪异模式中使用非标准的盒子模型,这些浏览器的width
属性不是内容的宽度,而是内容、内边距和边框的宽度的总和。
小提示:这个问题面试官会要求说出几种解决方法。
DOM结构
float + margin
实现 .box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
margin-left: 200px;
background-color: red;
}
calc
计算宽度 .box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
width: calc(100% - 200px);
float: right;
background-color: red;
}
float + overflow
实现 .box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
overflow: hidden;
background-color: red;
}
flex
实现这里不是最佳答案,应该是使用flex-basis
实现更合理
.box {
height: 200px;
display: flex;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
background-color: blue;
}
.box-right {
flex: 1; // 设置flex-grow属性为1,默认为0
overflow: hidden;
background-color: red;
}
小提示:如果平常自身有使用场景可结合使用场景进行讲解,比如我在这里使用过的场景是CORS和Nginx反向代理。
同源策略限制、安全性考虑
协议、IP和端口不一致都是跨域行为
小提示:如果你提到JSONP,面试官肯定会问你整个详细的实现过程,所以一定要搞懂JSONP的实现原理,如果不是很理解可以自己起一个Express服务实践一下。
Web前端事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。
缺点:JSONP只能发起GET请求
这里给出几个链接:
https://www.cnblogs.com/iovec/p/5312464.html
https://zhangguixu.github.io/2016/12/02/JSONP/
https://segmentfault.com/a/1190000015597029
前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。
解决方法:验证JSONP的调用来源(Referer),服务端判断Referer是否是白名单,或者部署随机Token来防御。
不严谨的 content-type导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://youdomain.com?callback=douniwan
, 然后返回 douniwan({ data })
,那假如请求 http://youdomain.com?callback=
不就返回 ({ data })
了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback
参数,直接当 html 解析了,就是一个赤裸裸的 XSS 了。
解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback
后的参数并且限制长度(进行字符转义,例如<换成<,>换成>)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。
可以将执行的代码转发到服务端进行校验JSONP内容校验,再返回校验结果。
小提示:如果你回答跨域解决方案CORS,那么面试官一定会问你实现CORS的响应头信息Access-Control-Allow-Origin。
CORS(跨域资源共享 Cross-origin resource sharing)允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服跨域问题,它需要浏览器和服务器的同时支持。
浏览器端会自动向请求头添加origin字段,表明当前请求来源。
服务器端需要设置响应头的Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin等字段,指定允许的方法,头部,源等信息。
请求分为简单请求和非简单请求,非简单请求会先进行一次OPTION方法进行预检,看是否允许当前跨域请求。
请求方法是以下三种方法之一:
HEAD
GET
POST
HTTP的请求头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
后端的响应头信息:
Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。
Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求
JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据
Nginx反向代理
postMessage
document.domain
相对于HTTP1.0,HTTP1.1的优化:
缓存处理:多了Entity tag,If-Unmodified-Since, If-Match, If-None-Match等缓存信息(HTTTP1.0 If-Modified-Since,Expires)
带宽优化及网络连接的使用
错误通知的管理
Host头处理
长连接:HTTP1.1中默认开启Connection:keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
相对于HTTP1.1,HTTP2的优化:
HTTP2支持二进制传送(实现方便且健壮),HTTP1.x是字符串传送
HTTP2支持多路复用
HTTP2采用HPACK压缩算法压缩头部,减小了传输的体积
HTTP2支持服务端推送
小提示:如果平常有遇到过缓存的坑或者很好的利用缓存,可以讲解一下自己的使用场景。如果没有使用注意过缓存问题你也可以尝试讲解一下和我们息息相关的Webpack构建(每一次构建静态资源名称的hash值都会变化),它其实就跟缓存相关。有兴趣的同学可以查看张云龙的博客大公司里怎样开发和部署前端代码?。
缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商。
Expires(HTTP1.0):Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差。另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
缺点:使用的是绝对时间,如果服务端和客户端的时间产生偏差,那么会导致命中缓存产生偏差。
Pragma(HTTP1.0):HTTP1.0时的遗留字段,当值为"no-cache"时强制验证缓存,Pragma禁用缓存,如果又给Expires定义一个还未到期的时间,那么Pragma字段的优先级会更高。服务端响应添加'Pragma': 'no-cache',浏览器表现行为和刷新(F5)类似。
Cache-Control(HTTP1.1):有很多属性,不同的属性代表的意义也不同:
private:客户端可以缓存
public:客户端和代理服务器都可以缓存
max-age=t:缓存内容将在t秒后失效
no-cache:需要使用协商缓存来验证缓存数据
no-store:所有内容都不会缓存
请注意no-cache指令很多人误以为是不缓存,这是不准确的,no-cache的意思是可以缓存,但每次用应该去想服务器验证缓存是否可用。no-store才是不缓存内容。当在首部字段Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。命中强缓存的表现形式:Firefox浏览器表现为一个灰色的200状态码。Chrome浏览器状态码表现为200 (from disk cache)或是200 OK (from memory cache)。
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可。
如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified
if-Unmodified-Since: 从某个时间点算起, 是否文件没有被修改,使用的是相对时间,不需要关心客户端和服务端的时间偏差。
如果没有被修改:则开始`继续'传送文件,服务器返回: 200 OK
如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误)
这两个的区别是一个是修改了才下载一个是没修改才下载。如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag。
Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)
If-Match:条件请求,携带上一次请求中资源的ETag,服务器根据这个字段判断文件是否有新的修改
If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。
不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304.
但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。
浏览器地址栏中写入URL,回车浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿(最快)
F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就胆胆襟襟的发送一个请求带上If-Modify-since
Ctrl+F5告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作
对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略
对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新
对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件
小提示:如果做过类似优化的同学,可能就比较好回答,没有做过类似优化的同学可以重点讲解一下懒加载(当然我这里被面试官追问过懒加载的Webpack配置问题)。同时不知道使用Vue技术栈的同学们有没有仔细观察过Vue CLI 3构建的html文件中的link标签的rel属性。
Vue-Router路由懒加载(利用Webpack的代码切割)
使用CDN加速,将通用的库从vendor进行抽离
Nginx的gzip压缩
Vue异步组件
服务端渲染SSR
如果使用了一些UI库,采用按需加载
Webpack开启gzip压缩
如果首屏为登录页,可以做成多入口
Service Worker缓存文件处理
使用link标签的rel属性设置 prefetch(这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低,prefetch通常用于加速下一次导航)、preload(preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度)
全局变量
环境变量
自己HACK一个@符号,指向特定的路径
HACK require
方法
这种问题还是附上参考链接
https://segmentfault.com/a/1190000010998044
http://chashaobao.net/2017/09/03/alias-require-hack/
https://www.zhihu.com/question/26621212
小提示:同类型的问题还可以是原型链、继承、闭包等,这种概念性的问题你肯定不是一句两句能说清楚的,建议在理解之后自己尝试总结一下,如何把重要的知识点用简短的话语说明白。
了解作用域链之前我们要知道一下几个概念:
函数的生命周期
变量和函数的声明
Activetion Object(AO)、Variable Object(VO)
函数的生命周期:
创建:JS解析引擎进行预解析,会将函数声明提前,同时将该函数放到全局作用域中或当前函数的上一级函数的局部作用域中。
执行:JS引擎会将当前函数的局部变量和内部函数进行声明提前,然后再执行业务代码,当函数执行完退出时,释放该函数的执行上下文,并注销该函数的局部变量。
变量和函数的声明:如果变量名和函数名声明时相同,函数优先声明。
Activetion Object(AO)、Variable Object(VO):
AO:Activetion Object(活动对象)
VO:Variable Object(变量对象)
VO对应的是函数创建阶段,JS解析引擎进行预解析时,所有的变量和函数的声明,统称为Variable Object。该变量与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。VO是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
变量 (var, 变量声明);
函数声明 (FunctionDeclaration, 缩写为FD);
函数的形参
AO对应的是函数执行阶段,当函数被调用执行时,会建立一个执行上下文,该执行上下文包含了函数所需的所有变量,该变量共同组成了一个新的对象就是Activetion Object。该对象包含了:
函数的所有局部变量
函数的所有命名参数
函数的参数集合
函数的this指向
作用域链:
当代码在一个环境中创建时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。如果是函数执行阶段,那么将其activation object(AO)作为作用域链第一个对象,第二个对象是上级函数的执行上下文AO,下一个对象依次类推。
在《JavaScript深入之变量对象》中讲到,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
null
和undefined
有什么区别么小提示:如果面试者使用的是Vue技术栈,那么响应式原理是一个必问的问题,同时面试官经常也会问Vue 3.0在响应式原理上的优化方案。
如果对于响应式原理不是很清楚可以查看我之前写的文章基于Vue实现一个简易MVVM/数据劫持的实现。
小提示:这个题目问到的概率还是蛮大的,这里面试官询问了我浏览器端和Node端的Event Loop有什么不同点。如果想要知道更多浏览器端的Event Loop机制可以查看我之前写的文章你真的理解$nextTick么/JS引擎线程和事件触发线程/事件循环机制。
事件触发线程管理的任务队列是如何产生的呢?事实上这些任务就是从JS引擎线程本身产生的,主线程在运行时会产生执行栈,栈中的代码调用某些异步API时会在任务队列中添加事件,栈中的代码执行完毕后,就会读取任务队列中的事件,去执行事件对应的回调函数,如此循环往复,形成事件循环机制。JS中有两种任务类型:微任务(microtask)和宏任务(macrotask),在ES6中,microtask称为 jobs,macrotask称为 task:
宏任务:script (主代码块)、setTimeout
、setInterval
、setImmediate
、I/O 、UI rendering
微任务:process.nextTick
(Nodejs) 、Promise
、Object.observe
、MutationObserver
Node.js中Event Loop和浏览器中Event Loop有什么区别
┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 NextTickQueue 以及 MicroTaskQueue 的回调
│ ┌──────────┴────────────┐
│ │ pendingcallbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 NextTickQueue 以及 MicroTaskQueue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 NextTickQueue 以及 MicroTaskQueue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 closecallbacks、timers、setImmediate)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 NextTickQueue 以及 MicroTaskQueue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 NextTickQueue 以及 MicroTaskQueue 的回调
│ ┌──────────┴────────────┐
└──┤ closecallbacks │<————— socket.on('close', ...)
└───────────────────────┘
Node.js中宏任务分成了几种类型,并且放在了不同的task queue里。不同的task queue在执行顺序上也有区别,微任务放在了每个task queue的末尾:
setTimeout/setInterval
属于 timers 类型;
setImmediate
属于 check 类型;
socket 的 close 事件属于 close callbacks 类型;
其他 MacroTask 都属于 poll 类型。
process.nextTick
本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;
所有 MicroTask 的执行时机在不同类型的 MacroTask 切换后。
idle/prepare 仅供内部调用,我们可以忽略。
pending callbacks 不太常见,我们也可以忽略。
浏览器使用流式布局模型 (Flow Based Layout)
浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree
有了RenderTree就能知道所有节点的样式,计算节点在页面上的大小和位置,把节点绘制到页面上
由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,通常需要多次计算且要花费3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一
浏览器渲染过程如下:
解析HTML,生成DOM树
解析CSS,生成CSSOM树
将DOM树和CSSOM树结合,生成渲染树(Render Tree)
Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会会写一篇博客)
何时发生回流:
添加或删除可见的DOM元素
元素的位置发生变化
元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
页面一开始渲染的时候(这肯定避免不了)
浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
何时发生重绘(回流一定会触发重绘):
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化,浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。你访问以下属性或方法时,浏览器会立刻清空队列:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
width
、height
getComputedStyle()
getBoundingClientRect()
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,**最好避免使用上面列出的属性,他们都会刷新渲染队列。**如果要使用它们,最好将值缓存起来。
CSS:
避免使用table布局。
尽可能在DOM树的最末端改变class。
避免设置多层内联样式。
将动画效果应用到position
属性为absolute
或fixed
的元素上
避免使用CSS表达式(例如:calc()
)
CSS3硬件加速(GPU加速)
JavaScript:
避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性
避免频繁操作DOM,创建一个documentFragment
,在它上面应用所有DOM操作,最后再把它添加到文档中
也可以先为元素设置display: none
,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
小提示:进入现场面试需要注意好好准备自己的简历,面试官一般会根据项目进行问答。
一开始面试官就发了两张笔试题试卷,总共四道题目,大致考了以下知识点:
作用域
原型链(例如实例属性和原型属性一样,删除实例属性后可以继续访问原型属性问题)
宏任务和微任务的打印顺序
Array.prototype.map
的第二个参数
答完试卷面试官就开始问简历上的一些项目,我记得其中几个问题如下(事实上他问的一些问题和简历不是很相关):
你们产品的服务器部署在哪里
你是如何实现一个Tooltip组件的,能写一下怎么使用这个组件么(这算什么问题...)
我认识你们海康的一些开发,我知道你们的产品按套数卖的...
我当场就感受到了面试官问的问题很敷衍,可能他觉得我的简历不够好,又或者觉得我能力不行,接下来面试官又让我做了一道算法题...
1块、4块、5块,求总数n块的最小硬币数
当时没做出来,非科班出身可能做这些确实有些困难,也没有系统的学习,面试官看我很困难的样子,就换了一道题。
1、1、2、3、5、8...计算第n个数的值(斐波那契数列)
这道题还是做出来了,毕竟比较简单,然后面试官说今天先到这里,面试结果会在一星期内通知,然后回来的那天晚上就收到了面试没过的通知。
还是蛮感谢这次现场面试的经历,让我知道如果自身不够硬,到哪里都会很被动。面试的好处不仅仅在于检验自己到底有多少能力,更应该发现自身的不足,同时不断的去弥补这些不足。于是我再次捧起之前搁置的《算法导论》,并且创建了一个算法学习演示文档I-Algorithms,希望可以简化《算法导论》的一些理论知识,使大家对于算法的学习可以变得更加系统全面和简单,也希望通过这个学习使得算法面试会变得更加得心应手,希望感兴趣的同学可以star一下。
小提示:这里我简单讲解了一下Vue中的
v-html
防范XSS攻击。
XSS,即 Cross Site Script,中译是跨站脚本攻击;其原本缩写是 CSS,但为了和层叠样式表(Cascading Style Sheet)有所区分,因而在安全领域叫做 XSS。
XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
攻击者对客户端网页注入的恶意脚本一般包括 JavaScript,有时也会包含 HTML 和 Flash。有很多种方式进行 XSS 攻击,但它们的共同点为:将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM。
反射型 XSS 只是简单地把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接(攻击者可以将恶意链接直接发送给受信任用户,发送的方式有很多种,比如 email, 网站的私信、评论等,攻击者可以购买存在漏洞网站的广告,将恶意链接插入在广告的链接中),或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。最简单的示例是访问一个链接,服务端返回一个可执行脚本:
const http = require('http');
functionhandleReequest(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
res.write('');
res.end();
}
const server = new http.Server();
server.listen(8001, '127.0.0.1');
server.on('request', handleReequest);
存储型 XSS 会把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。比较常见的一个场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码:
// 例如在评论中输入以下留言
// 如果请求这段留言的时候服务端不做转义处理,请求之后页面会执行这段恶意代码
基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击:
XSS:
Submit
点击 Submit 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:
onclick=alert(/xss/)
用户提交之后,页面代码就变成了:
testLink
此时,用户点击生成的链接,就会执行对应的脚本。
HttpOnly 防止劫取 Cookie:HttpOnly 最早由微软提出,至今已经成为一个标准。浏览器将禁止页面的Javascript 访问带有 HttpOnly 属性的Cookie。上文有说到,攻击者可以通过注入恶意脚本获取用户的 Cookie 信息。通常 Cookie 中都包含了用户的登录凭证信息,攻击者在获取到 Cookie 之后,则可以发起 Cookie 劫持攻击。所以,严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
输入检查:不要相信用户的任何输入。对于用户的任何输入要进行检查、过滤和转义。建立可信任的字符和 HTML 标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。在 XSS 防御中,输入检查一般是检查用户输入的数据中是否包含 <,> 等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。而在一些前端框架中,都会有一份 decodingMap, 用于对用户输入所包含的特殊字符或标签进行编码或过滤,如 <,>,script,防止 XSS 攻击:
// vuejs 中的 decodingMap// 在 vuejs 中,如果输入带 script 标签的内容,会直接过滤掉const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
'
': '\n'
}
输出检查:用户的输入会存在问题,服务端的输出也会存在问题。一般来说,除富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。例如利用 sanitize-html 对输出内容进行有规则的过滤之后再输出到页面中。
CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。Cookie 主要用于以下三个方面:
会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
个性化设置(如用户自定义设置、主题等)
浏览器行为跟踪(如跟踪分析用户行为等)
而浏览器所持有的 Cookie 分为两种:
Session Cookie(会话期 Cookie):会话期 Cookie 是最简单的Cookie,它不需要指定过期时间(Expires)或者有效期(Max-Age),它仅在会话期内有效,浏览器关闭之后它会被自动删除。
Permanent Cookie(持久性 Cookie):与会话期 Cookie 不同的是,持久性 Cookie 可以指定一个特定的过期时间(Expires)或有效期(Max-Age)。
res.setHeader('Set-Cookie', ['mycookie=222', 'test=3333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);
上述代码创建了两个 Cookie:mycookie 和 test,前者属于会话期 Cookie,后者则属于持久性 Cookie。
使登录用户访问攻击者的网站,发起一个请求,由于 Cookie 中包含了用户的认证信息,当用户访问攻击者准备的攻击环境时,攻击者就可以对服务器发起 CSRF 攻击。
在这个攻击过程中,攻击者借助受害者的 Cookie 骗取服务器的信任,但并不能拿到 Cookie,也看不到 Cookie 的内容。而对于服务器返回的结果,由于浏览器同源策略的限制,攻击者也无法进行解析。(攻击者的网站虽然是跨域的,但是他构造的链接是源网站的,跟源网站是同源的,所以能够携带cookie发起访问)。
但是攻击者无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。例如删除数据、修改数据,新增数据等,无法获取数据。
验证码:验证码被认为是对抗 CSRF 攻击最简洁而有效的防御方法。从上述示例中可以看出,CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。但验证码并不是万能的,因为出于用户考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。
Referer Check:根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的"源"。
添加token验证:要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
小提示:这道题是给自己挖了一个坑,抱着学习的心态尝试使用Graphql技术,却没有好好理解是在什么场景下为了解决什么问题才应该使用,也没有好好准备如何描述新技术,往往这种不熟悉的技术自己在简历中应该留存一些心眼,尽量不要提,否则答不上来会很尴尬,让面试官怀疑你的项目成分。
GraphQL是一种API查询语言。API接口的返回值可以从静态变为动态,即调用者来声明接口返回什么数据,可以进一步解耦前后端。在Graphal中,预先定义Schema和声明Type来达到动态获取接口数据的目的:
对于数据模型的抽象是通过Type来描述的
对于接口获取数据的逻辑是通过Schema来描述的
接口数量众多维护成本高
接口扩展成本高
接口响应的数据格式无法预知
减少无用数据的请求, 按需获取
强类型约束(API的数据格式让前端来定义,而不是后端定义)
Type简单可以分为两种,一种叫做Scalar Type(标量类型),另一种叫做Object Type(对象类型):
Scalar Type(标量类型):内建的标量包含,String、Int、Float、Boolean、Enum
Object Type(对象类型):感觉类似于TypeScript的接口类型
Type Modifier(类型修饰符):用于表明是否必填等
定义了字段的类型、数据的结构,描述了接口数据请求的规则
查询类型:query(查询)、mutation(更改)和subscription(订阅)
query(查询):当获取数据时,应当选取Query类型
mutation(更改):当尝试修改数据时,应当使用mutation类型
subscription(订阅):当希望数据更改时,可以进行消息推送,使用subscription类型
提供相关Query所返回数据的逻辑。Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来。解析的过程可能是递归的,只要遇到非标量类型,会尝试继续解析,如果遇到标量类型,那么解析完成,这个过程叫做解析链。
$nextTick
的实现原理小提示:如果面试者使用的是Vue技术栈,那么
$nextTick
的原理是一个高频问题,面试者借此可以追问的东西较多,例如浏览器的Event Loop、微任务和宏任务、Node.js的Event Loop、异步更新DOM(响应式的数据for循环改变了1000次为什么视图只更新了一次)、$nextTick
历史版本问题等等。
这个如果不是很清楚的具体可查看我之前写的文章你真的理解$nextTick么。
若是行内元素,给其父元素设置text-align:center
即可实现行内元素水平居中
若是块级元素,该元素设置margin:0 auto
即可(元素需要定宽)
若是块级元素,设置父元素为flex布局,子元素设置margin:0 auto
即可(子元素不需要定宽)
使用flex 2012年版本布局,可以轻松的实现水平居中,子元素设置如下:
// flex容器
// flex项目
.box {
width: 200px;
height: 200px;
display: flex;
// 使内部的flex项目水平居中
justify-content: center;
background-color: pink;
}
/* .box-center {
width: 50%;
background-color: greenyellow;
} */
使用绝对定位和CSS3新增的属性transform
(这个属性还和GPU硬件加速、固定定位相关)
.box {
width: 200px;
height: 200px;
position: relative;
background-color: pink;
}
.box-center {
position: absolute;
left:50%;
// width: 50%;
height: 100%;
// 通过 translate() 方法,元素从其当前位置移动,根据给定的 left(x 坐标) 和 top(y 坐标) 位置参数:
// translate(x,y) 定义 2D 转换。
// translateX(x) 定义转换,只是用 X 轴的值。
// translateY(y) 定义转换,只是用 Y 轴的值。
// left: 50% 先整体向父容器的左侧偏移50%,此时是不能居中的,因为元素本身有大小
// 接着使用transform使用百分比向左偏移本身的宽度的一半实现水平居中(这里的百分比以元素本身的宽高为基准)
transform:translate(-50%,0);
background-color: greenyellow;
}
使用绝对定位和margin-left
(元素定宽)
.box {
width: 200px;
height: 200px;
position: relative;
background-color: pink;
}
.box-center {
position: absolute;
left:50%;
height: 100%;
// 类似于transform// width: 50%;// margin-left: -25%;
width: 100px;
margin-left: -50px;
background-color: greenyellow;
}
若元素是单行文本, 则可设置line-height
等于父元素高度
若是块级元素,设置父元素为flex布局,子元素设置margin: auto 0
即可(子元素不需要定宽)
若元素是行内块级元素,基本思想是使用display: inline-block, vertical-align: middle
和一个伪元素让内容块处于容器中央:
.box {
height: 100px;
}
.box::after, .box-center{
display:inline-block;
vertical-align:middle;
}
.box::after{
content:'';
height:100%;
}
可用 vertical-align
属性(vertical-align
只有在父层为 td 或者 th 时才会生效,,对于其他块级元素,例如 div、p 等,默认情况是不支持的),为了使用vertical-align
,我们需要设置父元素display:table
, 子元素 display:table-cell;vertical-align:middle
:
.box {
height: 100px;
display: table;
}
.box-center{
display: table-cell;
vertical-align:middle;
}
可用 Flex 2012版, 这是CSS布局未来的趋势。Flexbox是CSS3新增属性,设计初衷是为了解决像垂直居中这样的常见布局问题:
.box {
height: 100px;
display: flex;
align-items: center;
}
优点:内容块的宽高任意, 优雅的溢出. 可用于更复杂高级的布局技术中. 缺点:IE8/IE9不支持、需要浏览器厂商前缀、渲染上可能会有一些问题。
可用 transform
,设置父元素相对定位:
.box {
height: 100px;
position: relative;
background-color: pink;
}
.box-center {
position: absolute;
top: 50%;
transform: translate(0, -50%);
background-color: greenyellow;
}
缺点:IE8不支持, 属性需要追加浏览器厂商前缀,可能干扰其他 transform
效果,某些情形下会出现文本或元素边界渲染模糊的现象。
设置父元素相对定位,子元素如下css样式:
.box {
position:relative;
height: 100px;
background-color: pink;
}
.box-center{
position:absolute;
top:50%;
// 注意不能使用百分比// margin的百分比计算是相对于父容器的width来计算的,甚至包括margin-top和margin-bottom
height: 50px;
margin-top: -25px;
}
设置父元素相对定位, 子元素如下css样式:
.box {
position:relative;
width: 200px;
height: 200px;
background-color: pink;
}
.box-center{
position:absolute;
top: 0;
bottom: 0;
margin: auto 0;
height: 100px;
background-color: greenyellow;
}
Flex布局(子元素是块级元素)
.box {
display: flex;
width: 100px;
height: 100px;
background-color: pink;
}
.box-center{
margin: auto;
background-color: greenyellow;
}
```css
- Flex布局
```css
.box {
display: flex;
width: 100px;
height: 100px;
background-color: pink;
justify-content: center;
align-items: center;
}
.box-center{
background-color: greenyellow;
}
绝对定位实现(定位元素定宽定高)
.box {
position: relative;
height: 100px;
width: 100px;
background-color: pink;
}
.box-center{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin: auto;
width: 50px;
height: 50px;
background-color: greenyellow;
}
小提示:如果在项目中使用过,可简单介绍一下自己使用Flex解决过什么问题,这里我在项目中印象比较深刻的是使用Flex解决上面内容高度不固定,下面内容高度自动撑满父容器剩余高度的问题。
如果不是很清楚Flex,可以查看阮一峰的文章Flex 布局教程:语法篇。面试官追问,那么除了Flex,你还知道Grid么?这个由于兼容性问题,我一直没有好好研究过,这里可查看阮一峰的文章CSS Grid网格布局教程。
bind
的源码实现小提示:这里我回答使用函数柯里化加上
apply
或者call
可实现bind
,面试官追问了一些具体的实现细节。
后来我自己粗糙的实现了一下,仅供参考:
Function.prototype.myCall = function (obj) {
obj.fn = thislet args = [...arguments].splice(1)
let result = obj.fn(...args)
delete obj.fn
return result
}
Function.prototype.myApply = function (obj) {
obj.fn = thislet args = arguments[1]
let result
if (args) {
result = obj.fn(...args)
} else {
result = obj.fn()
}
delete obj.fn
return result
}
Function.prototype.myBind = function (obj) {
let context = obj || windowlet _this = thislet _args = [...arguments].splice(1)
returnfunction () {
let args = arguments// 产生副作用// return obj.fn(..._args, ...args)return _this.apply(context, [..._args, ...args])
}
}
functionmyFun (argumentA, argumentB) {
console.log(this.value)
console.log(argumentA)
console.log(argumentB)
returnthis.value
}
let obj = {
value: 'ziyi2'
}
console.log(myFun.myCall(obj, 11, 22))
console.log(myFun.myApply(obj, [11, 22]))
console.log(myFun.myBind(obj, 33)(11, 22))
小提示:这个问题我当时懵了一下,一下子没反应过来面试官想要问什么,就答了这两者在CSS优先级上有区别,然后由于遇到不会的问题有些紧张就多说了一些废话,但显然这不是面试官想要的答案并且消耗了面试官面试的耐心,说他问的不是这个。这里再次提示大家,如果你感觉你说不清楚,但是你又知道一点,我建议你说不知道,不要纠结,面试官不会因为你不知道一个问题就PASS你,相反你说了一些无关紧要的废话,反而在消耗面试官的耐性,增加负面印象。
伪类和伪元素是用来修饰不在文档树中的部分,比如,一句话中的第一个字母,或者是列表中的第一个元素。下面分别对伪类和伪元素进行解释:
伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过:hover来描述这个元素的状态。虽然它和普通的css类相似,可以为已有的元素添加样式,但是它只有处于dom树无法描述的状态下才能为元素添加样式,所以将其称为伪类。
伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。
伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档树外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素。
CSS3规范中的要求使用双冒号(::)表示伪元素,以此来区分伪元素和伪类,比如::before和::after等伪元素使用双冒号(::),:hover和:active等伪类使用单冒号(:)。除了一些低于IE8版本的浏览器外,大部分浏览器都支持伪元素的双冒号(::)表示方法。
对于滴滴的这次面试,我感觉到自己准备的不是很充分,尤其是自己简历上的项目技术Graphql。同时对于自己不会的题目强行做了一些解释说明,其实应该简洁明了的告诉面试官不会。
小提示:这里我说了很多,从借用构造函数到组合继承到寄生组合继承,但面试官其实最想听到的是寄生组合继承。面试官还追问我具体要如何实现寄生组合继承。当然这里其实问的问题还可以很多,比如ES6的类继承和ES5中的继承有什么区别。
如果对于继承以及继承的区别不是很清楚的,可以随便看看我之前写的大笔记js类和继承。
小提示:这个建议大家好好回忆一下,例如子元素是相对父元素的padding、border还是content进行定位之类的,当时面试官问的就这么细。
小提示:面试官只是问了一下具体的使用场景,没有问实现原理。
functiondebounce (fn, wait = 1000) {
let timeOutId
returnfunction () {
let context = thisif (timeOutId) {
clearTimeout(timeOutId)
}
timeOutId = setTimeout(() => {
fn.apply(context, arguments)
}, wait)
}
}
functiondebounceImmediate (fn, wait = 1000, immediate) {
let timeOutId, context, args
const later = (immediate) => setTimeout(() => {
if (!immediate) {
fn.apply(context, args)
timeOutId = context = args = null
}
}, wait)
returnfunction () {
if (!timeOutId) {
timeOutId = later(true)
if (immediate) {
fn.apply(this, arguments)
}
context = this
args = arguments
} else {
clearTimeout(timeOutId)
timeOutId = later(false)
}
}
}
functionthrottle (fn, wait) {
let timeoutId = nullreturnfunction () {
let context = thisif (!timeoutId) {
timeoutId = setTimeout(() => {
fn.apply(context, arguments)
timeoutId = null
}, wait)
}
}
}
小提示:这个问题面试官问的很细,绝对是想问你是否阅读过源码。他首先问computed的实现原理,其次问了这样一个问题:现在有两个computed计算值,其中一个computed计算值为什么可以依赖另外一个computed计算值。这里顺便将watch的实现原理也贴上。
watch的分类:
deep watch(深层次监听)
user watch(用户监听)
computed watcher(计算属性)
sync watcher(同步监听)
watch实现过程:
watch的初始化在data初始化之后(此时的data已经通过Object.defineProperty
的设置成响应式)
watch的key会在Watcher里进行值的读取,也就是立马执行get获取value(从而实现data对应的key执行getter实现对于watch的依赖收集),此时如果有immediate
属性那么立马执行watch对应的回调函数
当data对应的key发生变化时,触发user watch实现watch回调函数的执行
computed的属性是动态挂载到vm实例上的,和普通的响应式数据在data里声明不同
设置computed的getter,如果执行了computed对应的函数,由于函数会读取data属性值,因此又会触发data属性值的getter函数,在这个执行过程中就可以处理computed相对于data的依赖收集关系了
首次计算computed的值时,会执行vm.computed属性对应的getter函数(用户指定的computed函数,如果没有设置getter,那么将当前指定的函数赋值computed属性的getter),进行上述的依赖收集
如果computed的属性值又依赖了其他computed计算属性值,那么会将当前target暂存到栈中,先进行其他computed计算属性值的依赖收集,等其他计算属性依赖收集完成后,在从栈中pop出来,继续进行当前computed的依赖收集
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
returnthis.firstName + ' ' + this.lastName
}
}
})
由于 this.firstName
和 this.lastName
(上面是Vue官方示例)都是响应式变量,因此会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候Dep.target
就是这个 computed watcher,具体步骤如下:
data 属性初始化 getter setter
computed 计算属性初始化,提供的函数将用作属性 vm.fullName
的 getter
当首次获取 fullName
计算属性的值时,Dep 开始依赖收集
在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定firstName
和lastName
为fullName
的依赖,并建立依赖关系
当firstName
或lastName
发生变化时,根据依赖关系,触发 fullName
的重新计算
如果计算值没有发生变化,不会触发视图更新
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。
小提示:这个问题当时完全不知道,哎,官方源码的套路太深了......
这里希望有大神可以补充说明一下。
beforeCreated
和created
中间都做了什么)
初始化 data
、props
、computed
、watcher
、provide
。官方源码具体位置src/core/instance/init.js
:
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
51信用卡的这次面试其实面试官考察的点还是蛮深入的,问了一些Vue底层源码的实现,总体感觉自己回答的还可以,但是面试官说:你应该去阿里...
小提示:这个直接回答不知道,问题较大,我这里猜测一下是类似Babel和AST抽象语法树相关,有空去看下源码。
这个问题希望同学可以补充一下。
小提示:这个问题在回答懒加载的过程中,面试官追问懒加载的Webpack配置,我说了和代码切割相关。
关于懒加载,这里推荐一篇非常好的文章:Webpack 大法之 Code Splitting。
require
和module.exports
)小提示:这个问题其实是非常常见的问题,建议大家阅读一下源码,有些也可能会问一下比较简单的问题,例如
module.exports
和exports
的区别,或者也可能问CommonJS引入和ES6引入的区别。
小提示:这个问题是个大坑阿,我这里直接回答我什么都不擅长,这样回答显然面试官是不会不满意的,建议大家在面试前好好想想自己到底擅长啥。
小提示:这里React真的好久没用了,几乎忘记了,大致说了下单向数据流、双向数据绑定、数据监听方式、JSX以及Vue的单文件组件、函数式编程、Vue的指令之类的。
这个问题希望同时熟悉React和Vue的同学可以补充一下。
这个问题希望同学可以补充一下。
小提示:面试官这里应该想问DOM渲染的过程中可能有哪些情况会阻塞渲染。我当时回答不知道。
这个问题希望同学可以补充一下。
小提示:回答了宏任务和微任务。
小提示:这个问题我专门发了一篇掘金文章,但是很多人好像都不是很感兴趣的样子,但是面试官真的就问了这样一个问题。
这里推荐我之前写的掘金文章基于Vue实现一个简易MVVM/MV*设计模式的演变历史,一开始重点讲解了MVC、MVP以及MVVM的演变过程和区别。
小提示:真的忘记的差不多了,就简单说了只能在同一层叠上下文中进行
z-index
值比较、和绝对定位的关系,z-index
值不需要设置过大,只需要理清楚层级关系即可。面试官追问了z-index
值和background
的覆盖关系,还追问了绝对定位元素以及后来居上的准则。面试官还问了z-index默认值是什么,0
和auto
有没有区别?真的对于CSS可能平常就用的不多,所以这个问题答的不是很好。
可能面试官最想知道的是下面这张图:
这里附上张鑫旭的文章深入理解CSS中的层叠上下文和层叠顺序。
这里由于回答了定位,面试官追问固定定位的元素是相对于什么进行定位?相对定位会脱离正常文档流么?绝对定位是相对于什么元素进行定位?
小提示:CSS3动画硬件加速?CSS3动画的性能问题(重绘和重流,是否需要脱离正常文档流)?这个我当时答不知道,确实平常用的很少,如果熟悉Vue过渡动画的同学可以讲讲过渡动画?
小提示:我的回答:地图算么?基于OpenLayers设计过地图的Vue组件库。
对于可视化希望同学可以补充一下。
小提示:这个问题简直就是给人挖坑。
这里简单实现一下(其实应该使用flex-basis
属性):
小提示:面试官追问事件委托有什么优点(起码两个以上)、
target
/currentTarget
/relateTarget
具体指向什么目标。
小提示:这个如果做过什么规范或者开发工具之类的,应该比较好回答。
总体来说这次面试面得很细,有些知识点已经忘记,建议大家面试前把一些感觉不是很熟悉的原生知识点回忆起来,尤其是在开发中都不怎么会使用一些CSS样式设计的童鞋(现在很多都是组件库的设计方案,样式早已经封装掉了)。
小提示:当时直接回答不知道,确实Webpack我只会用,还没了解过内部的实现原理和构成。这个后续无论如何都要好好理解一下原理。
这个问题希望同学可以补充一下。
小提示:工作中没有遇到过需要上传下载大型文件,所以这个问题当时老老实实回答不知道。具体应该和断点续传相关,可能也需要回答一些
range
的头部信息等。
小提示:好久没用过React了,大致只知道Racct是单向数据流的,利用高阶组件可以实现类似于Vue的双向数据绑定。
这个问题希望同学可以补充一下。
小提示:当时怕说错,老老实实回答不知道。后来查了一下应该和缓存以及HTTP请求拦截相关。
这个问题希望同学可以补充一下。
小提示:只知道上传的头信息是
application/x-www-form-urlencoded
,也可以对上传的文件的数据进行拦截处理,例如对上传文件的信息进行加密处理。
这个问题希望同学可以补充一下。
其实这一次面试自己感觉面试的不是很好(尽管面试官问的确实比我上面列出的问题多),因为有好几个问题自己确实不清楚。这里再次建议大家不知道就是回答不知道,这样不会对面试官造成一些负面印象。这一次面试能够通过运气占了很大一部分。
小提示:当时面试官问的蛮好玩的,他问从开始写一个.vue文件开始到DOM渲染到页面上,Vue做了哪些工作。然后我当时没理解面试官是要问vue-loader?DOM树的渲染过程?来来回回试探性的问了面试官几次,才理解原来面试官想知道Vue源码的整个实现过程。
大家如果想了解Vue源码实现的整个粗略过程,可以看下之前写的文章基于Vue实现一个简易MVVM/Vue的运行机制简述。
小提示:由于这边涉及到一些海康的设备(上下位机通信),面试官问我如何知道上位机软件给下位机设备发送了5次信息。这个其实大部分Web前端开发在工作上很难遇到类似的问题,辛亏我以前毕业设计中做过上下位机的TCP通讯。后来我从Leader面那里了解到二面面试官应该是做iot物联网开发这一块的。
请求帧数据结构如下:
小提示:这个问题当时回答不知道,其实后面想想最简单的办法是先找出广告元素的一些通用特性,然后在Chrome插件中通过注入脚本的形式将这些广告元素隐藏掉。
这里不知道有没有更好的其他方式,例如不知道Service Work对请求拦截处理是否可以有效屏蔽广告等,这个问题希望同学可以补充一下。
小提示:这里需要分基本类型和引用类型,面试官在这里具体想问的是
Object.is
的实现原理。这是面试官问我的第一个问题,当时直接回答不知道,内心都觉得接下来要凉凉了。
小提示:这里问的是Vue源码对于视图更新的优化。我这里的回答是乱糟糟的,希望有同学能够给出一个精准并且简短的回答。
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then
和 MessageChannel
,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
另外,关于waiting
变量,这是很重要的一个标志位,它保证flushSchedulerQueue
回调($nextTick中执行)允许被置入callbacks
一次。
因为Vue的事件机制是通过事件队列来调度执行,会等主进程执行空闲后进行调度,所以先会去等待所有的同步代码执行完成之后再去一次更新。这样的性能优势很明显,比如:
现在有这样的一种情况,mounted
的时候test
的值会被循环执行++1000次。每次++时,都会根据响应式触发setter->Dep->Watcher->update->run
。如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。所以Vue实现了一个queue队列,在下一个tick(或者是当前tick的微任务阶段)统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对的DOM的0变成1000。保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick(或者是当前tick的微任务阶段)的时候调用,大大优化了性能。
执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理(保证需要更新DOM或者Watcher视图更新的方法flushSchedulerQueue只会被推入异步执行的$nextTick回调数组一次) -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)->
Vue是异步更新Dom的,Dom的更新放在下一个宏任务或者当前宏任务的末尾(微任务)中进行执行
由于VUE的数据驱动视图更新是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。在同一事件循环中的数据变化后,DOM完成更新,立即执行nextTick(callback)
内的回调。
vue和react一样,对dom的修改都是异步的。它会在队列里记录你对dom的操作并进行diff操作,后一个操作会覆盖前一个,然后更新dom。
小提示:我猜这里面试官想问的是Grid,当时说不知道。
小提示:感谢CBU技术部的面试官。
一旦给元素加上absolute
或float
就相当于给元素加上了display:block
absolute
元素覆盖正常文档流内元素(不用设z-index,自然覆盖)
可以减少重绘和回流的开销(如absolute+ top:-9999em
,或absolute + visibility:hidden
,将动画效果放到absolute
元素中)
static
,默认值。位置设置为static的元素,它始终会处于文档流给予的位置。
inherit
,规定应该从父元素继承 position 属性的值。但是任何的版本的 Internet Explorer (包括 IE8)都不支持属性值 “inherit”。
fixed
,生成绝对定位的元素。默认情况下,可定位于相对于浏览器窗口的指定坐标。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。不论窗口滚动与否,元素都会留在那个位置。但当祖先元素具有transform
属性且不为none时,就会相对于祖先元素指定坐标,而不是浏览器窗口。
absolute
,生成绝对定位的元素,相对于距该元素最近的已定位的祖先元素进行定位。此元素的位置可通过 “left”、”top”、”right” 以及 “bottom” 属性来规定。
relative
,生成相对定位的元素,相对于该元素在文档中的初始位置进行定位。通过 “left”、”top”、”right” 以及 “bottom” 属性来设置此元素相对于自身位置的偏移。
浮动、绝对定位和固定定位会脱离文档流,相对定位不会脱离文档流,绝对定位相对于该元素最近的已定位的祖先元素,如果没有一个祖先元素设置定位,那么参照物是body层。
绝对定位相对于包含块的起始位置:
如果祖先元素是块级元素,包含块则设置为该元素的内边距边界。
如果祖先元素是行内元素,包含块则设置为该祖先元素的内容边界。
问答题:
定位的元素的起始位置为父包含块的内边距(不会在border里,除非使用负值,会在padding里)
定位的元素的margin还是能起作用的
background属性是会显示在border里的
z-index是有层叠层级的,需要考虑同一个层叠上下文的层叠优先级
z-index是负值不会覆盖包含块的背景色(但是如果有内容,会被包含块的内容覆盖)
z-index的值影响的元素是定位元素以及flex盒子
上面一个定位元素,下面一个正常流的元素,定位元素会覆盖在正常流元素之上,除非给z-index是负值
页面根元素html天生具有层叠上下文,称之为“根层叠上下文”
这一次面试官问我的第一个问题Object.is
就没答上来,不过面试官显然没有因为开头答的不好就否定面试者。大家如果在面试时第一个问题就答不上来,不要慌,要保持良好的心态,把接下来能答的问题好好答上来。可能很多同学会疑问,好像还有好几个问题感觉没答上来,但是可能只要有一个问题答的非常出彩,仍然可以弥补那些没答上来的问题(这里面试官当时说Vue源码的实现过程我说的比较清楚,还没有一个面试者答的比我更清楚的)。
三面是Leader现场面,我当时特别担心有赞二面的情况发生,冷不丁又给你来一道算法题,这些真是我最不擅长的点。因为有点心虚我就问了下在阿里的师兄(师兄可能也做招聘工作,当时还怪我没有找他内推...),他说现场面其实最主要的是好好准备简历上的内容,面试官一般都会根据简历进行问答,还说他当时面试阿里时会让他画一些框架层次图(这个我当时没在意,结果面试官确实让我根据其中某个项目画一个框架层次图)。Leader面的时候在场的有两个面试官和一个HR。
先是进来一个气场很足的Leader,看起来很权威,但是问问题还蛮随意的,就简单的让我介绍一下自己做的项目,然后翻看了我做的一些东西。感觉他好像有点心不在焉,翻看的很随意,我在回答问题的时候用余光关注了一下大佬的表情,感觉他在我项目经历那一块停留了非常长的时间。
我正回答着自己的项目经历,Leader二和HR进来了,等我回答完Leader一就让Leader二开始面我。Leader二就问了我其中的两个项目。问我的第一个项目是自己做的公司内部的工具,他问这个平台有什么可以衡量的数据表明公司内部人员的使用情况。我回答当时因为领导觉得没必要做,就没有做数据统计这一块,告诉了他数据库里的一些真实数据情况。然后他问PV、UV应该怎么统计(我当时还厚脸皮的问他PV和UV是什么)?如果访问的页面出不来PV怎么统计?页面有没有做什么行为监测?页面访问量过大怎么处理?我大致讲了一些我的思路。
接着问我第二个项目(Low Code相关),我就回答了这个项目的技术体系,从以前做了什么到现在做到什么程度,到未来需要做成什么样,统统仔细的说了一遍。Leader二就问我未来做成什么样能不能思考一下怎么做,给了我5分钟的时间(这期间他一直反复的在翻阅我的简历)。然后我就假装思考了5分钟左右,其实脑子里一片空白,当时对于未来要做成什么样还只是个构思。然后Leader二还是很体贴的,他说你可以在墙上画一画(墙上可以写字),我就大致画了画,Leader二问我能不能画一画这个项目的框架层次图,我就简单的画了画...最后Leader二直接说你们做的太Low了,这个(Low Code)在我们这里已经是两年前的技术了...(这个我还是要解释下,我所在的部门从开始用Vue到目前只有短短的两年时间,在这两年时间里技术体系还是飞速的在沉淀和发展,我离开之前已经构思并实现了部分Vue技术栈的Low Code解决方案,如果这方面感兴趣的同学也可以找我沟通)。
Leader二还蛮好玩的,他说Low Code如果真的做出来了,都没前端什么事情了,那你干嘛去?顺着这个问题他还问我未来的前端应该怎么发展?未来前端有哪些可以挖掘的点?我回答了一些Graphql、可视化等,我还说了一个特别搞笑的回答,我说从以往的发展来看,前端应该抢占后端的资源,把后端限制我们的事情让前端也能做,让前端更加解放。Leader二当场就进行了反驳,说是要有价值才做,而不是为了能做而做,吓得我不轻...然后Leader二还详细的跟我解释了未来发展这个问题他希望得到什么回答,当时还是觉得Leader二蛮亲切的。
Leader二问完以后HR就接着问我了以下几个问题:
为什么要离开现在的公司
以前公司的岗位制度是什么样
你是校招进去的么
你现在的岗位等级情况
你的绩效情况
你领导对你的评价是怎么样的
领导是不是经常找你沟通
然后Leader一顺着HR问了一个小问题:
你未来对于你的职业有什么规划
最后问我还有什么想问的,我当时已经被三个人问的有点迷迷糊糊了,然后想了想说没有。
这次现场面其实我感觉自己面得不是很好,总感觉自己要挂了。总共面了将近1个半小时左右,尤其是Leader二的问题很多不是他想要的答案,但是最终居然过了。
企业智能事业部Leader面后又收到了HR面的面试通知,这一轮面试大致问了以下问题:
你为什么要离开现在的公司
你们公司的岗位等级是怎么评定的,你现在是什么岗位等级
谈谈你在公司的绩效情况
你觉得你做的最有成就感的一件事
你一般解决问题的方法有哪些
你是因为什么契机选择做前端
你有对你所在的公司做过什么流程或制度规范上的改进么
你最近在看什么书,和工作相关么,你为什么要看这些书
看到你之前还面试了其他两个部门都挂在了一面,你感觉是什么原因
你期望的薪资待遇是多少
小提示:这里HR会问的其实不止这些问题,例如你为什么喜欢Web前端这个岗位、你未来的职业规划、你觉得你的优点和缺点有哪些、为什么选择阿里巴巴、对之前几个面试官做下评价、你用过阿里的哪些产品顺便谈谈这些产品的优缺点、你对于互联网是怎么理解的...
对于HR面还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等),你觉得你做的最有成就感的一件事(你要是说个简单的,HR会觉得你工作能力不强),你一般解决问题的方法有哪些(HR当然也想考察你解决问题的能力,你要是说什么百度啊之类的HR当然会觉得你解决问题的能力不强),你期望的薪资待遇是多少(你要是不喜欢这家公司,可以期望高一些,你要是很喜欢这家公司面试过程很愉快上浮个30%左右,面试过程一般上浮个20%左右)。