前端面试经验(重排重绘+Promise异步操作)深度绝对够

提示:这是一次字节跳动前端二面的失败总结,希望能帮助小伙伴们理解一些知识点。

文章目录

  • 前言
  • 一、面试第一翻车点----重排和重绘
    • 1.我的回答
    • 2.接下来我自己挖的坑出来了
    • 3.总结
      • 3.1重排
      • 3.2重绘
      • 3.3如何避免重排和重绘
  • 二、面试第二翻车点----Promise
    • 1.为什么会翻车
    • 2.Promise讲解
      • 2.1 走进ajax
        • ajax的使用过程
      • 2.2 走进axios
      • 2.3 ajax与axios
      • 2.4 走进异步操作
      • 2.5 深入Promise
  • 三、总结


前言

本文将分享一下字节跳动公司前端二面(没过)经验,首先会问一些前端的学习路径以及简历中项目的相关模块,然后再会问一下JS,CSS的内容,最后会编程题,包括一个JS代码题和一个算法题。

一、面试第一翻车点----重排和重绘

当听到面试官问到这个问题,当时心情非常激动,觉得这么简单,但是之后却是麻烦不断。

1.我的回答

重绘:就是当界面中某个DOM的样式需要发生变化,例如字体颜色的改变等等,但是并没有影响整个界面的布局变化,我们只需要重新对这个颜色进行修改即可;
重排:就是当整个界面布局发生变化,例如尺寸等等,或者一些DOM元素的消失、新增等等,那么浏览器需要重新渲染,就会发生重排;
总结:就是发生重排一定会发生重绘,但是发生重绘不一定发生重排。

2.接下来我自己挖的坑出来了

面试官:按照你说的,那么重绘不一定重排,举个例子如果发生了重绘什么情况下会发生重排呢?
:emmmm,那既然重绘都发生了重排,那就是说其实这本质是一个重排了,也就是说要么这次改变是单独发生了重绘,要么这次改变发生的是重排+重绘。
面试官:那举个例子什么情况下重排,什么情况下重绘?
:就是元素的字体颜色,大小,这种简单变化就重绘,但是页面布局变化了,例如盒子大小变化了就重排,或者按钮的消失和新增也重排。
面试官:那要是按钮的消失,用CSS的属性消失的,而不是删除,那这是重排还是重绘?
:重绘叭。
面试官:那display的和visibility和opacity呢都是重排重绘?
:重排叭。(已经开始出错了)
面试官:那换你说的就是只要位置改变是不是就要重排,重新进行计算呢,那我的界面只有一个盒子,来回拖动呢,横着拖动,或者竖着拖动,无限任何距离呢,是重排重绘啊?
:(脑子炸了,心里想,我就是知道大概,具体没了解这么透彻,八股文真的不够啊,还得具体到情况)emmmmm,就是我觉得叭位置变了就是重排了,因为一般盒子拖拽我就用那个JS写的,每次都要计算嘛,所以位置一直改变,所以发生的都是重排。
面试官:如果不用JS呢?用动画呢?
:transform,那就是重绘?(哎,错误总是在无知的时候脱口而出)
面试官:你可以课下在了解一下。
:好的(心里一万句:我是xx,懂得都懂,省略了)。

3.总结

3.1重排

任何改变元素的位置和元素尺寸的大小都会发生重排,例如:

  • 添加或者删除可见的DOM元素
  • 浏览器窗口的尺寸的变化
  • 页面渲染初始化(必须发生的)
  • 元素位置的改变(GPU硬件加速的动画导致的改变除外,3.3中会讲解)
  • 一些元素尺寸的改变,例如内外边距,大小,边框等
  • 读取元素属性或者几何属性,例如offsetLeft,clientTop,width,height,display,table,flex等等
  • 填充内容使得文本的改变或者是图片大小的改变而引起计算宽度和高度的改变

3.2重绘

更新了元素的绘制属性,但是没有改变整个布局,而只是将元素的外观重新绘制,这就是重绘,例如:

  • 外观属性:opacity,visibility,background,box-shadow等等
  • 文字:font

