找工作帮助(前端、Java、全栈、测试、实施、运维),简历,面试题

一、掌握技能

2.1、前端开发工程师

1、掌握HTML5、CSS3、JavaScript、ES6、TypeScript、AJAX、Node.JS、Express、Vue、Vuex、Pinia、ES-Lint、Vite、ElementUI、React、Redux、React Native、Antd、Antd Mobile、微信开发与响应式布局等前端开发技术;

2、擅长基于win32以及Linux平台的JavaEE全栈开发。掌握程序逻辑、常用算法、JavaSE、Servlet、Spring、Spring MVC、MyBatis、Spring Boot、Maven、Git、基于Spring Cloud的微服务开发技术;

3、擅长基于MongoDB、MySQL、SQLServer数据库的分布式后台开发技术,能熟练使用T-SQL操作数据库;

4、具备良好的面向对象编程经验,深入理解OO、AOP思想,具有较强的分析设计能力,熟悉常用设计模式;

5、思维清晰,有责任心,具有良好的学习能力,能够快速适应新领域,能承受较大的工作压力,能确保工作进度和质量按既定计划进行。

2.2、Java开发工程师

1、擅长基于win32以及Linux平台的JavaEE全栈开发。掌握程序逻辑、常用算法、JavaSE、Servlet、Spring、Spring MVC、MyBatis、Spring Boot、Maven、Git、基于Spring Cloud的微服务开发技术;

2、掌握HTML5、CSS3、JavaScript、ES6、TypeScript、AJAX、Node.JS、Express、Vue、Vuex、Pinia、ES-Lint、Vite、ElementUI、React、Redux、React Native、Antd、Antd Mobile、微信开发与响应式布局等前端开发技术;

3、擅长基于MongoDB、MySQL、SQLServer数据库的分布式后台开发技术,能熟练使用T-SQL操作数据库;

4、具备良好的面向对象编程经验,深入理解OO、AOP思想,具有较强的分析设计能力,熟悉常用设计模式;

5、思维清晰,有责任心,具有良好的学习能力,能够快速适应新领域,能承受较大的工作压力,能确保工作进度和质量按既定计划进行。

2.3、全栈开发工程师

1、擅长基于win32以及Linux平台的JavaEE全栈开发。掌握程序逻辑、常用算法、JavaSE、Servlet、Spring、Spring MVC、MyBatis、Spring Boot、Maven、Git、基于Spring Cloud的微服务开发技术;

2、掌握HTML5、CSS3、JavaScript、ES6、TypeScript、AJAX、Node.JS、Express、Vue、Vuex、Pinia、ES-Lint、Vite、ElementUI、React、Redux、React Native、Antd、Antd Mobile、微信开发与响应式布局等前端开发技术;

3、擅长基于MongoDB、MySQL、SQLServer数据库的分布式后台开发技术,能熟练使用T-SQL操作数据库;

4、具备良好的面向对象编程经验,深入理解OO、AOP思想,具有较强的分析设计能力,熟悉常用设计模式;

5、思维清晰,有责任心,具有良好的学习能力,能够快速适应新领域,能承受较大的工作压力,能确保工作进度和质量按既定计划进行。

2.4、软件测试工程师

1、拥有扎实的计算机基础、系统学习过软件工程理论与技术、拥有独立开发前后端分离的互联网项目经验。

2、掌握C#、Java、JavaScript等多种程序语言,熟悉Junit、Docker、Python、JMeter、Selenium、Appium与PostMan等技术。

3、熟悉Linux系统、手机系统;熟悉HTTP/HTTPS协议;熟悉sql语句;熟练使用Mysql、SQLServer等数据库。

4、掌握功能测试、性能测试、接口测试、APP专项测试、安全测试。

5、逻辑思维能力好,有较强的业务学习能力,具有良好的团队协作精神。

6、工作认真、细致,有责任感,有较强的沟通能力,能适应高强度的工作。

7、善于发现问题,具备分析问题、解决问题的能力。

8、工作有激情,乐于学习、分享专业知识、具有良好文档编写能力。

2.5、运维实施工程师

1、拥有扎实的计算机软件、硬件与网络基础、系统学习过软件工程理论与技术、拥有独立开发前后端分离的互联网项目经验。

2、熟悉SQL Server、MySql等数据库的安装和维护,熟悉SQL语句,具备一定Sql语句编写能力。

3、掌握C#、Java、JavaScript等多种程序语言,熟悉Docker、JavaWeb、nginx、tomcat配置及安全防护操作。

4、了解服务器安全方面的配置及DDOS、XSS、sql注入等的安全防范措施。

5、熟悉Linux的基本操作,掌握常见的中间件部署与配置,熟悉Java以及数据库Mysql等的安装、配置和维护。

6、熟悉阿里云、腾讯云等云计算技术;掌握前端、UI、Photoshop图像处理技术。

7、具有良好的沟通能力及沟通技巧;能够较好地满足客户需求;责任心强,善于学习和交流。

8、可以不定期出差、有较强的Demo和演讲能力。

9、工作认真、细致,有责任感,有较强的沟通能力,能适应高强度的工作。

二、面试题

1、牛客网

https://www.nowcoder.com/
综合常用型:IT人的求职神器网站,求职+面试+题库。
牛客网是IT求职神器,集合了BAT等大量互联网名气的面试题,并提供在线做题的功能。


2、赛码网

http://www.91saima.cn/
主要突出点:商业合作名企多,有成套的名企面试题,包含求职。
赛码网网站由刷题+考试+求职等部分形成,商业合作的名企多,京东、网易、今日头条等都用它做笔试平台。本身有在线编程功能,但是题目一般。

 

https://www.yuque.com/cuggz

JavaScript 部分
1. JavaScript 有哪些数据类型,它们的区别?
JavaScript 共有八种数据类型,分别是Undefined、Null、Boolean、
Number、String、Object、Symbol、BigInt。
其中Symbol 和BigInt 是ES6 中新增的数据类型:
●Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了
解决可能出现的全局变量冲突的问题。
●BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,
使用BigInt 可以安全地存储和操作大整数,即使这个数已经超出了
Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
●栈:原始数据类型(Undefined、Null、Boolean、Number、String)
●堆:引用数据类型(对象、数组和函数)
两种类型的区别在于存储位置的不同:
●原始数据类型直接存储在栈(stack)中的简单数据段,占据空间
小、大小固定,属于被频繁使用数据,所以放入栈中存储;
●引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固
定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈
中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引
用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
●在数据结构中,栈中数据的存取方式为先进后出。
●堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大
小来规定。
在操作系统中,内存被分为栈区和堆区:
2
●栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的
值等。其操作方式类似于数据结构中的栈。
●堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可
能由垃圾回收机制回收。
2. 数据类型检测的方式有哪些
(1)typeof
其中数组、对象、null 都会被判断为object,其他判断都正确。
(2)instanceof
instanceof 可以正确判断对象的类型,其内部运行机制是判断在其
原型链中能否找到该类型的原型。
可以看到,instanceof 只能正确判断引用数据类型,而不能判断基
本数据类型。instanceof 运算符可以用来测试一个对象在其原型链
中是否存在一个构造函数的prototype 属性。
(3) constructor
3
constructor 有两个作用,一是判断数据的类型,二是对象实例通过
constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象
来改变它的原型,constructor 就不能用来判断数据类型了:
(4)Object.prototype.toString.call()
Object.prototype.toString.call() 使用Object 对象的原型方法
toString 来判断数据类型:
同样是检测对象obj 调用toString 方法,obj.toString()的结果和
Object.prototype.toString.call(obj)的结果不一样,这是为什
么?
这是因为toString 是Object 的原型方法,而Array、function 等类
型作为Object 的实例,都重写了toString 方法。不同的对象类型调
用toString 方法时,根据原型链的知识,调用的是对应的重写之后
4
的toString 方法(function 类型返回内容为函数体的字符串,Array
类型返回元素组成的字符串…),而不会去调用Object 上原型
toString 方法(返回对象的具体类型),所以采用obj.toString()
不能得到其对象类型,只能将obj 转换为字符串类型;因此,在想要
得到对象的具体类型时,应该调用Object 原型上的toString 方法。
3. null 和undefined 区别
首先Undefined 和Null 都是基本数据类型,这两个基本数据类型
分别都只有一个值,就是undefined 和null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般
变量声明了但还没有定义的时候会返回undefined,null 主要用于
赋值给一些可能会返回对象的变量,作为初始化。
undefined 在JavaScript 中不是一个保留字,这意味着可以使用
undefined 来作为一个变量名,但是这样的做法是非常危险的,它会
影响对undefined 值的判断。我们可以通过一些方法获得安全的
undefined 值,比如说void 0。
当对这两种类型使用typeof 进行判断时,Null 类型化会返回
“object”,这是一个历史遗留的问题。当使用双等号对两种类型的
值进行比较时会返回true,使用三个等号时会返回false。
4. intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的prototype 属性是否出现
在对象的原型链中的任何位置。
5
5. 如何获取安全的undefined 值?
因为undefined 是一个标识符,所以可以被当作变量来使用和赋值,
但是这样会影响undefined 的正常判断。表达式void ___ 没有返
回值,因此返回结果是undefined。void 并不改变表达式的结果,
只是让表达式不返回值。因此可以用void 0 来获得undefined。
6. Object.is() 与比较操作符“===”、“==” 的区别?
使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进
行强制类型转化后再进行比较。
使用三等号(===)进行相等判断时,如果两边的类型不一致时,不
会做强制类型准换,直接返回false。
使用Object.is 来进行相等判断时,一般情况下和三等号的判断相
同,它处理了一些特殊的情况,比如-0 和+0 不再相等,两个NaN
是相等的。
7. 什么是JavaScript 中的包装类型?
在JavaScript 中,基本类型是没有属性和方法的,但是为了便于操
作基本类型的值,在调用基本类型的属性或方法时JavaScript 会在
后台隐式地将基本类型的值转换为对象,如:
6
在访问'abc'.length 时, JavaScript 将'abc' 在后台转换成
String('abc'),然后再访问其length 属性。
JavaScript 也可以使用Object 函数显式地将基本类型转换为包装类
型:
也可以使用valueOf 方法将包装类型倒转成基本类型:
看看如下代码会打印出什么:
答案是什么都不会打印,因为虽然包裹的基本类型是false,但是
false 被包裹成包装类型后就成了对象,所以其非值为false,所以
循环体中的内容不会运行。
8. 为什么会有BigInt 的提案?
JavaScript 中Number.MAX_SAFE_INTEGER 表示最⼤安全数字,计算
结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩
数除外)。但是⼀旦超过这个范围,js 就会出现计算不准确的情况,
这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提
出了BigInt 来解决此问题。
7
9. 如何判断一个对象是空对象
使用JSON 自带的.stringify 方法来判断:
使用ES6 新增的方法Object.keys()来判断:
10. const 对象的属性可以修改吗
const 保证的并不是变量的值不能改动,而是变量指向的那个内存地
址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值
就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的
内存地址,保存的只是一个指针,const 只能保证这个指针是固定不
变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
11. 如果new 一个箭头函数的会怎么样
箭头函数是ES6 中的提出来的,它没有prototype,也没有自己的this
指向,更不可以使用arguments 参数,所以不能New 一个箭头函数。
new 操作符的实现步骤如下:
1.创建一个对象
2.将构造函数的作用域赋给新对象(也就是将对象的__proto__属性
指向构造函数的prototype 属性)
8
3.指向构造函数中的代码,构造函数中的this 指向该对象(也就是
为这个对象添加属性和方法)
4.返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
12. 箭头函数的this 指向哪⾥?
箭头函数不同于传统JavaScript 中的函数,箭头函数并没有属于⾃
⼰的this,它所谓的this 是捕获其所在上下⽂的this 值,作为⾃
⼰的this 值,并且由于没有属于⾃⼰的this,所以是不会被new
调⽤的,这个所谓的this 也不会被改变。
可以⽤Babel 理解⼀下箭头函数:
转化后:
13. 扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷
贝到当前对象之中。
9
上述方法实际上等价于:
Object.assign 方法用于对象的合并,将源对象(source)的所有可
枚举属性,复制到目标对象(target)。Object.assign 方法的第一
个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对
象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面
的属性)。
同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符
内部的同名属性会被覆盖掉。
利用上述特性就可以很方便的修改对象的部分属性。在redux 中的
reducer 函数规定必须是一个纯函数,reducer 中的state 对象要求
不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,
然后产生一个新的对象返回。
需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每
次只能展开一层数组。
下面是数组的扩展运算符的应用:
10
将数组转换为参数序列
复制数组
要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷
贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都
是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。
合并数组
如果想在数组内合并数组,可以这样:
扩展运算符与解构赋值结合起来,用于生成数组
需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一
位,否则会报错。
将字符串转为真正的数组
任何Iterator 接口的对象,都可以用扩展运算符转为真正的数组
11
比较常见的应用是可以将某些数据结构转为数组:
用于替换es5 中的Array.prototype.slice.call(arguments)写法。
使用Math 函数获取数组中特定的值
14. Proxy 可以实现什么功能?
在Vue3.0 中通过Proxy 来替换原本的Object.defineProperty
来实现数据响应式。
Proxy 是ES6 中新增的功能,它可以用来自定义对象中的操作。
代表需要添加代理的对象,handler 用来自定义对象中的操作,比如
可以用来自定义set 或者get 函数。
下面来通过Proxy 来实现一个数据响应式:
12
在上述代码中,通过自定义set 和get 函数的方式,在原本的逻辑
中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出
通知。
当然这是简单版的响应式实现,如果需要实现一个Vue 中的响应式,
需要在get 中收集依赖,在set 派发更新,之所以Vue3.0 要使用
Proxy 替换原本的API 原因在于Proxy 无需一层层递归为每个属
性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现
有一些数据更新不能监听到,但是Proxy 可以完美监听到任何方式
的数据改变,唯一缺陷就是浏览器的兼容性不好。
15. 常用的正则表达式有哪些?
16. 对JSON 的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编
程语言读取和作为数据格式来传递。
在项目开发中,使用JSON 作为前后端数据交换的方式。在前端通过
将一个符合JSON 格式的数据结构序列化为
JSON 字符串,然后将它传递到后端,后端通过JSON 格式的字符串
解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
13
因为JSON 的语法是基于js 的,因此很容易将JSON 和js 中的对
象弄混,但是应该注意的是JSON 和js 中的对象不是一回事,JSON
中对象格式更加严格,比如说在JSON 中属性值不能为函数,不能出
现NaN 这样的属性值等,因此大多数的js 对象是不符合JSON 对
象的格式的。
在js 中提供了两个函数来实现js 数据结构和JSON 格式的转换
处理,
JSON.stringify 函数,通过传入一个符合JSON 格式的数据结构,
将其转换为一个JSON 字符串。如果传入的数据结构不符合JSON 格
式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合
规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化
为JSON 格式的字符串。
JSON.parse() 函数,这个函数用来将JSON 格式的字符串转换为一
个js 数据结构,如果传入的字符串不是标准的JSON 格式的字符串
的话,将会抛出错误。当从后端接收到JSON 格式的字符串时,可以
通过这个方法来将其解析为一个js 数据结构,以此来进行数据的访
问。
17. JavaScript 脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载JavaScript 文件。js 延
迟加载有助于提高页面加载速度。
一般有以下几种方式:
defer 属性:给js 脚本添加defer 属性,这个属性会让脚本的加
载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文
件,这样的话就能使页面的渲染不被阻塞。多个设置了defer 属性
14
的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是
这样。
async 属性:给js 脚本添加async 属性,这个属性会使脚本异步
加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行js
脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async
属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次
执行。
动态创建DOM 方式:动态创建DOM 标签的方式,可以对文档的加载
事件进行监听,当文档加载完成后再动态的创建script 标签来引入
js 脚本。
使用setTimeout 延迟方法:设置一个定时器来延迟加载js 脚本文

