本文总结了前端老司机经常问题的一些问题并结合个人总结给出了比较详尽的答案。网易阿里腾讯校招社招必备知识点。
原理讲解参考:前端增长-重新定义大前端
官方博客:前端学堂 fed123.com
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大原则是:单一职责原则和开放封闭原则。单一职责只是抽象的类尽量保持功能专一,开闭原则指设计的时候要考虑好扩展,对修改关闭,对扩展开放。
对于前端领域来说,目前前端框架做掉了很多事情,搭建好项目框架之后,开发的就行就是填功能。所编写的模块和组件的模式也比较固定,可以根据具体情况来实现。
7.你会用VUE,你会用React,你读得懂这两个架构的源码吗?你懂他俩的基本设计模式吗?让你去构建一个类似的框架你如何下手?
angular
特点: 数据双向绑定-》数据驱动开发的思想
html标签化的模板,模块化思想
数据绑定,控制器,依赖注入,
服务,指令,过滤器…
优点: 比较完善规范,文档、社区比较活跃
模块清晰,代码明了
缺点: 功能规范太固定,开发发挥空间小。
相对react和vue,不够轻量化
扩展性不够灵活
react
特点: 强大的组件化思想,任意封装、组合
独创JSX语法,virtual dom智能patch,灵活高效
轻量,易扩展,模块清晰,代码明了
社区生态完善,组件库、插件库丰富
缺点: 组件难以在复杂交互场景复用
侧重于做组件,做view展示层,对于业务逻辑等封装治理不如angular强大
JSX中html模板不够完备和健壮,比如一些属性变换写法,绑定事件大小写
vue
特点: 文档丰富,容易上手
模板较完备,声明式渲染,插值表达式与指令系统,
事件处理器,修饰符,计算属性 ,简单易用,功能强
社区生态完善,组件库、插件库丰富
缺点: 轻量框架使用是要结合生态插件组件使用,项目初始配置比较麻烦,
不过可以参考各种场景的标准模板配置,很多脚手架
声明式渲染与命令式渲染: 这个涉及到函数式编程中的一个声明式编程和命令式编程的概念。
比如命令式编程:
声明式编程:
声明式编程隐藏了函数处理细节,命令式编程则需要处理细节。
声明式编程的好处是简单化,易于理解,减少劳动量。比如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你可以吗?
可以的,说一下原理,需要将 .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
, andleft
. 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 whenoverflow
ishidden
,scroll
,auto
, oroverlay
), 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有什么用
11.5 前端动画渲染机制了解吗?硬件加速原理?
动画可以看做是一个连续的帧序列的组合。我们把网页的动画分成两大类 —— 一类是合成器动画,一类是非合成器动画(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的性能问题与动画调试
12.你了解js的数据结构吗?基本数据类型有哪些?复杂数据类型有哪些?在内存是如何表现的?
参考MDN,最新的 ECMAScript 标准定义了 7 种数据类型:
除 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;
5. Object.prototype.toString.call(xx) 来打印原型判断类型
13.你可以用js去实现一个单向、双向、循环链表吗?你可以实现查找、插入、删除操作吗?
可以在这里试一下:在线编程环境
链表:
插入链表节点:
删除链表节点:
双向链表:
循环链表:
下面给一个最简单的单项链表示例:
14.你了解基本常见算法吗?快速排序写一个?要是限制空间利用你该如何写?
快速排序:
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
选择排序:
(1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
(2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
(3)直到所有都排序
冒泡排序:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
直接插入排序:
(1)将待排序数组取一个数值插入到已排序数组中合适的位置
(2)重复取数据,直到所有数据取完
15.你了解贪心算法、动态规划、分治算法、回溯算法等常见的算法吗?
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
- 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
贪心算法的基本思路:
1.建立数学模型来描述问题。
2.把求解的问题分成若干个子问题。
3.对每一子问题求解,得到子问题的局部最优解。
4.把子问题的解局部最优解合成原来解问题的一个解。
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)
类似于回溯法,也是一种在问题的解空间树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 对象的执行算法。
很好理解,比如上面给的一个异常处理的代码,写个简单的示例。
浏览器可以跑下结果看看:
这就是策略模式,不同的情况,输出的结果是不一样的。
18.写一个事件监听函数呗?实现once、on、remove、emit功能
19.node.js的实现层是什么?
20.node的事件循环机制是怎样的?node的child_process模块有几个api,分别的作用是什么?
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。下面是一个libuv引擎中的事件循环的模型:
注:模型中的每一个方块代表事件循环的一个阶段
这个模型是node官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章。我会在文末把文章地址贴出来,有兴趣的朋友可以亲自与看看原文。
我们知道Linux中有个高效多路IO复用的poll/select模型,增强改进有个epoll模型。参考这里
上面这个node的poll模型中,我们可以大致分析出node中的事件循环的顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
以上各阶段的名称是根据我个人理解的翻译,为了避免错误和歧义,下面解释的时候会用英文来表示这些阶段。
这些阶段大致的功能如下:
- timers: 这个阶段执行定时器队列中的回调如
setTimeout()
和setInterval()
。 - I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和
setImmediate()
的回调。 - idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check:
setImmediate()
的回调会在这个阶段执行。 - close callbacks: 例如
socket.on('close', ...)
这种close事件的回调。
详情参考:浏览器与node环境的事件循环机制
22.http1.0与1.1协议的区别?node是如何实现http模块的?
25.nginx相关配置了解过吗?
27.小程序架构
28. vue v-model 语法糖?vue push式更新?vue computed和watch的区别?
29. redux dispatch一个action之后的更新过程
知道connect的时候出于性能优化的考虑做了一层浅比较。