3.3如何避免重排和重绘

  • 避免频繁使用style,尽量在css中写好
  • CSS3硬件加速(GPU加速)使用transform,opacity,filters等避免重绘
  • 使用visibility代替display:none
  • 避免频繁操作DOM节点
  • 使用防抖,节流技术控制频率

二、面试第二翻车点----Promise

1.为什么会翻车

面试官:了解过promise吗?
:(woc,终于问到了,太熟悉了)emmmm,了解过的
面试官:那说说你的项目中都哪里用到了promise,以及怎么用的?
:(??????思索了好久)emmmm,我没用过啊(不过有个axios用到了then()很多)
面试官:啊?那你的项目中怎么用的异步操作?
:我忘了,emmmm,但是我了解promise。
面试官:你了解啥?
:(woc,这么大的范围,你问我一句这么笼统的,我怎么答啊?)那个可以给点提示吗?
面试官:算了,我们下一个环节吧,来个代码题叭。
:(有苦说不出,这不凉了吗,还代码啥啊,开摆了)

2.Promise讲解

在讲解promise之前先总结一下之前的翻车对话,其实我当时就是不会读axios,不然我自信点说出来就有后续了,请大家跟我读:艾克斯伊欧姿,艾克斯伊欧姿,艾克斯伊欧姿!!!

2.1 走进ajax

ajax的全名是async JavaScript and XML(异步JavaScript和XML),起着串联前后台交互的作用,并且默认异步执行机制,同时也可以同步(async = false),但是默认异步(async = true),有以下优点:

  • 不需要插件,可以直接原生js使用
  • 用户体验好,不需要刷新界面就可以更新数据
  • 减轻服务端的负担
ajax的使用过程
  1. 创建一个ajax对象,即创建XHR(异步调用对象)
var xhr = new XMLHttpRequest();
  1. 创建http请求,有方法和URL
var xhr = new XMLHttpRequest();
xhr.open('get','./data.php');
//括号中的是方法(get/post,url,true)最后一项默认是true为异步可以不用写,如果是同步写成false
  1. 设置http的请求头部以及状态变化的函数
var xhr = new XMLHttpRequest();
xhr.open('get','./data.php');
//括号中的是方法(get/post,url,true)最后一项默认是true为异步可以不用写,如果是同步写成false
xhr.setRequestHeader();
  1. 发送http请求
var xhr = new XMLHttpRequest();
xhr.open('get','./data.php');
//括号中的是方法(get/post,url,true)最后一项默认是true为异步可以不用写,如果是同步写成false
xhr.setRequestHeader();
xhr.send();
//如果方法是get直接xhr.send(),参数为空,
//如果方法是post,需要带参数xhr.send(参数);
  1. 获取响应后请求的结果,即根据状态码返回结果
var xhr = new XMLHttpRequest();
xhr.open('get','./data.php');
//括号中的是方法(get/post,url,true)最后一项默认是true为异步可以不用写,如果是同步写成false
xhr.setRequestHeader();
xhr.send();
//如果方法是get直接xhr.send(),参数为空,
//如果方法是post,需要带参数xhr.send(参数);
xhr.onreadyStateChange = function(){
	if(xhr.readyState === 4 && xhr.status === 200){
		console.log(xhr.responseText);
	}
}
 readyState === 0 : 表示未初始化完成,也就是 open 方法还没有执行 
 readyState === 1 : 表示配置信息已经完成,也就是执行完 open 之后 
 readyState === 2 : 表示 send 方法已经执行完成
 readyState === 3 : 表示正在解析响应内容
 readyState === 4 : 表示响应内容已经解析完毕,可以在客户端使用了

2.2 走进axios

首先axios是一个基于promise的HTTP库,可以用于浏览器端和node.js端。本质上是对小黄人(XHR)的封装,但是却是Promise的实现版本,符合最新ES规范,有以下特点:

  • 从浏览器中创建XMLHttpRequests
  • 从node.js创建http请求
  • 支持Promise的API
  • 拦截请求和响应请求
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF

