这篇文章中所列出的面试题可以供所有需要页面前端技术的职位使用。
本文中包括React和Vue两个框架的技术内容,以及一些框架之外的通用内容,可以根据所面试的职位需要选择使用。
专题系列文章:
Javascript 通用内容部分
什么是防抖?什么是节流?它们之间有哪些异同点?
防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
节流(throttle):事件在n秒内多次触发,只在第一次触发中执行函数。
防抖和节流本质上都是优化高频率执行代码的一种手段。如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖 和 节流 的方式来减少调用频率。
相同点:
- 都可以通过使用
setTimeout
实现。 - 目的都是,降低回调执行频率。节省计算资源。
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用
clearTimeout
和setTimeout
实现。 - 函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
- 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。
Promise
如何实现链式调用?
链式调用即.then().then().then().then()
的使用形式,可以在then()
中返回一个新的Promise
来实现。
var
、let
、const
之间都有哪些区别?
在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量。使用var
能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明。在函数中使用使用var
声明变量时候,该变量是局部的;而如果在函数内不使用var
,该变量是全局的。
let
是ES6新增的命令,用来声明变量。用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效,不存在变量提升。并且let
不允许在相同作用域中重复声明。
const
声明一个只读的常量,一旦声明,常量的值就不能改变。const
实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量。对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的,并不能确保改变量的结构不变。
它们之间的区别有:
var
声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
;let
和const
不存在变量提升,它们所声明的变量一定要在声明后使用,否则报错。var
不存在暂时性死区;let
和const
存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。var
不存在块级作用域;let
和const
存在块级作用域。var
允许重复声明变量;let
和const
在同一作用域不允许重复声明变量。var
和let
可以修改声明的变量;const声明一个只读的常量。一旦声明,常量的值就不能改变。
使用选择建议:
能用const
的情况尽量使用const
,其他情况下大多数使用let
,避免使用var
。
==
和 ===
区别,分别在什么情况使用?
==
(相等运算符):
相等运算符==
在比较相同类型的数据时,与严格运算符===
完全一样。
在比较不同类型的数据时,相等运算符==
会先将数据进行类型转换,然后再用严格相等运算符===
比较。类型转换规则如下:
- 原始类型的值
原始类型的数据会转换成数值类型再进行比较。字符串和布尔值都会转换成数值。
- 对象与原始类型值比较
对象(这里指广义的对象,包括数值和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。
undefined
和null
undefined
和null
与其他类型的值比较时,结果都为false
,它们互相比较时结果为true
。
===
(严格运算符):
- 不同类型值
如果两个值的类型不同,直接返回false
。
- 同一类的原始类型值
同一类型的原始类型的值(数值number、字符串string、布尔值boolean)比较时,值相同就返回true
,值不同就返回false
。
- 同一类的复合类型值/高级类型
两个复合类型(对象Object、数组Array、函数Funtion)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。即“地址指针”是否相等。
undefined
和null
与自身严格相等。
深拷贝浅拷贝的区别?
JavaScript中存在两大数据类型:基本类型和引用类型。基本类型数据保存在在栈内存中,引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,保存在栈中。
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
深拷贝则开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样,浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
说说你对闭包的理解?
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure),也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
在 JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
说说你对作用域链的理解
作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。作用域决定了代码区块中变量和其他资源的可见性。任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
JavaScript 遵循的是词法作用域。词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了。
当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
说说你对于原型链的理解
JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身。
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
说说你对内存泄露的理解
Javascript中如何实现继承?
继承(inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。
Javascript常见的继承方式:
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承
- 原型式继承
- 寄生式继承
谈谈你对this
对象的理解
函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。
this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
根据不同的使用场合,this有不同的值,主要分为下面几种情况:
- 默认绑定
- 隐式绑定
new
绑定- 显式绑定
在 ES6 的语法中还提供了箭头函数语法,可以在代码书写时就能确定 this 的指向(编译时绑定)。
Javascript中执行上下文和执行栈分别是什么?
执行上下文是一种对Javascript代码执行环境的抽象概念,也就是说只要有Javascript代码运行,那么它就一定是运行在执行上下文中。执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象。
- 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
- Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。当Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中。每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中。引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文。
typeof
和instanceof
的区别是什么?
typeof
操作符返回一个字符串,表示未经计算的操作数的类型。
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
什么是事件代理?
事件代理,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素。事件流的都会经过三个阶段: 捕获阶段 -> 目标阶段 -> 冒泡阶段,而事件委托就是在冒泡阶段完成。
事件委托会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素。当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
适合事件委托的事件有:click
,mousedown
,mouseup
,keydown
,keyup
,keypress
。
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
- 减少整个页面所需的内存,提升整体性能。
- 动态绑定,减少重复工作。
但是使用事件委托也是存在局限性:
focus
、blur
这些事件没有事件冒泡机制,所以无法进行委托绑定事件。mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
new
操作符都完成了哪些操作?
new
操作符用于创建一个给定构造函数的实例对象。在这个流程中,new关键字主要做了以下的工作:
- 创建一个新的对象obj。
- 将对象与构建函数通过原型链连接起来。
- 将构建函数中的this绑定到新建的对象obj上。
- 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。
bind
、call
、apply
之间有什么区别?
call
、apply
、bind
作用是改变函数执行时的上下文,可以改变函数运行时的this
指向。
apply
接受两个参数,第一个参数是this
的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次。
call
方法的第一个参数也是this
的指向,后面传入的是一个参数列表(apply
传入的是数组)。跟apply
一样,改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次。
bind
方法和call
很相似,第一参数也是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数。
如何理解Proxy?
用于定义基本操作的自定义行为。修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程。Proxy
主要用于创建一个对象的代理,从而实现基本操作的拦截和自定义。
如何理解Generator?
Generator
函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。执行 Generator
函数会返回一个遍历器对象,可以依次遍历 Generator
函数内部的每一个状态。
Promise有哪些使用场景?
Promise
是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大。主要可以使用在Ajax异步访问、异步加载资源等场景中。
什么是尾递归?有哪些应用场景?
递归在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。
尾递归在普通尾调用的基础上,多出了2个特征:
- 在尾部调用的是函数自身。
- 可通过优化,使得计算仅占用常量栈空间。
在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出。这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
什么是函数式编程?与命令式编程相比有哪些优缺点?
函数式编程是一种"编程范式"(programming paradigm),一种编写程序的方法论。目前主要的编程范式有三种:命令式编程,声明式编程和函数式编程。
相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程。函数式编程就是要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果。函数是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值。
优点
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况。
- 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响。
- 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性。
- 隐性好处。减少代码量,提高维护性。
缺点
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销。
- 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式。
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作。
函数式编程中都有哪些概念?
函数是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值。
函数组合,函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输入,那他们就可以进行组合。
纯函数是对给定的输入返还相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变
。
高级函数,就是以函数作为输入或者输出的函数被称为高阶函数,通过高阶函数抽象过程,注重结果。
柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程。
组合函数是将多个函数组合成一个函数。
什么是Javascript数字精度丢失问题?有什么解决方法?
浮点数是一种表示数字的标准,整数也可以用浮点数的格式来存储。在Javascript中,现在主流的数值类型是Number
,而Number
采用的是IEEE754规范中64位双精度浮点数编码。这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。
存储二进制时小数点的偏移量最大为52位,最多可以表达的位数是2^53=9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 Javascript 最多能表示的精度。
计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则符号位+(指数位+指数偏移量的二进制)+小数部分
存储二进制的科学记数法。因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差。
解决大数的问题你可以引用第三方库 bignumber.js
,原理是把所有数字当作字符串,重新实现了计算逻辑,缺点是性能比原生差很多。
如何实现函数缓存?函数缓存都有哪些应用场景?
函数缓存,就是将函数运算过的结果进行缓存,本质上就是用空间(缓存存储)换时间(计算过程)。常用于缓存数据计算结果和缓存对象。实现函数缓存主要依靠闭包、柯里化、高阶函数。
如何判断一个元素是否位于可视区域之内?
判断一个元素是否在可视区域,我们常用的有三种办法:
offsetTop
、scrollTop
,offsetTop
是元素的上外边框至包含元素的上内边框之间的像素距离,可以通过el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
来完成判断。getBoundingClientRect
,如果一个元素在视窗之内的话,那么它一定满足下面四个条件:top
大于等于 0;left
大于等于 0;bottom
小于等于视窗高度;right
小于等于视窗宽度。
Intersection Observer
。
如何完成大文件断点续传上传的功能?
断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分。每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。
一般实现方式有两种:
- 服务器端返回,告知从哪开始。
- 浏览器端自行处理。
上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可。如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可
什么是单点登录?一般如何实现?
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作。当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证。
如何防御常见的Web攻击?
常见的Web攻击方式有:
- XSS (Cross Site Scripting) 跨站脚本攻击。允许攻击者将恶意代码植入到提供给其它用户使用的页面中,攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
- CSRF(Cross-site request forgery)跨站请求伪造。攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求,利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
- SQL注入攻击。是通过将恶意的 Sql查询或添加语句插入到应用的输入参数中,再在后台 Sql服务器上解析执行进行的攻击。
CSS 部分
简单介绍一下盒模型
当对一个文档进行布局(layout)的时候,浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型(CSS basic box model),将所有元素表示为一个个矩形的盒子(box)
一个盒子由四个部分组成:content
、padding
、border
、margin
。
CSS中哪些属性是可以继承的?
在css中,继承是指的是给父元素设置一些属性,后代元素会自动拥有这些属性。关于继承属性,可以分成:
- 字体系列。
- 文本系列。
- 元素可见性。
- 表格布局属性。
- 列表属性。
- 引用。
- 光标属性。
em
、px
、rem
、vh/vw
这些单位之间有什么不同点?
em是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(1em = 16px
)。
px,表示像素,所谓像素就是呈现在我们显示器上的一个个小点,每个像素点都是大小等同的,所以像素为计量单位被分在了绝对长度单位中。
rem,相对单位,相对的只是HTML根元素font-size
的值。
vw ,就是根据窗口的宽度,分成100等份,100vw
就表示满宽,50vw
就表示一半宽。(vw
始终是针对窗口的宽),同理,vh则为窗口的高度。
设备像素、CSS像素、设备独立像素、dpr、ppi之间有哪些区别?
设备像素(device pixels),又称为物理像素,指设备能控制显示的最小物理单位,不一定是一个小正方形区块,也没有标准的宽高,只是用于显示丰富色彩的一个“点”而已。
CSS像素(css pixel, px), 适用于web编程,在 CSS 中以 px 为后缀,是一个长度单位。在 CSS 规范中,长度单位可以分为两类,绝对单位以及相对单位,px是一个相对单位,相对的是设备像素(device pixel),一般情况,页面缩放比为1,1个CSS像素等于1个设备独立像素。
设备独立像素(Device Independent Pixel),与设备无关的逻辑像素,代表可以通过程序控制使用的虚拟像素,是一个总体概念,包括了CSS像素。
dpr(device pixel ratio,设备像素比),代表设备独立像素到设备像素的转换关系,dpr = 设备像素 / 设备独立像素
。
ppi (pixel per inch,每英寸像素),表示每英寸所包含的像素点数目,更确切的说法应该是像素密度。数值越高,说明屏幕能以更高密度显示图像。
有哪些方式可以隐藏一个页面元素?
通过css实现隐藏元素方法有如下:
display: none
visibility: hidden
opacity: 0
- 设置
height
、width
模型属性为0 position: absolute
clip-path
让一个元素水平垂直居中的方法有哪些?如果元素的宽高是不确定的,又要如何做?
实现元素水平垂直居中的方式:
- 利用定位+margin:auto,可以适用于不确定宽高的情况。
- 利用定位+margin:负值
- 利用定位+transform,可以适用于不确定宽高的情况。
- table布局
- flex布局,可以适用于不确定宽高的情况。
- grid布局,可以适用于不确定宽高的情况。
说说Flex模型和它的应用场景
Flexible Box 简称 flex,意为”弹性布局”,可以简便、完整、响应式地实现各种页面布局。采用Flex布局的元素,称为flex容器container,它的所有子元素自动成为容器成员,称为flex项目item。容器中默认存在两条轴,主轴和交叉轴,呈90度关系。项目默认沿主轴排列,通过flex-direction来决定主轴的方向,每根轴都有起点和终点,这对于元素的对齐非常重要。
如何定义一个Grid布局?
Grid 布局即网格布局,是一个二维的布局方式,由纵横相交的两组网格线形成的框架性布局结构,能够同时处理行与列,擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。
设置display: grid/inline-grid
的元素就是网格布局容器,这样就能出发浏览器渲染引擎的网格布局算法。其中grid-template-columns
属性设置列宽,grid-template-rows
属性设置行高。
什么是回流和重绘?
在HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:
-
回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置。
-
重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制。
回流触发时机
回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:
- 添加或删除可见的DOM元素。
- 元素的位置发生变化。
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)。
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 页面一开始渲染的时候(这避免不了)。
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)。
重绘触发时机
触发回流一定会触发重绘。可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)。除此之外还有一些其他引起重绘行为:
- 颜色的修改。
- 文本方向的修改。
- 阴影的修改。
如何减少触发回流和重绘
- 如果想设定元素的样式,通过改变元素的
class
类名 (尽可能在 DOM 树的最里层)。 - 避免设置多项内联样式。
- 应用元素的动画,使用
position
属性的fixed
值或absolute
值(如前文示例所提)。 - 避免使用
table
布局,table
中每个元素的大小以及内容的改动,都会导致整个table
的重新计算。 - 对于那些复杂的动画,对其设置
position: fixed/absolute
,尽可能地使元素脱离文档流,从而减少对其他元素的影响。 - 使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘。 - 避免使用 CSS 的 Javascript 表达式。
什么是响应式设计?
响应式网站设计(Responsive Web design)是一种网络页面设计布局,页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整。描述响应式界面最著名的一句话就是“Content is like water”,即“如果将屏幕看作容器,那么内容就像水一样”
响应式网站常见特点:
- 同时适配PC + 平板 + 手机等。
- 标签导航在接近手持终端设备时改变为经典的抽屉式导航。
- 网站的布局会根据视口来调整模块的大小和位置。
响应式设计的基本原理是通过媒体查询检测不同的设备屏幕尺寸做处理,为了处理移动端,页面头部必须有meta
声明viewport
。实现响应式布局的方式有如下:
- 媒体查询
- 百分比
vw/vh
rem
常用的提升CSS性能的方法都有哪些?
- 内联首屏关键CSS。
- 异步加载CSS。
- 资源压缩。
- 合理使用选择器。
- 减少使用昂贵的属性。
- 不要使用@import。
如何使用CSS绘制一个三角形和一个圆形
|
|
border-radius
属性。
React 部分
Real DOM与Virtual DOM之间有什么区别?
Real DOM即真实 DOM,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实 DOM 结构。
Virtual Dom即虚拟 DOM,本质上是以 JavaScript 对象形式存在的对 DOM 的描述。创建虚拟 DOM 目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟 DOM 对象的节点与真实 DOM 的属性一一对应。
Real DOM和Virtual DOM的区别如下:
- 虚拟 DOM 不会进行排版与重绘操作,而真实 DOM 会频繁重排与重绘。
- 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”。
真实 DOM 的优势
- 易用。
真实 DOM 的缺点
- 效率低,解析速度慢,内存占用量过高。
- 性能差:频繁操作真实 DOM,易于导致重绘与回流。
虚拟 DOM 的优势
- 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难。
- 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能。
- 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行。
虚拟 DOM 的缺点
- 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢。
React组件之间如何通信?
父组件向子组件传递
由于React的数据流动为单向的,父组件向子组件传递是最常见的方式。父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过props属性就能接收父组件传递过来的参数。
子组件向父组件传递
子组件向父组件通信的基本思路是,父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值。
兄弟组件之间的通信
如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过使用父组件传递。
父组件向后代组件传递
父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样。使用context提供了组件之间通讯的一种方式,可以共享数据,其他组件都能读取对应的数据。
非关系组件传递
如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信。
组件上的key
有什么作用?
React 也存在 Diff 算法,而元素key
属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染。因此key
的值需要为每一个元素赋予一个确定的标识。key
属性在使用时的注意要点为:
key
应该是唯一的。key
不要使用随机值,随机数在下一次 render 时,会重新生成一个数字。- 使用
index
作为key
值,对性能没有优化。
什么是受控组件?什么是非受控组件?它们都有哪些应用场景?
受控组件,就是受我们控制的组件,组件的状态全程响应外部数据。受控组件一般需要初始状态和一个状态更新事件函数。
非受控组件,就是不受我们控制的组件,一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态。当需要时,可以使用 ref
查询 DOM
并查找其当前值。
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理。如果选择非受控组件的话,控制能力较弱,表单数据就由DOM本身处理,但更加方便快捷,代码量少。
如何理解高阶组件?它有哪些应用场景?
高阶函数(Higher-order function),至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入;
- 输出一个函数。
在React中,高阶组件即接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数,并不是一个组件。高阶组件的实现方式,本质上是一个装饰者设计模式。
高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等。
React Hook的出现解决了哪些问题?
Hook
是 React 16.8 的新增特性。它可以让你在不编写 class
的情况下使用 state
以及其他的 React 特性
至于为什么引入hook
,官方给出的动机是解决长时间使用和维护react过程中常遇到的问题,例如:
- 难以重用和共享组件中的与状态相关的逻辑;
- 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面;
- 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题;
- 由于业务变动,函数组件不得不改为类组件等等。
在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作。因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理。
hooks
能够更容易解决状态相关的重用的问题:
- 每调用
useHook
一次都会生成一份独立的状态; - 通过自定义
hook
能够更好的封装我们的功能。
hooks
是一种函数式编程方式,每个功能都包裹在函数中,整体风格更清爽,更优雅。hooks
的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks
能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks
。
React Router v6中提供的Router都有哪些?
React Router v6 中提供了五种 Router,分别是:
BrowserRouter
,这是一个基于history
的 Router。MemoryRouter
,这个 Router 会将路由记录放入内存,主要用于调试使用。HashRouter
,这个 Router 会使用#
引导的路由拼合在 URL 中。这种 Router 拥有比较好的兼容性。NativeRouter
,这个 Router 主要用于支持 React Native 的开发。StaticRouter
,这个 Router 用于支持在 NodeJS 中 React APP 的渲染。
React Router v6中常用的组件有哪些?
React Router v6 中常用的组件主要有以下这些:
BrowserRouter
,浏览器路由控制组件。HashRouter
,基于Hash的URL路由控制组件。Route
,路由节点组件。Link
,路由跳转链接组件。NavLink
,支持展示当前路由激活状态的路由跳转链接组件。Outlet
,子路由组件的渲染插入点组件。Routes
,路由节点定义组件。Await
,用于支持组件异步加载、渲染的组件。Form
,用于调用 Router 中action
方法的组件。
如何理解React Router v6中引入的loader
和action
?
loader
函数主要用于在渲染路由节点之前异步加载数据。在路由节点组件里可以使用 useLoaderData
Hook来获取通过 loader
加载的数据。
action
则是用于处理路由节点组件中的 Form
提交的数据。相对于 loader
的异步数据读取,action
则主要是用于写入数据。
如何在使用React Router v6时捕获其中出现的错误?
在 React Router v6 中捕获组件中抛出的错误可以通过定义一个错误捕获组件,并在路由节点定义中的 errorElement
属性中引用使用。
在错误捕获组件中可以使用 useRouteError
Hook来获取其所捕获的具体错误。
如何理解immutable?在项目中使用immutable有哪些好处?
Immutable即不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据,对 Immutable对象的任何修改或添加删除操作都会返回一个新的 Immutable对象。
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构):
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费。
也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享)。如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
Immutable 对象应用在项目当中时,可以使项目无需使用深比较即可确定一个对象的最新状态,可以在一定程度上提高项目的性能。
如何提高组件的渲染效率?如何避免不必要的渲染?
React 基于虚拟 DOM 和高效 Diff算法的完美配合,实现了对 DOM最小粒度的更新,大多数情况下,React对 DOM的渲染效率足以我们的业务日常。复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,避免不必要的渲染则是业务中常见的优化手段之一。
组件渲染的时机是组件中的状态发生了变化,这种状态的变化就会导致渲染。如果一个组件树中父组件出发了重新渲染,但是其子组件缺没有发生任何改变,那么就可以避免无谓的渲染。所以在 React 页面设计的过程中,建议将页面进行更小的微粒化。当组件的颗粒变小以后,当某一个状态发生变化的时候,就会有很多组件免于不必要的渲染。
【精通级别】简述 React 中的 Diff 算法原理
React 通过引入 Virtual DOM 的概念,极大地避免无效的Dom操作,使我们的页面的构建效率提到了极大的提升。而 diff 算法就是更高效地通过对比新旧 Virtual DOM 来找出真正的Dom变化之处。
传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 $O(n^3)$,React将算法进行一个优化,复杂度降为 $O(n)$。
React 中 diff 算法主要遵循三个层级的策略:
- Tree层级
- Conponent 层级
- Element 层级
Tree层级
DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较,只有删除、创建操作,没有移动操作。新树中,R节点下没有了子节点A,那么直接删除子节点A,在D节点下创建子节点A以及下属节点。
Component层级
如果是同一个类的组件,则会继续往下 diff 运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的。当 component D
换成了 component G
后,即使两者的结构非常类似,也会将D
删除再重新创建G
。
Element层级
对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识。 diff 算法提供了 3 种节点操作,分别为 INSERT MARKUP
(插入)、MOVE EXISTING
(移动)和 REMOVE NODE
(删除)。通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。
【精通级别】谈谈你对Fiber架构的理解?Fiber架构解决了什么问题?
JavaScript引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待。如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿,而这也正是 React 15 的 Stack Reconciler所面临的问题。
当 React在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断。如果组件较大,那么js线程会一直执行,然后等到整棵VDOM树计算完成后,才会交给渲染的线程。这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况。
React Fiber 是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化,是对 React 核心算法的一次重新实现。从Facebook在 React Conf 2017 会议上确认,React Fiber 在React 16 版本发布。
在React中,主要做了以下的操作:
- 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务;
- 增加了异步任务,调用
requestIdleCallback
api,浏览器空闲的时候执行; - Dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行;
从架构角度来看,Fiber 是对 React 核心算法(即调和过程)的重写。从编码角度来看,Fiber 是 React 内部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的虚拟DOM。
一个 Fiber 就是一个 JavaScript 对象,包含了元素的信息、该元素的更新操作队列、类型。Fiber 把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行,即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber节点
实现的上述方式的是requestIdleCallback
方法。window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件。
简要叙述一下React JSX转换成Real DOM的过程
React 通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些“变化”更新到屏幕上。
React 将 JSX 渲染为 Real DOM的过程如下:
- 使用
React.createElement
或JSX
编写 React 组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...)
,Babel帮助我们完成了这个转换的过程。 createElement
函数对key
和ref
等特殊的props
进行处理,并获取defaultProps
对默认props
进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象。ReactDOM.render
将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM。
优化React性能的手段都有哪些?
React凭借virtual DOM和diff算法拥有高效的性能,但是某些情况下,性能明显可以进一步提高。
除避免不必要的渲染的优化手段之外, 常见性能优化常见的手段有如下:
- 避免使用内联函数。如果我们使用内联函数,则每次渲染时都会创建一个新的函数实例。
- 使用 React Fragments 避免额外标记。
- 使用 Immutable。Immutable 可以显著的减少渲染次数。
- 使用懒加载组件。
- 优化事件绑定方式。
- 使用服务端渲染。
React项目中如何捕获错误?
React 16 引用了错误边界的概念。
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 Javascript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
Vue 2.x 部分
什么是SPA?与MPA有哪些区别?
SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
组件的生命周期触发顺序是什么?
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初 |
created | 组件实例已经完全创建 |
beforeMount | 组件挂载之前 |
mounted | 组件挂载到实例上去之后 |
beforeUpdate | 组件数据发生变化,更新之前 |
updated | 组件数据更新之后 |
beforeDestroy | 组件实例销毁之前 |
destroyed | 组件实例销毁之后 |
activated | keep-alive 缓存的组件激活时 |
deactivated | keep-alive 缓存的组件停用时调用 |
errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
父组件如何调用子组件的方法、状态,这样做的弊端是什么?
在模板中的组件标签上使用refs属性,然后在逻辑层中使用组件实例的 $refs
属性读取此组件实例中的方法和状态,这样做的弊端主要是会造成组件内部状态会被外部更改,当组件内的方法使用时可能会产生无法预知的错误,且会造成组件间的强耦合,不利于维护。
为什么组件里data
字段需要使用function
进行返回?
Vue 实例的时候定义 data
属性既可以是一个对象,也可以是一个函数。当定义好一个组件的时候, Vue 最终都会通过Vue.extend()
构成组件实例。
- 根实例对象
data
可以是对象也可以是函数(根实例是单例),不会产生数据污染情况。 - 组件实例对象
data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。采用函数的形式,initData
时会将其作为工厂函数都会返回全新data
对象。
如何提升首屏加载速度?
首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。首屏加载可以说是用户体验中最重要的环节。
首屏时间可以通过 DOMContentLoad
或者 performance
来计算出首屏时间。
在页面渲染的过程中导致加载速度慢的因素可能有以下原因:
- 网络延时问题;
- 资源文件体积是否过大;
- 资源是否重复发送请求去加载了;
- 加载脚本的时候,渲染内容堵塞了。
常见的几种首屏优化方式:
- 减小入口文件积;
- 静态资源本地缓存;
- UI框架按需加载;
- 图片资源的压缩;
- 组件重复打包;
- 开启GZip压缩;
- 使用SSR。
Vue组件间的通讯方式都有哪些?
组件是 Vue 最强大的功能之一,Vue 中每一个.vue
我们都可以视之为一个组件。通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信,即指组件(.vue
)通过某种方式来传递信息以达到某个目的。
组件间通信的分类可以分成以下几种:
- 父子组件之间的通信;
- 兄弟组件之间的通信;
- 祖孙与后代组件之间的通信;
- 非关系组件间之间的通信。
以下八种通讯方案都是 Vue 中比较常用的:
- 通过
props
传递; - 通过
$emit
触发自定义事件; - 使用
ref
; - EventBus;
$parent
或$root
;attrs
与listeners
;- Provide 与 Inject;
- Vuex。
谈谈你对双向绑定的理解
单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了。
而双向绑定由三个重要部分构成
- 数据层(Model):应用的数据及业务逻辑。
- 视图层(View):应用的展示效果,各类UI组件。
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来。
根据这个分层的架构方案,可以用一个专业术语进行称呼:MVVM,这里的控制层的核心功能便是 “数据双向绑定” 。它的主要职责就是:
- 数据变化后更新视图;
- 视图变化后更新数据。
当然,它还有两个主要部分组成
- 监听器(Observer):对所有数据的属性进行监听;
- 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数。
为什么v-if
和v-for
不建议一起使用?
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true
值的时候被渲染
v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组或者对象,而 item
则是被迭代的数组元素的别名。
在 Vue 中,v-for
优先级是比 v-if
高,在进行if
判断的时候,v-for
是比 v-if
先进行判断。当同时用在同一个元素上,带来性能方面的浪费,每次渲染都会先循环再进行条件判断。为了避免出现这种情况,则在外层嵌套 template
(页面渲染不生成dom节点),在这一层进行 v-if
判断,然后在内部进行 v-for
循环。
如何理解v-show
和v-if
之间的区别?
Vue 中 v-show
与 v-if
的作用效果是相同的(不考虑 v-else
),都能控制元素在页面是否显示,其用法也是相同的。
v-show
与 v-if
之间的不同点主要在于:
- 控制手段不同:
v-show
是通过操作 CSS 来控制元素的展示的,v-if
则是通过对 DOM 树的操作。 - 编译过程不同:
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于 CSS 切换。 - 编译条件不同:
v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染。v-show
不会触发组件的生命周期。
在性能消耗方面 v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗。
slot有哪些使用场景?
在HTML中 slot
元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符。该占位符可以在后期使用自己的标记语言填充。与之对应的 template
元素不会展示到页面中,需要用先获取它的引用,然后添加到DOM中。
在 Vue 中 slot
在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填充(替换组件模板中 slot
位置),作为承载分发内容的出口。
通过 slot
可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理。如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事。通过 slot
插槽向组件内部指定位置传递内容,就可以完成这个复用组件在不同场景的应用。
如何缓存当前的组件?已经缓存的组件怎么更新?
keep-alive
是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive
可以设置以下props属性:
include
:字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
:字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
: 数字。最多可以缓存多少组件实例。
已经缓存的组件可以使用 beforeRouterEnter
和 activated
两个钩子来更新其内部的数据。
如何自定义指令?
在 Vue 中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系统。所有 v-
开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,除了核心功能默认内置的指令 (v-model
和 v-show
),Vue 也允许注册自定义指令。
自定义指令分为全局注册与局部注册。全局注册主要是通过 Vue.directive
方法进行注册。Vue.directive
第一个参数是指令的名字(不需要 v-
前缀),第二个参数可以是对象数据,也可以是一个指令函数。
自定义指令也像组件那样存在钩子函数:
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的VNode
更新时调用,但是可能发生在其子VNode
更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。componentUpdated
:指令所在组件的VNode
及其子VNode
全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
简要说明一下 Vue 的 diff 算法
diff 算法是一种通过同层的树节点进行比较的高效算法,其整体策略为:深度优先,同层比较。有两个特点:
- 比较只会在同层级进行, 不会跨层级比较;
- 在diff比较的过程中,循环从两边向中间比较。
diff 算法在很多场景下都有应用,在 Vue 中,作用于虚拟 DOM 渲染成真实 DOM 的新旧 VNode
节点比较。
如何处理Vue项目中出现的错误?
在Vue 中定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理。
主要的错误来源包括:
- 后端接口错误,这种错误可以通过处理网络请求的
Response
先完成一层拦截。 - 代码中本身逻辑错误,这种错误主要通过设置全局错误处理函数来处理。在 Vue 2.5.0 之后可以通过新引入的
errorCaptured
钩子来捕获子孙组件中产生的错误。s
【精通级别】Vue 的依赖收集是如何实现的?
1.响应式数据:Vue的响应式系统通过将普通的Javascript对象转化为响应式对象,使得对象的属性能够被追踪和监视。这通过使用Object.defineProperty
来实现,将属性转化为getter/setter,在访问和修改属性时触发依赖收集和更新。
2.依赖收集器:Vue使用依赖收集器来管理和存储组件与响应式数据之间的依赖关系。依赖收集器负责收集依赖、触发更新和通知相关的组件进行更新。它通过Dep类来实现,每个响应式对象都对应一个Dep实例,Dep实例中保存了依赖于该响应式对象的所有观察者(Watchers)。
3.观察者模式:观察者模式是依赖收集的核心机制。Vue中的观察者(Watcher)负责建立组件与响应式数据之间的依赖关系,并在依赖发生变化时触发相应的更新。每个组件实例都对应一个或多个观察者,它们通过访问响应式数据的属性来建立依赖关系。
4.自动更新:通过依赖收集器和观察者模式,Vue能够自动追踪依赖关系,并在依赖发生变化时自动更新相关的组件。当响应式数据的属性被访问时,观察者会收集这个属性的依赖关系,并将其与当前的组件实例关联起来。当属性发生变化时,观察者会通知相关的组件进行更新。
【精通级别】Vue 2.x 中如何实现 Portal ?
在 Vue 中有两种方式来实现这种效果,一种是使用指令,操作真实 dom,使用熟知的 dom 操作方法将指令所在的元素 append
到另外一个 dom 节点上去。另一种方式就是定义一套组件,将组件内的 vnode
转移到另外一个组件中去,然后各自渲染。
Vue 3.x 部分
Vue 3.x的设计目标是什么?都做了哪些优化?
Vue 3.x 的设计目标简而言之就是更小更快更友好。
- 更小,Vue 3 移除了一些不常用的 API,并且引入了 TreeShaking 可以将无用的模块剪除。
- 更快主要体现在编译方面:
- 优化了 diff 算法;
- 静态提升;
- 事件监听缓存;
- SSR 优化。
- 更友好体现在兼顾 Vue 2 的 Options API 的同时引入了 Composition API,增强了代码的逻辑阻止和代码复用能力。
为什么使用Proxy API替代defineProperty API?
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。通过 defineProperty
上的两个属性 get
和 set
可以实现响应式设计。但是这种设计存在以下缺陷:
- 检测不到对象属性的添加和删除;
- 数组API方法无法监听到;
- 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,会造成性能问题。
Proxy
的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。通过 Proxy
的劫持,可以直接返回一个新对象,于是就可以通过操作新对象达到实现响应式的目的。
Composition API与Vue 2.x的Options API有哪些不同?
Options API,即以vue为后缀的文件,通过定义 methods
,computed
,watch
,data
等属性与方法,共同处理页面逻辑。当组件变得复杂,就会导致对应属性的列表也会增长,导致组件难以阅读和理解。
Compositon API 将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。这样就在逻辑组织和逻辑复用两个方面做了明显的提升。相比 Options API,Composition API主要有以下特点:
- 在逻辑组织和逻辑复用方面,Composition API是优于Options API。
- 因为Composition API几乎是函数,会有更好的类型推断。
- Composition API对 Tree-Shaking 友好,代码也更容易压缩。
- Composition API中见不到
this
的使用,减少了this
指向不明的情况。 - 如果是小型组件,可以继续使用 Options API,也是十分友好的。
【熟练级别】简述一下Tree-shaking
Tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。
在 Vue2 中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。而 Vue3 源码引入 Tree-shaking 特性,将全局 API 进行分块。如果不使用其某些功能,它们将不会包含在基础包中。
Tree-shaking 是基于ES6模板语法(import
与exports
),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。Tree-shaking 主要是完成了以下两件事:
- 编译阶段利用ES6 Module判断哪些模块已经加载;
- 判断那些模块和变量未被使用或者引用,进而删除对应代码。