让JS 最后加载:将js 脚本放在文档的底部,来使js 脚本尽可能
的在最后来加载执行。
18. 什么是DOM 和BOM?
DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象
主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,
这个对象主要定义了与浏览器进行交互的法和接口。BOM 的核心是
window,而window 对象具有双重角色,它既是通过js 访问浏览器
窗口的一个接口,又是一个Global(全局)对象。这意味着在网页
中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方
法存在。window 对象含有location 对象、navigator 对象、screen
15
对象等子对象,并且DOM 的最根本的对象document 对象也是BOM
的window 对象的子对象。
19. escape、encodeURI、encodeURIComponent 的区别
encodeURI 是对整个URI 进行转义,将URI 中的非法字符转换为合
法字符,所以对于一些在URI 中有特殊意义的字符不会进行转义。
encodeURIComponent 是对URI 的组成部分进行转义,所以一些特殊
字符也会得到转义。
escape 和encodeURI 的作用相同,不过它们对于unicode 编码为
0xff 之外字符的时候会有区别,escape 是直接在字符的unicode
编码前加上%u,而encodeURI 首先会将字符转换为UTF-8 的格式,
再在每个字节前加上%。
20. 对AJAX 的理解,实现一个AJAX 请求
AJAX 是Asynchronous JavaScript and XML 的缩写,指的是通过
JavaScript 的异步通信,从服务器获取XML 文档从中提取数据,
再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX 请求的步骤:
创建一个XMLHttpRequest 对象。
在这个对象上使用open 方法创建一个HTTP 请求,open 方法所需
要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可
以通过setRequestHeader 方法来为请求添加头信息。还可以为这个
对象添加一个状态监听函数。一个XMLHttpRequest 对象一共有5
个状态,当它的状态变化时会触发onreadystatechange 事件,可以
16
通过设置监听函数,来处理请求成功后的结果。当对象的readyState
变为4 的时候,代表服务器返回的数据接收完成,这个时候可以通
过判断请求的状态,如果状态是2xx 或者304 的话则代表返回正常。
这个时候就可以通过response 中的数据来对页面进行更新了。
当对象的属性和监听函数设置完成后,最后调用sent 方法来向服务
器发起请求,可以传入参数作为发送的数据体。
使用Promise 封装AJAX:
17
21. 什么是尾调用,使用尾调用有什么好处?
尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行
栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上
下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,
因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上
下文,从而节省了内存,这就是尾调用优化。但是ES6 的尾调用优
化只在严格模式下开启,正常模式是无效的。
22. ES6 模块与CommonJS 模块有什么异同?
ES6 Module 和CommonJS 模块的区别:
CommonJS 是对模块的浅拷⻉,ES6 Module 是对模块的引⽤,即ES6
Module 只存只读,不能改变其值,也就是指针指向不能变,类似const;
import 的接⼝是read-only(只读状态),不能修改其变量值。即
不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对
commonJS 对重新赋值(改变指针指向),但是对ES6 Module 赋值会
编译报错。
ES6 Module 和CommonJS 模块的共同点:
CommonJS 和ES6 Module 都可以对引⼊的对象进⾏赋值,即对对象内
部属性的值进⾏改变。
23. for...in 和for...of 的区别
for…of 是ES6 新增的遍历方式,允许遍历一个含有iterator 接口
的数据结构(数组、对象等)并且返回各项的值,和ES3 中的for…
in 的区别如下
for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
18
for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而
for … of 只遍历当前对象不会遍历原型链;
对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原
型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结:for...in 循环主要是为了遍历对象而生,不适用于遍历数组;
for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以
及Generator 对象。
24. ajax、axios、fetch 的区别
(1)AJAX
Ajax 即“AsynchronousJavascriptAndXML”(异步JavaScript 和
XML),是指一种创建交互式网页应用的网页开发技术。它是一种在
无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在
后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。
这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行
更新。传统的网页(不使用Ajax)如果需要更新内容,必须重载整
个网页页面。其缺点如下:
本身是针对MVC 编程,不符合前端MVVM 的浪潮
基于原生XHR 开发,XHR 本身的架构不清晰
不符合关注分离(Separation of Concerns)的原则
配置和调用方式非常混乱,而且基于事件的异步模型不友好。
(2)Fetch
fetch 号称是AJAX 的替代品,是在ES6 出现的,使用了ES6 中的
promise 对象。Fetch 是基于promise 设计的。Fetch 的代码结构比
19
起ajax 简单多。fetch 不是ajax 的进一步封装,而是原生js,没有
使用XMLHttpRequest 对象。
fetch 的优点:
语法简洁,更加语义化
基于标准Promise 实现,支持async/await
更加底层,提供的API 丰富(request, response)
脱离了XHR,是ES 规范里新的实现方式
fetch 的缺点:
fetch 只对网络请求报错,对400,500 都当做成功的请求,服务器
返回400,500 错误码时并不会reject,只有网络错误这些导致请
求不能完成时,fetch 才会被reject。
fetch 默认不会带cookie , 需要添加配置项: fetch(url,
{credentials: 'include'})
fetch 不支持abort , 不支持超时控制, 使用setTimeout 及
Promise.reject 的实现的超时控制并不能阻止请求过程继续在后台
运行,造成了流量的浪费
fetch 没有办法原生监测请求的进度,而XHR 可以
(3)Axios
Axios 是一种基于Promise 封装的HTTP 客户端,其特点如下:
浏览器端发起XMLHttpRequests 请求
node 端发起http 请求
支持Promise API
20
监听请求和返回
对请求和返回进行转化
取消请求
自动转换json 数据
客户端支持抵御XSRF 攻击
25. 对原型、原型链的理解
在JavaScript 中是使用构造函数来新建一个对象的,每一个构造函
数的内部都有一个prototype 属性,它的属性值是一个对象,这个
对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用
构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个
指针指向构造函数的prototype 属性对应的值,在ES5 中这个指针
被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在
浏览器中都实现了__proto__ 属性来访问这个属性,但是最好不要
使用这个属性,因为它不是规范中规定的。ES5 中新增了一个
Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原
型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么
它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原
型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一
般来说都是Object.prototype 所以这就是新建的对象为什么能够
使用toString() 等方法的原因。
特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实
体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对
象也会继承这一改变。
21
26. 原型链的终点是什么?如何打印出原型链的终点?
22
由于Object 是构造函数,原型链终点Object.prototype.__proto__,
而Object.prototype.__proto__=== null // true,所以,原型链
的终点是null。原型链上的所有原型都是对象,所有的对象最终都
是由Object 构造的,而Object.prototype 的下一级是
Object.prototype.__proto__。
27. 对作用域、作用域链的理解
1)全局作用域和函数作用域
(1)全局作用域
最外层函数和最外层函数外面定义的变量拥有全局作用域
所有未定义直接赋值的变量自动声明为全局作用域
所有window 对象的属性拥有全局作用域
全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空
间,容易引起命名冲突。
(2)函数作用域
函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访
问到
作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
23
使用ES6 中新增的let 和const 指令可以声明块级作用域,块级作用
域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代
码片段)
let 和const 声明的变量不会有变量提升,也不可以重复声明
在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量
限制在循环内部。
作用域链:
在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个
变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域
查找,依次向上级作用域查找,直到访问到window 对象就被终止,
这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有
序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个
包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是
当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全
局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域
链向后查找。
28. 对this 对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的
对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
24
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作
为函数来调用时,this 指向全局对象。
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,
this 指向这个对象。
第三种是构造器调用模式,如果一个函数用new 调用时,函数执行
前会新创建一个对象,this 指向这个新创建的对象。
第四种是apply 、call 和bind 调用模式,这三个方法都可以显
示的指定调用函数的this 指向。其中apply 方法接收两个参数:
一个是this 绑定的对象,一个是参数数组。call 方法接收的参数,
第一个是this 绑定的对象,后面的其余参数是传入函数执行的参数。
也就是说,在使用call() 方法时,传递给函数的参数必须逐个列举
出来。bind 方法通过传入一个对象,返回一个this 绑定了传入对
象的新函数。这个函数的this 指向除了使用new 时会被改变,其
他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是apply、call
和bind 调用模式,然后是方法调用模式,然后是函数调用模式。
29. call() 和apply() 的区别?
它们的作用一模一样,区别仅在于传入参数的形式的不同。
apply 接受两个参数,第一个参数指定了函数体内this 对象的指向,
第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类
数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
call 传入的参数数量不固定,跟apply 相同的是,第一个参数也是
代表函数体内的this 指向,从第二个参数开始往后,每个参数被依
次传入函数。
25
30. 异步编程的实现方式?
JavaScript 中的异步机制可以分为以下几种:
回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函
数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦
合度太高,不利于代码的可维护。
Promise 的方式,使用Promise 的方式可以将嵌套的回调函数作为
链式调用。但是使用这种方法,有时会造成多个then 的链式调用,
可能会造成代码的语义不够明确。
generator 的方式,它可以在函数的执行过程中,将函数的执行权转
移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行
的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权
给转移回来。因此在generator 内部对于异步操作的方式,可以以
同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控
制权转移回来,因此需要有一个自动执行generator 的机制,比如
说co 模块等方式来实现generator 的自动执行。
async 函数的方式,async 函数是generator 和promise 实现的
一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个
await 语句的时候,如果语句返回一个promise 对象,那么函数将
会等待promise 对象的状态变为resolve 后再继续向下执行。因此
可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动
执行。
31. 对Promise 的理解
26
Promise 是异步编程的一种解决方案,它是一个对象,可以获取异步
操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,
它比传统的解决方案回调函数和事件更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束
的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一
个对象,从它可以获取异步操作的消息。Promise 提供统一的API,
各种异步操作都可以用同样的方法进行处理。
(1)Promise 的实例有三个状态:
Pending(进行中)
Resolved(已完成)
Rejected(已拒绝)
当把一件事情交给promise 时,它的状态就是Pending,任务完成了
状态就变成了Resolved、没有完成失败了就变成了Rejected。
(2)Promise 的实例有两个过程:
pending -> fulfilled : Resolved(已完成)
pending -> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
Promise 的特点:
对象的状态不受外界影响。promise 对象代表一个异步操作,有三种
状态,pending(进行中)、fulfilled(已成功)、rejected(已失
败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他
操作都无法改变这个状态,这也是promise 这个名字的由来——“承
诺”;
27
一旦状态改变就不会再变,任何时候都可以得到这个结果。promise
对象的状态改变,只有两种可能:从pending 变为fulfilled,从
pending 变为rejected。这时就称为resolved(已定型)。如果改
变已经发生了,你再对promise 对象添加回调函数,也会立即得到这
个结果。这与事件(event)完全不同,事件的特点是:如果你错过
了它,再去监听是得不到结果的。
Promise 的缺点:
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
当处于pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始
还是即将完成)。
总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise
是一个构造函数,接收一个函数作为参数,返回一个Promise 实例。
一个Promise 实例有三种状态,分别是pending、resolved 和
rejected,分别代表了进行中、已成功和已失败。实例的状态只能由
pending 转变resolved 或者rejected 状态,并且状态一经改变,
就凝固了,无法再被改变了。
状态的改变是通过resolve() 和reject() 函数来实现的,可以在
异步操作结束后调用这两个函数改变Promise 实例的状态,它的原
型上定义了一个then 方法,使用这个then 方法可以为两个状态的
改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的
末尾执行。
注意:在构造Promise 的时候,构造函数内部的代码是立即执行的
28
32. Promise 解决了什么问题
在工作中经常会碰到这样一个需求,比如我使用ajax 发一个A 请求
后,成功后拿到数据,需要把数据传给B 请求;那么需要如下编写代
码:
上面的代码有如下缺点:
后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致
多个ajax 请求嵌套的情况,代码不够直观。
如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要
前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上
编写代码,导致代码不够直观。
Promise 出现之后,代码变成这样:
这样代码看起了就简洁了很多,解决了地狱回调的问题。
29
33. 对async/await 的理解
async/await 其实是Generator 的语法糖,它能实现的效果都能用
then 链来实现,它是为优化then 链而开发出来的。从字面上来看,
async 是“异步”的简写,await 则为等待,所以很好理解async 用
于申明一个function 是异步的,而await 用于等待一个异步方法
执行完成。当然语法上强制规定await 只能出现在asnyc 函数中,先
来看看async 函数返回了什么:
所以,async 函数返回的是一个Promise 对象。async 函数(包含
函数语句、函数表达式、Lambda 表达式)会返回一个Promise 对象,
如果在函数中return 一个直接量,async 会把这个直接量通过
Promise.resolve() 封装成Promise 对象。
async 函数返回的是一个Promise 对象,所以在最外层不能用
await 获取其返回值的情况下,当然应该用原来的方式:then() 链
来处理这个Promise 对象,就像这样:
30
那如果async 函数没有返回值,又该如何?很容易想到,它会返回
Promise.resolve(undefined)。
联想一下Promise 的特点——无等待,所以在没有await 的情况下
执行async 函数,它会立即执行,返回一个Promise 对象,并且,
绝不会阻塞后面的语句。这和普通返回Promise 对象的函数并无二
致。
注意:Promise.resolve(x) 可以看作是new Promise(resolve =>
resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将
其封装成Promise 实例。
34. async/await 的优势
单一的Promise 链并不能发现async/await 的优势,但是,如果需
要处理由多个Promise 组成的then 链的时候,优势就能体现出来
了(很有意思,Promise 通过then 链来解决多层回调的问题,现在
又用async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于
上一个步骤的结果。仍然用setTimeout 来模拟异步操作:
31
现在用Promise 方式来实现这三个步骤的处理:
输出结果result 是step3() 的参数700 + 200 = 900。doIt() 顺
序执行了三个步骤,一共用了300 + 500 + 700 = 1500 毫秒,和
console.time()/console.timeEnd() 计算的结果一致。
如果用async/await 来实现呢,会是这样:
32
结果和之前的Promise 实现是一样的,但是这个代码看起来是不是
清晰得多,几乎跟同步代码一样
35. async/await 对比Promise 的优势
代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是then 的
链式调⽤也会带来额外的阅读负担
Promise 传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,
⾮常优雅
错误处理友好,async/await 可以⽤成熟的try/catch,Promise 的
错误捕获⾮常冗余
调试友好,Promise 的调试很差,由于没有代码块,你不能在⼀个返
回表达式的箭头函数中设置断点,如果你在⼀个.then 代码块中使⽤
调试器的步进(step-over)功能,调试器并不会进⼊后续的.then 代
码块,因为调试器只能跟踪同步代码的每⼀步。
36. 对象创建的方式有哪些?
一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大
量相似对象的时候,会产生大量的重复代码。但js 和一般的面向对
象的语言不同,在ES6 之前它没有类的概念。但是可以使用函数来
进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:
33
(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装
创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一
个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是
简单的封装了复用代码,而没有建立起对象和类型间的关系。
(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,
只要一个函数是通过new 来调用的,那么就可以把它称为构造函数。
执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数
的prototype 属性,然后将执行上下文中的this 指向这个对象,
最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因
为this 的值指向了新建的对象,因此可以使用this 给对象赋值。
构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建
立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存
在一个缺点就是,造成了不必要的函数对象的创建,因为在js 中函
数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次
都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有
的实例都可以通用的。
(3)第三种模式是原型模式,因为每一个函数都有一个prototype
属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例
都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方
法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决
了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有
办法通过传入参数来初始化值,另一个是如果存在一个引用类型如
Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用
类型值的改变会影响所有的实例。
34
(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自
定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存
在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化
对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的
解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使
用了两种不同的模式,所以对于代码的封装性不够好。
(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创
建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以
实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式
很好地对上面的混合模式进行了封装。
(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实
现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,
在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函
数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法
实现对象的识别。
37. 对象继承的方式有哪些?
(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在
的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,
容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传
递参数。
(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子
类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不
能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函
数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
35
(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数
组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属
性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继
承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我
们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构
造函数,造成了子类型的原型中多了很多不必要的属性。
(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已
有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,
然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为
了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5
中定义的Object.create() 方法就是原型式继承的实现。缺点与原
型链方式相同。
(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于
封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,
然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解
是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这
个对象不是自定义类型时。缺点是没有办法实现函数的复用。
(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类
型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式
组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这
样就避免了创建不必要的属性。
38. 哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
36
意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局
变量,而使这个变量一直留在内存中无法被回收。
被遗忘的计时器或回调函数:设置了setInterval 定时器,而忘记
取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被
一直留在内存中,而无法被回收。
脱离DOM 的引用:获取一个DOM 元素的引用,而后面这个元素被删
除,由于一直保留了对这个元素的引用,所以它也无法被回收。
闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
VUE 部分
1. Vue 的基本原理
当一个Vue 实例创建时, Vue 会遍历data 中的属性, 用
Object.defineProperty ( vue3.0 使用proxy ) 将它们转为
getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时
通知变化。每个组件实例都有相应的watcher 程序实例,它会在组
件渲染的过程中把属性记录为依赖,之后当依赖项的setter 被调用
时,会通知watcher 重新计算,从而致使它关联的组件得以更新。
37
2. 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()来劫持各个属性的setter,getter,在数
据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几
个步骤:
1.需要observe 的数据对象进行递归遍历,包括子属性对象的属性,
都加上setter 和getter 这样的话,给这个对象的某个值赋值,就会
触发setter,那么就能监听到了数据变化
2.compile 解析模板指令,将模板中的变量替换成数据,然后初始化
渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数
据的订阅者,一旦数据有变动,收到通知,更新视图
3.Watcher 订阅者是Observer 和Compile 之间通信的桥梁,主要做
的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己②
自身必须有一个update()方法③待属性变动dep.notice()通知时,
能调用自身的update()方法,并触发Compile 中绑定的回调,则功
成身退。
4.MVVM 作为数据绑定的入口,整合Observer、Compile 和Watcher
三者,通过Observer 来监听自己的model 数据变化,通过Compile
来解析编译模板指令,最终利用Watcher 搭起Observer 和Compile
之间的通信桥梁,达到数据变化-> 视图更新;视图交互变化(input)
-> 数据model 变更的双向绑定效果。
38
3. MVVM、MVC、MVP 的区别
MVC、MVP 和MVVM 是三种常见的软件架构设计模式,主要通过分离
关注点的方式来组织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有
的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户
事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,
可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗
长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离Model、View 和Controller 的方式来组织代码结构。
其中View 负责页面的显示逻辑,Model 负责存储页面的业务数据,
以及对相应数据的操作。并且View 和Model 应用了观察者模式,
当Model 层发生改变的时候它会通知有关View 层更新页面。
39
Controller 层是View 层和Model 层的纽带,它主要负责用户与应
用的响应操作,当用户与页面产生交互的时候,Controller 中的事
件触发器就开始工作了,通过调用Model 层,来完成对Model 的修
改,然后Model 层再去通知View 层更新。
(2)MVVM
MVVM 分为Model、View、ViewModel:
Model 代表数据模型,数据和业务逻辑都在Model 层中定义;
View 代表UI 视图,负责数据的展示;
ViewModel 负责监听Model 中数据的改变并且控制视图的更新,处理
用户交互操作;
Model 和View 并无直接关联,而是通过ViewModel 来进行联系的,
Model 和ViewModel 之间有着双向数据绑定的联系。因此当Model 中
的数据改变时会触发View 层的刷新,View 中由于用户交互操作而改
变的数据也会在Model 中同步。
这种模式实现了Model 和View 的数据自动同步,因此开发者只需要
专注于数据的维护操作即可,而不需要自己操作DOM。
40
(3)MVP
MVP 模式与MVC 唯一不同的在于Presenter 和Controller。在
MVC 模式中使用观察者模式,来实现当Model 层数据发生变化的时
候,通知View 层的更新。这样View 层和Model 层耦合在一起,
当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对
代码的复用性造成一些问题。MVP 的模式通过使用Presenter 来实
现对View 层和Model 层的解耦。MVC 中的Controller 只知道
Model 的接口,因此它没有办法控制View 层的更新,MVP 模式中,
View 层的接口暴露给了Presenter 因此可以在Presenter 中将
Model 的变化和View 的变化绑定在一起,以此来实现View 和
Model 的同步更新。这样就实现了对View 和Model 的解耦,
Presenter 还包含了其他的响应逻辑。
4. slot 是什么?有什么作用?原理是什么?
slot 又名插槽,是Vue 的内容分发机制,组件内部的模板引擎使用
slot 元素作为承载分发内容的出口。插槽slot 是子组件的一个模板
标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决
定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。
默认插槽:又名匿名插槽,当slot 没有指定name 属性值的时候一个
默认显示插槽,一个组件内只有有一个匿名插槽。
41
具名插槽:带有具体名字的插槽,也就是带有name 属性的slot,一
个组件可以出现多个具名插槽。
作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也
可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可
以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过
来的数据决定如何渲染该插槽。
实现原理:当子组件vm 实例化时,获取到父组件传入的slot 标签的
内容,存放在vm.$slot 中,默认插槽为vm.$slot.default,具名插
槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇
到slot 标签,使用$slot 中的内容进行替换,此时可以为插槽传递
数据,若存在数据,则可称该插槽为作用域插槽。
5. $nextTick 原理及作用
Vue 的nextTick 其本质是对JavaScript 执行原理EventLoop 的
一种应用。
nextTick 的核心是利用了如Promise 、MutationObserver 、
setImmediate、setTimeout 的原生JavaScript 方法来模拟对应的
微/宏任务的实现,本质是为了利用JavaScript 的这些异步回调任
务队列来实现Vue 框架中自己的异步回调队列。
nextTick 不仅是Vue 内部的异步队列的调用方法,同时也允许开发
者在实际项目中使用这个方法来满足实际应用中对DOM 更新数据时
机的后续逻辑处理
nextTick 是典型的将底层JavaScript 执行原理应用到具体案例中
的示例,引入异步更新队列机制的原因∶
42
如果是同步更新,则多次对一个或多个属性赋值,会频繁触发UI/DOM
的渲染,可以减少一些无用渲染
同时由于VirtualDOM 的引入,每一次状态发生变化后,状态变化的
信号会发送给组件,组件内部使用VirtualDOM 进行计算得出需要更
新的具体的DOM 节点,然后对DOM 进行更新操作,每次更新状态后
的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所
以异步渲染变得更加至关重要
Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作
DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而DOM2
需要从DOM1 中获取数据,那这时就会发现DOM2 的视图并没有更新,
这时就需要用到了nextTick 了。
由于Vue 的DOM 操作是异步的,所以,在上面的情况中,就要将DOM2
获取数据的操作写在$nextTick 中。
所以,在以下情况下,会用到nextTick:
在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变
化的DOM 结构的时候,这个操作就需要方法在nextTick()的回调函
数中。
在vue 生命周期中,如果在created()钩子进行DOM 操作,也一定要
放在nextTick()的回调函数中。
因为在created()钩子函数中,页面的DOM 还未渲染,这时候也没办
法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在
nextTick()的回调函数中。
43
6. Vue 单页应用与多页应用的区别
概念:
SPA 单页面应用(SinglePage Web Application),指只有一个主页
面的应用,一开始只需要加载一次js、css 等相关资源。所有内容都
包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换
相关组件,仅仅刷新局部资源。
MPA 多页面应用(MultiPage Application),指有多个独立页面的
应用,每个页面必须重复加载js、css 等相关资源。多页应用跳转,
需要整页资源刷新。
区别:
44
7. Vue 中封装的数组方法有哪些,其如何实现页面更新
在Vue 中,对响应式处理利用的是Object.defineProperty 对数据进
行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数
组的截取变化等,所以需要对这些操作进行hack,让Vue 能监听到
其中的变化。
那Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是
Vue 中对这些方法的封装:
45
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组
的__ob__,也就是它的Observer 对象,如果有新的值,就调用
observeArray 继续对新的值观察变化(也就是通过target__proto__
== arrayMethods 来改变了数组实例的型),然后手动调用notify,
通知渲染watcher,执行update。
8. Vue data 中某一个属性的值发生改变后,视图会立即同步执
行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之
后DOM 立即变化,而是按一定的策略进行DOM 的更新。Vue 在更新
DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,
并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher 被多次触发,只会被推入到队列中一次。这种在
缓冲时去除重复数据对于避免不必要的计算和DOM 操作是非常重要
的。然后,在下一个的事件循环tick 中,Vue 刷新队列并执行实际
(已去重的)工作。
9. 简述mixin、extends 的覆盖逻辑
(1)mixin 和extends
mixin 和extends 均是用于合并、拓展组件的, 两者均通过
mergeOptions 方法实现合并。
mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例
对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin
钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
46
(2)mergeOptions 的执行过程
规范化选项( normalizeProps 、normalizelnject 、
normalizeDirectives)
对未合并的选项,进行判断
10. 子组件可以直接改变父组件的数据吗?
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组
件的单向数据流。每次父级组件发生更新时,子组件中所有的prop
都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中
发出警告。
47
Vue 提倡单向数据流,即父级props 的更新会流向子组件,但是反
过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据
流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用
复杂时,debug 的成本会非常高。
只能通过$emit 派发一个自定义事件,父组件接收到后,由父组件
修改。
11. 对React 和Vue 的理解,它们的异同
相似之处:
都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理
交给相关的库;
都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
都使用了Virtual DOM(虚拟DOM)提高重绘性能;
都有props 的概念,允许组件间的数据传递;
都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用
性。
不同之处:
1)数据流
Vue 默认支持数据双向绑定,而React 一直提倡单向数据流
2)虚拟DOM
Vue2.x 开始引入"Virtual DOM",消除了和React 在这方面的差异,
但是在具体的细节还是有各自的特点。
48
Vue 宣称可以更快地计算出Virtual DOM 的差异,这是由于它在渲染
过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
对于React 而言,每当应用的状态被改变时,全部子组件都会重新渲
染。当然,这可以通过PureComponent/shouldComponentUpdate 这
个生命周期方法来进行控制,但Vue 将此视为默认的优化。
3)组件化
React 与Vue 最大的不同是模板的编写。
Vue 鼓励写近似常规HTML 的模板。写起来很接近标准HTML 元素,只
是多了一些属性。
React 推荐你所有的模板通用JavaScript 的语法扩展——JSX 书写。
具体来讲:React 中render 函数是支持闭包特性的,所以import 的
组件在render 中可以直接调用。但是在Vue 中,由于模板中使用的
数据都必须挂在this 上进行一次中转,所以import 一个组件完了
之后,还需要在components 中再声明下。
4)监听数据变化的实现原理不同
Vue 通过getter/setter 以及一些函数的劫持,能精确知道数据变
化,不需要特别的优化就能达到很好的性能
React 默认是通过比较引用的方式进行的, 如果不优化
(PureComponent/shouldComponentUpdate)可能导致大量不必要的
vDOM 的重新渲染。这是因为Vue 使用的是可变数据,而React 更强
调数据的不可变。
5)高阶组件
49
react 可以通过高阶组件(HOC)来扩展,而Vue 需要通过mixins 来
扩展。
高阶组件就是高阶函数,而React 的组件本身就是纯粹的函数,所以
高阶函数对React 来说易如反掌。相反Vue.js 使用HTML 模板创建视
图组件,这时模板无法有效的编译,因此Vue 不能采用HOC 来实现。
6)构建工具
两者都有自己的构建工具:
React ==> Create React APP
Vue ==> vue-cli
7)跨平台
React ==> React Native
Vue ==> Weex
12. Vue 的优点
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有
几十kb ;
简单易学:国人开发,中文文档,不存在语言障碍,易于理解和学
习;
双向数据绑定:保留了angular 的特点,在数据操作方面更为简单;
组件化:保留了react 的优点,实现了html 的封装和重用,在构
建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代
码的修改,只需要操作数据就能完成相关操作;
50
虚拟DOM:dom 操作是非常耗费性能的,不再使用原生的dom 操作
节点,极大解放dom 操作,但具体操作的还是dom 不过是换了另一
种方式;
运行速度更快:相比较于react 而言,同样是操作虚拟dom,就性
能而言, vue 存在很大的优势。
13. assets 和static 的区别
相同点: assets 和static 两个都是存放静态资源文件。项目中所
需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件
下,这是相同点
不相同点:assets 中存放的静态资源文件在项目打包时,也就是运
行npm run build 时会将assets 中放置的静态资源文件进行打包
上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后
的静态资源文件最终也都会放置在static 文件中跟着index.html
一同上传至服务器。static 中放置的静态资源文件就不会要走打包
压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。
因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是
static 中的资源文件由于没有进行压缩等操作,所以文件的体积也
就相对于assets 中打包后的文件提交较大点。在服务器中就会占据
更大的空间。
建议: 将项目中template 需要的样式文件js 文件等都可以放置在
assets 中,走打包这一流程。减少体积。而项目中引入的第三方的
资源文件如iconfoont.css 等文件可以放置在static 中,因为这
些引入的第三方文件已经经过处理,不再需要处理,直接上传。
14. delete 和Vue.delete 删除数组的区别
51
delete 只是被删除的元素变成了empty/undefined 其他的元素的
键值还是不变。
Vue.delete 直接删除了数组改变了数组的键值。
15. Vue 模版编译原理
vue 中的模板template 无法被浏览器解析并渲染,因为这不属于浏
览器的标准,不是正确的HTML 语法,所有需要将template 转化成一
个JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对
应的HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成
为模板编译。模板编译又分三个阶段,解析parse,优化optimize,
生成generate,最终生成可执行函数render。
解析阶段:使用大量的正则表达式对template 字符串进行解析,将
标签、指令、属性等转化为抽象语法树AST。
优化阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在
页面重渲染的时候进行diff 比较时,直接跳过这一些静态节点,优
化runtime 的性能。
生成阶段:将最终的AST 转化为render 函数字符串。
16. vue 初始化页面闪动问题
使用vue 开发时,在vue 初始化之前,由于div 是不归vue 管的,所
以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类
似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还
是有必要让解决这个问题的。
首先:在css 里加上以下代码:
52
如果没有彻底解决问题, 则在根元素加上style="display:
none;" :style="{display: 'block'}"
17. MVVM 的优缺点?
优点:
分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者
逻辑的重⽤性: ⽐如视图(View)可以独⽴于Model 变化和修改,⼀
个ViewModel 可以绑定不同的"View"上,当View 变化的时候Model
不可以不变,当Model 变化的时候View 也可以不变。你可以把⼀些
视图逻辑放在⼀个ViewModel⾥⾯,让很多view 重⽤这段视图逻辑
提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代

⾃动更新dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从
繁琐的⼿动dom 中解放
缺点:
Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,
有可能是你View 的代码有Bug,也可能是Model 的代码有问题。数
据绑定使得⼀个位置的Bug 被快速传递到别的位置,要定位原始出问
题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写
在View 的模版当中的,这些内容是没办法去打断点debug 的
⼀个⼤的模块中model 也会很⼤,虽然使⽤⽅便了也很容易保证了数
据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
53
对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护
的成本都会⽐较⾼。
18. v-if 和v-for 哪个优先级更高?如果同时出现,应如何优
化?
v-for 优先于v-if 被解析,如果同时出现,每次渲染都会先执行循
环再判断条件,无论如何循环都不可避免,浪费了性能。
要避免出现这种情况,则在外层嵌套template,在这一层进行v-if
判断,然后在内部进行v-for 循环。如果条件出现在循环内部,可通
过计算属性提前过滤掉那些不需要显示的项。
19. 对Vue 组件化的理解
1.组件是独立和可复用的代码组织单元。组件系统是Vue 核心特性之
一,它使开发者使用小型、独立和通常可复用的组件构建大型应用;
2.组件化开发能大幅提高应用开发效率、测试性、复用性等;
3.组件使用按分类有:页面组件、业务组件、通用组件;
4.vue 的组件是基于配置的,我们通常编写的组件是组件配置而非组
件,框架后续会生成其构造函数,它们基于VueComponent,扩展于
Vue;
5.vue 中常见组件化技术有:属性prop,自定义事件,插槽等,它们
主要用于组件通信、扩展等;6.合理的划分组件,有助于提升应用性
能;
6.组件应该是高内聚、低耦合的;
7.遵循单向数据流的原则。
20. 对vue 设计原则的理解
54
1.渐进式JavaScript 框架:与其它大型框架不同的是,Vue 被设计
为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上
手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工
具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应
用提供驱动。
2.易用性:vue 提供数据响应式、声明式模板语法和基于配置的组件
系统等核心特性。这些使我们只需要关注应用的核心业务即可,只要
会写js、html 和css 就能轻松编写vue 应用。
3.灵活性:渐进式框架的最大优点就是灵活性,如果应用足够小,我
们可能仅需要vue 核心特性即可完成功能;随着应用规模不断扩大,
我们才可能逐渐引入路由、状态管理、vue-cli 等库和工具,不管是
应用体积还是学习难度都是一个逐渐增加的平和曲线。
4.高效性:超快的虚拟DOM 和diff算法使我们的应用拥有最佳的性能
表现。追求高效的过程还在继续,vue3 中引入Proxy 对数据响应式
改进以及编译器中对于静态内容编译的改进都会让vue 更加高效。
21. 说一下Vue 的生命周期
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、
编译模版、挂载Dom -> 渲染、更新-> 渲染、卸载等⼀系列过程,
称这是Vue 的⽣命周期。
1.beforeCreate(创建前):数据观测和初始化事件还未开始,此时
data 的响应式追踪、event/watcher 都还没有被设置,也就是说不
能访问到data、computed、watch、methods 上的方法和数据。
55
2.created(创建后) :实例创建完成,实例上配置的options 包
括data、computed、watch、methods 等都配置完成,但是此时渲染
得节点还未挂载到DOM,所以不能访问到$el 属性。
3.beforeMount(挂载前):在挂载开始之前被调用,相关的render
函数首次被调用。实例已完成以下的配置:编译模板,把data 里面
的数据和模板生成html。此时还没有挂载html 到页面上。
4.mounted(挂载后):在el 被新创建的vm.$el 替换,并挂载到实
例上去之后调用。实例已完成以下的配置:用上面编译好的html 内
容替换el 属性指向的DOM 对象。完成模板中的html 渲染到html 页
面中。此过程中进行ajax 交互。
5.beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应
式数据更新了,但是对应的真实DOM 还没有被渲染。
6.updated(更新后) :在由于数据更改导致的虚拟DOM 重新渲染和
打补丁之后调用。此时DOM 已经根据响应式数据的变化更新了。调
用时,组件DOM 已经更新,所以可以执行依赖于DOM 的操作。然而
在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更
新无限循环。该钩子在服务器端渲染期间不被调用。
7.beforeDestroy(销毁前):实例销毁之前调用。这一步,实例仍
然完全可用,this 仍能获取到实例。
8.destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示
的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例
也会被销毁。该钩子在服务端渲染期间不被调用。
56
另外还有keep-alive 独有的生命周期,分别为activated 和
deactivated。用keep-alive 包裹的组件在切换时不会进行销毁,而
是缓存到内存中并执行deactivated 钩子函数,命中缓存渲染后会执
行activated 钩子函数。
22. Vue 子组件和父组件执行顺序
加载渲染过程:
1.父组件beforeCreate
2.父组件created
3.父组件beforeMount
4.子组件beforeCreate
5.子组件created
6.子组件beforeMount
7.子组件mounted
8.父组件mounted
更新过程:
1. 父组件beforeUpdate
2.子组件beforeUpdate
3.子组件updated
4.父组件updated
销毁过程:
1. 父组件beforeDestroy
57
2.子组件beforeDestroy
3.子组件destroyed
4.父组件destoryed
23. created 和mounted 的区别
created:在模板渲染成html 前调用,即通常初始化某些属性值,然
后再渲染成视图。
mounted:在模板渲染成html 后调用,通常是初始化页面完成后,再
对html 的dom 节点进行一些需要的操作。
4. 一般在哪个生命周期请求异步数据
我们可以在钩子函数created、beforeMount、mounted 中进行调用,
因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的
数据进行赋值。
推荐在created 钩子函数中调用异步请求,因为在created 钩子函
数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR 不支持beforeMount 、mounted 钩子函数,放在created 中有
助于一致性。
24. keep-alive 中的生命周期哪些
keep-alive 是Vue 提供的一个内置组件,用来对组件进行缓存——
在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
58
如果为一个组件包裹了keep-alive,那么它会多出两个生命周期:
deactivated、activated。同时,beforeDestroy 和destroyed 就
不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发deactivated 生命周期;
当组件被切回来时,再去缓存里找这个组件、触发activated 钩子
函数。
25. 路由的hash 和history 模式的区别
Vue-Router 有两种模式:hash 模式和history 模式。默认的路由模
式是hash 模式。
1. hash 模式
简介: hash 模式是开发中默认的模式,它的URL 带着一个#,例如:
http://www.abc.com/#/vue,它的hash 值就是#/vue。
特点:hash 值会出现在URL 里面,但是不会出现在HTTP 请求中,对
后端完全没有影响。所以改变hash 值,不会重新加载页面。这种模
式的浏览器支持度很好,低版本的IE 浏览器也支持这种模式。hash
路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
原理: hash 模式的主要原理就是onhashchange()事件:
使用onhashchange()事件的好处就是,在页面的hash 值发生变化时,
无需向后端发起请求,window 就可以监听事件的改变,并按规则加
载相应的代码。除此之外,hash 值变化对应的URL 都会被浏览器记
59
录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后
端服务器,但是页面的hash 值和对应的URL 关联起来了。
2. history 模式
简介: history 模式的URL 中没有#,它使用的是传统的路由分发模
式,即用户在输入一个URL 时,服务器会接收这个请求,并解析这个
URL,然后做出相应的逻辑处理。
特点: 当使用history 模式时, URL 就像这样:
http://abc.com/user/id。相比hash 模式更加好看。但是,history
模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
API: history api 可以分为两大部分,切换历史状态和修改历史状
态:
修改历史状态: 包括了HTML5 History Interface 中新增的
pushState() 和replaceState() 方法,这两个方法应用于浏览器的
历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修
改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要
做到改变url 但又不刷新页面的效果,就需要前端用上这两个API。
切换历史状态: 包括forward()、back()、go()三个方法,对应浏
览器的前进,后退,跳转操作。
虽然history 模式丢弃了丑陋的#。但是,它也有自己的缺点,就是
在刷新页面的时候,如果没有相应的路由或资源,就会刷出404 来。
如果想要切换到history 模式,就要进行以下配置(后端也要进行配
置):
60
3. 两种模式对比
调用history.pushState() 相比于直接修改hash,存在以下优势:
pushState() 设置的新URL 可以是与当前URL 同源的任意URL;而
hash 只可修改# 后面的部分,因此只能设置与当前URL 同文档的
URL;
pushState() 设置的新URL 可以与当前URL 一模一样,这样也会把
记录添加到栈中;而hash 设置的新值必须与原来不一样才会触发动
作将记录添加到栈中;
pushState() 通过stateObject 参数可以添加任意类型的数据到记
录中;而hash 只可添加短字符串;
pushState() 可额外设置title 属性供后续使用。
hash 模式下,仅hash 符号之前的url 会被包含在请求中,后端如果
没有做到对路由的全覆盖,也不会返回404 错误;history 模式下,
前端的url 必须和实际向后端发起请求的url 一致,如果没有对用的
路由处理,将返回404 错误。
hash 模式和history 模式都有各自的优势和缺陷,还是要根据实际
情况选择性的使用。
26. Vue-router 跳转和location.href 有什么区别
使用location.href= /url 来跳转,简单方便,但是刷新了页面;
使用history.pushState( /url ) ,无刷新页面,静态跳转;
61
引进router ,然后使用router.push( /url ) 来跳转,使用了diff
算法,实现了按需加载,减少了dom 的消耗。其实使用router 跳
转和使用history.pushState() 没什么差别的,因为vue-router 就
是用了history.pushState() ,尤其是在history 模式下。
27. Vuex 的原理
Vuex 是一个专为Vue.js 应用程序开发的状态管理模式。每一个
Vuex 应用的核心就是store(仓库)。“store” 基本上就是一个
容器,它包含着你的应用中大部分的状态( state )。
Vuex 的状态存储是响应式的。当Vue 组件从store 中读取状态的
时候,若store 中的状态发生变化,那么相应的组件也会相应地得
到高效更新。
改变store 中的状态的唯一途径就是显式地提交(commit)
mutation。这样可以方便地跟踪每一个状态的变化。
62
Vuex 为Vue Components 建立起了一个完整的生态圈,包括开发中的
API 调用一环。
(1)核心流程中的主要功能:
Vue Components 是vue 组件,组件会触发(dispatch)一些事件或
动作,也就是图中的Actions;
在组件中发出的动作,肯定是想获取或者改变数据的,但是在vuex
中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提
交(Commit)到Mutations 中;
然后Mutations 就去改变(Mutate)State 中的数据;
当State 中的数据被改变之后,就会重新渲染(Render)到Vue
Components 中去,组件展示更新后的数据,完成一个流程。
(2)各模块在核心流程中的主要功能:
Vue Components∶ Vue 组件。HTML 页面上,负责接收用户操作等交
互行为,执行dispatch 方法触发对应action 进行回应。
dispatch∶操作行为触发方法,是唯一能执行action 的方法。
actions∶ 操作行为处理模块。负责处理Vue Components 接收到的
所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册
的顺序依次触发。向后台API 请求的操作就在这个模块中进行,包括
触发其他action 以及提交mutation 的操作。该模块提供了Promise
的封装,以支持action 的链式触发。
commit∶状态改变提交操作方法。对mutation 进行提交,是唯一能
执行mutation 的方法。
63
mutations∶状态改变操作方法。是Vuex 修改state 的唯一推荐方法,
其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且
方法名只能全局唯一。操作之中会有一些hook 暴露出来,以进行
state 的监控等。
state∶ 页面状态管理容器对象。集中存储Vuecomponents 中data
对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需
的数据从该对象中进行读取,利用Vue 的细粒度数据响应机制来进行
高效的状态更新。
getters∶ state 对象读取方法。图中没有单独列出该模块,应该被
包含在了render 中,Vue Components 通过该方法读取全局state 对
象。
总结:
Vuex 实现了一个单向数据流,在全局拥有一个State 存放数据,当
组件要更改State 中的数据时,必须通过Mutation 提交修改信息,
Mutation 同时提供了订阅者模式供外部插件调用获取State 数据
的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)
或批量的同步操作需要走Action ,但Action 也是无法直接修改
State 的,还是需要通过Mutation 来修改State 的数据。最后,根
据State 的变化,渲染到视图上。
28. Vuex 和localStorage 的区别
(1)最重要的区别
vuex 存储在内存中
64
localstorage 则以文件的方式存储在本地,只能存储字符串类型的
数据,存储对象需要JSON 的stringify 和parse 方法进行处理。读
取内存比读取硬盘速度要快
(2)应用场景
Vuex 是一个专为Vue.js 应用程序开发的状态管理模式。它采用集
中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一
种可预测的方式发生变化。vuex 用于组件之间的传值。
localstorage 是本地存储,是将数据存储到浏览器的方法,一般是
在跨页面传递数据时使用。
Vuex 能做到数据的响应式,localstorage 不能
(3)永久性
刷新页面时vuex 存储的值会丢失,localstorage 不会。
注意:对于不变的数据确实可以用localstorage 可以代替vuex,但
是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件
改变了该数据源,希望另一个组件响应该变化时,localstorage 无
法做到,原因就是区别1。
29. Redux 和Vuex 有什么区别,它们的共同思想
(1)Redux 和Vuex 区别
Vuex 改进了Redux 中的Action 和Reducer 函数,以mutations 变化
函数取代Reducer,无需switch,只需在对应的mutation 函数里改
变state 值即可
Vuex 由于Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要
生成新的State 即可
65
Vuex 数据流的顺序是∶View 调用store.commit 提交对应的请求到
Store 中对应的mutation 函数->store 改变(vue 检测到数据变化自
动渲染)
通俗点理解就是,vuex 弱化dispatch,通过commit 进行store 状
态的一次更变;取消了action 概念,不必传入特定的action 形式进
行指定变更;弱化reducer,基于commit 参数直接对数据进行转变,
使得框架更加简易;
(2)共同思想
单—的数据源
变化可以预测
本质上:redux 与vuex 都是对mvvm 思想的服务,将数据从视图中抽
离的一种方案;
形式上:vuex 借鉴了redux,将store 作为全局的数据中心,进行
mode 管理;
30. 为什么要用Vuex 或者Redux
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组
件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通
过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通
常会导致代码无法维护。
所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在
这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,
任何组件都能获取状态或者触发行为。
66
另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,
代码将会变得更结构化且易维护。
31. Vuex 有哪几种属性?
有五种,分别是State、Getter、Mutation 、Action、Module
state => 基本数据(数据源存放地)
getters => 从基本数据派生出来的数据
mutations => 提交更改数据的方法,同步
actions => 像一个装饰器,包裹mutations,使之可以异步。
modules => 模块化Vuex
32. Vuex 和单纯的全局对象有什么区别?
Vuex 的状态存储是响应式的。当Vue 组件从store 中读取状态的
时候,若store 中的状态发生变化,那么相应的组件也会相应地得
到高效更新。
不能直接改变store 中的状态。改变store 中的状态的唯一途径就
是显式地提交(commit) mutation。这样可以方便地跟踪每一个状态
的变化,从而能够实现一些工具帮助更好地了解我们的应用。
33. 为什么Vuex 的mutation 中不能做异步操作?
Vuex 中所有的状态更新的唯一途径都是mutation,异步操作通过
Action 来提交mutation 实现,这样可以方便地跟踪每一个状态的
变化,从而能够实现一些工具帮助更好地了解我们的应用。
每个mutation 执行完成后都会对应到一个新的状态变更,这样
devtools 就可以打个快照存下来,然后就可以实现time-travel 了。
67
如果mutation 支持异步操作,就没有办法知道状态是何时更新的,
无法很好的进行状态的追踪,给调试带来困难。
34. Vue3.0 有什么更新
(1)监测机制的改变
3.0 将带来基于代理Proxy 的observer 实现,提供全语言覆盖的
反应性跟踪。
消除了Vue 2 当中基于Object.defineProperty 的实现所存在的
很多限制:
(2)只能监测属性,不能监测对象
检测属性的添加和删除;
检测数组索引和长度的变更;
支持Map、Set、WeakMap 和WeakSet。
(3)模板
作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,
而3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重
新渲染,提升了渲染的性能。
同时,对于render 函数的方面,vue3.0 也会进行一系列更改来方
便习惯直接使用api 来生成vdom 。
(4)对象式的组件声明方式
vue2.x 中的组件是通过声明的方式传入一系列option , 和
TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功
能,但是比较麻烦。
68
3.0 修改了组件的声明方式,改成了类式的写法,这样使得和
TypeScript 的结合变得很容易
(5)其它方面的更改
支持自定义渲染器,从而使得weex 可以通过自定义渲染器的方式来
扩展,而不是直接fork 源码来改的方式。
支持Fragment(多个根节点)和Protal(在dom 其他部分渲染组
建内容)组件,针对一些特殊的场景做了处理。
基于tree shaking 优化,提供了更多的内置功能。
35. defineProperty 和proxy 的区别
Vue 在实例初始化时遍历data 中的所有属性, 并使用
Object.defineProperty 把这些属性全部转为getter/setter。这样
当追踪数据发生变化时,setter 会被自动调用。
Object.defineProperty 是ES5 中一个无法shim 的特性,这也就
是Vue 不支持IE8 以及更低版本浏览器的原因。
但是这样做有以下问题:
1.添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象
没有在初始化进行响应式处理, 只能通过$set 来调用
Object.defineProperty()处理。
2.无法监控到数组下标和长度的变化。
Vue3 使用Proxy 来监控数据的变化。Proxy 是ES6 中提供的功能,
其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚
举,函数调用等)。相对于Object.defineProperty(),其有以下特
点:
69
1.Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可
以监听同级结构下的所有属性变化,包括新增属性和删除属性。
2.Proxy 可以监听数组的变化。
36. Vue3.0 为什么要用proxy?
在Vue2 中, 0bject.defineProperty 会改变原始数据,而Proxy
是创建对象的虚拟表示,并提供set 、get 和deleteProperty 等
处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,
有以下特点∶
不需用使用Vue.$set 或Vue.$delete 触发响应式。
全方位的数组变化检测,消除了Vue2 无效的边界情况。
支持Map,Set,WeakMap 和WeakSet。
Proxy 实现的响应式原理与Vue2 的实现原理相同,实现方式大同小
异∶
get 收集依赖
Set、delete 等触发依赖
对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执
行依赖相关的收集或触发逻辑。
37. 虚拟DOM 的解析过程
虚拟DOM 的解析过程:
首先对将要插入到文档中的DOM 树结构进行分析,使用js 对象将
其表示出来,比如一个元素对象,包含TagName、props 和Children
70
这些属性。然后将这个js 对象树给保存下来,最后再将DOM 片段
插入到文档中。
当页面的状态发生改变,需要对页面的DOM 的结构进行调整的时候,
首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象
树和旧的对象树进行比较,记录下两棵树的的差异。
最后将记录的有差异的地方应用到真正的DOM 树中去,这样视图就
更新了。
38. DIFF 算法的原理
在新老虚拟DOM 对比时:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则
删除该节点重新创建节点进行替换
如果为相同节点,进行patchVnode,判断如何对该节点的子节点进
行处理,先判断一方有子节点一方没有子节点的情况(如果新的
children 没有子节点,将旧的子节点移除)
比较如果都有子节点,则进行updateChildren,判断如何对这些新
老节点的子节点进行操作(diff 核心)。
匹配时,找到相同的子节点,递归比较子节点
在diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使
得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children
都为多个子节点时才需要用核心的Diff 算法进行同层级比较。
39. Vue 中key 的作用
vue 中key 值的作用可以分为两种情况来考虑:
71
第一种情况是v-if 中使用key。由于Vue 会尽可能高效地渲染元
素,通常会复用已有元素而不是从头开始渲染。因此当使用v-if 来
实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个
元素就会被复用。如果是相同的input 元素,那么切换前后用户的
输入不会被清除掉,这样是不符合需求的。因此可以通过使用key 来
唯一的标识一个元素,这个情况下,使用key 的元素不会被复用。
这个时候key 的作用是用来标识一个独立的元素。
第二种情况是v-for 中使用key。用v-for 更新已渲染过的元素列
表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改
变,Vue 不会移动DOM 元素来匹配数据项的顺序,而是简单复用此
处的每个元素。因此通过为每个列表项提供一个key 值,来以便Vue
跟踪元素的身份,从而高效的实现复用。这个时候key 的作用是为
了高效的更新渲染虚拟DOM。
key 是为Vue 中vnode 的唯一标记,通过这个key,diff 操作可
以更准确、更快速
更准确:因为带key 就不是就地复用了,在sameNode 函数a.key
=== b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用key 的唯一性生成map 对象来获取对应节点,比遍历
方式更快
React 部分
1. React 的事件和普通的HTML 事件有什么不同?
区别:
对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
72
对于事件函数处理语法,原生事件为字符串,react 事件为函数;
react 事件不能采用return false 的方式来阻止浏览器的默认行
为,而必须要地明确地调用preventDefault()来阻止默认行为。
合成事件是react 模拟原生DOM 事件所有能力的一个事件对象,其
优点如下:
兼容所有浏览器,更好的跨平台;
将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
方便react 统一管理和事务机制。
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒
泡绑定到document 上,所以尽量避免原生事件与合成事件混用,如
果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到
document 上合成事件才会执行。
2. React 组件中怎么做事件代理?它的原理是什么?
React 基于Virtual DOM 实现了一个SyntheticEvent 层(合成事件
层),定义的事件处理器会接收到一个合成事件对象的实例,它符合
W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,
所有的事件都自动绑定在最外层上。
在React 底层,主要对合成事件做了两件事:
事件委派:React 会把所有的事件绑定到结构的最外层,使用统一的
事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部
事件监听和处理函数。
自动绑定:React 组件中,每个方法的上下文都会指向该组件的实例,
即自动绑定this 为当前组件。
73
3. React 高阶组件、Render props、hooks 有什么区别,为什
么要不断迭代
这三者是目前react 解决代码复用的主要方式:
高阶组件(HOC)是React 中用于复用组件逻辑的一种高级技巧。HOC
自身不是React API 的一部分,它是一种基于React 的组合特性而
形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组
件的函数。
render props 是指一种在React 组件之间使用一个值为函数的
prop 共享代码的简单技术,更具体的说,render prop 是一个用于
告知组件需要渲染什么内容的函数prop。
通常,render props 和高阶组件只渲染一个子节点。让Hook 来服
务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个
虚拟滚动条组件或许会有一个renderltem 属性,或是一个可见的容
器组件或许会有它自己的DOM 结构)。但在大部分场景下,Hook 足
够了,并且能够帮助减少嵌套。
(1)HOC
官方解释∶
高阶组件(HOC)是React 中用于复用组件逻辑的一种高级技巧。HOC
自身不是React API 的一部分,它是一种基于React 的组合特性而
形成的设计模式。
简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参
数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
74
HOC 的优缺点∶
优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
缺点∶ hoc 传递给被包裹组件的props 容易和被包裹后的组件重名,
进而被覆盖
(2)Render props
官方解释∶
"render prop"是指一种在React 组件之间使用一个值为函数的
prop 共享代码的简单技术
具有render prop 的组件接受一个返回React 元素的函数,将render
的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其
他有效的标识符。
75
由此可以看到,render props 的优缺点也很明显∶
优点:数据共享、代码复用,将组件内的state 作为props 传递给调
用者,将渲染逻辑交给调用者。
缺点:无法在return 语句外访问数据、嵌套写法不够优雅
(3)Hooks
官方解释∶
Hook 是React 16.8 的新增特性。它可以让你在不编写class 的情
况下使用state 以及其他的React 特性。通过自定义hook,可以
复用代码逻辑。
76
以上可以看出,hook 解决了hoc 的prop 覆盖的问题,同时使用的方
式解决了render props 的嵌套地狱的问题。hook 的优点如下∶
使用直观;
解决hoc 的prop 重名问题;
解决render props 因共享数据而出现嵌套地狱的问题;
能在return 之外使用数据的问题。
需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。
总结∶
Hoc、render props 和hook 都是为了解决代码复用的问题,但是hoc
和render props 都有特定的使用场景和明显的缺点。hook 是
react16.8 更新的新的API,让组件逻辑复用更简洁明了,同时也解
决了hoc 和render props 的一些缺点。
4. Component, Element, Instance 之间有什么区别和联系?
元素:一个元素element 是一个普通对象(plain object),描述了对
于一个DOM 节点或者其他组件component,你想让它在屏幕上呈现成
什么样子。元素element 可以在它的属性props 中包含其他元素(译
注:用于形成元素树)。创建一个React 元素element 成本很低。元素
element 创建之后是不可变的。
组件:一个组件component 可以通过多种方式声明。可以是带有一个
render()方法的类,简单点也可以定义为一个函数。这两种情况下,
它都把属性props 作为输入,把返回的一棵元素树作为输出。
77
实例:一个实例instance 是你在所写的组件类component class 中
使用关键字this 所指向的东西(译注:组件实例)。它用来存储本地状
态和响应生命周期事件很有用。
函数式组件(Functional component)根本没有实例instance。类组
件(Class component)有实例instance,但是永远也不需要直接创建
一个组件的实例,因为React 帮我们做了这些。
5. React.createClass 和extends Component 的区别有哪些?
React.createClass 和extends Component 的bai 区别主要在于:
(1)语法区别
createClass 本质上是一个工厂函数,extends 的方式更加接近最新
的ES6 规范的class 写法。两种方式在语法上的差别主要体现在方法
的定义和静态属性的声明上。
createClass 方式的方法定义使用逗号,隔开,因为creatClass 本
质上是一个函数,传递给它的是一个Object;而class 的方式定义
方法时务必谨记不要使用逗号隔开,这是ES6 class 的语法规范。
(2)propType 和getDefaultProps
React.createClass:通过proTypes 对象和getDefaultProps()方法
来设置和获取props.
React.Component:通过设置两个属性propTypes 和defaultProps
(3)状态的区别
React.createClass:通过getInitialState()方法返回一个包含初
始值的对象
React.Component:通过constructor 设置初始状态
78
(4)this 区别
React.createClass:会正确绑定this
React.Component:由于使用了ES6,这里会有些微不同,属性并不
会自动绑定到React 类的实例上。
(5)Mixins
React.createClass:使用React.createClass 的话,可以在创建组
件时添加一个叫做mixins 的属性,并将可供混合的类的集合以数组
的形式赋给mixins。
如果使用ES6 的方式来创建组件,那么React mixins 的特性将不
能被使用了。
6. React 如何判断什么时候重新渲染组件?
组件状态的改变可以因为props 的改变,或者直接通过setState 方
法改变。组件获得新的状态,然后React 决定是否应该重新渲染组件。
只要组件的state 发生变化,React 就会对组件进行重新渲染。这是
因为React 中的shouldComponentUpdate 方法默认返回true,这就
是导致每次更新都重新渲染的原因。
当React 将要渲染组件时会执行shouldComponentUpdate 方法来看它
是否返回true(组件应该更新,也就是重新渲染)。所以需要重写
shouldComponentUpdate 方法让它根据情况返回true 或者false 来
告诉React 什么时候重新渲染什么时候跳过重新渲染。
7. React 中可以在render 访问refs 吗?为什么?
不可以,render 阶段DOM 还没有生成,无法获取DOM。DOM 的获取
需要在pre-commit 阶段和commit 阶段:
79
8. React setState 调用之后发生了什么?是同步还是异步?
(1)React 中setState 后发生了什么
在代码中调用setState 函数之后,React 会将传入的参数对象与组
件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和
过程,React 会以相对高效的方式根据新的状态构建React 元素树
并且着手重新渲染整个UI 界面。
在React 得到元素树之后,React 会自动计算出新的树与老树的节
点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,
React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,
这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁setState。React 会将state 的改变压入栈中,
在合适的时机,批量更新state 和视图,达到提高性能的效果。
(2)setState 是同步还是异步的
80
假如所有setState 是同步的,意味着每执行一次setState 时(有可
能一个同步代码中,多次setState),都重新vnode diff + dom 修
改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代
码中的多个setState 合并成一次组件更新。所以默认是异步的,但
是在一些情况下是同步的。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而
不同。在源码中,通过isBatchingUpdates 来判断setState 是先
存进state 队列还是直接更新,如果值为true 则执行异步操作,
为false 则直接更新。
异步:在React 可以控制的地方,就为true,比如在React 生命
周期事件和合成事件中,都会走合并操作,延迟更新的策略。
同步:在React 无法控制的地方,比如原生事件,具体就是在
addEventListener 、setTimeout、setInterval 等事件中,就只能
同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数:
setState 设计为异步,可以显著的提升性能。如果每次调用setState
都进行一次更新,那么意味着render 函数会被频繁调用,界面重新
渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后
进行批量更新;
如果同步更新了state,但是还没有执行render 函数,那么state
和props 不能保持同步。state 和props 不能保持一致性,会在开发
中产生很多的问题;
9. React 组件的state 和props 有什么区别?
(1)props
81
props 是一个从外部传进组件的参数,主要作为就是从父组件向子组
件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新
的props 来重新渲染子组件,否则子组件的props 以及展现形式不会
改变。
(2)state
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只
能在constructor 中初始化,它算是组件的私有属性,不可通过外部
访问和修改,只能通过组件内部的this.setState 来修改,修改state
属性会导致组件的重新渲染。
(3)区别
props 是传递给组件的(类似于函数的形参),而state 是在组件
内被组件自己管理的(类似于在一个函数内声明的变量)。
props 是不可修改的,所有React 组件都必须像纯函数一样保护它
们的props 不被更改。
state 是在组件中创建的,一般在constructor 中初始化state。
state 是多变的、可以修改,每次setState 都异步更新的。
10. React 中的props 为什么是只读的?
this.props 是组件之间沟通的一个接口,原则上来讲,它只能从父
组件流向子组件。React 具有浓重的函数式编程的思想。
提到函数式编程就要提一个概念:纯函数。它有几个特点:
给定相同的输入,总是返回相同的输出。
过程没有副作用。
不依赖外部状态。
82
this.props 就是汲取了纯函数的思想。props 的不可以变性就保证的
相同的输入,页面显示的内容是一样的,并且不会产生副作用
11. React 中怎么检验props?验证props 的目的是什么?
React 为我们提供了PropTypes 以供验证使用。当我们向Props 传入
的数据无效(向Props 传入的数据类型和验证的数据类型不符)就会
在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的
问题。并且,它还可以让程序变得更易读。
当然,如果项目汇中使用了TypeScript,那么就可以不用PropTypes
来校验,而使用TypeScript 定义接口来校验props。
2. React 废弃了哪些生命周期?为什么?
被废弃的三个函数都是在render 之前,因为fber 的出现,很可能因
为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外
的一个原因则是,React 想约束使用者,好的框架能够让人不得已写
出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及
即将废弃的生命周期分析入手
1) componentWillMount
首先这个函数的功能完全可以使用componentDidMount 和
constructor 来代替,异步获取的数据的情况上面已经说明了,而如
83
果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在
constructor 中执行,除此之外,如果在willMount 中订阅事件,
但在服务端这并不会执行willUnMount 事件,也就是说服务端会导
致内存泄漏所以componentWilIMount 完全可以不使用,但使用者有
时候难免因为各种各样的情况在componentWilMount 中做一些操作,
那么React 为了约束开发者,干脆就抛掉了这个API
2) componentWillReceiveProps
在老版本的React 中,如果组件自身的某个state 跟其props 密
切相关的话,一直都没有一种很优雅的处理方式去更新state,而是
需要在componentWilReceiveProps 中判断前后两个props 是否相
同,如果不同再将新的props 更新到相应的state 上去。这样做一
来会破坏state 数据的单一数据源,导致组件状态变得不可预测,
另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一
个可以横向滑动的列表,当前高亮的Tab 显然隶属于列表自身的时,
根据传入的某个值,直接定位到某个Tab。为了解决这些问题,React
引入了第一个新的生命周期:getDerivedStateFromProps。它有以下
的优点∶
●getDSFP 是静态方法,在这里不能使用this,也就是一个纯函数,
开发者不能写出副作用的代码
●开发者只能通过prevState 而不是prevProps 来做对比,保证了
state 和props 之间的简单关系以及不需要处理第一次渲染时
prevProps 为空的情况
●基于第一点,将状态变化(setState)和昂贵操作(tabChange)
区分开,更加便于render 和commit 阶段操作或者说优化。
3) componentWillUpdate
84
与componentWillReceiveProps 类似,许多开发者也会在
componentWillUpdate 中根据props 的变化去触发一些回调。但
不论是componentWilReceiveProps 还是componentWilUpdate,都
有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有
可能会被调用多次,这显然是不可取的。与componentDidMount 类
似, componentDidUpdate 也不存在这样的问题,一次更新中
componentDidUpdate 只会被调用一次,所以将原先写在
componentWillUpdate 中的回调迁移至componentDidUpdate
就可以解决这个问题。
另外一种情况则是需要获取DOM 元素状态,但是由于在fber 中,
render 可打断,可能在wilMount 中获取到的元素状态很可能与实际
需要的不同,这个通常可以使用第二个新增的生命函数的解决
getSnapshotBeforeUpdate(prevProps, prevState)
4) getSnapshotBeforeUpdate(prevProps, prevState)
返回的值作为componentDidUpdate 的第三个参数。与willMount 不
同的是,getSnapshotBeforeUpdate 会在最终确定的render 执行之
前执行,也就是能保证其获取到的元素状态与didUpdate 中获取到的
元素状态相同。官方参考代码:
85
12. React 16.X 中props 改变后在哪个生命周期中处理
在getDerivedStateFromProps 中进行处理。
这个生命周期函数是为了替代componentWillReceiveProps 存在的,
所以在需要使用componentWillReceiveProps 时,就可以考虑使用
getDerivedStateFromProps 来进行替代。
两者的参数是不相同的,而getDerivedStateFromProps 是一个静态
函数,也就是这个函数不能通过this 访问到class 的属性,也并不
推荐直接访问属性。而是应该通过参数提供的nextProps 以及
prevState 来进行判断,根据新传入的props 来映射到state。
86
需要注意的是,如果props 传入的内容不需要影响到你的state,那
么就需要返回一个null,这个返回值是必须的,所以尽量将其写到
函数的末尾:
13. React 16 中新生命周期有哪些
关于React16 开始应用的新生命周期:
可以看出,React16 自上而下地对生命周期做了另一种维度的解读:
Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被
React 暂停,这一点和React16 引入的Fiber 架构(我们后面会重
点讲解)是有关的;
87
Pre-commit 阶段:所谓“commit”,这里指的是“更新真正的DOM 节
点”这个动作。所谓Pre-commit,就是说我在这个阶段其实还并没
有去更新真实的DOM,不过DOM 信息已经是可以读取的了;
Commit 阶段:在这一步,React 会完成真实DOM 的更新工作。Commit
阶段,我们可以拿到真实DOM(包括refs)。
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、
“卸载”这三个广义的划分方式。它们分别对应到:
挂载过程:
constructor
getDerivedStateFromProps
render
componentDidMount
更新过程:
getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
卸载过程:
componentWillUnmount
14. React-Router 的实现原理是什么?
客户端路由实现的思想:
88
基于hash 的路由:通过监听hashchange 事件,感知hash 的变化
改变hash 可以直接通过location.hash=xxx
基于H5 history 路由:
改变url 可以通过history.pushState 和resplaceState 等,会
将URL 压入堆栈,同时能够应用history.go() 等API
监听url 的变化可以通过自定义事件触发实现
react-router 实现的思想:
基于history 库来实现上述不同的客户端路由实现思想,并且能够
保存历史记录等,磨平浏览器差异,上层无感知
通过维护的列表,在每次URL 发生变化的回收,通过配置的路由路
径,匹配到对应的Component,并且render
15. react-router 里的Link 标签和a 标签的区别
从最终渲染的DOM 来看,这两者都是链接,都是标签,区别是∶
<Link>是react-router 里实现路由跳转的链接,一般配合<Route>
使用,react-router 接管了其默认的链接跳转行为,区别于传统的
页面跳转,<Link> 的“跳转”行为只会触发相匹配的<Route>对应的
页面内容更新,而不会刷新整个页面。
<Link>做了3 件事情:
有onclick 那就执行onclick
click 的时候阻止a 标签默认事件
根据跳转href(即是to),用history (web 前端路由两种方式之一,
history & hash)跳转,此时只是链接变了,并没有刷新页面而<a>
89
标签就是普通的超链接了,用于从当前页面跳转到href 指向的另一
个页面(非锚点情况)。
a 标签默认事件禁掉之后做了什么才实现了跳转?
16. 对Redux 的理解,主要解决什么问题
React 是视图层框架。Redux 是一个用来管理数据状态和UI 状态的
JavaScript 应用工具。随着JavaScript 单页应用(SPA)开发日趋
复杂, JavaScript 需要管理比任何时候都要多的state(状态),
Redux 就是降低管理难度的。(Redux 支持React、Angular、jQuery
甚至纯JavaScript)。
在React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但
React 中组件间通信的数据流是单向的,顶层组件可以通过props
属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄
弟组件之间同样不能。这样简单的单向数据流支撑起了React 中的
数据可控性。
当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也
将越来越不好管理。管理不断变化的state 非常困难。如果一个
model 的变化会引起另一个model 变化,那么当view 变化时,就
可能引起对应model 以及另一个model 的变化,依次地,可能会引
起另一个view 的变化。直至你搞不清楚到底发生了什么。state 在
什么时候,由于什么原因,如何变化已然不受控制。当系统变得错
综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果
90
这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、
服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当
复杂。
Redux 提供了一个叫store 的统一仓储库,组件通过dispatch 将
state 直接传入store, 不用通过其他的组件。并且组件通过
subscribe 从store 获取到state 的改变。使用了Redux,所有的
组件都可以从store 中获取到所需的state,他们也能从store 获
取到state 的改变。这比组件之间互相传递数据清晰明朗的多。
主要解决的问题:
单纯的Redux 只是一个状态机,是没有UI 呈现的,react- redux 作
用是将Redux 的状态机和React 的UI 呈现绑定在一起,当你dispatch
action 改变state 的时候,会自动更新页面。
17. Redux 状态管理器和变量挂载到window 中有什么区别
两者都是存储数据以供后期使用。但是Redux 状态更改可回溯——
Time travel,数据多了的时候可以很清晰的知道改动在哪里发生,
完整的提供了一套状态管理模式。
随着JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比
任何时候都要多的state (状态)。这些state 可能包括服务器
响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括UI
状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器
等等。
管理不断变化的state 非常困难。如果一个model 的变化会引起另
一个model 变化,那么当view 变化时,就可能引起对应model 以
及另一个model 的变化,依次地,可能会引起另一个view 的变化。
91
直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,
如何变化已然不受控制。当系统变得错综复杂的时候,想重现问题
或者添加新功能就会变得举步维艰。
如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调
优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前
所未有的复杂性,难道就这么放弃了吗?当然不是。
这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混
淆在一起:变化和异步。可以称它们为曼妥思和可乐。如果把二者
分开,能做的很好,但混到一起,就变得一团糟。一些库如React 视
图在视图层禁止异步和直接操作DOM 来解决这个问题。美中不足的
是,React 依旧把处理state 中数据的问题留给了你。Redux 就是
为了帮你解决这个问题。
18. Redux 和Vuex 有什么区别,它们的共同思想
(1)Redux 和Vuex 区别
Vuex 改进了Redux 中的Action 和Reducer 函数,以mutations 变化
函数取代Reducer,无需switch,只需在对应的mutation 函数里改
变state 值即可
Vuex 由于Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要
生成新的State 即可
Vuex 数据流的顺序是∶View 调用store.commit 提交对应的请求到
Store 中对应的mutation 函数->store 改变(vue 检测到数据变化自
动渲染)
通俗点理解就是,vuex 弱化dispatch,通过commit 进行store 状
态的一次更变;取消了action 概念,不必传入特定的action 形式
92
进行指定变更;弱化reducer,基于commit 参数直接对数据进行转
变,使得框架更加简易;
(2)共同思想
单—的数据源
变化可以预测
本质上∶ redux 与vuex 都是对mvvm 思想的服务,将数据从视图中
抽离的一种方案。
19. Redux 中间件是怎么拿到store 和action? 然后怎么处
理?
redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api
源码中每个middleware 接受2 个参数, Store 的getState 函数和
dispatch 函数,分别获得store 和action,最终返回一个函数。该
函数会被传入next 的下一个middleware 的dispatch 方法,并返
回一个接收action 的新函数, 这个函数可以直接调用next
(action),或者在其他需要的时刻调用,甚至根本不去调用它。调
用链中最后一个middleware 会接受真实的store 的dispatch 方
法作为next 参数,并借此结束调用链。所以,middleware 的函数
签名是({ getState,dispatch })=> next => action。
20. React Hooks 解决了哪些问题?
React Hooks 主要解决了以下问题:
(1)在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组
件连接到store)解决此类问题可以使用render props 和高阶组
93
件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会
使代码难以理解。由providers,consumers,高阶组件,render props
等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在DevTools
过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状
态逻辑提供更好的原生途径。
可以使用Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试
并复用。Hook 使我们在无需修改组件结构的情况下复用状态逻辑。
这使得在组件间或社区内共享Hook 变得更便捷。
(2)复杂组件变得难以理解
在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常
常在componentDidMount 和componentDidUpdate 中获取数据。但
是,同一个componentDidMount 中可能也包含很多其它的逻辑,如
设置事件监听,而之后需在componentWillUnmount 中清除。相互关
联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同
一个方法中组合在一起。如此很容易产生bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处
不在。这也给测试带来了一定挑战。同时,这也是很多人将React 与
状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概
念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函
数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你
还可以使用reducer 来管理组件的内部状态,使其更加可预测。
(3)难以理解的class
94
除了代码复用和代码管理会遇到困难外,class 是学习React 的一
大屏障。我们必须去理解JavaScript 中this 的工作方式,这与其
他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法
提案,这些代码非常冗余。大家可以很好地理解props,state 和自
顶向下的数据流,但对class 却一筹莫展。即便在有经验的React
开发者之间,对于函数组件与class 组件的差异也存在分歧,甚至
还要区分两种组件的使用场景。
为了解决这些问题,Hook 使你在非class 的情况下可以使用更多的
React 特性。从概念上讲,React 组件一直更像是函数。而Hook 则
拥抱了函数,同时也没有牺牲React 的精神原则。Hook 提供了问题
的解决方案,无需学习复杂的函数式或响应式编程技术
21. React Hook 的使用限制有哪些?
React Hooks 的限制主要有两条:
不要在循环、条件或嵌套函数中调用Hook;
在React 的函数组件中调用Hook。
那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进React
组件的开发模式。在旧有的开发模式下遇到了三个问题。
组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、
render props 及状态管理框架。
复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致
关联部分难以拆分。
人和机器都很容易混淆类。常见的有this 的问题,但在React 团
队中还有类难以优化的问题,希望在编译优化层面做出一些改进。这
95
三个问题在一定程度上阻碍了React 的后续发展,所以为了解决这
三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了
Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用Hook 呢?因为Hooks
的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、
条件或嵌套函数很有可能导致数组取值错位,执行错误的Hook。当
然,实质上React 的源码里不是数组,是链表。
这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为
了避免这样的情况,可以引入ESLint 的Hooks 检查插件进行预防。
22. React diff 算法的原理是什么?
实际上,diff 算法探讨的就是虚拟DOM 树发生变化后,生成DOM 树
更新补丁的方式。它通过对比新旧两株虚拟DOM 树的变更差异,将
更新补丁作用于真实DOM,以最小成本完成视图更新。
具体的流程如下:
真实的DOM 首先会映射为虚拟DOM;
当虚拟DOM 发生变化后,就会根据差距计算生成patch,这个patch
是一个结构化的数据,内容包含了增加、更新、移除等;
根据patch 去更新真实的DOM,反馈到用户的界面上。
96
一个简单的例子:
这里,首先假定ExampleComponent 可见,然后再改变它的状态,让
它不可见。映射为真实的DOM 操作是这样的,React 会创建一个
div 节点。
当把visbile 的值变为false 时,就会替换class 属性为hidden,
并重写内部的innerText 为hidden。这样一个生成补丁、更新差异
的过程统称为diff 算法。
diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进
行复杂度的优化:
97
策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对
比)
这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法
是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发
现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用
于进一步的比较,这就提升了比对效率。
策略二:如果组件的class 一致,则默认为相似的树结构,否则默
认为不同的树结构。(基于组件进行对比)
在组件比对的过程中:
如果组件是同一类型则进行树比对;
如果不是则直接放入补丁中。
只要父组件类型不同, 就会被重新渲染。这也就是为什么
shouldComponentUpdate、PureComponent 及React.memo 可以提高
性能的原因。
策略三:同一层级的子节点,可以通过标记key 的方式进行列表对
比。(基于节点进行对比)
元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操
作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移
动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要
的作用。通过标记key 的方式,React 可以直接移动DOM 节点,降
低内耗。
23. React key 是干嘛用的为什么要加?key 主要是解决哪一
类问题的
98
Keys 是React 用于追踪哪些列表中元素被修改、被添加或者被移除
的辅助标识。在开发过程中,我们需要保证某个元素的key 在其同
级元素中具有唯一性。
在React Diff 算法中React 会借助元素的Key 值来判断该元素
是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染
此外,React 还需要借助Key 值来判断元素与本地状态的关联关系。
注意事项:
key 值一定要和具体的元素—一对应;
尽量不要用数组的index 去作为key;
不要在render 的时候用随机数或者其他操作给元素加上不稳定的
key,这样造成的性能开销比不加key 的情况下更糟糕。
24. React 与Vue 的diff 算法有何不同?
diff 算法是指生成更新补丁的方式,主要应用于虚拟DOM 树变化后,
更新真实DOM。所以diff 算法一定存在这样一个过程:触发更新→
生成补丁→ 应用补丁。
React 的diff 算法,触发更新的时机主要在state 变化与hooks
调用之后。此时触发虚拟DOM 树变更遍历,采用了深度优先遍历算
法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方
式。将单一节点比对转化为了3 种类型节点的比对,分别是树、组
件及元素,以此提升效率。
树比对:由于网页视图中较少有跨层级节点移动,两株虚拟DOM 树
只对同一层次的节点进行比较。
99
组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接
放入到补丁中。
元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点
操作对应真实的DOM 剪裁操作。
以上是经典的React diff 算法内容。自React 16 起,引入了Fiber
架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了
FiberNode 与FiberTree 进行重构。fiberNode 使用了双链表的结
构,可以直接找到兄弟节点与子节点。整个更新过程由current 与
workInProgress 两株树双缓冲完成。workInProgress 更新完成后,
再通过修改current 相关指针指向新节点。
Vue 的整体diff 策略与React 对齐,虽然缺乏时间切片能力,但
这并不意味着Vue 的性能更差,因为在Vue 3 初期引入过,后期因
为收益不高移除掉了。除了高帧率动画,在Vue 中其他的场景几乎
都可以使用防抖和节流去提高响应性能。
25. react 最新版本解决了什么问题,增加了哪些东西
React 16.x 的三大新特性Time Slicing、Suspense、hooks
Time Slicing(解决CPU 速度问题)使得在执行任务的期间可以随时
暂停,跑去干别的事情,这个特性使得react 能在性能极其差的机器
跑时,仍然保持有良好的性能
Suspense (解决网络IO 问题)和lazy 配合,实现异步加载组件。能
暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react
出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,
使用的是T 异步但是同步的写法,这是最好的解决异步问题的方式
100
提供了一个内置函数componentDidCatch,当有错误发生时,可以友
好地展示fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)
抛出的异常; 可以复用错误组件。
(1)React16.8
加入hooks,让React 函数式组件更加灵活,hooks 之前,React 存
在很多问题:
在组件间复用状态逻辑很难
复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
class 组件的this 指向问题
难以记忆的生命周期
hooks 很好的解决了上述问题,hooks 提供了很多方法
useState 返回有状态值,以及更新这个状态值的函数
useEffect 接受包含命令式,可能有副作用代码的函数。
useContext 接受上下文对象(从React.createContext 返回的值)
并返回当前上下文值,
useReducer useState 的替代方案。接受类型为(state,action)
=> newState 的reducer,并返回与dispatch 方法配对的当前状态。
useCalLback 返回一个回忆的memoized 版本,该版本仅在其中一个
输入发生更改时才会更改。纯函数的输入输出确定性o useMemo 纯
的一个记忆函数o useRef 返回一个可变的ref 对象,其Current 属
性被初始化为传递的参数,返回的ref 对象在组件的整个生命周期
内保持不变。
useImperativeMethods 自定义使用ref 时公开给父组件的实例值
101
useMutationEffect 更新兄弟组件之前,它在React 执行其DOM 改变
的同一阶段同步触发
useLayoutEffect DOM 改变后同步触发。使用它来从DOM 读取布局并
同步重新渲染
(2)React16.9
重命名Unsafe 的生命周期方法。新的UNSAFE_前缀将有助于在代码
review 和debug 期间,使这些有问题的字样更突出废弃
javascrip:形式的URL。以javascript:开头的URL 非常容易遭受攻
击,造成安全漏洞。
废弃"Factory"组件。工厂组件会导致React 变大且变慢。
act()也支持异步函数,并且你可以在调用它时使用await。
使用<React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回
归可能会很方便
(3)React16.13.0
支持在渲染期间调用setState,但仅适用于同一组件
可检测冲突的样式规则并记录警告
废弃unstable_createPortal,使用CreatePortal
将组件堆栈添加到其开发警告中,使开发人员能够隔离bug 并调试其
程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
26. 在React 中页面重新加载时怎样保留数据?
这个问题就设计到了数据持久化,主要的实现方式有以下几种:
102
Redux:将页面的数据存储在redux 中,在重新加载页面时,获取Redux
中的数据;
data.js:使用webpack 构建的项目,可以建一个文件,data.js,将
数据保存data.js 中,跳转页面后获取;
sessionStorge:在进入选择地址页面之前,componentWillUnMount
的时候,将数据存储到sessionStorage 中,每次进入页面判断
sessionStorage 中有没有存储的那个值,有,则读取渲染数据;没
有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页
面,清掉存储的sessionStorage,保证下次进入是初始化的数据
history API:History API 的pushState 函数可以给历史记录关联
一个任意的可序列化state,所以可以在路由push 的时候将当前页
面的一些信息存到state 中,下次返回到这个页面的时候就能从
state 里面取出离开前的数据重新渲染。react-router 直接可以支
持。这个方法适合一些需要临时存储的场景。
27. 为什么使用jsx 的组件中没有看到使用react 却需要引入
react?
本质上来说JSX 是React.createElement(component,
props, ...children)方法的语法糖。在React 17 之前,如果使用了
JSX,其实就是在使用React, babel 会把组件转换
为CreateElement 形式。在React 17 之后,就不再需要引入,因
为babel 已经可以帮我们自动引入react。
28. Redux 中间件是什么?接受几个参数?柯里化函数两端的
参数具体是什么?
103
Redux 的中间件提供的是位于action 被发起之后,到达reducer
之前的扩展点,换而言之,原本view -→> action -> reducer ->
store 的数据流加上中间件后变成了view -> action -> middleware
-> reducer -> store ,在这一环节可以做一些"副作用"的操作,如
异步请求、打印日志等。
applyMiddleware 源码:
从applyMiddleware 中可以看出∶
redux 中间件接受一个对象作为参数,对象的参数上有两个字段
dispatch 和getState,分别代表着Redux Store 上的两个同名函
数。
柯里化函数两端一个是middewares,一个是store.dispatch
29. 组件通信的方式有哪些
⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传props 的⽅式,
向⼦组件进⾏通讯
104
⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递
props 进⾏通讯,此props 为作⽤域为⽗组件⾃身的函数,⼦组件
调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的
作⽤域中
兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,结合上⾯两种⽅
式由⽗节点转发信息进⾏通信
跨层级通信: Context 设计⽬的是为了共享那些对于⼀个组件树⽽
⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于
跨越多层的全局数据通过Context 通信再适合不过
发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们
可以通过引⼊event 模块进⾏通信
全局状态管理⼯具: 借助Redux 或者Mobx 等全局状态管理⼯具进⾏
通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产
⽣新的状态
性能优化部分
1. 懒加载的概念
懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片
数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,
如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口
的那一部分图片数据,这样就浪费了性能。
如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视
化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网
105
页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,
页面列表较长(长列表)的场景中。
2. 懒加载的特点
减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,
同时也减小了浏览器的负担。
提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,
这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
防止加载过多图片而影响其他资源文件的加载:会影响网站应用的
正常使用。
3. 懒加载的实现原理
图片的加载是由src 引起的,当对src 赋值时,浏览器就会请求图片
资源。根据这个原理,我们使用HTML5 的data-xxx 属性来储存图片
的路径,在需要加载图片的时候,将data-xxx 中图片的路径赋值给
src,这样就实现了图片的按需加载,即懒加载。
注意:data-xxx 中的xxx 可以自定义,这里我们使用data-src 来定
义。
懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可
视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,
获取图片的真实地址并赋值给图片即可。
使用原生JavaScript 实现懒加载:
知识点:
window.innerHeight 是浏览器可视区的高度
document.body.scrollTop
106
document.documentElement.scrollTop 是浏览器滚动的过的距离
imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距
离)
图片加载条件: img.offsetTop < window.innerHeight +
document.body.scrollTop;
图示:
代码实现:
107
4. 回流与重绘的概念及触发条件
(1)回流
当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏
览器会重新渲染部分或者全部文档的过程就称为回流。
下面这些操作会导致回流:
页面的首次渲染
浏览器的窗口大小发生变化
元素的内容发生变化
元素的尺寸或者位置发生变化
元素的字体大小发生变化
激活CSS 伪类
查询某些属性或者调用某些方法
添加或者删除可见的DOM 元素
108
在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,
所以当触发回流时,会导致周围的DOM 元素重新排列,它的影响范围
有两种:
全局范围:从根节点开始,对整个渲染树进行重新布局
局部范围:对渲染树的某部分或者一个渲染对象进行重新布局
(2)重绘
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位
置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
下面这些操作会导致重绘:
color、background 相关属性:background-color、background-image