具体axios内容可以跳转到 Vue axios 详细介绍(核心使用、封装、个性化配置,破万字)进行学习。

2.3 ajax与axios

ajax不是不流行了,只是它存在回调地狱的问题,如果要连环发送请求,就得一直嵌套,而现在用的流行的axios库,其实也是ajax,只是用promise对它进行了封装,可以链式调用发送请求,所以现在项目普遍使用的是axios。

2.4 走进异步操作

最开始使用的是callback回调函数到Promise,再到后来的Generator,最后是async/await。

  • 最初的callback回调函数容易发生回调地狱,不能try catch捕获错误,也不能return
  • 后来的Promise解决了回调地狱的问题,通过不断地.then()返回Promise进行异步操作,但是缺点是不能取消Promise,错误需要回调函数进行捕获
  • Generator可以控制函数的执行
  • 最后的async/await其实就是Promise的语法糖

2.5 深入Promise

Promise有三种状态,分别是 pending 、 resolved 、rejected,其中pending可以转换为另外两种状态,如下:

const p1 = new Promise((resolve,rejecte)=>{
})
console.log('p1',p1);
//此时结果为Pending,默认的
const p2 = new Promise((resolve,reject)=>{
	resolve();
})
console.log('p2',p2);
//此时结果为fulfilled,也就是resovled
const p3 = new Promise((resolve,reject)=>{
	reject();
})
console.log('p3',p3);
//此时结果为rejected

Promise是一个构造函数,使用new关键字进行创建实例对象。Promise函数中自带executor执行器,executor执行器中有两个默认函数参数resolve,reject,其中then是Promise原型上的一个方法,实例在调用该方法的时候,可以通过原型来调用,即Promise.prototype.then(),好处是不需要每次实例化都创建一个then(),节省内存。

Promise的手撕代码,如下:

function Promise(executor){
	this.status = 'pending'
	this.value = null       //value就是resovel
	this.reason = null      //reason就是reject
	//定义两个队列数组,使得可以达到异步的操作
	this.onFulfilledArray = []   //定义成功的队列数组
	this.onRejectedArray = []    //定义失败的队列数组

	const resolve = value =>{
		if(value instanceof Promise){
			return value.then(resolve,reject)
		}
		setTimeout(()=>{
			if(this.status === 'pending'){
				this.value = value
				this.status = 'fulfilled'
				this.onFulfilledArray.foreach(func=>{
					func(value)
				})
			}
		})
	}

	const reject = reason =>{
		setTimeout(()=>{
			if(this.status === 'pending'){
				this.reason = reason
				this.status = rejected
				this.onRejectedArray.foreach(func =>{
					func(reason)
				})
			}
		})
	}

	executor(resolve,reject)
}

Promise.protptype.then = function(onfulfilled,onrejected){
	onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
	onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}

	if(this.status === 'fulfilled'){
		onfulfilled(this.value)
	}
	if(this.status === 'rejected'){
		onrejected(this.reason)
	}
	if(this.status === 'pending'){
		this.onFulfilledArray.push(onfulfilled)
		this.onRejectedArray.push(onrejected)
	}
}

Promise实例的基本代码结构,如下:

//ES6 箭头函数写法
let promise = new Promise((resolve,reject)=>{
    if(/判断条件/){
        resolve()//承诺实现
    }else{
		reject()//承诺实效
    }
})
promise.then(res=>{
		//处理承诺实现方法
},err=>{
        //处理承诺失效方法     
})

结论

  • Promise具有凝固性,即状态只能改变一次,然后就不能再改变了
  • Promise.then()的第二个参数可以抛出错误
  • Promise本身不是一个异步函数,在executor执行器中运行的代码是同步的,而其中的then()方法是异步的
console.log('步骤1');
new Promise((resolve,reject)=>{
    console.log('步骤2');
})
console.log('步骤3')

//执行结果
### 步骤1
### 步骤2
### 步骤3

console.log('步骤1');
new Promise((resolve,reject)=>{
    console.log('步骤2');
    resolve()
}).then(res=>{
    console.log('步骤3');
})
console.log('步骤4')

