共计 20087 个字符,预计需要花费 51 分钟才能阅读完成。
本文总结了前端老司机经常问题的一些问题并结合个人总结给出了比较详尽的答案。
关于知识点原理和详细,参考讲堂的视频教程 :前端增长 - 重新定义大前端
课程知识在不断更新,本片内容也逐步更新
官方博客:FED123 前端学堂
1. 关于性能优化说说 js 文件摆放顺序、减少请求、雪碧图等等原理,说下 window.performance.timing api 是干什么的?
浏览器是按照文档流解析 html,为了更快构建 DOM 树和渲染树将页面呈现到屏幕上,建议是降 js 放在文档 dom 树结尾,body 标签闭合前。
浏览器并发 HTTP 请求有限制(6 个左右),加载页面 html 后开始解析,解析到外链资源比如 js css 和图片,就会发 http 请求获取对应资源。减少请求就是减少这些资源请求, 可以 css 资源合并,js 资源合并,图片资源合并同时做 lazyload,区分首屏非首屏接口,按需请求数据。
雪碧图是一种图片资源的合并方法,将一些小图片合成一张图,通过 background-position 来定位到对应部分。
window.performance.timing 参考下前端页面性能指标数据计算方法, performance 接口属于 w3c 标准 hight resolution time 中的一部分,通过 navigation timeline api、performance timeline api,user timing api,resource timeline api 这四个接口做了增强实现。其中 navigation timeline api 中 PerformanceTiming 接口数据放在 performance.timing 这个对象上。主要记录了浏览器从跳转开始的各个时间点的时间,比如 navigationStart 是页面开始跳转时间,fetchStart 是页面开始时间,domainLookupStart 是 DNS 开始时间,domainLookupEnd 是 DNS 结束时间,查找到 DNS 后建立 http 链接,connectStart 和 connectEnd 分别是链接开始和结束时间,然后是 requestStart 开始发起请求时间,responseStart 开始响应时间,responseEnd 响应结束时间。然后是苟安 DOM 树时间,分别是 domLoading, domInteractive, domContentLoad 和 domComplete 时间,分别对应 document.readyState 状态 loading、interactive 和 complete。最后是页面 onload,分别是 loadEventStart 和 loadEventEnd 时间节点。
可以通过这个接口统计前端的页面性能数据。
domainLookupStart – fetchStart = appCache 时间,这段时间浏览器首先检查缓存
domainLookupEnd -domainLookupStart = DNS 时间
connectEnd – connectStart = TCP 时间
responseStart – requestStart = FTTB 首字节时间,或者说是服务器响应等待时间
domContentLoad – navigationStart = 页面 pageLoad 时间
loadEventEnd – navigationStart = 页面 onLoad 时间
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
2. 请你描述下一个网页是如何渲染出来的,dom 树和 css 树是如何合并的,浏览器的运行机制是什么,什么是否会造成渲染阻塞?
参考下:浏览器工作原理 浏览器渲染与阻塞原理
第一部分通过 performance.time 这个 api 我们可以了解浏览器加载网页的流程,浏览器边加载 html 边构建 DOM 树,当然会有容错和修正机制。浏览器解析到行内 css 和内联 css 会立马加入到构建渲染树,解析到外链 css 就开始加载,加载完之后也会合并到渲染树的构建中,最后将渲染树和 DOM 做节点链路匹配,也叫 layout 阶段,计算每个 DOM 元素最终在屏幕上显示的大小和位置。遍历顺序为从左至右,从上到下,绘制在屏幕上,layout 过程中可能会触发页面回流和重绘,比如某个外链 css 加载完解析之后合并构建到渲染树中,其中某个 css 改变了 DOM 树种某个元素的定位(改成绝对定位)或者改变了长宽边距等位置信息,会触发重新 layout,所以会回流 reflow。重绘是比如 css 改变了之前的背景图片颜色,浏览器会重新绘制。
会有渲染阻塞,浏览器刷新的频率大概是 60 次 / 秒,也就是说刷新一次大概时间为 16ms,如果浏览器对每一帧的渲染工作超过了这个时间,页面的渲染就会出现卡顿的现象。浏览器内核中有 3 个最主要的线程:JS 线程,UI 渲染线程,事件处理线程。此外还有 http 网络线程,定时器任务线程,文件系统处理线程等等。
JS 线程负责 JS 代码解析编译执行,称为主线程。常说‘浏览器是单线程’指的是 JS 主线程只能有一个,主线程执行同步任务,会阻塞 UI 渲染线程。JS 线程核心是 js 引擎(IE9+: Chakra firefox:monkey chrome:v8)。webworker 可以创建多个 js 线程,但是受主线程控制,主要用于 cpu 密集型计算。
UI 渲染线程当然是负责构建渲染树,执行页面元素渲染。核心是渲染引擎(firefox:gecko、chrome/safari:webkit),由于 JS 可以操作 DOM 元素处理样式等,JS 主线程是执行同步任务的,所以设计上 JS 引擎线程和 GUI 渲染线程是互斥的。也就是说 JS 引擎处于运行状态时,GUI 渲染线程将处于冻结状态。
事件处理线程,由于浏览器是事件驱动的,事件处理线程用来控制事件回调处理,浏览器触发某个事件后会把事件回调函数放到任务队列中,可以看下下面会提到。
其他线程统称工作线程,如处理 ajax 的线程,dom 事件线程、定时器线程、读写文件的线程等,工作线程的任务完成之后,会推入到一个任务队列(task queue)
总结一下,渲染阻塞有两个方面:
js 主线程执行时间长会导致渲染线程阻塞,影响渲染。我们也称为 longtask
渲染线程自身阻塞,渲染时间达不到帧率 60,会看起来卡顿,比如回流或者重绘等,或者 css 效率太低,动画处理不合适,导致渲染耗时
3. 请简述下 js 引擎的工作原理,js 是怎样处理事件的 eventloop,宏任务源 tasks 和微任务源 jobs 分别有哪些?js 是如何构造抽象语法树(AST)的?
js 引擎只执行同步任务, 异步任务会有工作线程来执行,当需要进行异步操作(定时器、ajax 请求、dom 事件注册等),主线程会发一个异步任务的请求,相应的工作线程接受请求;当工作线程完成工作之后,通知主线程;主线程接收到通知之后,会执行一定的操作(回调函数)。主线程和工作线程之间的通知机制叫做事件循环。
调用栈(call stack): 主线程执行时生成的调用栈
任务队列(task queue): 工作线程完成任务后会把消息推到一个任务队列,消息就是注册时的回调函数
当调用栈为空时,主线程会从任务队列里取一条消息并放入当前的调用栈当中执行,主线程会一直重复这个动作直到消息队列为空。这个过程就叫做事件循环(event-loop)。
关于宏任务和微任务,参考 事件流、事件模型、事件循环概念理解?浏览器线程理解与 microtask 与 macrotask
ES6 新引入了 Promise 标准,同时浏览器实现上多了一个 microtask 微任务概念。在 ECMAScript 中,microtask 称为 jobs,macrotask 可称为 task。
macrotask 宏任务 tasks,也就是上面说到的任务队列的任务。执行栈上的每个任务都属于宏任务,主线程执行完执行栈的任务,从任务队列取新的任务。宏任务执行时不会中断,会一次性执行完,为了及时渲染数据,主线程执行完一个宏任务之后,会执行一次渲染。
task–》渲染 –》宏任务 –》渲染 …..
microtask 微任务 jobs,可以看成是插队需要及时处理的任务,会在当前主线程 task 任务执行后,渲染线程渲染之前,执行完当前积累所有的微任务。
task–》jobs –》渲染 –》宏任务 –》jobs –》渲染 …..
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
AST 参考:程序语言进阶之 DSL 与 AST 实战解析
将抽象语法树之前要先了解下 NLP 中文法的概率。任何一种语言,具体说就是 DSL,都有自己的一套文法,用来表示这套语言的逻辑规范。不同的文法写出来的语法表达式也不一样。我们根据语法表达式来解析语言,就可以形成一个 AST 抽象语法树。然后可以作进一步处理。我常用的是 PEG 解析表达式语法。可以很轻松的写出语法的每一条产生式规则,来构造生成 AST。所谓 AST 可以理解成按照一定语法结构组成的词汇流,每个词汇有特定的语法含义,比如说这是一个声明,这个一个操作符等等。
上面这个图是苹果最早做的 KHTML 渲染引擎中的 KJS(javascript 引擎),他是基于 AST 来实现的 JavaScript 语言解析的,先通过词法分析得到 JSTokens 流,然后经过语法分析得到抽象语法树,然后经过字节码生成器,转换成字节码。字节码经过 JavaScript 虚拟机 JIT 编译成机器码,然后执行。这是最初的设计架构,后来苹果公司基于此重构出了 webkit 渲染引擎,google 基于 webkit 单独维护,称为 blink 渲染引擎,chrome 的 JS 引擎改造为 V8 引擎。参考:简述 Chromium, CEF, Webkit, JavaScriptCore, V8, Blink
举个例子常用的 babel 插件的原理就是基于 babylon 词法语法分析器生成抽象语法树,将代码文本转换成按照特定语法组合的 token 流集合,然后经过 babtlon-traverse 这个组件来负责处理遍历语法树,访问每个 token 节点,通过对 token 的处理,可以生成我们需要的 AST 语法树,然后再通过 babylon-generator 这个组件来做代码生成,根据 AST 生成代码。比如可以将 箭头函数 转换成 function 函数。
浏览器中,通过开发者调试工具分析就能看到,下载完 js 脚本后,首先浏览器要先解析代码 =》初始化上下文环境 =》执行代码,整个是 evaluate script 的过程,解析代码的过程也是编译 js 的过程所以看最前面第一步就是 compile script,将 js 代码编译成字节码(这一块涉及到浏览器 js 引擎的优化,v8 引擎是编译成字节码,后面经过 JIT 解析执行(这个参考 你不知道的 LLVM 编译器 可以提升效率做动态优化), 这个类似于 java、C# 这些需要将源代码编译成中间语言,然后在虚拟机执行,javascript 编译成字节码后面也是在虚拟机执行),然后就开始执行脚本。
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
4. 你是否考虑全面你编写的整个函数,或者整个功能的容错性与扩展性?怎样构建一个组件是最合理最科学的,对于错误的处理是否有统一的方式方法?
扩展性主要是从功能上考虑,容错性是从数据上考虑。
设计开发组件的时候首先要设计好数据模型,当然可以和后端共同约定一个标准,后面只要是这部分都用这个标准字段。后面可以对标准字段做扩展,开发时候要做容错和数据响应式开发。
功能这部分其实可以从基础功能和扩展功能来看,基础功能可以在原有组件上做根据数据来展示。扩展功能可以通过组件结合的形式来处理。
我主要考虑的是组件复用,可以将一类组件归类,比如商品卡片,基本都是头图加标题行动点,价格,按钮。这就是最基础的一个组件。扩展性可以通过数据来做响应式的展示,比如新增一个描述,数据模型新增描述字段,有描述字段卡片上就展示描述,没有就不展示。像点击按钮的加购功能可以单独做成功能组件,统一处理,而不放在卡片上。因为这种加购往往附带的是商业逻辑,有很多业务逻辑要处理,独立出来反而更利于维护和拓展。
错误处理我们这边是基于组件的方式来处理,开发一个错误处理的功能组件,提供 thenable 的能力,区分不同的错误类型,提供统一埋点做监控和记录。
5. 浏览器缓存的基本策略,什么时候该缓存什么时候不该缓存,以及对于控制缓存的字段的相关设置是否清楚?
参考下:HTTP 协商缓存 VS 强缓存原理
前面介绍 navigation api 时候介绍了浏览器加载页面的各个关键时间节点。和缓存相关的主要有两部分
appcache,这部分是离线缓存,在 fetchStart 和 domainLookupStart 之间,这部分参考 whatwg 标准已经弃用,建议用 serviceworker。这里也不做介绍。
HTTP 缓存这部分是在 requestStart 开始,发起资源 http 请求开始,这部分涉及到强缓存和协商缓存。浏览器对于请求过得资源会缓存下来请求的响应数据,后面请求时会先从缓存查找匹配的请求的响应头,如果命中强缓存(判断 cache-control 和 expires 信息)那么直接从缓存获取响应数据,不会再发送 http 请求。如果没有命中浏览器会发送请求到服务器,同时会携带第一次请求的响应头的缓存相关 header 字段(last-modified/if-modified-since, Etag/if-none-match), 服务端根据这些请求头判断是否走缓存,如果走缓存,服务端会返回新的响应头,但不返回数据,浏览器会更新响应头,从缓存拿数据。如果不走缓存,服务端就会返回新的响应头和数据,然后浏览器更新缓存的数据。
-》强缓存,判断依据是 expires(http 1.0 协议规定)和 cache-control(http 1.1 协议规定)字段,expires 是绝对时间,cache-control 有可选值 no-cache(不使用本地缓存,走协商缓存),no-store(禁止浏览器缓存数据,每次都是重新获取数据),public(可以被客户端和中间商 CDN 做缓存),private(只能客户端缓存,CDN 不能缓存)
-》协商缓存,用到的响应头字段是 last-modified/if-modified-since, Etag/if-none-match,这是两对哈,每队 / 前面一个是服务端返回的 response header 中的字段,/ 后面是请求头 request 携带的头部字段,第一次请求资源浏览器会返回 last-modified(最后修改时间),后面再次请求请求头会带上 if-modified-since,当然这个值和上次浏览器返回的 last-modified 是一样的,然后浏览器判断如果文件没有变化,那么返回 304 Not Modified http code,响应请求头不会携带 last-modified 字段,浏览器从缓存取数据,也不用更新 last-modified 字段,如果有修改,那么响应头返回新的 last-modified 字段数据,返回响应内容。Etag/if-none-match 这一对是同样的逻辑,不同之处是用 etag 标识来判断文件是否修改,而不是用时间,因为服务器时间可能会变的,还会收到时区的影响。还有一点是每次请求都会返回 etag 字段,即使没有变化。
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
6. 你是否可以利用面向对象的思维去抽象你的功能,你会构建一个 class(ES6)吗?你对于前端架构的理解?
我目前开发分情况用不同的技术框架。
如果单纯开发导购页面,比如一个商品列表页面,这种为了加载性能和操作体验,我是不考虑用框架的,也不用 class,单纯用自己开发的原生 ES 框架自己控制页面模块生命周期,基于函数式编程写 stateless 组件。尽量减少复杂度,简单化。
如果是开发功能性组件,我是会用面型对象的模式来做开发。面向对象的核心是封装、继承、多态。封装就是将具体化为抽象,抽象成 class,封装抽象出来的属性和方法。继承是因为抽象可以有层级,比如对异常处理,参数异常可以抽象成一类,状态异常可以抽象成一类,参数异常和状态异常有共通的地方,比如结构上都会返回异常的名称和描述,这就可以抽象一层公共父类,然后这两个异常继承自公共父类,这就是集成。多态也是随着继承而来的,比如参数异常和状态异常都继承了 name 这个属性,都可以实现对应的 get 方法,但是他们的实现结果可定是不一样的,根据自身类的抽象来实现,调用的时候调用同样的方法也就有不同的表现。比如参数异常和状态异常都继承了 toString 的方法,在调用各自的实例的 toString 方法时,输出的数据是不一样的。另外设计的 2 大原则是:单一职责原则和开放封闭原则。单一职责只是抽象的类尽量保持功能专一,开闭原则指设计的时候要考虑好扩展,对修改关闭,对扩展开放。
export class RuntimeException {constructor(message) {this._message = message;}
get name() {return 'RuntimeException';}
get message() {return this._message;}
toString() {return this.name + ':' + this.message;}
}
export class IllegalStateException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'IllegalStateException';}
}
export class InvalidArgumentException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'InvalidArgumentException';}
}
export class NotImplementedException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'NotImplementedException';}
}
对于前端领域来说,目前前端框架做掉了很多事情,搭建好项目框架之后,开发的就行就是填功能。所编写的模块和组件的模式也比较固定,可以根据具体情况来实现。
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
7. 你会用 VUE,你会用 React,你读得懂这两个架构的源码吗?你懂他俩的基本设计模式吗?让你去构建一个类似的框架你如何下手?
angular
特点:数据双向绑定 -》数据驱动开发的思想
html 标签化的模板,模块化思想
数据绑定,控制器,依赖注入,
服务,指令,过滤器…
优点:比较完善规范,文档、社区比较活跃
模块清晰,代码明了
缺点:功能规范太固定,开发发挥空间小。
相对 react 和 vue,不够轻量化
扩展性不够灵活
react
特点:强大的组件化思想,任意封装、组合
独创 JSX 语法,virtual dom 智能 patch,灵活高效
轻量,易扩展,模块清晰,代码明了
社区生态完善,组件库、插件库丰富
新特性:hooks,错误边界等优化
缺点:组件难以在复杂交互场景复用
侧重于做组件,做 view 展示层,对于业务逻辑等封装治理不如 angular 强大
JSX 中 html 模板不够完备和健壮,比如一些属性变换写法,绑定事件大小写
vue
特点:文档丰富,容易上手
模板较完备,声明式渲染,插值表达式与指令系统,
事件处理器,修饰符,计算属性,简单易用,功能强
社区生态完善,组件库、插件库丰富
缺点:轻量框架使用是要结合生态插件组件使用,项目初始配置比较麻烦,
不过可以参考各种场景的标准模板配置,很多脚手架
声明式渲染与命令式渲染:这个涉及到函数式编程中的一个声明式编程和命令式编程的概念。
比如命令式编程:
let a = []
for(let i=0; i< 10; i++){a.push(i*10)
}
声明式编程:
let a = []
arr.forEach(i=>{a.push(i*10)
})
声明式编程隐藏了函数处理细节,命令式编程则需要处理细节。
声明式编程的好处是简单化,易于理解,减少劳动量。比如 vue 中的指令绑定事件,绑定属性都是这样。@click,:title 等等,用的时候很方便,这正是声明式编程最直观的好处。
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
8. 你了解的 ES6 只是 const、let、promise 吗?你考虑过 ES6 提出的真正趋势吗?
怎么可能,我又不是你。ES6 中最常用的像变量定义这部分用 let、const 可以避免一些坑,异步处理可以用 promise,不过我到喜欢用 async/await 更简洁好用。
还有简写的箭头函数,代码看起来更清晰。
“…” 变量析构和组装,函数默认值
“ 模板字符串,便于字符拼接;标签模板 功能
Object 对象的扩展,for in,Object.keys
Set 和 Map
class 和 module 相关
发展趋势:总体来说前端开发更规范,更简单,语法更完备和成熟。支持的功能增强,开发效率提升,体验增强。
ES6 的模块化相关支持,可以更好地支持模块化开发。
原生支持 class,面向对象的编程,概念更容易理解,易于软件开发和集成。
异步操作规范化,异步编程更简单
9. 你会用 less,那么让你去写一个 loader 你可以吗?
参考下:程序语言进阶之 DSL 与 AST 实战解析
可以的,说一下原理,需要将 .less 文件最终解析成 CSS,less 是一种 DSL,我们可以现根据 less 预发,先将其解析成 AST,然后解析成 CSS 即可。我推荐用 PEG.js 这种解析表达式语法更简单些,只需要描述产生式规则即可。也可以自己根据 LESS 预发来写正则表达来匹配规则,然后转换成 css。比较常用的是 PostCSS,处理流程如下:参考官方文档
PostCSS 的处理流程也是经过词法解析语法分析,将读取到的文件字符转化成词汇流 tokens,根据语法分析,根据 less 的语法,解析成一个 AST。
source string → tokens → AST
核心组件有:
词法分析器 Tokenizer (lib/tokenize.es6)
语法分析器 Parser (lib/parse.es6, lib/parser.es6)
插件处理器 Processor (lib/processor.es6)
代码生成器 Stringifier (lib/stringify.es6, lib/stringifier.es6)
10.webpack 你也会用,你了解其中原理吗?你知道分析打包依赖的过程吗?你知道 tree-shaking 是如何干掉无用重复的代码的吗?
大家之前应该用过 gulp,grunt 这种代码打包工具,定义不同的打包任务和打包流程。我用的比较多的 rollup 这个打包工具,配置起来比较简单些。
webpack 也是用来做代码打包,可以做代码分析,拆分,混淆,压缩等等,基于他的插件扩展机制可以做很多事情。分析 webpack 的原理,可以先从 webpack 配置文件说起。参考:webpack 编译代码原理介绍 用 webpack4 和一些插件提升代码编译速度
首先作为打包工具,要定义打包的输入 entry 和输出 output;然后是定义 webpack 要用到的 module,比如 babel js loader,cssloader 等等。执行编译具体的流程是:
加载 webpack 配置文件 –》根据配置初始化编译器 compiler –》找到入口,根据 loader 配置开始编译入口文件以及层层依赖 –》编译完成之后,可以得到所有编译过的文件和依赖关系结构 –》根据依赖关系将模块组装成一个个包含多个模块的 chunk,然后根据配置写到输出文件。
webpack 构建流程可分为以下三大阶段。
初始化:启动构建,读取与合并配置参数,加载 plugin, 实例化 Compiler
编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件中的内容,再找到该 Module 依赖的 Module,递归的进行编译处理
输出:将编译后的 Module 组合成 Chunk, 将 Chunk 转换成文件,输出到文件系统中
分析依赖是在编译过程中完成的,从入口查找依赖,最后形成依赖关系。为了提高效率,可以记录分析过的依赖,这样下次遇到同样的模块就不用再分析,直接引用编译过的依赖就可以了。
tree-shaking 的名字原理一样,就是摇一摇大树,落下来的叶子都是冗余的部分。Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也增加了 tree-shaking 的功能。其实在更早,google closure compiler 也做过类似的事情。三个工具的效果和使用各不相同,使用方法可以通过官网文档去了解。
tree shaking 的目的是去掉无用代码,减少代码体积。其实对于编译的编程语言对应的编译器基本都有判断哪些代码不会影响输出,从而在编译时移除这些代码的功能,称为 DCE(dead code elimination)。tree shaking 是 DCE 的一种实现,传统的是消除没有引用不会执行的代码,tree shaking 主要是要消除没有用的代码。
Dead Code 一般具有以下几个特征
•代码不会被执行,不可到达
•代码执行的结果不会被用到
•代码只会影响死变量(只写不读)
在前端代码打包处理中,最终都会有个代码压缩混淆的环节,这个环节其实会完成 DCE 的工作,会将这些 dead code 移除。
但是 uglify 代码是只是单个单个文件处理,并不能分析出这个代码有没有被其他文件用到,当然也不会对这些为被调用的函数做处理,如上图 uglify 就不会去除没用到的 get 函数,所以就需要 tree shaking。tree shaking 是有限制的,只能消除函数和 import/export 的变量,不会处理 import/export 的 class(因为 javascript 动态语言特性使得分析比较困难,可能导致以外的错误,side effect 比较大),对于纯函数处理效果较好。
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
11. 你真的熟练使用 css 吗,那你知道 position 有几个属性吗
具体参考 https://github.com/wintercn/b…
static:无特殊定位,对象遵循正常文档流。top,right,bottom,left 等属性不会被应用。
relative:对象遵循正常文档流,但将依据 top,right,bottom,left 等属性在正常文档流中偏移位置,相对定位相对的是它原本在文档流中的位置而进行的偏移。而其层叠通过 z -index 属性定义。占据的文档空间不会随 top / right / left / bottom 等属性的偏移而发生变动,也就是说它后面的元素是依据 (top / left / right / bottom 等属性生效之前) 进行的定位,这点一定要理解。
absolute:对象脱离正常文档流,使用 top,right,bottom,left 等属性进行绝对定位。而其层叠通过 z -index 属性定义。使用 absoulte 或 fixed 定位的话,必须指定 left、right、top、bottom 属性中的至少一个,否则 left/right/top/bottom 属性会使用它们的默认值 auto,这将导致对象遵从正常的 HTML 布局规则,在前一个对象之后立即被呈递,简单讲就是都变成 relative,会占用文档空间,这点非常重要,很多人使用 absolute 定位后发现没有脱离文档流就是这个原因。
fixed:对象脱离正常文档流,使用 top,right,bottom,left 等属性以窗口为参考点进行定位,当出现滚动条时,对象不会随着滚动。而其层叠通过 z -index 属性定义。
sticky: The element is positioned according to the normal flow of the document, and then offset relative to its nearest scrolling ancestor and containing block (nearest block-level ancestor), including table-related elements, based on the values of top, right, bottom, and left. The offset does not affect the position of any other elements.This value always creates a new stacking context. Note that a sticky element “sticks” to its nearest ancestor that has a “scrolling mechanism” (created when overflow is hidden, scroll, auto, or overlay), even if that ancestor isn’t the nearest actually scrolling ancestor. This effectively inhibits any “sticky” behavior (see the Github issue on W3C CSSWG).
absolute 就只能根据祖先类元素 (父类以上) 进行定位,而这个祖先类还必须是以 postion 非 static 方式定位的,举个例子,a 元素使用 absoulte 定位,它会从父类开始找起,寻找以 position 非 static 方式定位的祖先类元素(注意,一定要是直系祖先才算哦~),直到 <html> 标签为止,这里还需要注意的是,relative 和 static 方式在最外层时是以 <body> 标签为定位原点的,而 absoulte 方式在无父级是 position 非 static 定位时是以 <html> 作为原点定位。
参考:position 属性
关于 Layout and the containing block,看下官方介绍的 contain block,另外相关的点是 BFC:如何创建块级格式化上下文(block formatting context),BFC 有什么用
12 前端动画渲染机制了解吗?硬件加速原理?
参考:浏览器渲染流水线解析与网页动画性能优化
动画可以看做是一个连续的帧序列的组合。我们把网页的动画分成两大类 —— 一类是合成器动画,一类是非合成器动画(UC 内部也将其称为内核动画或者 Blink Animation,虽然这不是 Chrome 官方的术语)。
合成器动画顾名思义,动画的每一帧都是由 Layer Compositor 生成并输出的,合成器自身驱动着整个动画的运行,在动画的过程中,不需要新的 Main Frame 输入;
非合成器动画,每一帧都是由 Blink 生成,都需要产生一个新的 Main Frame;
合成器动画又可以分为两类:
合成器本身触发并运行的,比如最常见的网页惯性滚动,包括整个网页或者某个页内可滚动元素的滚动;
Blink 触发然后交由合成器运行,比如说传统的 CSS Translation 或者新的 Animation API,如果它们触发的动画经由 Blink 判断可以交由合成器运行;
Blink 触发的动画,如果是 Transform 和 Opacity 属性的动画基本上都可以由合成器运行,因为它们没有改变图层的内容。不过即使可以交由合成器运行,它们也需要产生一个新的 Main Frame 提交给合成器来触发这个动画,如果这个 Main Frame 包含了大量的图层变更,也会导致触发的瞬间卡顿,页端事先对图层结构进行优化可以避免这个问题。
非合成器动画也可以分为两类:
使用 CSS Translation 或者 Animation API 创建的动画,但是无法由合成器运行;
使用 Timer 或者 rAF 由 JS 驱动的动画,比较典型的就是 Canvas/WebGL 游戏,这种动画实际上是由页端自己定义的,浏览器本身并没有对应的动画的概念,也就是说浏览器本身是不知道这个动画什么时候开始,是否正在运行,什么时候结束,这些完全是页端自己的内部逻辑;
合成器动画和非合成器动画在渲染流水线上有较大的差异,后者更复杂,流水线更长。上面四种动画的分类,按渲染流水线的复杂程度和理论性能排列(复杂程度由低到高,理论性能由高到低):
合成器本身触发并运行的动画;
Blink 触发,合成器运行的动画;
Blink 触发,无法由合成器运行的动画;
由 Timer/rAF 驱动的 JS 动画;
开启硬件加速的方法很多,比如 transform: translate3d(0,0,0); 加了之后,在 chrome 开发者工具中的 layer 栏目下可以看到多了一层 composition layer,同时给出了理由描述是开启了 3D transform,这个元素就放入了 Composited Layer 中托管,其动画效果都是在单独一个图形层上面处理,不会影响其它层。
什么情况下能使元素获得自己的层?虽然 Chrome 的启发式方法 (heuristic) 随着时间在不断发展进步,但是从目前来说,满足以下任意情况便会创建层:
3D 或透视变换 (perspective transform) CSS 属性
使用加速视频解码的 元素
拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
混合插件 (如 Flash)
对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
拥有加速 CSS 过滤器的元素
元素有一个包含复合层的后代节点 (换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z -index 属性,人为干扰复合层的排序,可以有效减少 chrome 创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。
关于层的介绍:gpu-accelerated-compositing-in-chrome
理解 CSS animations 和 transitions 的性能问题与动画调试
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
13. 你了解 js 的数据结构吗?基本数据类型有哪些?复杂数据类型有哪些?在内存是如何表现的?
参考 MDN,最新的 ECMAScript 标准定义了 7 种数据类型:
6 种原始类型:
Boolean
Null
Undefined
Number
String
Symbol (ECMAScript 6 新定义)
和 Object
除 Object 以外的所有类型都是不可变的(值本身无法被改变)。例如,与 C 语言不同,JavaScript 中字符串是不可变的。JavaScript 中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变。
标准的 ” 对象, 和函数【复杂数据类型】
日期:内建的 Date 对象
数组和类型数组:
数组是一种使用整数作为键 (integer-key-ed) 属性和长度 (length) 属性之间关联的常规对象。此外,数组对象还继承了 Array.prototype 的一些操作数组的便捷方法。例如, indexOf (搜索数组中的一个值) or push (向数组中添加一个元素),等等。这使得数组是表示列表或集合的最优选择。
类型数组 (Typed Arrays) 是 ECMAScript Edition 6 中新定义的 JavaScript 内建对象,提供了一个基本的二进制数据缓冲区的类数组视图。
集合对象 Map、WeakMap、Set、WeakSet:这些数据结构把对象的引用当作键,其在 ECMAScript 第 6 版中有介绍。当 Map 和 WeakMap 把一个值和对象关联起来的时候,Set 和 WeakSet 表示一组对象。Map 和 WeakMaps 之间的差别在于,在前者中,对象键是可枚举的。
结构化数据 JSON:JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式
参考:标准全局内置对象
两种类型:
1. ECMAScript 变量包含两种不同类型的值:基本类型值、引用类型值;
2. 基本类型值:指的是保存在栈内存中的简单数据段;
3. 引用类型值:指的是那些保存在堆内存中的对象,意思是,变量中保存的实际上只是一个指针,这个指针执行内存中的另一个位置,由该位置保存对象;
两种访问方式:
4. 基本类型值:按值访问,操作的是他们实际保存的值;
5. 引用类型值:按引用访问,当查询时,我们需要先从栈中读取内存地址,然后再顺藤摸瓜地找到保存在堆内存中的值;
数据复制
基本类型变量的复制:从一个变量向一个变量复制时,会在栈中创建一个新值,然后把值复制到为新变量分配的位置上;
引用类型变量的复制:复制的是存储在栈中的指针,将指针复制到栈中未新变量分配的空间中,而这个指针副本和原指针执行存储在堆中的同一个对象;复制操作结束后,两个变量实际上将引用同一个对象;因此改变其中的一个,将影响另一个;
三种变量类型检测
1. Typeof 操作符是检测基本类型的最佳工具;
2. 如果变量值是 null 或者对象,typeof 将返回“object”;结合 null == null 来判断
3. Instanceof 用于检测引用类型,可以检测到具体的,它是什么类型的实例;
4. 如果变量是给定引用类型的实例,instanceof 操作符会返回 true;
- Object.prototype.toString.call(xx) 来打印原型判断类型
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
14. 你可以用 js 去实现一个单向、双向、循环链表吗?你可以实现查找、插入、删除操作吗?
可以在这里试一下:在线编程环境
链表:
插入链表节点:
删除链表节点:
双向链表:
循环链表:
下面给一个最简单的单项链表示例:
/**
** 先创建一个节点类,记录当前数据,和下个节点,如果是双向链表,就包含 prev
** prev: 对上个节点的引用
** next: 对下个节点的应用
**/
class Node{constructor(data){
this.data = data;
this.next = null;
}
}
/**
** 创建链表,head 是链表中的一个起始节点,关于单项链表,双向链表和循环链表参考文章介绍
** find: 找到数据所在的节点,这里是示例,其实应该有个唯一标识
** insert: 在指定节点后面插入节点
**/
class LinkTable{constructor(data){
this.head = null;
this.end = null;
if(data){this.head = new Node(data)
}
}
find(data){
let start = this.head;
while(start.data != data){start = start.next;}
return start;
}
insert(data,node){let nod = new Node(data);
nod.next = node.next;
item.next = nod;
}
}
关于知识点原理和详细,参考讲堂的视频教程:前端增长 - 重新定义大前端
14. 你了解基本常见算法吗?快速排序写一个?要是限制空间利用你该如何写?
快速排序:
(1)在数据集之中,选择一个元素作为 ” 基准 ”(pivot)。
(2)所有小于 ” 基准 ” 的元素,都移到 ” 基准 ” 的左边;所有大于 ” 基准 ” 的元素,都移到 ” 基准 ” 的右边。
(3)对 ” 基准 ” 左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
选择排序:
(1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
(2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
(3)直到所有都排序
冒泡排序:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
直接插入排序:
(1)将待排序数组取一个数值插入到已排序数组中合适的位置
(2)重复取数据,直到所有数据取完
15. 你了解贪心算法、动态规划、分治算法、回溯算法等常见的算法吗?
- 贪心算法
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
贪心算法的基本思路:
1. 建立数学模型来描述问题。
2. 把求解的问题分成若干个子问题。
3. 对每一子问题求解,得到子问题的局部最优解。
4. 把子问题的解局部最优解合成原来解问题的一个解。
- 动态规划算法
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
能采用动态规划求解的问题的一般要具有 3 个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
- 分治算法
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为 n 的问题,若该问题可以容易地解决(比如说规模 n 较小)则直接解决,否则将其分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
- 回溯法
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)
- 分支限界法
类似于回溯法,也是一种在问题的解空间树 T 上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出 T 中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
由于求解目标不同,导致分支限界法与回溯法在解空间树 T 上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树 T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树 T。
16. 你是如何理解前端架构的?你了解持续集成吗?
架构,我理解主要做:系统分解、服务分层的工作。
持续集成(Continuous integration,简称 CI)。项目是一个迭代一个迭代快速开发,每个迭代开发不同的 feature,所有的 feature 合在一起构成完整的功能。
持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。
Martin Fowler 说过,” 持续集成并不能消除 Bug,而是让它们非常容易发现和改正。”
与持续集成相关的,还有两个概念,分别是持续交付和持续部署。
17. 你了解基本的设计模式吗?举例单例模式、策略模式、代理模式、迭代模式、发布订阅模式。。。?
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。
- 单例模式:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
- 策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
很好理解,比如上面给的一个异常处理的代码,写个简单的示例。
export class RuntimeException {constructor(message) {this._message = message;}
get name() {return 'RuntimeException';}
get message() {return this._message;}
toString() {return this.name + ':' + this.message;}
}
export class IllegalStateException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'IllegalStateException';}
}
export class InvalidArgumentException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'InvalidArgumentException';}
}
export class NotImplementedException extends RuntimeException {constructor(message) {super(message);
}
get name() {return 'NotImplementedException';}
}
export function funcWrapper(args){
try{if(!args) throw new InvalidArgumentException('args undefined')
if(args == 1) throw new IllegalStateException('args illegal')
}catch(e){console.log(e.toString())
}
}
浏览器可以跑下结果看看:
这就是策略模式,不同的情况,输出的结果是不一样的。
18. 写一个事件监听函数呗?实现 once、on、remove、emit 功能
19.node.js 的实现层是什么?
20.node 的事件循环机制是怎样的?node 的 child_process 模块有几个 api, 分别的作用是什么?
22.http1.0 与 1.1 协议的区别?node 是如何实现 http 模块的?
25.nginx 相关配置了解过吗?
27 小程序架构
28. vue v-model 语法糖?vue push 式更新?vue computed 和 watch 的区别?
- redux dispatch 一个 action 之后的更新过程
connect 的时候出于性能优化的考虑做了一层浅比较。
本文总结了前端老司机经常问题的一些问题并结合个人总结给出了比较详尽的答案。
关于知识点原理和详细,参考讲堂的视频教程 :前端增长 - 重新定义大前端
课程知识在不断更新,本片内容也逐步更新