outline 相关属性: outline-color 、outline-width 、
text-decoration
border-radius、visibility、box-shadow
注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回
流。
5. 如何避免回流与重绘?
减少回流与重绘的措施:
操作DOM 时,尽量在低层级的DOM 节点进行操作
不要使用table 布局, 一个小的改动可能会使整个table 进行重新
布局
使用CSS 的表达式
109
不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样
式。
使用absolute 或者fixed,使元素脱离文档流,这样他们发生变化
就不会影响其他元素
避免频繁操作DOM,可以创建一个文档片段documentFragment,在它
上面应用所有DOM 操作,最后再把它添加到文档中
将元素先设置display: none,操作结束后再把它显示出来。因为在
display 属性为none 的元素上进行的DOM 操作不会引发回流和重绘。
将DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插
着写。这得益于浏览器的渲染队列机制。
浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操
作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行
批处理。这样就会让多次的回流、重绘变成一次回流重绘。
上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作
进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发
一次回流。
6. 如何优化动画?
对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作
DOM,就就会导致页面的性能问题,我们可以将动画的position 属性
设置为absolute 或者fixed,将动画脱离文档流,这样他的回流就
不会影响到页面了。
110
7. documentFragment 是什么?用它跟直接操作DOM 的区别是
什么?
MDN 中对documentFragment 的解释:
DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。
它被作为一个轻量版的Document 使用,就像标准的document 一样,
存储由节点(nodes)组成的文档结构。与document 相比,最大的区
别是DocumentFragment 不是真实DOM 树的一部分,它的变化不会触
发DOM 树的重新渲染,且不会导致性能等问题。
当我们把一个DocumentFragment 节点插入文档树时,插入的不是
DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM 操
作时,我们就可以将DOM 元素插入DocumentFragment,之后一次性
的将所有的子孙节点插入文档中。和直接操作DOM 相比, 将
DocumentFragment 节点插入DOM 树时,不会触发页面的重绘,这样
就大大提高了页面的性能。
8. 对节流与防抖的理解
函数防抖是指在事件被触发n 秒后再执行回调,如果在这n 秒内事
件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避
免因为用户的多次点击向后端发送多次请求。
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触
发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,
只有一次能生效。节流可以使用在scroll 函数的事件监听上,通过
事件节流来降低事件调用的频率。
防抖函数的应用场景:
111
按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊
事件的最后⼀次, 还有搜索联想词功能类似⽣存环境请⽤
lodash.debounce
节流函数的适⽤场景:
拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
缩放场景:监控浏览器resize
动画场景:避免短时间内多次触发动画引起性能问题
9. 实现节流函数和防抖函数
函数防抖的实现:
函数节流的实现:
112
10. 如何对项目中的图片进行优化?
1.不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片
完全可以用CSS 去代替。
2.对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪
费带宽。一般图片都用CDN 加载,可以计算出适配屏幕的宽度,然
后去请求相应裁剪好的图片。
3.小图使用base64 格式
4.将多个图标文件整合到一张图片中(雪碧图)
5.选择正确的图片格式:
113
对于能够显示WebP 格式的浏览器尽量使用WebP 格式。因为WebP
格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥
有肉眼识别无差异的图像质量,缺点就是兼容性并不好
小图使用PNG,其实对于大部分图标这类图片,完全可以使用SVG 代