//执行结果
### 步骤1
### 步骤2
### 步骤4
### 步骤3

then()的第二个参数可以是抛出错误,当然catch()也可以抛出错误,其区别在于前者只能处理当前Promise异步失败的回调,而后者可以处理整个Promise链上发生的异步失败的回调,准确说就是整体错误。

Promise的链式调用
链式调用的最主要作用就是异步事件同步化,例如:

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve()
	},2000)
}).then(res=>{
	console.log('步骤1')
})

let promise2 = new Promise((resolve,reject)=>{
	resolve()
}).then(res=>{
	console.log('步骤2')
})

//执行结果
###步骤2
###步骤1

显然是异步事件,并不是按照顺序结构执行的,所以用链式调用即可达到效果,如下:

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve()
	},2000)
}).then(res=>{
	console.log('步骤1')
	return new Promise((resolve,reject)=>{
		resolve()
	})
}).then(res=>{
	console.log('步骤2')
})

//执行结果
###步骤1
###步骤2

Promise链式调用的规则

  • Promise对象若无返回值的时候,会默认返回一个新的Promise,并且不定义的话自动返回fulfilled
  • Promise对象若返回值不是一个Promise,那么将会自动转换成一个fulfilled的Promise对象
  • Promise对象可以手动返回一个新的Promise,其状态可以我们自己决定,通过三元表达式方式
  • Promise对象若抛出错误error,则返回一个reject的Promise对象,将其作为参数传递给下一个Promise链的then()的第二个参数或者是catch()方法
  • Promise的值可以发生穿透现象,即使中间的then()没有回调参数,那么下面的then()都可以接收该参数
  • Promise只能传递一个参数,如果传递多个的话,必须用数组或对象进行封装,否则多余参数都将返回undefined

Promise.all()和Promise.race()
Promise.all()一般用于将多个实例组装成一个实例,简单来说就是多个Promise,如果都是res的话(成功),那就返回一个数组,包括所有成功的值,但是当有rej(失败)的时候,则返回最先rej的值即可。

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve('The shy')
	},2000)
})

let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve('The shy')
	},2000)
})

Promise.all([promise1,promise2]).then(data =>{
	console.log(data)
})

//执行结果
["The shy","The shy"]

Promise.race()意思为赛跑的意思,一般和setTimeout连用,输出最快的那个结果,无论是res还是rej。

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve('The shy')
	},2000)
})

let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
		resolve('Rookie')
	},4000)
})

Promise.race([promise1,promise2]).then(data =>{
	console.log(data)
})

//2s后执行结果
The shy

Promise的应用场景

  • 解决地狱回调问题
  • 利用Promise状态改变一次之后将不可再改变的特性,用于避免操作重复的场景
  • 一个页面,有多个请求,我们需求所有的请求都返回数据后再一起处理渲染,使用Promise.all
  • 验证多个请求结果是否都是满足条件,使用Promise.all
  • 中间件功能使用,接口返回的数据量比较大,在一个then 里面处理 显得臃肿,多个渲染数据分别给个then,让其各司其职
  • 下个请求依赖上个请求的结果,使用Promise.then
  • 请求超时提示,使用Promise.race

Promise的缺点

  • Promise一旦生成就立刻执行,并且无法中途退出
  • Promise执行器内部的代码如果在resole或reject改变状态后出现报错,是无法通过then方法第二个参数和catch捕获到,必须通过内部回调或者用try catch的方式来抛出错误

三、总结

本文大致就讲这些吧,首先讲一下浏览器的重排和重绘的区别,以及一些优化方法,其次重点讲一下ajax和axios的使用,以及Promise的入门到熟练应用,本文更是对一次面试的总结,因为只有总结面试的不足,并且通过这种方式强迫自己挖深知识,最终才能成功。

感谢大家的关注,我将持续更新一些前端面试的常考和重难点内容,预祝大家都可以面试成功,拿下offer!!!

你可能感兴趣的:(面试,前端,职场和发展)