照片使用JPEG
11. 常见的图片格式及使用场景
(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图
片格式几乎没有对数据进行压缩,所以BMP 格式的图片通常是较大的
文件。
(2)GIF 是无损的、采用索引色的点阵图。采用LZW 压缩算法进行
编码。文件小,是GIF 格式的优点,同时,GIF 格式还具有支持动画
以及透明的优点。但是GIF 格式仅支持8bit 的索引色,所以GIF 格
式适用于对色彩要求不高同时需要文件体积较小的场景。
(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采
用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,
与GIF 相比,JPEG 不适合用来存储企业Logo、线框类的图。因为有
损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF
更大。
(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图
片格式,PNG-8 是非常好的GIF 格式替代者,在可能的情况下,应该
尽可能的使用PNG-8 而不是GIF,因为在相同的图片效果下,PNG-8
具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而GIF
并不支持。除非需要动画的支持,否则没有理由使用GIF 而不是PNG-8。
114
(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它
压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小
要比BMP 小得多。当然,PNG24 的图片还是要比JPEG、GIF、PNG-8
大得多。
(6)SVG 是无损的矢量图。SVG 是矢量图意味着SVG 图片由直线和曲
线以及绘制它们的方法组成。当放大SVG 图片时,看到的还是线和曲
线,而不会出现像素点。这意味着SVG 图片在放大时,不会失真,所
以它非常适合用来绘制Logo、Icon 等。
(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无
损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web 而
生的,什么叫为Web 而生呢?就是说相同质量的图片,WebP 具有更
小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个
图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,
进而降低访问延迟,提升访问体验。目前只有Chrome 浏览器和Opera
浏览器支持WebP 格式,兼容性不太好。
在无损压缩的情况下,相同质量的WebP 图片,文件大小要比PNG 小
26%;
在有损压缩的情况下,具有相同图片精度的WebP 图片,文件大小要
比JPEG 小25%~34%;
WebP 图片格式支持图片透明度,一个无损压缩的WebP 图片,如果要
支持透明度只需要22%的格外文件大小。
12. 如何⽤webpack 来优化前端性能?
⽤webpack 优化前端性能是指优化webpack 的输出结果,让打包的最
终结果在浏览器运⾏快速⾼效。
115
压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以
利⽤webpack 的UglifyJsPlugin 和ParallelUglifyPlugin 来压缩
JS⽂件, 利⽤cssnano (css-loader?minimize)来压缩css
利⽤CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为CDN 上
对应的路径。可以利⽤webpack 对于output 参数和各loader 的
publicPath 参数来修改资源路径
Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启
动webpack 时追加参数--optimize-minimize 来实现
Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做
到按需加载,同时可以充分利⽤浏览器缓存
提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,
利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码
13. 如何提⾼webpack 的构建速度?
1.多⼊⼝情况下,使⽤CommonsChunkPlugin 来提取公共代码
2.通过externals 配置来提取常⽤库
3.利⽤DllPlugin 和DllReferencePlugin 预编译资源模块通过
DllPlugin 来对那些我们引⽤但是绝对不会修改的npm 包来进⾏预
编译,再通过DllReferencePlugin 将预编译的模块加载进来。
4.使⽤Happypack 实现多线程加速编译
5.使⽤webpack-uglify-parallel 来提升uglifyPlugin 的压缩速
度。原理上webpack-uglify-parallel 采⽤了多核并⾏压缩来提升
压缩速度
6.使⽤Tree-shaking 和Scope Hoisting 来剔除多余代码
116
前端工程化部分
1. webpack 与grunt、gulp 的不同?
Grunt、Gulp 是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,
就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活
跃的社区,丰富的插件,能⽅便的打造各种⼯作流。
Webpack 是基于模块化打包的⼯具: ⾃动化处理模块,webpack 把⼀
切当成模块,当webpack 处理应⽤程序时,它会递归地构建⼀个依
赖关系图(dependency graph),其中包含应⽤程序需要的每个模块,
然后将所有这些模块打包成⼀个或多个bundle。
因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script 代
替Grunt、Gulp,npm script 同样可以打造任务流。
2. webpack、rollup、parcel 优劣?
webpack 适⽤于⼤型复杂的前端站点构建: webpack 有强⼤的loader
和插件⽣态,打包后的⽂件实际上就是⼀个⽴即执⾏函数,这个⽴即
执⾏函数接收⼀个参数,这个参数是模块对象,键为各个模块的路径,
值为模块内容。⽴即执⾏函数内部则处理模块之间的引⽤,执⾏模块
等,这种情况更适合⽂件依赖复杂的应⽤开发。
rollup 适⽤于基础库的打包,如vue、d3 等: Rollup 就是将各个模
块打包进⼀个⽂件中,并且通过Tree-shaking 来删除⽆⽤的代码,
可以最⼤程度上降低代码体积,但是rollup 没有webpack 如此多的的
如代码分割、按需加载等⾼级功能,其更聚焦于库的打包,因此更适
合库的开发。
117
parcel 适⽤于简单的实验性项⽬: 他可以满⾜低⻔槛的快速看到效
果,但是⽣态差、报错信息不够全⾯都是他的硬伤,除了⼀些玩具项
⽬或者实验项⽬不建议使⽤。
3. 有哪些常⻅的Loader?
file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对URL
去引⽤输出的⽂件
url-loader:和file-loader 类似,但是能在⽂件很⼩的情况下以
base64 的⽅式把⽂件内容注⼊到代码中去
source-map-loader:加载额外的Source Map ⽂件,以⽅便断点调

image-loader:加载并且压缩图⽚⽂件
babel-loader:把ES6 转换成ES5
css-loader:加载CSS,⽀持模块化、压缩、⽂件导⼊等特性
style-loader:把CSS 代码注⼊到JavaScript 中,通过DOM 操作
去加载CSS。
eslint-loader:通过ESLint 检查JavaScript 代码
注意:在Webpack 中,loader 的执行顺序是从右向左执行的。因为
webpack 选择了compose 这样的函数式编程方式,这种方式的表达式
执行是从右向左的。
4. 有哪些常⻅的Plugin?
define-plugin:定义环境变量
html-webpack-plugin:简化html⽂件创建
118
uglifyjs-webpack-plugin:通过UglifyES 压缩ES6 代码
webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
webpack-bundle-analyzer: 可视化webpack 输出⽂件的体积
mini-css-extract-plugin: CSS 提取到单独的⽂件中,⽀持按需加

5. bundle,chunk,module 是什么?
bundle:是由webpack 打包出来的⽂件;
chunk:代码块,⼀个chunk 由多个模块组合⽽成,⽤于代码的合并
和分割;
module:是开发中的单个模块,在webpack 的世界,⼀切皆模块,⼀
个模块对应⼀个⽂件,webpack 会从配置的entry 中递归开始找出所
有依赖的模块。
6. Loader 和Plugin 的不同?
不同的作⽤:
Loader 直译为"加载器"。Webpack 将⼀切⽂件视为模块,但是webpack
原⽣是只能解析js⽂件,如果想将其他⽂件也打包的话,就会⽤到
loader 。所以Loader 的作⽤是让webpack 拥有了加载和解析⾮
JavaScript⽂件的能⼒。
Plugin 直译为"插件"。Plugin 可以扩展webpack 的功能,让webpack
具有更多的灵活性。在Webpack 运⾏的⽣命周期中会⼴播出许多事
件,Plugin 可以监听这些事件,在合适的时机通过Webpack 提供的
API 改变输出结果。
不同的⽤法:
119
Loader 在module.rules 中配置,也就是说他作为模块的解析规则
⽽存在。类型为数组,每⼀项都是⼀个Object ,⾥⾯描述了对于
什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数
( options )
Plugin 在plugins 中单独配置。类型为数组,每⼀项是⼀个plugin
的实例,参数都通过构造函数传⼊。
7. webpack 热更新的实现原理?
webpack 的热更新⼜称热替换(Hot Module Replacement),缩写为
HMR。这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧
的模块。
原理:
120
⾸先要知道server 端和client 端都做了处理⼯作:
第⼀步,在webpack 的watch 模式下,⽂件系统中某⼀个⽂件发⽣
修改,webpack 监听到⽂件变化,根据配置⽂
件对模块重新编译打包,并将打包后的代码通过简单的JavaScript
对象保存在内存中。
第⼆步是webpack-dev-server 和webpack 之间的接⼝交互,⽽在
这⼀步,主要是dev-server 的中间件webpack- dev-middleware
和webpack 之间的交互,webpack-dev-middleware 调⽤webpack
暴露的API 对代码变化进⾏监控,并且告诉webpack,将代码打包
到内存中。
第三步是webpack-dev-server 对⽂件变化的⼀个监控,这⼀步不同
于第⼀步,并不是监控代码变化重新打包。当我们在配置⽂件中配置
了devServer.watchContentBase 为true 的时候,Server 会监听
这些配置⽂件夹中静态⽂件的变化,变化后会通知浏览器端对应⽤进
⾏live reload。注意,这⼉是浏览器刷新,和HMR 是两个概念。
第四步也是webpack-dev-server 代码的⼯作,该步骤主要是通过
sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建
⽴⼀个websocket ⻓连接,将webpack 编译打包的各个阶段的状态
信息告知浏览器端,同时也包括第三步中Server 监听静态⽂件变化
的信息。浏览器端根据这些socket 消息进⾏不同的操作。当然服务
端传递的最主要信息还是新模块的hash 值,后⾯的步骤根据这⼀
hash 值来进⾏模块热替换。
webpack-dev-server/client 端并不能够请求更新的代码,也不会执
⾏热更模块操作, ⽽把这些⼯作⼜交回给了webpack ,
121
webpack/hot/dev-server 的⼯作就是根据
webpack-dev-server/client 传给它的信息以及dev-server 的配
置决定是刷新浏览器呢还是进⾏模块热更新。当然如果仅仅是刷新浏
览器,也就没有后⾯那些步骤了。
HotModuleReplacement.runtime 是客户端HMR 的中枢,它接收到上
⼀步传递给他的新模块的hash 值, 它通过
JsonpMainTemplate.runtime 向server 端发送Ajax 请求,服务端
返回⼀个json,该json 包含了所有要更新的模块的hash 值,获
取到更新列表后,该模块再次通过jsonp 请求,获取到最新的模块
代码。这就是上图中7、8、9 步骤。
⽽第10 步是决定HMR 成功与否的关键步骤,在该步骤中,
HotModulePlugin 将会对新旧模块进⾏对⽐,决定是否更新模块,在
决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模
块间的依赖引⽤。
最后⼀步,当HMR 失败后,回退到live reload 操作,也就是进⾏
浏览器刷新来获取最新打包代码。
8. Babel 的原理是什么?
babel 的转译过程也分为三个阶段,这三步具体是:
解析Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语
法分析的过程;
转换Transform: 对于AST 进⾏变换⼀系列的操作,babel 接受得
到AST 并通过babel-traverse 对其进⾏遍历,在此过程中进⾏添
加、更新及移除等操作;
122
⽣成Generate: 将变换后的AST 再转换为JS 代码, 使⽤到的模
块是babel-generator。
9. git 和svn 的区别
git 和svn 最大的区别在于git 是分布式的,而svn 是集中式的。
因此我们不能再离线的情况下使用svn。如果服务器出现问题,就没
有办法使用svn 来提交代码。
svn 中的分支是整个版本库的复制的一份完整目录,而git 的分支
是指针指向某次提交,因此git 的分支创建更加开销更小并且分支
上的变化不会影响到其他人。svn 的分支变化会影响到所有的人。
svn 的指令相对于git 来说要简单一些,比git 更容易上手。
GIT 把内容按元数据方式存储,而SVN 是按文件:因为git 目录是处
于个人机器上的一个克隆版的版本库,它拥有中心版本库上所有的东
西,例如标签,分支,版本记录等。
GIT 分支和SVN 的分支不同:svn 会发生分支遗漏的情况,而git 可
以同一个工作目录下快速的在几个分支间切换,很容易发现未被合并
的分支,简单而快捷的合并这些文件。
GIT 没有一个全局的版本号,而SVN 有
123
GIT 的内容完整性要优于SVN:GIT 的内容存储使用的是SHA-1 哈希
算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题
时降低对版本库的破坏
10. 经常使用的git 命令?
11. git pull 和git fetch 的区别
git fetch 只是将远程仓库的变化下载下来,并没有和本地分支合并。
git pull 会将远程仓库的变化下载下来,并和当前分支合并。
12. git rebase 和git merge 的区别
git merge 和git rebase 都是用于分支合并,关键在commit 记录
的处理上不同:
git merge 会新建一个新的commit 对象,然后两个分支以前的
commit 记录都指向这个新commit 记录。这种方法会保留之前每个
分支的commit 历史。
git rebase 会先找到两个分支的第一个共同的commit 祖先记录,
然后将提取当前分支这之后的所有commit 记录,然后将这个
commit 记录添加到目标分支的最新提交后面。经过这个合并后,两
个分支合并后的commit 记录就变为了线性的记录了。
浏览器部分
1. 什么是XSS 攻击?
124
(1)概念
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在
网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信
息如cookie 等。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合
在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意
代码的执行。
攻击者可以通过这种攻击方式可以进行以下操作:
获取页面的数据,如DOM、cookie、localStorage;
DOS 攻击,发送合理请求,占用服务器资源,从而使用户无法访问服
务器;
破坏页面结构;
流量劫持(将链接指向某网站);
(2)攻击类型
XSS 可以分为存储型、反射型和DOM 型:
存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,
脚本从服务器传回并执行。
反射型指的是攻击者诱导用户访问一个带有恶意代码的URL 后,服
务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,
浏览器端解析这段带有XSS 代码的数据后当做脚本执行,最终完成
XSS 攻击。
DOM 型指的通过修改页面的DOM 节点形成的XSS。
1)存储型XSS 的攻击步骤:
125
1.攻击者将恶意代码提交到⽬标⽹站的数据库中。
2.⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接
在HTML 中返回给浏览器。
3.⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏
为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评
论、⽤户私信等。
2)反射型XSS 的攻击步骤:
1.攻击者构造出特殊的URL,其中包含恶意代码。
2.⽤户打开带有恶意代码的URL 时,⽹站服务端将恶意代码从URL
中取出,拼接在HTML 中返回给浏览器。
3.⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏
为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
反射型XSS 跟存储型XSS 的区别是:存储型XSS 的恶意代码存在
数据库⾥,反射型XSS 的恶意代码存在URL ⾥。
反射型XSS 漏洞常⻅于通过URL 传递参数的功能,如⽹站搜索、跳
转等。由于需要⽤户主动打开恶意的URL 才能⽣效,攻击者往往会
结合多种⼿段诱导⽤户点击。
3)DOM 型XSS 的攻击步骤:
1.攻击者构造出特殊的URL,其中包含恶意代码。
126
2.⽤户打开带有恶意代码的URL。
3.⽤户浏览器接收到响应后解析执⾏,前端JavaScript 取出URL
中的恶意代码并执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏
为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
DOM 型XSS 跟前两种XSS 的区别:DOM 型XSS 攻击中,取出和执
⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,
⽽其他两种XSS 都属于服务端的安全漏洞。
2. 如何防御XSS 攻击?
可以看到XSS 危害如此之大, 那么在开发网站时就要做好防御措施,
具体措施如下:
可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服
务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到
HTML 中的代码做好充分的转义。对于DOM 型的攻击,主要是前端脚
本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对
可能出现的恶意代码情况进行判断。
使用CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资
源可以加载和执行,从而防止恶意代码的注入攻击。
1.CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览
器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由
浏览器自己来实现。
127
2. 通常有两种方式来开启CSP , 一种是设置HTTP 首部中的
Content-Security-Policy,一种是设置meta 标签的方式<meta
http-equiv="Content-Security-Policy">
对一些敏感信息进行保护,比如cookie 使用http-only,使得脚本
无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
3. 什么是CSRF 攻击?
(1)概念
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三
方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击
网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过
后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用cookie 会在同源请求中携带发送给服务器
的特点,以此来实现用户的冒充。
(2)攻击类型
常见的CSRF 攻击有三种:
GET 类型的CSRF 攻击,比如在网站中的一个img 标签里构建一个
请求,当用户打开这个网站的时候就会自动发起提交。
POST 类型的CSRF 攻击,比如构建一个表单,然后隐藏它,当用户
进入页面时,自动提交这个表单。
链接类型的CSRF 攻击,比如在a 标签的href 属性里构建一个请
求,然后诱导用户去点击。
4. 如何防御CSRF 攻击?
CSRF 攻击可以使用以下方法来防护:
128
进行同源检测,服务器根据http 请求头中origin 或者referer
信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当
origin 或者referer 信息都不存在的时候,直接阻止请求。这种方
式的缺点是有些情况下referer 可以被伪造,同时还会把搜索引擎
的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是
相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字
段会告诉服务器该网页是从哪个页面链接过来的)
使用CSRF Token 进行验证,服务器向用户返回一个随机数Token ,
当网站再次发起请求时,在请求参数中加入服务器端返回的token ,
然后服务器对这个token 进行验证。这种方法解决了使用cookie
单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点
就是,我们需要给网站中的所有请求都添加上这个token,操作比较
繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过
负载平衡转移到了其他的服务器,但是这个服务器的session 中没
有保留这个token 的话,就没有办法验证了。这种情况可以通过改
变token 的构建方式来解决。
对Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域
名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器
发送请求的时候,从cookie 中取出这个字符串,添加到URL 参数
中,然后服务器通过对cookie 中的数据和参数中的数据进行比较,
来进行验证。使用这种方式是利用了攻击者只能利用cookie,但是
不能访问获取cookie 的特点。并且这种方法比CSRF Token 的方法
更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果
网站存在XSS 漏洞的,那么这种方式会失效。同时这种方式不能做
到子域名的隔离。
129
在设置cookie 属性的时候设置Samesite ,限制cookie 不能作为
被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种
模式,一种是严格模式,在严格模式下cookie 在任何情况下都不可
能作为第三方Cookie 使用,在宽松模式下,cookie 可以被请求是
GET 请求,且会发生页面跳转的请求所使用。
5. 有哪些可能引起前端安全的问题?
跨站脚本(Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了
与CSS 区分所以被称作XSS。早期常⻅于⽹络论坛, 起因是⽹站没
有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖
⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不
限于JavaScript / CSS / Flash 等;
iframe 的滥⽤: iframe 中的内容是由第三⽅来提供的,默认情况下
他们不受控制,他们可以在iframe 中运⾏JavaScirpt 脚本、Flash
插件、弹出对话框等等,这可能会破坏前端⽤户体验;
跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击
者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信
息或设定信息等某些状态更新,属于被动攻击
恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数
时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被
植⼊恶意代码很容易引起安全问题。
6. 网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
(1)DNS 劫持: (输⼊京东被强制跳转到淘宝这就属于dns 劫持)
130
DNS 强制解析: 通过修改运营商的本地DNS 记录,来引导⽤户流量到
缓存服务器
302 跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可
以进⾏劫持处理的,再对劫持的内存发起302 跳转的回复,引导⽤户
获取内容
(2)HTTP 劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http
明⽂传输,运营商会修改你的http 响应内容(即加⼴告)
(3)DNS 劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS
劫持,⽽http 劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将
HTTP 加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
7. 浏览器渲染进程的线程有哪些
浏览器的渲染进程的线程总共有五种:
(1)GUI 渲染线程
131
负责渲染浏览器页面,解析HTML、CSS,构建DOM 树、构建CSSOM 树、
构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,
该线程就会执行。
注意:GUI 渲染线程和JS 引擎线程是互斥的,当JS 引擎执行时GUI
线程会被挂起,GUI 更新会被保存在一个队列中等到JS 引擎空闲时
立即被执行。
(2)JS 引擎线程
JS 引擎线程也称为JS 内核,负责处理Javascript 脚本程序,解析
Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任
务的到来,然后加以处理,一个Tab 页中无论什么时候都只有一个
JS 引擎线程在运行JS 程序;
注意:GUI 渲染线程与JS 引擎线程的互斥关系,所以如果JS 执行的
时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
(3)时间触发线程
时间触发线程属于浏览器而不是JS 引擎,用来控制事件循环;当JS
引擎执行代码块如setTimeOut 时(也可是来自浏览器内核的其他线
程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发
线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加
到待处理队列的队尾,等待JS 引擎的处理;
注意:由于JS 的单线程关系,所以这些待处理队列中的事件都得排
队等待JS 引擎处理(当JS 引擎空闲时才会去执行);
(4)定时器触发进程
132
定时器触发进程即setInterval 与setTimeout 所在线程;浏览器定
时计数器并不是由JS 引擎计数的,因为JS 引擎是单线程的,如果处
于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时
并触发定时器,计时完毕后,添加到事件队列中,等待JS 引擎空闲
后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,
定时器只是在指定时间点将任务添加到事件队列中;
注意:W3C 在HTML 标准中规定,定时器的定时时间不能小于4ms,如
果是小于4ms,则默认为4ms。
(5)异步http 请求线程
XMLHttpRequest 连接后通过浏览器新开一个线程请求;
检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更
事件,将回调函数放入事件队列中,等待JS 引擎空闲后执行;
8. 僵尸进程和孤儿进程是什么?
孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些
子进程都会成为孤儿进程。孤儿进程将被init 进程(进程号为1)所
收养,并由init 进程对它们完成状态收集工作。
僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用
的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之
为僵死进程。
9. 如何实现浏览器内多个标签页之间的通信?
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。
因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让
133
标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通
信方法如下:
使用websocket 协议,因为websocket 协议可以实现服务器推送,
所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数
据,然后由服务器向其他标签页推送转发。
使用ShareWorker 的方式,shareWorker 会在页面存在的生命周期
内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。
这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个
线程,然后通过这个共享的线程来实现数据的交换。
使用localStorage 的方式,我们可以在一个标签页对localStorage
的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就
可以通过这个监听事件来获取到数据。这个时候localStorage 对象
就是充当的中介者的角色。
使用postMessage 方法,如果我们能够获得对应标签页的引用,就
可以使用postMessage 方法,进行通信。
10. 对浏览器的缓存机制的理解
浏览器缓存的全过程:
浏览器第一次加载资源,服务器返回200,浏览器从服务器下载资源
文件,并缓存资源文件与response header,以供下次加载时对比使
用;
下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上
一次返回200 时的时间差,如果没有超过cache-control 设置的
max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果
浏览器不支持HTTP1.1,则使用expires 头判断是否过期;
134
如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向
服务器发送带有If-None-Match 和If-Modified-Since 的请求;
服务器收到请求后,优先根据Etag 的值判断被请求的文件有没有做
修改,Etag 值一致则没有修改,命中协商缓存,返回304;如果不
一致则有改动,直接返回新的资源文件带上新的Etag 值并返回200;
如果服务器收到的请求没有Etag 值,则将If-Modified-Since 和
被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;
不一致则返回新的last-modified 和文件并返回200;
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了JS
或CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户
端浏览器就会重新下载新的JS 或CSS 文件,以保证用户能够及时
获得网站的最新更新。
11. 协商缓存和强缓存的区别
135
(1)强缓存
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必
再向服务器发起请求。
强缓存策略可以通过两种方式来设置,分别是http 头信息中的
Expires 属性和Cache-Control 属性
(1)服务器通过在响应头中添加Expires 属性,来指定资源的过期
时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发
送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存
在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用
户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的
结果。
(2)Expires 是http1.0 中的方式,因为它的一些缺点,在HTTP
1.1 中提出了一个新的头部属性就是Cache-Control 属性,它提供
了对资源的缓存的更精确的控制。它有很多不同的值,
Cache-Control 可设置的字段:
public:设置了该字段值的资源表示可以被任何对象(包括:发送请
求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是
使用max-age=来精确控制;
private:设置了该字段值的资源只能被用户浏览器缓存,不允许任
何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,
通常都要设置这个字段值,避免代理服务器(CDN)缓存;
no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了
变化,如果资源未发生变化,则直接使用缓存好的资源;
136
no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起
新的请求,拉取最新的资源;
max-age=:设置缓存的最大有效期,单位为秒;
s-maxage=:优先级高于max-age=,仅适用于共享缓存(CDN),优先
级高于max-age 或者Expires 头;
max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,
但是不能超过给定的时间限制。
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方
式一起使用时,Cache-Control 的优先级要高于Expires。
no-cache 和no-store 很容易混淆:
no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也
就是说没有强缓存,但是会有协商缓存;
no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。
(2)协商缓存
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如
果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会
发挥作用了。
上面已经说到了,命中协商缓存的条件有两个:
max-age=xxx 过期了
值为no-store
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发
生修改,则返回一个304 状态,让浏览器使用本地的缓存副本。如
果资源发生了修改,则返回修改后的资源。
137
协商缓存也可以通过两种方式来设置,分别是http 头信息中的
Etag 和Last-Modified 属性。
(1)服务器通过在响应头中添加Last-Modified 属性来指出资源最
后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加
一个If-Modified-Since 的属性,属性值为上一次资源返回时的
Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性
来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做
了修改。如果资源没有修改,那么返回304 状态,让客户端使用本
地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种
方法有一个缺点,就是Last-Modified 标注的最后修改时间只能精
确到秒级,如果某些文件在1 秒钟以内,被修改多次的话,那么文件
已将改变了但是Last-Modified 却没有改变,这样会造成缓存命中
的不准确。
(2)因为Last-Modified 的这种可能发生的不准确性,http 中提
供了另外一种方式,那就是Etag 属性。服务器在返回资源的时候,
在头信息中添加了Etag 属性,这个属性是资源生成的唯一标识符,
当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,
浏览器会在请求头中添加一个If-None-Match 属性,这个属性的值
就是上次返回的资源的Etag 的值。服务接收到请求后会根据这个值
来和资源当前的Etag 的值来进行比较,以此来判断资源是否发生改
变,是否需要返回资源。通过这种方式,比Last-Modified 的方式
更加精确。
当Last-Modified 和Etag 属性同时出现的时候,Etag 的优先级更
高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多
个服务器上资源的Last-Modified 应该保持一致,因为每个服务器
138
上Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置Etag
属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存
副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命
中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强
缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求
的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命
中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命
中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如
果协商缓存不命中,则浏览器返回最新的资源给浏览器。
12. 点击刷新按钮或者按F5、按Ctrl+F5 (强制刷新)、地址
栏回车有什么区别?
点击刷新按钮或者按F5:浏览器直接对本地的缓存文件过期,但是
会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对
文件检查新鲜度,返回结果可能是304,也有可能是200。
用户按Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且
不会带上If-Modifed-Since,If-None-Match,相当于之前从来没有
请求过,返回结果是200。
地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,
然后服务器检查新鲜度,最后返回内容。
13. 常见的浏览器内核比较
139
Trident:这种浏览器内核是IE 浏览器用的内核,因为在早期IE 占
有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根
据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准
支持不是很好。但是由于IE 的高市场占有率,微软也很长时间没有
更新Trident 内核,就导致了Trident 内核和W3C 标准脱节。还
有就是Trident 内核的大量Bug 等安全问题没有得到解决,加上一
些专家学者公开自己认为IE 浏览器不安全的观点,使很多用户开始
转向其他浏览器。
Gecko:这是Firefox 和Flock 所采用的内核,这个内核的优点就
是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,
但是代价是也显而易见就是要消耗很多的资源,比如内存。
Presto:Opera 曾经采用的就是Presto 内核,Presto 内核被称为
公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,
在处理JS 脚本等脚本语言时,会比其他的内核快3 倍左右,缺点就
是为了达到很快的速度而丢掉了一部分网页兼容性。
Webkit:Webkit 是Safari 采用的内核,它的优点就是网页浏览速
度较快,虽然不及Presto 但是也胜于Gecko 和Trident,缺点是
对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会
使一些编写不标准的网页无法正确显示。WebKit 前身是KDE 小组的
KHTML 引擎,可以说WebKit 是KHTML 的一个开源的分支。
Blink:谷歌在Chromium Blog 上发表博客,称将与苹果的开源浏览
器核心Webkit 分道扬镳,在Chromium 项目中研发Blink 渲染引
擎(即浏览器核心),内置于Chrome 浏览器之中。其实Blink 引
擎就是Webkit 的一个分支,就像webkit 是KHTML 的分支一样。
Blink 引擎现在是谷歌公司与Opera Software 共同研发,上面提到
140
过的,Opera 弃用了自己的Presto 内核,加入Google 阵营,跟随
谷歌一起研发Blink。
14. 浏览器的渲染过程
浏览器渲染主要有以下步骤:
首先解析收到的文档,根据文档定义构建一棵DOM 树,DOM 树是由
DOM 元素及属性节点组成的。
然后对CSS 进行解析,生成CSSOM 规则树。
根据DOM 树和CSSOM 规则树构建渲染树。渲染树的节点被称为渲染
对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和
DOM 元素相对应,但这种对应关系不是一对一的,不可见的DOM 元
素不会被插入渲染树。还有一些DOM 元素对应几个可见对象,它们
一般是一些具有复杂结构的元素,无法用一个矩形来描述。
当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏
览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。
这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位
置和大小。通常这一行为也被称为“自动重排”。
布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的paint
方法将它们的内容显示在屏幕上,绘制使用UI 基础组件。
大致过程如图所示:
141
注意:这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会
尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完
成之后再去构建和布局render 树。它是解析完一部分内容就显示一
部分内容,同时,可能还在通过网络下载其余内容。
15. 渲染过程中遇到JS 文件如何处理?
JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在
构建DOM 时,HTML 解析器若遇到了JavaScript,那么它会暂停文
档的解析,将控制权移交给JavaScript 引擎,等JavaScript 引擎
运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如
果想要首屏渲染的越快,就越不应该在首屏就加载JS 文件,这也是
都建议将script 标签放在body 标签底部的原因。当然在当下,并
不是说script 标签必须放在底部,因为你可以给script 标签添加
defer 或者async 属性。
16. 前端储存的⽅式有哪些?
cookies: 在HTML5 标准前本地储存的主要⽅式,优点是兼容性好,
请求头⾃带cookie⽅便,缺点是⼤⼩只有4k,⾃动请求头加⼊cookie
142
浪费流量,每个domain 限制20 个cookie,使⽤起来麻烦,需要⾃
⾏封装;
localStorage:HTML5 加⼊的以键值对(Key-Value)为标准的⽅式,
优点是操作⽅便,永久性储存(除⾮⼿动删除),⼤⼩为5M,兼容
IE8+ ;
sessionStorage:与localStorage 基本类似,区别是sessionStorage
当⻚⾯关闭后会被清理,⽽且与cookie、localStorage 不同,他不
能在所有同源窗⼝中共享,是会话级别的储存⽅式;
Web SQL:2010 年被W3C 废弃的本地数据库数据存储⽅案,但是主流
浏览器(⽕狐除外)都已经有了相关的实现,web sql 类似于SQLite,
是真正意义上的关系型数据库,⽤sql 进⾏操作,当我们⽤JavaScript
时要进⾏转换,较为繁琐;
IndexedDB:是被正式纳⼊HTML5 标准的数据库储存⽅案,它是NoSQL
数据库,⽤键值对进⾏储存,可以进⾏快速读取操作,⾮常适合web
场景,同时⽤JavaScript 进⾏操作会⾮常便。
17. 事件是什么?事件模型?
事件是用户操作网页时发生的交互动作,比如click/move, 事件除
了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事
件被封装成一个event 对象,包含了该事件发生时的所有相关信息
( event 的属性)以及可以对事件进行的操作( event 的方法)。
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现
代浏览器一共有三种事件模型:
DOM0 级事件模型,这种模型不会传播,所以没有事件流的概念,但
是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义
143
监听函数,也可以通过js 属性来指定监听函数。所有浏览器都兼容
这种方式。直接在dom 对象上注册事件名称,就是DOM0 写法。
IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理
阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听
事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到
document,依次检查经过的节点是否绑定了事件监听函数,如果有则
执行。这种模型通过attachEvent 来添加监听函数,可以添加多个
监听函数,会按顺序依次执行。
DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一
个过程是事件捕获阶段。捕获指的是事件从document 一直向下传播
到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有
则执行。后面两个阶段和IE 事件模型的两个阶段相同。这种事件模
型,事件绑定的函数是addEventListener,其中第三个参数可以指
定事件是否在捕获阶段执行。
18. 对事件循环的理解
因为js 是单线程运行的,在代码执行时,通过将不同函数的执行上
下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果
遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个
事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,
再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列
可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕
后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有
就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行
完成后再去执行宏任务队列中的任务。
144
Event Loop 执行顺序如下所示:
首先执行同步代码,这属于宏任务
当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执

执行所有微任务
当执行完所有微任务后,如有必要会渲染页面
然后开始下一轮Event Loop,执行宏任务中的异步代码
计算机网络部分
1. GET 和POST 的请求的区别
Post 和Get 是HTTP 请求的两种方法,其区别如下:
应用场景:GET 请求是一个幂等的请求,一般Get 请求用于对服务
器资源不会产生影响的场景,比如说请求一个网页的资源。而Post
不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比
如注册用户这一类的操作。
是否缓存:因为两者应用场景不同,浏览器一般会对Get 请求缓存,
但很少对Post 请求缓存。
145
发送的报文格式:Get 请求的报文中实体部分为空,Post 请求的报
文中实体部分一般为向服务器发送的数据。
安全性:Get 请求可以将请求的参数放入url 中向服务器发送,这
样的做法相对于Post 请求来说是不太安全的,因为请求的url 会
被保留在历史记录中。
请求长度:浏览器由于对url 长度的限制,所以会影响get 请求发
送数据时的长度。这个限制是浏览器规定的,并不是RFC 规定的。
参数类型:post 的参数传递支持更多的数据类型。
2. POST 和PUT 请求的区别
PUT 请求是向服务器端发送数据,从而修改数据的内容,但是不会增
加数据的种类等,也就是说无论进行多少次PUT 操作,其结果并没有
不同。(可以理解为时更新数据)
POST 请求是向服务器端发送数据,该请求会改变数据的种类等资源,
它会创建新的内容。(可以理解为是创建数据)
3. 常见的HTTP 请求头和响应头
HTTP Request Header 常见的请求头:
Accept:浏览器能够处理的内容类型
Accept-Charset:浏览器能够显示的字符集
Accept-Encoding:浏览器能够处理的压缩编码
Accept-Language:浏览器当前设置的语言
Connection:浏览器与服务器之间连接的类型
Cookie:当前页面设置的任何Cookie
146
Host:发出请求的页面所在的域
Referer:发出请求的页面的URL
User-Agent:浏览器的用户代理字符串
HTTP Responses Header 常见的响应头:
Date:表示消息发送的时间,时间的描述格式由rfc822 定义
server:服务器名称
Connection:浏览器与服务器之间连接的类型
Cache-Control:控制HTTP 缓存
content-type:表示后面的文档属于什么MIME 类型
常见的Content-Type 属性值有以下四种:
(1)application/x-www-form-urlencoded:浏览器的原生form 表
单, 如果不设置enctype 属性, 那么最终就会以
application/x-www-form-urlencoded 方式提交数据。该种方式提交
的数据放在body 里面,数据按照key1=val1&key2=val2 的方式进
行编码,key 和val 都进行了URL 转码。
(2)multipart/form-data:该种方式也是一个常见的POST 提交方
式,通常表单上传文件时使用该种方式。
(3)application/json:服务器消息主体是序列化后的JSON 字符
串。
(4)text/xml:该种方式主要用来提交XML 格式的数据。
4. 常见的HTTP 请求方法
GET: 向服务器获取数据;
147
POST:将实体提交到指定的资源,通常会造成服务器资源的修改;
PUT:上传文件,更新数据;
DELETE:删除服务器上的对象;
HEAD:获取报文首部,与GET 相比,不返回报文主体部分;
OPTIONS:询问支持的请求方法,用来跨域请求;
CONNECT:要求在与代理服务器通信时建立隧道,使用隧道进行TCP
通信;
TRACE: 回显服务器收到的请求,主要⽤于测试或诊断。
5. HTTP 1.1 和HTTP 2.0 的区别
二进制协议:HTTP/2 是一个二进制协议。在HTTP/1.1 版中,报文
的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是
二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是
二进制,并且统称为"帧",可以分为头信息帧和数据帧。帧的概念
是它实现多路复用的基础。
多路复用:HTTP/2 实现了多路复用,HTTP/2 仍然复用TCP 连接,
但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,
而且不用按照顺序一一发送,这样就避免了"队头堵塞"的问题。
数据流:HTTP/2 使用了数据流的概念,因为HTTP/2 的数据包是不
按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。
因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每
个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个
独一无二的编号。数据包发送时,都必须标记数据流ID ,用来区分
它属于哪个数据流。
148
头信息压缩:HTTP/2 实现了头信息压缩,由于HTTP 1.1 协议不带
状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重
复的,比如Cookie 和User Agent ,一模一样的内容,每次请求都
必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了
优化,引入了头信息压缩机制。一方面,头信息使用gzip 或
compress 压缩后再发送;另一方面,客户端和服务器同时维护一张
头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发
送同样字段了,只发送索引号,这样就能提高速度了。
服务器推送:HTTP/2 允许服务器未经请求,主动向客户端发送资源,
这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,
这样就可以相对减少一些延迟时间。这里需要注意的是http2 下服
务器主动推送的是静态资源,和WebSocket 以及使用SSE 等方式向
客户端发送即时数据的推送是不同的。
6. HTTP 和HTTPS 协议的区别
HTTP 和HTTPS 协议的主要区别如下:
HTTPS 协议需要CA 证书,费用较高;而HTTP 协议不需要;
HTTP 协议是超文本传输协议,信息是明文传输的,HTTPS 则是具有安
全性的SSL 加密传输协议;
使用不同的连接方式,端口也不同,HTTP 协议端口是80,HTTPS 协
议端口是443;
HTTP 协议连接很简单,是无状态的;HTTPS 协议是有SSL 和HTTP 协
议构建的可进行加密传输、身份认证的网络协议,比HTTP 更加安全。
7. HTTP2 的头部压缩算法是怎样的?
149
HTTP2 的头部压缩是HPACK 算法。在客户端和服务器两端建立“字典”,
用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,
可以达到50%~90%的高压缩率。
具体来说:
在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,
对于相同的数据,不再通过每次请求和响应发送;
首部表在HTTP/2 的连接存续期内始终存在,由客户端和服务器共同
渐进地更新;
每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前
的值。
例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请
求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
150
8. 说一下HTTP 3.0
HTTP/3 基于UDP 协议实现了类似于TCP 的多路复用数据流、传输可
靠性等功能,这套功能被称为QUIC 协议。
1.流量控制、传输可靠性功能:QUIC 在UDP 的基础上增加了一层来
保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一
些TCP 中的特性。
2.集成TLS 加密功能:目前QUIC 使用TLS1.3,减少了握手所花费的
RTT 数。
3.多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了
数据流的单独传输,解决了TCP 的队头阻塞问题。
151
4.快速握手:由于基于UDP,可以实现使用0 ~ 1 个RTT 来建立连接。
9. 什么是HTTPS 协议?
超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:
HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS 经
由HTTP 进行通信,利用SSL/TLS 来加密数据包。HTTPS 的主要目的
是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
HTTP 协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持
的风险,而协议TLS/SSL 具有身份验证、信息加密和完整性校验的功
能,可以避免此类问题发生。
安全层的主要职责就是对发起的HTTP 请求的数据进行加密操作和
对接收到的HTTP 的内容进行解密操作。
10. HTTPS 通信(握手)过程
HTTPS 的通信过程如下:
1.客户端向服务器发起请求,请求中包含使用的协议版本号、生成的
一个随机数、以及客户端支持的加密方法。
152
2.服务器端接收到请求后,确认双方使用的加密方法、并给出服务器
的证书、以及一个服务器生成的随机数。
3.客户端确认服务器证书有效后,生成一个新的随机数,并使用数字
证书中的公钥,加密这个随机数,然后发给服务器。并且还会提供
一个前面所有内容的hash 的值,用来供服务器检验。
4.服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供
前面所有内容的hash 值来供客户端检验。
5.客户端和服务器端根据约定的加密方法使用前面的三个随机数,生
成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。
11. DNS 完整的查询过程
DNS 服务器解析域名的过程:
首先会在浏览器的缓存中查找对应的IP 地址,如果查找到直接返回,
若找不到继续下一步
将请求发送给本地DNS 服务器,在本地域名服务器缓存中查询,如果
查找到,就直接将查找结果返回,若找不到继续下一步
本地DNS 服务器向根域名服务器发送请求,根域名服务器会返回一个
所查询域的顶级域名服务器地址
本地DNS 服务器向顶级域名服务器发送请求,接受请求的服务器查询
自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的
下一级的权威域名服务器的地址
本地DNS 服务器向权威域名服务器发送请求,域名服务器返回对应的
结果
本地DNS 服务器将返回结果保存在缓存中,便于下次使用
153
本地DNS 服务器将返回结果返回给浏览器
比如要查询www.baidu.com 的IP 地址,首先会在浏览器的缓存中
查找是否有该域名的缓存,如果不存在就将请求发送到本地的DNS
服务器中,本地DNS 服务器会判断是否存在该域名的缓存,如果不存
在,则向根域名服务器发送一个请求,根域名服务器返回负责.com
的顶级域名服务器的IP 地址的列表。然后本地DNS 服务器再向其
中一个负责.com 的顶级域名服务器发送一个请求,负责.com 的顶
级域名服务器返回负责.baidu 的权威域名服务器的IP 地址列表。
然后本地DNS 服务器再向其中一个权威域名服务器发送一个请求,
最后权威域名服务器返回一个对应的主机名的IP 地址列表。
12. OSI 七层模型
ISO 为了更好的使网络应用更为普及,推出了OSI 参考模型。
(1)应用层
154
OSI 参考模型中最靠近用户的一层,是为计算机用户提供应用接口,
也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:
HTTP,HTTPS,FTP,POP3、SMTP 等。
在客户端与服务器中经常会有数据的请求,这个时候就是会用到
http(hyper text transfer protocol)(超文本传输协议)或者https.
在后端设计数据接口时,我们常常使用到这个协议。
FTP 是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,
在一些资源网站,比如百度网盘``迅雷应该是基于此协议的。
SMTP 是simple mail transfer protocol(简单邮件传输协议)。在
一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。
(2)表示层
表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的
应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可
提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通
信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换
功能之一。
在项目开发中,为了方便数据传输,可以使用base64 对数据进行编
解码。如果按功能来划分,base64 应该是工作在表示层。
(3)会话层
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层
的通信由不同设备中的应用程序之间的服务请求和响应组成。
(4)传输层
155
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端
到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等
问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只
是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、
可靠的数据通路。我们通常说的,TCP UDP 就是在这一层。端口号既
是这里的“端”。
(5)网络层
本层通过IP 寻址来建立两个节点之间的连接,为源端的运输层送来
的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目
的端的运输层。就是通常说的IP 层。这一层就是我们经常说的IP 协
议层。IP 协议是Internet 的基础。我们可以这样理解,网络层规定
了数据包的传输路线,而传输层则规定了数据包的传输方式。
(6)数据链路层
将比特组合成字节,再将字节组合成帧,使用链路层地址(以太网使
用MAC 地址)来访问介质,并进行差错检测。
网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理
解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。
不过,在数据链路层上还增加了差错控制的功能。
(7)物理层
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。
规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、
中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的
传输介质。
OSI 七层模型通信特点:对等通信
156
对等通信,为了使数据分组从源传送到目的地,源端OSI 模型的每一
层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。
在每一层通信过程中,使用本层自己协议进行通信。
13. TCP 的三次握手和四次挥手
(1)三次握手
三次握手(Three-way Handshake)其实就是指建立一个TCP 连接时,
需要客户端和服务器总共发送3 个包。进行三次握手的主要作用就是
为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序
列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端
口,建立TCP 连接,并同步连接双方的序列号和确认号,交换TCP 窗
口大小信息。
刚开始客户端处于Closed 的状态,服务端处于Listen 状态。
157
第一次握手:客户端给服务端发一个SYN 报文,并指明客户端的初
始化序列号ISN,此时客户端处于SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1 的报文段不能携带数
据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的SYN 报文之后,会以自己的SYN
报文作为应答,并且也是指定了自己的初始化序列号ISN。同时会把
客户端的ISN + 1 作为ACK 的值,表示自己已经收到了客户端的SYN,
此时服务器处于SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y
第三次握手:客户端收到SYN 报文之后,会发送一个ACK 报文,当
然,也是一样把服务器的ISN + 1 作为ACK 的值,表示已经收到了
服务端的SYN 报文,此时客户端处于ESTABLISHED 状态。服务器收
到ACK 报文之后,也处于ESTABLISHED 状态,此时,双方已建立起
了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,
第二个报文段所以要+1),ACK 报文段可以携带数据,不携带数据则
不消耗序号。
那为什么要三次握手呢?两次不行吗?
为了确认双方的接收能力和发送能力都正常
如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是
客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传
输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其
158
中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是
在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到
达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是
就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要
服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的
确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
简单来说就是以下三步:
第一次握手:客户端向服务端发送连接请求报文段。该报文段中包含
自身的数据通讯初始序号。请求发送后,客户端便进入SYN-SENT 状
态。
第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发
送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成
后便进入SYN-RECEIVED 状态。
第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一
个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,
服务端收到这个应答后也进入ESTABLISHED 状态,此时连接建立成
功。
TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉
对方,什么样序号的报文段能够被正确接收。第三次握手的作用是
客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服
务器就没有办法知道自己的序号是否已被确认。同时这样也是为了
防止失效的请求报文段被服务器接收,而出现错误的情况。
(2)四次挥手
159
刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请
求。四次挥手的过程如下:
第一次挥手: 客户端会发送一个FIN 报文,报文中会指定一个序列
号。此时客户端处于FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,
主动关闭TCP 连接,进入FIN_WAIT1(终止等待1)状态,等待服务
端的确认。
第二次挥手:服务端收到FIN 之后,会发送ACK 报文,且把客户端
的序列号值+1 作为ACK 报文的序列号值,表明已经收到客户端的
报文了,此时服务端处于CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号
ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,
此时的TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收
160
到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务
端发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一
样,发给FIN 报文,且指定一个序列号。此时服务端处于LAST_ACK
的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段
(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK
(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到FIN 之后,一样发送一个ACK 报文作为应
答,且把服务端的序列号值+1 作为自己ACK 报文的序列号值,此
时客户端处于TIME_WAIT 状态。需要过一阵子以确保服务端收到自
己的ACK 报文之后才会进入CLOSED 状态,服务端收到ACK 报文之
后,就处于关闭连接了,处于CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段
(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)
状态。此时TCP 未释放掉,需要经过时间等待计时器设置的时间2MSL
后,客户端才进入CLOSED 状态。
那为什么需要四次挥手呢?
因为当服务端收到客户端的SYN 连接请求报文后,可以直接发送
SYN+ACK 报文。其中ACK 报文是用来应答的,SYN 报文是用来同步的。
但是关闭连接时,当服务端收到FIN 报文时,很可能并不会立即关闭
SOCKET,所以只能先回复一个ACK 报文,告诉客户端,“你发的FIN
报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能
发送FIN 报文,因此不能一起发送,故需要四次挥手。
161
简单来说就是以下四步:
第一次挥手:若客户端认为数据发送完成,则它需要向服务端发送连
接释放请求。
第二次挥手:服务端收到连接释放请求后,会告诉应用层要释放TCP
链接。然后会发送ACK 包,并进入CLOSE_WAIT 状态,此时表明客
户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因
为TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。
第三次挥手:服务端如果此时还有没发完的数据会继续发送,完毕后
会向客户端发送连接释放请求,然后服务端便进入LAST-ACK 状态。
第四次挥手:客户端收到释放请求后,向服务端发送确认应答,此时
客户端进入TIME-WAIT 状态。该状态会持续2MSL(最大段生存期,
指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段
内没有服务端的重发请求的话,就进入CLOSED 状态。当服务端收到
确认应答后,也便进入CLOSED 状态。
TCP 使用四次挥手的原因是因为TCP 的连接是全双工的,所以需要
双方分别释放到对方的连接,单独一方的连接释放,只代表不能再
向对方发送数据,连接处于的是半释放的状态。
最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止
发送给服务器的确认报文段丢失或者出错,从而导致服务器端不能
正常关闭。
HTML 部分
1. 对HTML 语义化的理解
162
语义化是指根据内容的结构化(内容语义化),选择合适的标签(代
码语义化)。通俗来讲就是用正确的标签做正确的事情。
语义化的优点如下:
对机器友好,带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬
取有效信息,有利于SEO。除此之外,语义类还支持读屏软件,根据
文章可以自动生成目录;
对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发
者能清晰的看出网页的结构,便于团队的开发与维护。
常见的语义化标签:
2. DOCTYPE(⽂档类型) 的作⽤
DOCTYPE 是HTML5 中一种标准通用标记语言的文档类型声明,它的目
的是告诉浏览器(解析器)应该以什么样(html 或xhtml)的文档类
型定义来解析文档,不同的渲染模式会影响浏览器对CSS 代码甚⾄
JavaScript 脚本的解析。它必须声明在HTML⽂档的第⼀⾏。
浏览器渲染页面的两种模式(可通过document.compatMode 获取,比
如,语雀官网的文档类型是CSS1Compat):
163
CSS1Compat:标准模式(Strick mode),默认模式,浏览器使用W3C
的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈
现页面。
BackCompat:怪异模式(混杂模式)(Quick mode),浏览器使用自己的
怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后
兼容的方式显示。
3. script 标签中defer 和async 的区别
如果没有defer 或async 属性,浏览器会立即加载并执行相应的脚本。
它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样
就阻塞了后续文档的加载。
下图可以直观的看出三者之间的区别:
其中蓝色代表js 脚本网络加载时间,红色代表js 脚本执行时间,绿
色代表html 解析。
defer 和async 属性都是去异步加载外部的JS 脚本文件,它们都不
会阻塞页面的解析,其区别如下:
执行顺序:多个带async 属性的标签,不能保证加载的顺序;多个带
defer 属性的标签,按照加载顺序执行;
脚本是否并行执行:async 属性,表示后续文档的加载和执行与js
脚本的加载和执行是并行进行的,即异步执行;defer 属性,加载后
续文档的过程和js 脚本的加载(此时仅加载不执行)是并行进行的
164
(异步),js 脚本需要等到文档所有元素解析完成之后才执行,
DOMContentLoaded 事件触发执行之前。
4. 行内元素有哪些?块级元素有哪些? 空(void)元素有那
些?
行内元素有:a b span img input select strong;
块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p;
空元素,即没有内容的HTML 元素。空元素是在开始标签中关闭的,
也就是空元素没有闭合标签:
常见的有:<br>、<hr>、<img>、<input>、<link>、<meta>;
鲜见的有:<area>、<base>、<col>、<colgroup>、<command>、<embed>、
<keygen>、<param>、<source>、<track>、<wbr>。
5. 浏览器是如何对HTML5 的离线储存资源进行管理和加载?
在线的情况下,浏览器发现html 头部有manifest 属性,它会请求
manifest 文件,如果是第一次访问页面,那么浏览器就会根据
manifest 文件的内容下载相应的资源并且进行离线存储。如果已经
访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线
的资源加载页面,然后浏览器会对比新的manifest 文件与旧的
manifest 文件,如果文件没有发生改变,就不做任何操作,如果文
件改变了,就会重新下载文件中的资源并进行离线存储。
离线的情况下,浏览器会直接使用离线存储的资源。
6. Canvas 和SVG 的区别
(1)SVG:
165
SVG 可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标
记语言XML 描述的2D 图形的语言,SVG 基于XML 就意味着SVG DOM
中的每个元素都是可用的,可以为某个元素附加Javascript 事件处
理器。在SVG 中,每个被绘制的图形均被视为对象。如果SVG 对象
的属性发生变化,那么浏览器能够自动重现图形。
其特点如下:
不依赖分辨率
支持事件处理器
最适合带有大型渲染区域的应用程序(比如谷歌地图)
复杂度高会减慢渲染速度(任何过度使用DOM 的应用都不快)
不适合游戏应用
(2)Canvas:
Canvas 是画布,通过Javascript 来绘制2D 图形,是逐像素进行渲
染的。其位置发生改变,就会重新进行绘制。
其特点如下:
依赖分辨率
不支持事件处理器
弱的文本渲染能力
能够以.png 或.jpg 格式保存结果图像
最适合图像密集型的游戏,其中的许多对象会被频繁重绘
注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一
系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是
166
一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等
属性。
7. 说一下HTML5 drag API
dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。
darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触
发。
drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。
CSS 部分
1. display 的block、inline 和inline-block 的区别
(1)block:会独占一行,多个元素会另起一行,可以设置width、
height、margin 和padding 属性;
(2)inline:元素不会独占一行,设置width、height 属性无效。
但可以设置水平方向的margin 和padding 属性,不能设置垂直方向
的padding 和margin;
(3)inline-block:将对象设置为inline 对象,但对象的内容作为
block 对象呈现,之后的内联对象会被排列在同一行内。
对于行内元素和块级元素,其特点如下:
(1)行内元素
167
设置宽高无效;
可以设置水平方向的margin 和padding 属性,不能设置垂直方向的
padding 和margin;
不会自动换行;
(2)块级元素
可以设置宽高;
设置margin 和padding 都有效;
可以自动换行;
多个块状,默认排列从上到下。
2. link 和@import 的区别
两者都是外部引用CSS 的方式,它们的区别如下:
link 是XHTML 标签,除了加载CSS 外,还可以定义RSS 等其他事务;
@import 属于CSS 范畴,只能加载CSS。
link 引用CSS 时,在页面载入时同时加载;@import 需要页面网页完
全载入以后加载。
link 是XHTML 标签,无兼容问题;@import 是在CSS2.1 提出的,低
版本的浏览器不支持。
link 支持使用Javascript 控制DOM 去改变样式;而@import 不支持。
3. CSS3 中有哪些新特性
新增各种CSS 选择器(: not(.input):所有class 不是“input”
的节点)
圆角(border-radius:8px)
168
多列布局(multi-column layout)
阴影和反射(Shadoweflect)
文字特效(text-shadow)
文字渲染(Text-decoration)
线性渐变(gradient)
旋转(transform)
增加了旋转,缩放,定位,倾斜,动画,多背景
4. 对CSSSprites 的理解
CSSSprites(精灵图),将一个页面涉及到的所有图片都包含到一张
大图中去,然后利用CSS 的background-image,background-repeat,
background-position 属性的组合进行背景定位。
优点:
利用CSS Sprites 能很好地减少网页的http 请求,从而大大提高了
页面的性能,这是CSS Sprites 最大的优点;
CSS Sprites 能减少图片的字节,把3 张图片合并成1 张图片的字节
总是小于这3 张图片的字节总和。
缺点:
在图片合并时,要把多张图片有序的、合理的合并成一张图片,还要
留好足够的空间,防止板块内出现不必要的背景。在宽屏及高分辨率
下的自适应页面,如果背景不够宽,很容易出现背景断裂;
CSSSprites 在开发的时候相对来说有点麻烦,需要借助photoshop
或其他工具来对每个背景单元测量其准确的位置。
169
维护方面:CSS Sprites 在维护的时候比较麻烦,页面背景有少许改
动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免
改动更多的CSS,如果在原来的地方放不下,又只能(最好)往下加
图片,这样图片的字节就增加了,还要改动CSS。
5. CSS 优化和提高性能的方法有哪些?
加载性能:
(1)css 压缩:将写好的css 进行打包压缩,可以减小文件体积。
(2)css 单一样式:当需要下边距和左边距的时候,很多时候会选
择使用margin:top 0 bottom 0 ; 但
margin-bottom:bottom;margin-left:left;执行效率会更高。
(3)减少使用@import,建议使用link,因为后者在页面加载时一
起加载,前者是等待页面加载完成之后再进行加载。
选择器性能:
(1)关键选择器(key selector)。选择器的最后面的部分为关键
选择器(即用来匹配目标元素的部分)。CSS 选择符是从右到左进行
匹配的。当使用后代选择器的时候,浏览器会遍历所有子元素来确定
是否是指定的元素等等;
(2)如果规则拥有ID 选择器作为其关键选择器,则不要为规则增加
标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们
了)。
(3)避免使用通配规则,如*{}计算次数惊人,只对需要用到的元素
进行选择。
(4)尽量少的去对标签进行选择,而是用class。
170
(5)尽量少的去使用后代选择器,降低选择器的权重值。后代选择
器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三
层,更多的使用类来关联每一个标签元素。
(6)了解哪些属性是可以通过继承而来的,然后避免对这些属性重
复指定规则。
渲染性能:
(1)慎重使用高性能属性:浮动、定位。
(2)尽量减少页面重排、重绘。
(3)去除空规则:{}。空规则的产生原因一般来说是为了预留样
式。去除这些空规则无疑能减少css 文档体积。
(4)属性值为0 时,不加单位。
(5)属性值为浮动小数0.**,可以省略小数点之前的0。
(6)标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。
(7)不使用@import 前缀,它会影响css 的加载速度。
(8)选择器优化嵌套,尽量避免层级过深。
(9)css 雪碧图,同一页面相近部分的小图标,方便使用,减少页
面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,
再使用。
(10)正确使用display 的属性,由于display 的作用,某些样式组
合会无效,徒增样式体积的同时也影响解析性能。
171
(11)不滥用web 字体。对于中文网站来说WebFonts 可能很陌生,
国外却很流行。web fonts 通常体积庞大,而且一些浏览器在下载web
fonts 时会阻塞页面渲染损伤性能。
可维护性、健壮性:
(1)将具有相同属性的样式抽离出来,整合并通过class 在页面中
进行使用,提高css 的可维护性。
(2)样式与内容分离:将css 代码定义到外部css 中。
6. 对CSS 工程化的理解
CSS 工程化是为了解决以下问题:
1.宏观设计:CSS 代码如何组织、如何拆分、模块结构怎样设计?
2.编码优化:怎样写出更好的CSS?
3.构建:如何处理我的CSS,才能让它的打包结果最优?
4.可维护性:代码写完了,如何最小化它后续的变更成本?如何确保
任何一个同事都能轻松接手?
以下三个方向都是时下比较流行的、普适性非常好的CSS 工程化实
践:
预处理器:Less、Sass 等;
重要的工程化插件: PostCss;
Webpack loader 等。
基于这三个方向,可以衍生出一些具有典型意义的子问题,这里我们
逐个来看:
172
(1)预处理器:为什么要用预处理器?它的出现是为了解决什么问
题?
预处理器,其实就是CSS 世界的“轮子”。预处理器支持我们写一
种类似CSS、但实际并不是CSS 的语言,然后把它编译成CSS 代码:
那为什么写CSS 代码写得好好的,偏偏要转去写“类CSS”呢?这
就和本来用JS 也可以实现所有功能,但最后却写React 的jsx 或
者Vue 的模板语法一样——为了爽!要想知道有了预处理器有多爽,
首先要知道的是传统CSS 有多不爽。随着前端业务复杂度的提高,
前端工程中对CSS 提出了以下的诉求:
1.宏观设计上:我们希望能优化CSS 文件的目录结构,对现有的CSS
文件实现复用;
2.编码优化上:我们希望能写出结构清晰、简明易懂的CSS,需要它
具有一目了然的嵌套层级关系,而不是无差别的一铺到底写法;我们
希望它具有变量特征、计算能力、循环能力等等更强的可编程性,这
样我们可以少写一些无用的代码;
3.可维护性上:更强的可编程性意味着更优质的代码结构,实现复用
意味着更简单的目录结构和更强的拓展能力,这两点如果能做到,自
然会带来更强的可维护性。
这三点是传统CSS 所做不到的,也正是预处理器所解决掉的问题。
预处理器普遍会具备这样的特性:
173
嵌套代码的能力,通过嵌套来反映不同css 属性之间的层级关系;
支持定义css 变量;
提供计算函数;
允许对代码片段进行extend 和mixin;
支持循环语句的使用;
支持将CSS 文件模块化,实现复用。
(2)PostCss:PostCss 是如何工作的?我们在什么场景下会使用
PostCss?
PostCss 仍然是一个对CSS 进行解析和处理的工具,它会对CSS 做
这样的事情:
它和预处理器的不同就在于,预处理器处理的是类CSS,而PostCss
处理的就是CSS 本身。Babel 可以将高版本的JS 代码转换为低版
本的JS 代码。PostCss 做的是类似的事情:它可以编译尚未被浏览
器广泛支持的先进的CSS 语法,还可以自动为一些需要额外兼容的
语法增加前缀。更强的是,由于PostCss 有着强大的插件机制,支
持各种各样的扩展,极大地强化了CSS 的能力。
PostCss 在业务中的使用场景非常多:
提高CSS 代码的可读性:PostCss 其实可以做类似预处理器能做的
工作;
174
当我们的CSS 代码需要适配低版本浏览器时, PostCss 的
Autoprefixer 插件可以帮助我们自动增加浏览器前缀;
允许我们编写面向未来的CSS:PostCss 能够帮助我们编译CSS
next 代码;
(3)Webpack 能处理CSS 吗?如何实现?
Webpack 能处理CSS 吗:
Webpack 在裸奔的状态下,是不能处理CSS 的,Webpack 本身是一
个面向JavaScript 且只能处理JavaScript 代码的模块化打包工
具;
Webpack 在loader 的辅助下,是可以处理CSS 的。
如何用Webpack 实现对CSS 的处理:
Webpack 中操作CSS 需要使用的两个关键的loader:css-loader
和style-loader
注意,答出“用什么”有时候可能还不够,面试官会怀疑你是不是在
背答案,所以你还需要了解每个loader 都做了什么事情:
css-loader:导入CSS 模块,对CSS 代码进行编译处理;
style-loader:创建style 标签,把CSS 内容写入标签。
在实际使用中,css-loader 的执行顺序一定要安排在style-loader
的前面。因为只有完成了编译过程,才可以对css 代码进行插入;
若提前插入了未编译的代码,那么webpack 是无法理解这坨东西的,
它会无情报错。
7. 常见的CSS 布局单位
常用的布局单位包括像素(px),百分比(%),em,rem,vw/vh。
175
(1)像素(px)是页面布局的基础,一个像素表示终端(电脑、手
机、平板等)屏幕所能显示的最小的区域,像素分为两种类型:CSS
像素和物理像素:
CSS 像素:为web 开发者提供,在CSS 中使用的一个抽象单位;
物理像素:只与设备的硬件密度有关,任何设备的物理像素都是固定
的。
(2)百分比(%),当浏览器的宽度或者高度发生变化时,通过百分
比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,
从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。
(3)em 和rem 相对于px 更具灵活性,它们都是相对长度单位,它
们之间的区别:em 相对于父元素,rem 相对于根元素。
em: 文本相对长度单位。相对于当前对象内文本的字体尺寸。如果
当前行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体
尺寸(默认16px)。(相对父元素的字体大小倍数)。
rem: rem 是CSS3 新增的一个相对单位,相对于根元素(html 元素)
的font-size 的倍数。作用:利用rem 可以实现简单的响应式布局,
可以利用html 元素中字体的大小与屏幕间的比值来设置font-size
的值,以此实现当屏幕分辨率变化时让元素也随之变化。
(4)vw/vh 是与视图窗口有关的单位,vw 表示相对于视图窗口的宽
度,vh 表示相对于视图窗口高度,除了vw 和vh 外,还有vmin 和vmax
两个相关的单位。
vw:相对于视窗的宽度,视窗宽度是100vw;
176
vh:相对于视窗的高度,视窗高度是100vh;
vmin:vw 和vh 中的较小值;
vmax:vw 和vh 中的较大值;
vw/vh 和百分比很类似,两者的区别:
百分比(%):大部分相对于祖先元素,也有相对于自身的情况比如
(border-radius、translate 等)
vw/vm:相对于视窗的尺寸
8. 水平垂直居中的实现
利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页
面的中心,然后再通过translate 来调整元素的中心点到页面的中心。
该方法需要考虑浏览器兼容问题。
利用绝对定位,设置四个方向的值都为0,并将margin 设置为auto,
由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上
的居中。该方法适用于盒子有宽高的情况:
177
利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页
面的中心,然后再通过margin 负值来调整元素的中心点到页面的中
心。该方法适用于盒子宽高已知的情况
使用flex 布局, 通过align-items:center 和
justify-content:center 设置容器的垂直和水平方向上为居中对齐,
然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的
问题,该方法在移动端用的较多:
9. 对BFC 的理解,如何创建BFC
先来看两个相关的概念:
Box: Box 是CSS 布局的对象和基本单位,⼀个⻚⾯是由很多个Box
组成的,这个Box 就是我们所说的盒模型。
Formatting context:块级上下⽂格式化,它是⻚⾯中的⼀块渲染区
域,并且有⼀套渲染规则,它决定了其⼦元素将如何定位,以及和其
他元素的关系和相互作⽤。
178
块格式化上下文(Block Formatting Context,BFC)是Web 页面的
可视化CSS 渲染的一部分,是布局过程中生成块级盒子的区域,也是
浮动元素与其他元素的交互限定区域。
通俗来讲:BFC 是一个独立的布局环境,可以理解为一个容器,在这
个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物
品。如果一个元素符合触发BFC 的条件,则BFC 中的元素布局不受外
部影响。
创建BFC 的条件:
根元素:body;
元素设置浮动:float 除none 以外的值;
元素设置绝对定位:position (absolute、fixed);
display 值为:inline-block、table-cell、table-caption、flex
等;
overflow 值为:hidden、auto、scroll;
BFC 的特点:
垂直方向上,自上而下排列,和文档流的排列方式一致。
在BFC 中上下相邻的两个容器的margin 会重叠
计算BFC 的高度时,需要计算浮动元素的高度
BFC 区域不会与浮动的容器发生重叠
BFC 是独立的容器,容器内部元素不会影响外部元素
每个元素的左margin 值和容器的左border 相接触
BFC 的作用:
179
解决margin 的重叠问题:由于BFC 是一个独立的区域,内部的元素
和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin
重叠的问题。
解决高度塌陷的问题:在对子元素设置浮动后,父元素会发生高度塌
陷,也就是父元素的高度变为0。解决这个问题,只需要把父元素变
成一个BFC。常用的办法是给父元素设置overflow:hidden。
创建自适应两栏布局:可以用来创建自适应两栏布局:左边的宽度固
定,右边的宽度自适应。
左侧设置float:left,右侧设置overflow: hidden。这样右边就触
发了BFC,BFC 的区域不会与浮动元素发生重叠,所以两侧就不会发
生重叠,实现了自适应两栏布局。
10. 元素的层叠顺序
层叠顺序,英文称作stacking order,表示元素发生层叠时有着特
定的垂直显示顺序。下面是盒模型的层叠规则:
180
对于上图,由上到下分别是:
(1)背景和边框:建立当前层叠上下文元素的背景和边框。
(2)负的z-index:当前层叠上下文中,z-index 属性值为负的元素。
(3)块级盒:文档流内非行内级非定位后代元素。
(4)浮动盒:非定位浮动元素。
(5)行内盒:文档流内行内级非定位后代元素。
(6)z-index:0:层叠级数为0 的定位元素。
(7)正z-index:z-index 属性值为正的定位元素。
注意: 当定位元素z-index:auto,生成盒在当前层叠上下文中的层
级为0,不会建立新的层叠上下文,除非是根元素。
11. 如何解决1px 问题?
1px 问题指的是:在一些Retina 屏幕的机型上,移动端页面的1px
会变得很粗,呈现出不止1px 的效果。原因很简单——CSS 中的1px
181
并不能和移动设备上的1px 划等号。它们之间的比例关系有一个专
门的属性来描述:
打开Chrome 浏览器,启动移动端调试模式,在控制台去输出这个
devicePixelRatio 的值。这里选中iPhone6/7/8 这系列的机型,输
出的结果就是2:
这就意味着设置的1px CSS 像素,在这个设备上实际会用2 个物理
像素单元来进行渲染,所以实际看到的一定会比1px 粗一些。
解决1px 问题的三种思路:
思路一:直接写0.5px
如果之前1px 的样式这样写:
可以先在JS 中拿到window.devicePixelRatio 的值,然后把这个
值通过JSX 或者模板语法给到CSS 的data 里,达到这样的效果
(这里用JSX 语法做示范):
然后就可以在CSS 中用属性选择器来命中devicePixelRatio 为某
一值的情况,比如说这里尝试命中devicePixelRatio 为2 的情况:
182
直接把1px 改成1/devicePixelRatio 后的值,这是目前为止最简
单的一种方法。这种方法的缺陷在于兼容性不行,IOS 系统需要8 及
以上的版本,安卓系统则直接不兼容。
思路二:伪元素先放大后缩小
这个方法的可行性会更高,兼容性也更好。唯一的缺点是代码会变多。
思路是先放大、后缩小:在目标元素的后面追加一个::after 伪元
素,让这个元素布局为absolute 之后、整个伸展开铺在目标元素上,
然后把它的宽和高都设置为目标元素的两倍,border 值设为1px。
接着借助CSS 动画特效中的放缩能力,把整个伪元素缩小为原来的
50%。此时,伪元素的宽高刚好可以和原有的目标元素对齐,而border
也缩小为了1px 的二分之一,间接地实现了0.5px 的效果。

https://www.yuque.com/cuggz

三、招聘网站

https://www.zhaopin.com/

https://www.51job.com/

https://www.zhipin.com/

https://www.lagou.com/

https://www.zh-hr.com/

https://www.liepin.com/

 

posted @ 2023-03-13 09:18  张果  阅读(571)  评论(0编辑  收藏  举报
AmazingCounters.com