本文出现的问题都是在8月阶段实际面试过程中遇到的,大小公司都有涉及,希望能够助力大家金九银十,也希望自己能够顺利找到工作。现在外面大环境的确太难了……
文中部分题目提供了参考资料以及自己整理的相关答案,可能不一定是最准确的,大家酌情参考。
如涉及到其他文章内容引用没有标明出处的,可以私聊补充更新。
Javascript
1.JS基本数据类型有哪些
基本数据类型:
- 字符串(String):表示文本数据,使用引号(单引号或双引号)括起来。例如:"Hello World"。
- 数字(Number):表示数值数据,包括整数和浮点数。例如:10、3.14。
- 布尔值(Boolean):表示逻辑值,只有两个值:true(真)和false(假)。
- null:表示空值,表示一个空对象指针。
- undefined:表示未定义的值,表示变量未被赋值。
- Symbol:表示唯一的标识符,用于对象属性的键。
【扩展】说一下undefined和null的区别
当谈到 JavaScript 中的 undefined 和 null 时,它们是两个不同的值,但都表示缺少值。它们之间的区别如下:
- undefined 是表示变量声明但未赋值的默认值。当尝试访问未初始化的变量时,它的值为 undefined。全局属性 undefined 表示原始值undefined。它是一个 JavaScript 的 原始数据类型 。undefined是全局对象的一个属性。也就是说,它是全局作用域的一个变量。undefined的最初值就是原始数据类型undefined。
- 值 null 特指对象的值未设置。它是 JavaScript 基本类型 之一,在布尔运算中被认为是falsy。它是一个特殊的关键字,表示变量不指向任何对象、数组或函数。值 null 是一个字面量,不像 undefined,它不是全局对象的一个属性。null 是表示缺少的标识,指示变量未指向任何对象。把 null 作为尚未创建的对象,也许更好理解。在 API 中,null 常在返回类型应是一个对象,但没有关联的值的地方使用。
2.const、let和var的区别
【参考资料】
3.call、apply和bind的区别
- 三者都可以改变函数的this对象指向。
- 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
- 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
- bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。
【参考】
5分钟带你搞懂 Javascript 中的this(包含apply、call、bind)
4.对闭包的理解
概念
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
重点的一句:闭包让你可以在一个内层函数中访问到其外层函数的作用域。
使用场景
- return一个内部函数,读取内部函数的变量;
- 函数作为参数
- IIFE(自执行函数)
- 封装对象的私有对象和私有方法;
5.new 一个构造函数的过程
用new操作符创建对象时发生的事情:
- 创建新对象
- 新对象原型[[prototype]] = 构造函数prototype
- this 指向新对象
- 执行构造函数
- 如果构造函数返回非空对象,就返回这个对象引用,不然返回创建的新对象
函数调用时前面不加new就按普通函数来 执行。加new后对其进行了相应的变动, 按构造函数来执行。
new的具体过程如下:
//例子:
function Person(name,age) {
this.userName = name;
this.userAge = age;
}
var personl = new Person('LULU',20)
1、创建一个新的空对象。(即实例对象)
obj = {}
2 、设 置 原 型 链
将新对象obj的 __proto__属性指向构造函数的prototype对象。(即所有实例对象通过__proto__可以访问原型对象。构造函数的原型被其所有实例对象共享。)
obj.__proto__= Person.prototype
3 、将构造函数的this改指向新对象obj并执行函数代码。
(即给实例对象设置属性 userName, userAge并赋值。)
var result = Person.apply(obj,['LULU',20])
4 、如果构造函数中没有人为返回一个对象类型的值,则返回这个新对象obj。否则直接返回那个对象类型值。(一般定义的构造函数中不写返回值。)
if (typeof(result) == 'object') {
return result;
}else{
return obj;
}
【扩展】手动实现一个new操作,参数为构造函数 及其传参
//构造函数
function Person(name,age) {
this.userName = name;
this.userAge = age;
}
Person.prototype.sayAge = function(){
console.Iog(this.userAge)
}
// new操作函数newFun
function newFun() {
var obj = {};
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
return typeof ret === 'object' ? ret: obj;
};
//调用 newFun
var s1 = newFun(Person, 'xiaoyun',20);
console.log(s1.userName) //xiaoyun
console.log(s1.sayAge()) //20
备注:[].shift.call(arguments):删除并返回参数列表arguments中第一个参数,即获得构造函数。arguments剩余参数为构数传参。arguments是类数组,没有数组方法shift,可更改shift方法的this指向从而运用到 arguments 上。
6.ES6高阶函数有哪些
ES6中的高阶函数有以下几种:
- map:对数组中的每个元素进行操作,并返回一个新的数组。
- filter:根据指定的条件过滤出数组中的元素,并返回一个新的数组。
- reduce:对数组中的元素进行累加或累积操作,并返回一个结果。
- forEach:对数组中的每个元素进行操作,没有返回值。
- some:判断数组中是否存在满足指定条件的元素,返回布尔值。
- every:判断数组中的所有元素是否都满足指定条件,返回布尔值。
- find:查找数组中满足指定条件的第一个元素,并返回该元素。
- findIndex:查找数组中满足指定条件的第一个元素的索引,并返回该索引。
- sort:对数组中的元素进行排序。
- flat:将多维数组转换为一维数组。
- map、filter、reduce等方法的变种,如flatMap、find、findIndex等。
这些高阶函数可以使代码更简洁、可读性更高,并且提供了一种函数式编程的方式来操作数组。
Typescript
1.TS对比JS的优势有哪些
CSS
1.说下什么是BFC
BFC到底是什么东西
BFC 全称:Block Formatting Context, 名为 "块级格式化上下文"。
W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性
怎样触发BFC
这里简单列举几个触发BFC使用的CSS属性
- overflow: hidden
- display: inline-block
- position: absolute
- position: fixed
- display: table-cell
- display: flex
【参考】
Vue
1.Vue2、3的模版编译渲染过程有哪些区别
2.虚拟Dom的优势是什么?性能一定更好吗?
虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-Native 和 Weex)
实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在Javascript对象中,虚拟DOM 表现为一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别
创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应
优点:
保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限; 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
【参考资料】
面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
3.Vue2和Vue3的响应式区别
- Vue2中响应式是通过defineProperty实现的
- Vue3中响应式是通过ES6的Proxy实现的
- Vue3中实现响应式数据的方法是ref和reactive
【扩展内容】 盘点 Vue3 与 Vue2 的区别
4.ref和reactive的区别
reactive特点
- reactive的参数一般是对象或者数组,他能够将复杂数据类型变为响应式数据。
- reactive的响应式是深层次的,底层本质是将传入的数据转换为Proxy对象
ref特点
- ref的参数一般是基本数据类型,也可以是对象类型
- 如果参数是对象类型,其实底层的本质还是reactive,系统会自动将ref转换为reactive,例如 ref(1) ===> reactive({value:1})
- 在模板中访问ref中的数据,系统会自动帮我们添加.value,在JS中访问ref中的数据,需要手动添加.value
- ref的底层原理同reactive一样,都是Proxy
5.Vue的双向数据绑定/响应式原理
【参考资料】
6.Vue2为什么不能监控到数组/对象的属性变更
首先Object.defineProperty是可以监听部分数组的更新动作的,但是由于它是通过遍历的方式来实现,当数组数据量小的时候没有问题,但是当数据量大的时候,处理上会出现明显的性能问题。也是出于“性能代价和获得的用户体验收益不成正比”的原因,所以Vue2没有去实现监听。Vue3的Proxy的劫持方式完美解决了这个问题。
7.说下Vue中的Slot,使用场景
使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
分类
slot可以分来以下三种:
- 默认插槽
具名插槽
- 子组件用name属性来表示插槽的名字,不传为默认插槽
作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上。父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用
<!--子组件--> <template> <slot name="footer" testProps="子组件的值"> <h3>没传footer插槽</h3> </slot> </template> <!--父组件--> <child> <!-- 把v-slot的值指定为作⽤域上下⽂对象 --> <template v-slot:default="slotProps"> 来⾃⼦组件数据:{{slotProps.testProps}} </template> <template #default="slotProps"> 来⾃⼦组件数据:{{slotProps.testProps}} </template> </child>
小结:
- v-slot属性只能在
<template>
上使用,但在只有默认插槽时可以在组件标签上使用 默认插槽名为default,可以省略default直接写v-slot
- 缩写为#时不能不写参数,写成#default
- 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"
- v-slot属性只能在
【参考资料】
8.为什么data 返回一个函数
- vue中组件是用来复用的,为了防止data复用,将其定义为函数。
- vue组件中的data数据都应该是相互隔离,互不影响的,组件每复用一次,data数据就应该被复制一次,之后,当某一处复用的地方组件内data数据被改变时,其他复用地方组件的data数据不受影响,就需要通过data函数返回一个对象作为组件的状态。
- 当我们将组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,拥有自己的作用域,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
- 当我们组件的date单纯的写成对象形式,这些实例用的是同一个构造函数,由于JavaScript的特性所导致,所有的组件实例共用了一个data,就会造成一个变了全都会变的结果。
9.JSX有了解吗,和template的区别;
10.vue里面针对错误的生命周期函数使用
11.说下Vuex or Pinia的使用
12.Vue.set的原理
Vue.set是Vue.js提供的一个全局方法,用于在响应式对象上设置响应式属性。它的原理是通过调用Vue实例的内部方法defineReactive来为对象设置新属性,并使其成为响应式属性。
具体原理如下:
- Vue.set方法接受三个参数:对象、属性名和属性值。
- 首先,它会判断对象是否是响应式的,如果不是,则直接返回。
- 然后,它会判断属性是否已经存在于对象中,如果存在,则直接更新属性的值。
- 如果属性不存在于对象中,它会通过defineReactive方法将属性设置为响应式属性。
- defineReactive方法会为属性创建一个Dep实例对象,用于收集依赖和触发更新。
- 然后,它会使用Object.defineProperty方法将属性设置为响应式,并在getter和setter中触发依赖收集和更新。
- 最后,它会触发属性的更新,通知相关的Watcher进行视图的更新。
React
1.React中setState什么时候是同步,什么时候是异步
在React中,setState有时是同步的,有时是异步的。
当在React事件处理函数(如onClick)或生命周期方法(如componentDidMount)中调用setState时,setState是异步的。这是因为React会对连续的setState调用进行批处理,以提高性能。
例如,在以下代码中,setState是异步的:
class MyComponent extends React.Component {
handleClick() {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新前的值
}
render() {
return (
<button onClick={this.handleClick.bind(this)}>Click me</button>
);
}
}
但是,当在setTimeout、Promise、原生事件监听器等异步场景中调用setState时,setState是同步的。
例如,在以下代码中,setState是同步的:
class MyComponent extends React.Component {
componentDidMount() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新后的值
}, 1000);
}
render() {
return (
<div>{this.state.count}</div>
);
}
}
网络/浏览器
1.说下Http1.0和Http2.0的区别
HTTP 超文本传输协议是位于 TCP/IP 体系结构中的应用层协议,它是万维网数据通信的基础。
HTTP1.1 的缺陷
- 高延迟 — 队头阻塞(Head-Of-Line Blocking)
- 无状态特性 — 阻碍交互
- 明文传输 — 不安全性
- 不支持服务端推送
http2是HTTP协议的的第二个主要版本,使用于万维网。HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,主要基于SPDY协议。和HTTP1的主要区别有:
- 1、http2采用二进制格式而非文本格式。
- 2、http2是完全多路复用的,而非有序并阻塞的。
- 3、http2采用了报头压缩,降低了成本。
- 4、http2让服务器可以主动将响应推送到客户端缓存中
HTTP2 的缺陷
- TCP 以及 TCP+TLS 建立连接的延时
- TCP 的队头阻塞并没有彻底解决
- 多路复用导致服务器压力上升
- 多路复用容易 Timeout
HTTP3是一个基于 UDP 协议的 QUIC 协议,它真正“完美”地解决了“队头阻塞”问题。
HTTP3的主要特点
- 改进的拥塞控制、可靠传输
- 快速握手
- 集成了 TLS 1.3 加密
- 多路复用
- 连接迁移
【参考资料】:
2.说下HTML渲染过程
- 构建 DOM树
<!---->
- 将 HTML 解析成许多 Tokens
- 将 Tokens 解析成 Objects
- 将 Objects 组合成为一个 DOM 树
- 构建 CSSOM - CSS对象模型
<!---->
- 解析 CSS 文件,并构建出一个 CSSOM 树(过程类似于 DOM 构建)
- 构建 Render树
<!---->
- 结合 DOM 和 CSSOM 构建出一颗 Render 树
构建过程遵循以下步骤:
- 浏览器从 DOM 树开始,遍历每一个“可见”节点
- 对于每一个"可见"节点,在 CSSOM 上找到匹配的样式并应用
- 生成 Render Tree
- Layout 布局
<!---->
- 计算出元素相对于 viewport 的相对位置
<!---->
- Paint 渲染
<!---->
- 将 Render 树转换成像素,显示在屏幕上
完整流程:
【扩展】JS文件引入对CRP的影响
- 解析 HTML 构建 DOM 时,遇到 JavaScript 会被阻塞;
- JavaScript 执行会被 CSSOM 构建阻塞,也就是说,JavaScript 必须等到 CSS 解析完成后才会执行(这只针对在头部放置 <style> 和 <link> 的情况,如果放在尾部,浏览器刚开始会使用 User Agent Style 构建 CSSOM)
- 如果使用异步脚本,脚本的网络请求优先级降低,且网络请求期间不阻塞 DOM 构建,直到请求完成才开始执行脚本
使用异步脚本,其实就是告诉浏览器几件事:
- 无需阻塞 DOM,在对 Script 执行网络请求期间可以继续构建 DOM,直到拿到 Script 之后再去执行
- 将该 Script 的网络请求优先级降低,延长响应时间,确保执行它之前 DOM 和 CSSOM 都构建完成
需要注意如下几点:
- 异步脚本是网络请求期间不阻塞 DOM,拿到脚本之后马上执行,执行时还是会阻塞 DOM,但是由于响应时间被延长,此时往往 DOM 已经构建完毕(下面的测验图片将会看到,CSSOM 也已经构建完毕而且页面很快就发生第一次渲染),异步脚本的执行发生在第一次渲染之后
- 只有外部脚本可以使用 async 关键字变成异步,而且注意其与延迟脚本(
<script defer />
)的区别,后者是在 Document 被解析完毕而 DOMContentLoaded 事件触发之前执行,前者则是在下载完毕后执行 - 对于使用 document.createElement 创建的
<script />
,默认就是异步脚本
【扩展】link标签在不同位置对CRP的影响
- link标签在结尾的时候,会引起重绘
- link标签在DOM中间位置时,会阻塞渲染
【扩展】CSS 匹配规则为何从右向左
CSS 匹配就发生在 Render Tree 构建时(Chrome Dev Tools 里叫做 Recalculate Style),此时浏览器构建出了 DOM,而且拿到了 CSS 样式,此时要做的就是把样式跟 DOM 上的节点对应上,浏览器为了提高性能需要做的就是快速匹配。
首先要明确一点,浏览器此时是给一个"可见"节点找对应的规则,这和 jQuery 选择器不同,后者是使用一个规则去找对应的节点,这样从左到右或许更快。但是对于前者,由于 CSS 的庞大,一个 CSS 文件中或许有上千条规则,而且对于当前节点来说,大多数规则是匹配不上的,到此为止,稍微想一下就知道,如果从右开始匹配(也是从更精确的位置开始),能更快排除不合适的大部分节点,而如果从左开始,只有深入了才会发现匹配失败,如果大部分规则层级都比较深,就比较浪费资源了。
除了上面这点,我们前面还提到 DOM 构建是"循序渐进的",而且 DOM 不阻塞 Render Tree 构建(只有 CSSOM 阻塞),这样也是为了能让页面更早有元素呈现。考虑如下情况,如果我们此时构建的只是部分 DOM,而此时 CSSOM 构建完成,浏览器此时需要构建 Render Tree,如果对每一个节点,找到一条规则进行从左向右匹配,则必须要求其子元素甚至孙子元素都在 DOM 上,但此时 DOM 未构建完成,显然会匹配失败。如果反过来,我们只需要查找该元素的父元素或祖先元素,它们肯定在当前的 DOM 中。
【扩展】window.onload和DOMContentLoaded的区别
- onload事件是当页面所有资源(包括DOM,样式表,脚本,图片等资源)全部加载完成后执行
- DOMContentLoaded事件在DOM结构加载完成后即触发,不等待其他资源的加载。更快
【扩展】重绘和重排
概念
重排是什么:重新生成布局。当DOM 的变化影响了元素的几何属性(宽和高)--比如改变边框宽度或给段落增加文字导致行数增加--浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。
重绘是什么:重新绘制。完成重排后,浏览器会重新绘制受影响的部分到屏幕中。这个过程称为重绘。
发生重排的情况
当页面布局和几何属性改变时发生“重排”,如下:
- 添加或删除可见的DOM 元素
- 元素位置改变
- 元素尺寸改变(包括外边距、内边距、边框厚度、宽度、高度等属性改变)
- 内容改变,例如:文本改变后图片被另一个不同尺寸的图片替代
- 页面渲染器初始化
- 浏览器窗口尺寸改变
强制刷新队列
获取布局信息的操作会导致列队刷新,以下属性和方法需要返回最新的布局信息,最好避免使用。
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)
clientTop:元素上边框的厚度,当没有指定边框厚底时,一般为0。
scrollTop:位于对象最顶端和窗口中可见内容的最顶端之间的距离,简单地说就是滚动后被隐藏的高度。
offsetTop:获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。
clientHeight:内容可视区域的高度,也就是说页面浏览器中可以看到内容的这个区域的高度,一般是最后一个工具条以下到状态栏以上的这个区域,与页面内容无关。
scrollHeight:IE、Opera 认为 scrollHeight 是网页内容实际高度,可以小于 clientHeight。FF 认为 scrollHeight 是网页内容高度,不过最小值是 clientHeight。
offsetHeight:获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)的高度。IE、Opera 认为 offsetHeight = clientHeight + 滚动条 + 边框。FF 认为 offsetHeight 是网页内容实际高度,可以小于clientHeight。offsetHeight在新版本的FF和IE中是一样的,表示网页的高度,与滚动条无关,chrome中不包括滚动条。
Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。
减少重排和重绘
- 合并样式修改
- 批量操作DOM
<!---->
- 脱离标准流的操作有以下3中:
<!---->
- 隐藏元素
- 使用文档碎片
- 拷贝节点
<!---->
- 避免多次触发布局
- 对于页面中比较复杂的动画,尽量将元素设置为绝对定位,操作元素的定位属性,这样只有这一个元素会回流,如果不是定位的话,容易引起其父元素以及子元素的回流。
【参考资料】
- 深入理解浏览器解析渲染 HTML
- How browsers work
- 浏览器渲染之link标签和script标签对页面的影响
- 重排(reflow)和重绘(repaint)
- 从输入URL到浏览器显示页面过程中都发生了什么 👍
- 讲清楚重排或回流、重绘
3.说下浏览器缓存
【扩展】Last-Modified和Etag的区别?哪个更好一些?
Last-Modified和ETag是HTTP协议中用于缓存验证的两种机制。
- Last-Modified:服务器在响应中添加一个Last-Modified头,该头包含了资源的最后修改时间。客户端在后续请求中可以通过发送If-Modified-Since头,将上次获取到的Last-Modified值发送给服务器进行验证。如果资源的最后修改时间与客户端提供的时间相同,则服务器返回304 Not Modified状态码,表示资源未被修改,客户端可以使用缓存副本。
- ETag:服务器在响应中添加一个ETag头,该头包含了资源的唯一标识符(通常是一个哈希值)。客户端在后续请求中可以通过发送If-None-Match头,将上次获取到的ETag值发送给服务器进行验证。如果资源的ETag与客户端提供的值相同,则服务器返回304 Not Modified状态码,表示资源未被修改,客户端可以使用缓存副本。
两者区别:
- Last-Modified是基于资源的最后修改时间进行验证,而ETag是基于资源的唯一标识符进行验证。
- Last-Modified的精度是以秒为单位,而ETag没有这个限制,可以更精确地表示资源的变化。
- ETag可以在某些情况下提供更好的性能,因为它不依赖于文件系统的时间戳,而是根据资源内容生成唯一标识符。比如回滚时Etag更加便捷。
【扩展】日常项目中涉及到哪些缓存?
- HTML:使用协商缓存。
- CSS&JS&图片:使用强缓存,文件命名带上hash值。
【扩展】webpack打包中的几种hash模式的区别
webpack给我们提供了三种哈希值计算方式,分别是hash、chunkhash和contenthash。那么这三者有什么区别呢?
- hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。
- chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
- contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。
显然,我们是不会使用第一种的。改了一个文件,打包之后,其他文件的hash都变了,缓存自然都失效了。这不是我们想要的。那chunkhash和contenthash的主要应用场景是什么呢?在实际在项目中,我们一般会把项目中的css都抽离出对应的css文件来加以引用。如果我们使用chunkhash,当我们改了css代码之后,会发现css文件hash值改变的同时,js文件的hash值也会改变。这时候,contenthash就派上用场了。
【参考资料】
4.说下V8垃圾回收机制?
在JavaScript中,数据类型分为两类,简单类型和引用类型,对于简单类型,内存是保存在栈(stack)空间中,复杂数据类型,内存是保存在堆(heap)空间中。
- 基本类型:这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的
- 引用类型:引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的。
而对于栈的内存空间,只保存简单数据类型的内存,由操作系统自动分配和自动释放。而堆空间中的内存,由于大小不固定,系统无法进行自动释放,这个时候就需要JS引擎来手动的释放这些内存。
V8 将堆分为两类新生代和老生代,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
- 副垃圾回收器 - Scavenge:主要负责新生代的垃圾回收。
- 主垃圾回收器 - Mark-Sweep & Mark-Compact:主要负责老生代的垃圾回收。
Scavange算法将新生代堆分为两部分,分别叫from-space和to-space,工作方式也很简单,就是将from-space中存活的活动对象复制到to-space中,并将这些对象的内存有序的排列起来,然后将from-space中的非活动对象的内存进行释放,完成之后,将from space 和to space进行互换,这样可以使得新生代中的这两块区域可以重复利用。
简单的描述就是:
- 标记活动对象和非活动对象
- 复制 from space 的活动对象到 to space 并对其进行排序
- 释放 from space 中的非活动对象的内存
- 将 from space 和 to space 角色互换
那么,垃圾回收器是怎么知道哪些对象是活动对象和非活动对象的呢?
有一个概念叫对象的可达性,表示从初始的根对象(window,global)的指针开始,这个根指针对象被称为根集(root set),从这个根集向下搜索其子节点,被搜索到的子节点说明该节点的引用对象可达,并为其留下标记,然后递归这个搜索的过程,直到所有子节点都被遍历结束,那么没有被标记的对象节点,说明该对象没有被任何地方引用,可以证明这是一个需要被释放内存的对象,可以被垃圾回收器回收。
新生代中的对象什么时候变成老生代的对象呢?
在新生代中,还进一步进行了细分,分为nursery子代和intermediate子代两个区域,一个对象第一次分配内存时会被分配到新生代中的nursery子代,如果进过下一次垃圾回收这个对象还存在新生代中,这时候我们移动到 intermediate 子代,再经过下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升。
老生代垃圾回收 - Mark-Sweep & Mark-Compact
新生代空间中的对象满足一定条件后,晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大,如果这个时候再使用scavenge算法的话,会出现两个问题:
- scavenge为复制算法,重复复制活动对象会使得效率低下
- scavenge是牺牲空间来换取时间效率的算法,而老生代支持的容量交大,会出现空间资源浪费问题
所以在老生代空间中采用了 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理) 算法。
Mark-Sweep
Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。
- 标记阶段:对老生代进行第一次扫描,标记活动对象
- 清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象
Mark-Compact
由于Mark-Sweep完成之后,老生代的内存中产生了很多内存碎片,若不清理这些内存碎片,如果出现需要分配一个大对象的时候,这时所有的碎片空间都完全无法完成分配,就会提前触发垃圾回收,而这次回收其实不是必要的。
为了解决内存碎片问题,Mark-Compact被提出,它是在是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。
全停顿 Stop-The-World
由于垃圾回收是在JS引擎中进行的,而Mark-Compact算法在执行过程中需要移动对象,而当活动对象较多的时候,它的执行速度不可能很快,为了避免JavaScript应用逻辑和垃圾回收器的内存资源竞争导致的不一致性问题,垃圾回收器会将JavaScript应用暂停,这个过程,被称为全停顿(stop-the-world)。
【扩展】如何判断哪个变量导致内存泄漏的?
在V8的垃圾回收机制中,内存泄漏通常指的是无法被垃圾回收器回收的对象。下面是一些判断哪个变量内存泄漏的常见方法:
- 使用内存分析工具:可以使用V8提供的内存分析工具,如Chrome DevTools中的Memory面板,来检测内存泄漏。这些工具可以显示内存使用情况、对象引用关系等信息,帮助定位内存泄漏的对象。
- 监测内存使用情况:通过监测内存使用情况,可以观察变量的内存占用是否随时间增长,如果是,则可能存在内存泄漏。可以使用V8提供的内存使用情况API,如process.memoryUsage(),来获取当前进程的内存使用情况。
- 分析代码逻辑:检查代码中是否存在未释放的资源,比如未关闭的文件句柄、未清除的定时器、未解绑的事件监听器等。这些资源如果没有正确释放,就会导致内存泄漏。
- 使用堆快照:可以使用V8提供的堆快照工具,如Chrome DevTools中的Heap Snapshot,来捕获当前堆中的对象快照,并分析对象的引用关系。通过分析堆快照,可以找到哪些对象被引用了但实际上不再需要,从而判断是否存在内存泄漏。
- 监测内存增长:可以在代码中设置定时器,定期检测内存的增长情况。如果发现内存持续增长,可能是因为有对象没有被正确释放,从而导致内存泄漏。
需要注意的是,判断内存泄漏并不是一件简单的事情,需要综合考虑多个因素。有时候内存增长可能是正常的,例如缓存的对象或者持久化的数据。因此,结合以上方法进行综合分析,可以更准确地判断哪个变量存在内存泄漏。
【参考资料】
5.说下浏览器事件循环机制的理解
【扩展】微任务和宏任务
【扩展】如何理解微任务优先于宏任务
从上图我们就可以知道,js的事件循环机制规定了
1.优先执行一个宏任务
2.判断是否有微任务(无:继续执行宏任务)
3.有:执行所有的微任务
4.开始下一个宏任务
在js中 script本身就是一个宏任务,所以执行异步队列的时候,会优先执行它的所有微任务,然后执行宏任务
【参考资料】
6.两个tab页如何通信
【参考资料】
7.说下Websocket的理解
【参考资料】
8.为什么出现跨域?哪些方式能够解决跨域?CORS是设置哪个请求头?
1.什么是跨域?
跨域全称为Cross-Origin Resource Sharing,意为跨域资源共享,是一种允许当前域(domain)的资源被其他域(domain)的脚本请求访问的机制,通常由于同源安全策略,浏览器会禁止这种跨域请求。
而我们所说的跨域问题是因为违反了浏览器的同源安全策略而产生的。
同源是指协议、域名、端口三者相同,即使两个不同的域名指向同一个ip地址也是非同源。
注意:跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
2.跨域的解决方法
- jsonp
- cors
- websocket
- postMessage
- document.domain+iframe
- Node代理
- Nginx代理
【参考】
3.CORS是设置哪个请求头
- Origin(来源):指示请求的源地址,即发起请求的网页的地址。例如,Origin: http://example.com
- Access-Control-Request-Method(访问控制请求方法):用于预检请求(OPTIONS请求)中,指示实际请求使用的HTTP方法。例如,Access-Control-Request-Method: GET。
- Access-Control-Request-Headers(访问控制请求头):用于预检请求(OPTIONS请求)中,指示实际请求中会包含的额外的自定义请求头。例如,Access-Control-Request-Headers: X-Custom-Header。
- Access-Control-Allow-Origin(允许的来源):用于服务器响应中,指示允许访问资源的来源。可以是单个的源地址,也可以是通配符(*)表示允许所有来源。例如,Access-Control-Allow-Origin: http://example.com
- Access-Control-Allow-Methods(允许的方法):用于服务器响应中,指示允许的HTTP方法。例如,Access-Control-Allow-Methods: GET, POST, PUT。
- Access-Control-Allow-Headers(允许的头部):用于服务器响应中,指示允许的自定义请求头。例如,Access-Control-Allow-Headers: X-Custom-Header。
- Access-Control-Allow-Credentials(允许凭证):用于服务器响应中,指示是否允许发送凭证(如cookies、HTTP认证或客户端SSL证明)。例如,Access-Control-Allow-Credentials: true。
9.Options预检请求出现的时机是什么
- 只有跨域的情况下,才会发生预请求
- 与前述简单请求不同,“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
- withCredentials为true不会产生预请求
- 请求头Content-Type为application/json会产生预请求
- 设置了用户自定义请求头会产生预检请求
- delete方法产生预检请求
预检请求不一定每一次都会产生
- 这个因为浏览器会对预检请求进行缓存
- 同时通过服务器端设置 Access-Control-Max-Age 字段来设置缓存时间
- 那么当第一次请求该 URL 时会发出 OPTIONS 请求,浏览器会根据返回的 Access-Control-Max-Age 字段缓存该请求的 OPTIONS 预检请求的响应结果(具体缓存时间还取决于浏览器的支持的默认最大值,取两者最小值,一般为 10 分钟)。在缓存有效期内,该资源的请求(URL 和 header 字段都相同的情况下)不会再触发预检。(chrome 打开控制台可以看到,当服务器响应 Access-Control-Max-Age 时只有第一次请求会有预检,后面不会了。注意要开启缓存,去掉 disable cache 勾选。)
- 在 Firefox 中,上限是24小时 (即 86400 秒)。
- 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。
- 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。
- Chromium 同时规定了一个默认值 5 秒。
- 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。
10.Http2.0有哪些新特性
- 二进制分帧
- 首部压缩
- 流量控制
- 多路复用
- 请求优先级
- 服务器推送
【参考】
11.什么是CDN,它的实现原理
【参考资料】
12.TCP三次握手说一下
在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过“三次握手”进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换TCP窗口大小信息,完成了三次握手,客户端和服务器端就可以开始传送数据。
第一次握手,是客户端向服务器端发起的,这是用来去发起一个连接建立的请求,那么这个报文中的SYN位会被标记为:1,所以我们也常把它叫做一个SYN包;
第二次握手,是由服务器向客户端发起的,是来确认服务器的一个请求链接的,这个报文中我们的ACK位和SYN位都被标记为:1,所以我们也把它叫做一个SYN-ACK报文;
第三次握手,同样是由客户端发个服务器端的,这是对服务器的上一个报文的一个确认报文,这个报文中的ACK位被标记为:1,所以我们也把它叫做一个ACK包。
以上就是TCP的三次握手。
延伸-TCP四次分手的含义
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,相应的就要断开TCP连接。那对于TCP的断开连接,这里就有了“四次分手”。
1.第一次分手:客户端发送断开请求
2.第二次分手:服务器收到断开请求,发送同意断开连接的请求
3.第三次分手:服务器发送请求断开连接
4.第四次分手:客户端收到,发送同意断开连接
1.当客户端发送断开请求,只是表示客户端已经没有数据要发送了,客户端告诉服务器,它的数据已经全部发送完毕了,但是,这个时候客户端还是可以接受来自服务器的数据(第一次分手)
2.当服务器收到断开请求时,表示它已经知道客户端没有数据发送了并发送同意断开连接的请求,但是服务器还是可以发送数据到客户端的(第二次分手)
3.当服务器发送同意断开连接的请求后,这个时候就表示服务器也没有数据要发送了,就会告诉客户端,我也没有数据要发送了(第三次分手)
4.当客户端收到服务器发送请求断开连接后,再去告诉服务端我已经知道你没有数据要发给我了,同意断开连接请求(第四次分手)
13.UDP协议了解吗
【参考】
工程化
1.构建工具使用的哪个?对比一下webpack和vite的差别
webpack core 是一个纯打包工具(对标 Rollup),而 Vite 其实是一个更上层的工具链方案,对标的是 (webpack + 针对 web 的常用配置 + webpack-dev-server)。
从底层原理上来说,Vite是基于esbuild预构建依赖。而esbuild是采用go语言编写,因为go语言的操作是纳秒级别,而js是以毫秒计数,所以vite比用js编写的打包器快10-100倍。
webpack启动图:
Vite启动图:
webpack:
分析依赖=> 编译打包=> 交给本地服务器进行渲染。首先分析各个模块之间的依赖,然后进行打包,在启动webpack-dev-server,请求服务器时,直接显示打包结果。webpack打包之后存在的问题:随着模块的增多,会造成打出的 bundle 体积过大,进而会造成热更新速度明显拖慢。
vite:
启动服务器=> 请求模块时按需动态编译显示。是先启动开发服务器,请求某个模块时再对该模块进行实时编译,因为现代游览器本身支持ES-Module,所以会自动向依赖的Module发出请求。所以vite就将开发环境下的模块文件作为浏览器的执行文件,而不是像webpack进行打包后交给本地服务器。
Webpack
Webpack 是一个基于打包器的构建工具,同一个入口文件的代码会打包成一个 Bundle 文件。Webpack 长期来的一个痛点是对于大规模应用的应用启动和热更新速度很慢。
当文件发生变动时,整个 JavaScript Bundle 文件会被 Webpack 重新构建,这也是为什么使用 Webpack 的大规模应用在应用启动和热更新时速度很慢的原因。这给进行大规模 JavaScript 应用的开发者造成了很差的开发体验。
Webpack工作流程
Webpack 打包过程:
- 从一个入口文件开始,基于代码文件中的所有 import,export,require 构建依赖树;
- 编译 JS/CSS 等模块;
- 使用算法排序、重写、连接代码;
- 优化。
开发环境的 Webpack:
- 打包所有代码;
- 启动 webpack-dev-server 托管打包好的代码;
- 启动 websocket 处理热更新 HMR。
应用规模越大,启动和热更新代码越慢。及时启动了热更新,每次代码变更也需要重新生产 Bundle 文件。
Vite
Vite 核心借助了浏览器的原生 ES Modules 和像 esbuild 这样的将代码编译成 native code 的打包工具。
Vite 主要有两方面组成:
- 一个开发服务器,基于 ESM 提供丰富的内建能力,比如速度快到惊人的模块热更新(HMR);
- 一套构建指令,使用 rollup 进行代码打包,且零配置即可输出用于生产环境的高度优化的静态代码。
Vite 的核心能力和 webpack + webpack-dev-server 相似,但是在开发者体验上有一些提升:
- 无论项目大小有多大,启动应用都只需更少的时间;
- 无论项目大小有多大,HMR(Hot Module Replacing)热更新都可以做到及时响应;
- 按需编译;
- 零配置,开箱即用;
- Esbuild 能力带来的 Typescript/jsx 的原生支持。
大型的 JavaScript 项目在开发和生产环境有比较差的性能表现,往往是因为我们使用的构建工具没有充分做到并行处理、内存优化和缓存。
Vite核心理念:Bundless 开发环境构建
浏览器的原生 ES Modules 能力允许在不将代码打包到一起的情况下运行 JavaScript 应用。Vite 的核心理念很简单,就是借助浏览器原生 ES Modules 能力,当浏览器发出请求时,为浏览器按需提供 ES Module 文件,浏览器获取 ES Module 文件会直接执行。
Vite应用启动
Vite 将应用中的模块分为依赖和源码两类,分别进行服务器启动时间的优化。
- 依赖模块,开发过程中基本不会变化。Vite 对依赖采用了 esbuild 预构建的方式,esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍;
- 源码模块,是用户自己开发的代码,会经常变动。
Vite 在浏览器请求时按需转换并以原生 ESM 方式提供源码,让浏览器接管了打包程序的部分工作。
Vite工作原理
Vite 通过原生 ES Modules 托管源代码,本质上是让浏览器来接管部分打包器的工作。Vite 只会在浏览器请求发生时,按需将源码转成 ES Modules 格式返回给浏览器,由浏览器加载并执行 ES Modules 文件。
Vite的热更新
在基于 Bundle 构建的构建器中,当一个文件变动时,重新构建整个 Bundle 文件是非常低效的,且随着应用规模的上升,构建速度会直线下降。
传统的构建器虽然提供了热更新的能力,但是也会存在随着应用规模上升,热更新速度显著下降的问题。
Vite 基于 ESM 按需提供源码文件,当一个文件被编辑后,Vite 只会重新编译并提供该文件。因此,无论项目规模多大,Vite 的热更新都可以保持快速更新。
此外,Vite 合理利用浏览器缓存来加速页面加载,源码模块请求根据 304 Not Modified 进行协商缓存;依赖模块请求通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦缓存,不会再次请求。
Vite 依托支持原生 ESM 模块的现代浏览器,极大的降低了应用的启动和重新构建时间。Vite 本质上是一个在开发环境为浏览器按需提供文件的 Web Server,这些文件包含源码模块和在第一次运行时使用 esbuild 预构建的依赖模块。
Vite 和 Webpack 的主要不同在于开发环境下对于源码如何被托管以及支持哪种模块规范。
【参考】:
【打包工具】- Vite 和 webpack 原理、优缺点对比
2.打包构建上做过哪些优化/项目构建部署比较慢,怎么来优化
3.动态引入组件的原理是什么,生成的chunk文件和代码的组件引入如何匹配
小程序
1.Taro开发小程序中遇到过哪些兼容性问题
2.当有新的小程序api出现后,taro没有实现该怎么使用
3.小程序渲染原理有了解吗
【参考资料】
4.小程序滚动穿透如何解决
解决办法:
- 先说H5的,比较简单,只需要通过弹窗的显示状态,来动态设置底层页面最外层View的overflow属性即可。当弹窗显示的时候,document.body.style.overflow = 'hidden'; 当弹窗隐藏的时候,document.body.style.overflow = 'scroll';
- 由于小程序没有DOM结构,上面的方案自然是不起任何作用(就是这么傲娇)。小程序要分两步来解决,先是mask蒙层会滑动穿透的问题,这个可以采用Taro的catchMove属性 来解决,官方文档上也有说,见下方图片。然后是弹窗内部滚动穿透的问题,采用的是将弹窗内部最外层View改为ScrollView,光这样改还不行,还要在ScrollView的外层再加个View并且加上catchMove属性。直接在ScrollView上面加catchMove不行,ScrollView不支持这个属性。
注:Taro的CatchMove属性是Taro 3.0.21版本才开始支持
如上操作,Taro 弹窗滚动穿透问题的问题就完美解决了。可同时兼容H5和小程序。
Hybrid混合开发
1.说一下hybrid混合开发中,app端和js端是如何实现通信的?或说下JSBridge的原理
在Hybrid模式下,H5会经常需要使用Native的功能,比如打开二维码扫描、调用原生页面、获取用户信息等,同时Native也需要向Web端发送推送、更新状态等,而JavaScript是运行在单独的JS Context中(Webview容器、JSCore等),与原生有运行环境的隔离,所以需要有一种机制实现Native端和Web端的双向通信,这就是JSBridge:以JavaScript引擎或Webview容器作为媒介,通过协定协议进行通信,实现Native端和Web端双向通信的一种机制。
通过JSBridge,Web端可以调用Native端的Java接口,同样Native端也可以通过JSBridge调用Web端的JavaScript接口,实现彼此的双向调用。
Web端和Native可以类比于Client/Server模式,Web端调用原生接口时就如同Client向Server端发送一个请求类似,JSB在此充当类似于HTTP协议的角色,实现JSBridge主要是两点:
- 将Native端原生接口封装成JavaScript接口
- 将Web端JavaScript接口封装成原生接口
【参考资料】
性能优化
1.日常开发中性能优化做过哪些
基本思路
整体的逻辑大体如下:
构建工具分析
- 体积:包体积是否存在杂质,能不能过滤出去?
- 构建速度:构建速度影响开发调试效率和发布相关环境的节奏。
针对体积的分析手段,可以使用webpack-bundle-analyzer来分析当前项目中是否存在重复依赖, 影子依赖, 模块体积差异等等问题,使用交互式可缩放树图可视化的来梳理自己项目应用中的问题,然后去找到并且优化它。
针对速度分析, 可以使用speed-measure-webpack-plugin来分析当前每个bundle模块打包构建时的时间数据,分别会包含loader, plugin等处理的时间。根据相关的数据来定点模块,判断是否有优化的空间。
项目分析
- NetWork: 网络面板,用于侦测浏览器资源下载与上传的能耗视图。
- Performance: 性能面板:用于侦测浏览器运行时的性能表现,得出项目运行的时序图,同时也可以分析页面的一些隐式问题,如 (内存泄漏)。
- Lighthouse: 性能评测(灯塔),基于当前浏览器页面的阶段性加载现状来为用户提供结果分析数据指标报告。
- 探针: 前端框架如React有相关Profiler的探针API和chrome插件,能够为开发者探测代码编写时的问题,避免重复或者是频繁异常的Repeat rendering。
性能监控平台,为线上的前端进行侦测,从接口调用、首屏性能、流量保活等等一系列的性能指标做度量平台。
如何针对通用问题进行标准化处理???
优化手段
构建工具优化
包体积大小
首先,针对包体积大小做一些调整,能做的事情大体上有以下几点:
- 按需加载:针对项目中比较大型的工具类库,如lodash、UI Design、use-hooks等类库,如果只是使用很小的一部分,就需要避免全量加载的情况。
- tree-shaking:通过ESM的特性,可以在构建阶段将项目模块中的死代码进行移除,从而保证干净的结构和最小的文件大小。
- minify:通过webpack相关的插件,可以为代码产物做混淆压缩,如JS minify与CSS minify,当项目的代码越大的时候,压缩的效果就越明显。
- CDN依赖:对于部分共有依赖可以将其放置在CDN中来进行加载,能够极大幅度的减少本地项目产物的体积大小,缺点就是无法进行按需加载,占用浏览器请求资源,具体的收益可以自己权衡。
构建速度
其次就是构建速度了,可以从以下几个方面来着手:
- 持久化缓存:webpack5本身集成,通过cache字段开启即可,webpack4可以通过cache-loader来实现,不过不足的地方是cache-loader仅对loader起作用。(目前该loader看起来已经暂停维护了)。
- 多进程构建:通过thread-loader可以开启多进程构建,可以提高部分环节的性能效率,比如babel-ast解析等等。webpack4的话则是使用happypack来实现。
- Building:使用基于Rust编写的高性能编译工具来替换默认的babel,比如将babel-loader替换为swc-loader,这个是一个可选的操作,目前市面上多数cli都支持了该选项,以供开发者抉择。
- Dll: dll可以将一些依赖库提前进行打包,能够有效的降低打包类库的次数,只有其修改后才会进行重新的构建,通常用于公用类库的处理。可以使用DllPlugin来配置对应的效果。
- Bundless: Vite是市面上非常火热的Bundless方案之一,已经可以看出越来越多的人开始使用它来代替传统Bundle方案,其优秀的构建速度, HMR深受好评。
项目方案
资源加载
- 懒加载:懒加载我对齐的定义是延迟资源的加载,直到齐第一次触达时在进行资源的引入和展示,这项技术被广泛用于spa-router、图片资源等非阻塞性资源的优化上。对于缩短关键路径渲染非常有帮助,如各位同学耳熟能详的首屏优化。
- 预加载:预加载的目的就是通过在渲染前预先拿到需要展示的数据,从而缩短关键承流页面呈现给用户的渲染时间,大多数情况下可以分为首页预载和关键页预载两方面。
- 首页预载:Web端提供了相关的pre-load、pre-render、pre-fetch相关API实现。小程序方面也提供了相关冷启动获取数据的能力,例如getBackgroundFetch。对于追求极致的性能体验的话不妨一试。
- 关键页预载:关键页预载的话就根据自身业务来实现了,参考商城业务中商品详情页、营销会场等页面都是用户比较熟悉的页面。因此,多数时候都会将页面跳转与数据请求共同进行的。
- 接口优化:耗时较久的接口如果阻塞核心链路,可以将其拆成多个粒度小的接口来渐进式的调用以此来保证页面优先加载。反之亦然,如果接口零散占用了太多的请求资源,则可以将其聚合成一个完整的大接口来获取数据。
- 缓存:浏览器缓存策略大体上分为强缓存和协商缓存。利用好缓存策略可以有效的降低资源的重复加载,从而提高网页的整体加载速度。
网络相关
在这个过程中,会有两个阶段大家耳熟能详,分别是DNS解析和HTTP连接(三次握手)。它们是这个过程中相对耗时的两个部分。基于此,可以针对网络来对项目做以下一些优化:
- DNS预解析: 了解DNS解析过程的同学都知道,相当耗时。而浏览器也提供了dns-prefetch的相关操作,用于抢先对origin进行解析。
- PreConnect:既然DNS可以预解析,那么请求连接的话也相对应的可以抢先执行。
- 内容分发网络CDN:通过内容分发网络CDN可以使用户就近获取所需要的内容资源,从而降低网络拥塞,提高用户访问响应速度和保证访问资源的命中率。
- HTTP/2:尽可能使用HTTP/2的新特性,它会带来一些意想不到的小惊喜。感兴趣的可以看《HTTP2简介和基于HTTP2的Web优化》
- Gzip:很早之前会开启Gzip的压缩方案来减少网络实际传输数据的大小,服务器会将请求资源进行压缩后传输给客户端。
交互体验
- 加载态:提到加载态,可能大多数同学第一想到的就是loading,基于loading的实现之上,很多产品都会对部分页面制作骨架组件/骨架屏,它的优点就是表达能力强,相比于简单的加载loading而言,骨架组件能够给用户呈现预期的页面与区块的显示效果。
- 虚拟列表:虚拟列表已经是近年来老生常谈的一个优化了,其场景主要用于页面中需要展示大量数据时的一个措施手段,如收获地区选择、无限滚动场景,任何需要堆叠数据显示的页面中或多或少都会出现它的身影。
- 想了解详细的场景和实现过程的可以查阅我之前写的相关文章《百万PV商城实践系列 - 前端长列表渲染优化实战》
浏览器
- 回流和重绘: 回流和重绘是一个消费资源的事情,一旦浏览器发生频繁的回流,会造成浏览器资源的紧张,导致项目或多或少出现卡顿的问题。现如今浏览器虽然已经为我们做了Layout相关的合并优化。但还是要从根本上去减少不必要的情况发生。而解决的方案大多数如下几种:
<!---->
- 代码编写:样式集中改变,可以有效减少重排次数,尽可能不要频繁的去获取和写入DOM,需要操作时应提前在内存中准备好对应的操作元素,从而一次性的将其进行读写。
- 隐藏修改:当涉及到DOM元素的频繁操作时,尽可能让其先排出文档流,直到最终操作完成后在将其渲染到页面上,以此来降低回流和重绘的频率。
- GPU合成:对于动画,可以使用transform代替position做一些事情,现如今大部分浏览器都会对transform进行借用GPU资源来做渲染优化。
<!---->
- RAF&RDC:RAF&RDC分别是requestAnimationFrame和requestIdleCallback的简称,合理的使用可以避免频繁逻辑修改导致延迟关键事件,如动画和输入响应的交互。一个是针对动画帧效果,一个针对页面执行的逻辑。
- 事件委托:通过事件委托来绑定对应的操作,可以有效的减少相关内存的消耗。现如今,我们已经不在需要太过关注原生事件的处理了,多数情况下框架类库都已经能够很好的开始工作。
图片
- WebP:webp与png一样,是一种图片格式,因为使用更优的图像数据压缩算法、更小的图片体积因此能够带来更好的访问速度优势。需要注意的是可能会朋友不支持webp格式的环境问题,这时候就需要进行降级处理避免出现用户无法访问图片的体验。
- 渐进式加载:渐进式是一个过程,渐进式加载的话就是让一张图片从模糊到清晰慢慢展示的一个过程,避免用户因为网络问题导致图片加载失败看到不友好的体验,本质上的话属于加载状态的一种。和骨架屏区别不大。
- 图片切割:在业务当中,会看到类似商品详情图、宣传海报等相关的图片展示,它本身是一个非常长的图片,且体积因为要保真的原因无法做一些压缩。因此针对类似的场景就可以选择图片切割的方案,将长图切割成若干份小图进行展示,搭配之前提到过的一些优化方案可以完美的避免加载渲染问题。
- 图片CDN:使用图片CDN的好处除了可以获得内容分发来提高响应速度外,还可以使用其提供的图片裁切功能动态的定义分辨率大小、清晰度展示等辅助其在页面上的展示。
总结
【参考】
2.错误监控上报怎么做
3.性能监控指标有哪些?首屏时间如何计算
【扩展】如何优化首屏时间
- http缓存(强缓存和协商缓存), 性价比最高的一个优化方式。需注意的是,浏览器不缓存XHR接口,自己可根据业务特性动态地选择缓存策略。比如一个体积很大的接口,但内容变更频率不是很频繁,这种情况就适合用协商缓存。
- cdn分发(减少传输距离)。通过在多台服务器部署相同的副本,当用户访问时,dns服务器就近(物理距离)选择一台文件服务器并返回它的ip。
- 前端的资源动态加载:
<!---->
- 路由动态加载
- 组件动态加载
- 图片懒加载(offScreen Image),越来越多的浏览器支持原生的懒加载,通过给img标签加上loading="lazy来开启懒加载模式。
<!---->
- 合并请求。这点在串行接口的场景中作用很明显,比如有两个串行的接口A和B,需要先请求A,然后根据接口A的返回结果去请求接口B。假如server和client的物理距离为D,那么这个串行的场景传输的物理路程为4D。如果合并成一个接口,物理路程可减小为2D。
- 页面使用骨架屏。意思是在首屏加载完成之前,通过渲染一些简单元素进行占位。骨架屏的好处在于可以减少用户等待时的急躁情绪。这点很有效,在很多成熟的网站都有大量应用。没有骨架屏的话,一个loading图也是可以的。
- 使用ssr渲染。
- service worker:通过sw离线更新缓存的能力,理论上能做到每次访问页面都无需下载html,且所有静态资源都已经在本地有缓存。
- 引入http2.0。http2.0对比http1.1,最主要的提升是传输性能,在接口小而多的时候性能优势会更加明显。
- 利用好http压缩。即使是最普通的gzip,也能把bootstrap.min.css压缩到原来的17%。可见,压缩的效果非常明显,特别是对于文本类的静态资源。另外,接口也是能压缩的。接口不大的话可以不用压缩,因为性价比低(考虑压缩和解压的时间)。值得注意的是,因为压缩算法的特性,文件越大,压缩效果会越好。
- 利用好script标签的async和defer这两个属性。
- 使用 WebP 代替jpg/png。
【参考】
4.如果让你设计一个监控系统,你会如何设计?
5.单页应用性能指标有FMP,了解吗
【参考】
6.单页应用首屏优化方式有哪些
【参考资料】
设计模式
1.依赖注入是什么
【参考资料】
依赖注入 in 前端 && Typescript 实现依赖注入
2.前端框架设计里面常见的设计模式有哪些
- 工厂模式
- 单例模式
- 代理模式
- 策略模式
- 观察者/发布-订阅模式
- 迭代器模式
- 装饰器模式
- 原型模式
【参考资料】
安全
1.对称加密和非对称加密
【参考资料】
2.网站攻击方式有哪些
【参考资料】
开放性题
1.前端如何处理并发
比如页面初始化需要10个请求,后端只让每次3个,怎么做
思路拆解:
- urls的长度为0时,results就没有值,此时应该返回空数组
- maxNum大于urls的长度时,应该取的是urls的长度,否则则是取maxNum
- 需要定义一个count计数器来判断是否已全部请求完成
- 因为没有考虑请求是否请求成功,所以请求成功或报错都应把结果保存在results集合中
- results中的顺序需和urls中的保持一致
// 并发请求函数
const concurrencyRequest = (urls, maxNum) => {
return new Promise((resolve) => {
if (urls.length === 0) {
resolve([]);
return;
}
const results = [];
let index = 0; // 下一个请求的下标
let count = 0; // 当前请求完成的数量
// 发送请求
async function request() {
if (index === urls.length) return;
const i = index; // 保存序号,使result和urls相对应
const url = urls[index];
index++;
console.log(url);
try {
const resp = await fetch(url);
// resp 加入到results
results[i] = resp;
} catch (err) {
// err 加入到results
results[i] = err;
} finally {
count++;
// 判断是否所有的请求都已完成
if (count === urls.length) {
console.log('完成了');
resolve(results);
}
request();
}
}
// maxNum和urls.length取最小进行调用
const times = Math.min(maxNum, urls.length);
for(let i = 0; i < times; i++) {
request();
}
})
}
function sendRequest(urls, max, callbackFunc) {
const REQUEST_MAX = max;
const TOTAL_REQUESTS_NUM = urls.length;
const blockQueue = []; // 等待排队的那个队列
let currentReqNumber = 0; // 现在请求的数量是
let numberOfRequestsDone = 0; // 已经请求完毕的数量是
const results = new Array(TOTAL_REQUESTS_NUM).fill(false); // 所有请求的返回结果,先初始化上
async function init() {
for (let i = 0; i < urls.length; i++) {
request(i, urls[i]);
}
}
async function request(index, reqUrl) {
// 这个index传过来就是为了对应好哪个请求,
// 放在对应的results数组对应位置上的,保持顺序
if (currentReqNumber >= REQUEST_MAX) {
await new Promise((resolve) => blockQueue.push(resolve)); // 阻塞队列增加一个 Pending 状态的 Promise,
// 进里面排队去吧,不放你出来,不resolve你,你就别想进行下面的请求
}
reqHandler(index, reqUrl); // {4}
}
async function reqHandler(index, reqUrl) {
currentReqNumber++; // {5}
try {
const result = await fetch(reqUrl);
results[index] = result;
} catch (err) {
results[index] = err;
} finally {
currentReqNumber--;
numberOfRequestsDone++;
if (blockQueue.length) {
// 每完成一个就从阻塞队列里剔除一个
blockQueue[0](); // 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled,
// 也就是执行resolve函数了,后面不就能继续进行了嘛
blockQueue.shift();
}
if (numberOfRequestsDone === TOTAL_REQUESTS_NUM) {
callbackFunc(results);
}
}
}
init();
}
// 测试数据
const allRequest = [
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=4",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=5",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=6",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=7",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=8",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=9",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=10",
];
sendRequest(allRequests, 2, (result) => console.log(result));
【参考资料】
2.前端处理大数据会有哪些影响
【参考资料】
3.对比React和Vue
4.说一下对SEO的了解
【参考】
5.设计离线包方案
从 WebView 初始化到 H5 页面最终渲染的整个过程:
可以看到,一个完整的WebView加载流程:
- 初始化webview;
- 建立连接,从后端下载开启WebView需要的相关文件数据包,包括网页中需要请求的文件以及模板Html;
- 加载与接受数据,页面的渲染,并通过实现android和js的交互
目前主流的优化方式主要包括:
- 针对 WebView 初始化:该过程大致需耗费 70~700ms。当客户端刚启动时,可以先提前初始化一个全局的 WebView 待用并隐藏,当用户访问了 WebView 时,直接使用这个 WebView 加载对应网页并展示。
- 针对向后端发送接口请求:在客户端初始化 WebView 的同时,直接由 Native 开始网络请求数据,当页面初始化完成后,向 Native 获取其代理请求的数据。
- 针对单页面加载的JS动态拼接 Html:采用多页面打包, 服务端渲染,以及构建时预渲染等方式。
- 针对加载页面资源的大小:可采用懒加载等方式,将需要较大资源的部分分离出来,等整体页面渲染完成后再异步请求分离出来的资源,以提升整体页面加载速度。
离线包的具体思路
默认情况下,我们先将页面需要的静态资源打包并预先内置到客户端的安装包中,当用户安装应用后,解压资源到本地存储中,当 WebView 加载某个 H5 页面时,拦截发出的所有 http 请求,查看请求的资源是否在本地存在,如果存在则直接加载本地资源;否则加载网络的资源。
【参考】
7.Vue3的Compisition API和React hooks有哪些区别
React Hooks底层是链表,每一个 Hook 的 next 是指向下一个 Hook 的;
Vue对数据的响应是基于 proxy 的,对数据直接代理观察。这种场景下,只要任何一个更改 data 的地方,相关的 function 或者 template 都会被重新计算,因此避开了 React 可能遇到的性能上的问题。
代码执行上:
- Vue Composition API 的 setup() 晚于 beforeCreate 钩子,早于 created 钩子被调用
- React Hooks 会在组件每次渲染时候运行,而 Vue setup() 只在组件创建时运行一次
由于 React Hooks 会多次运行,所以 render 方法必须遵守某些规则,比如:
不要在循环内部、条件语句中或嵌套函数里调用 Hooks
声明状态上:
React
useState 是 React Hooks 声明状态的主要途径
- 可以向调用中传入一个初始值作为参数
- 如果初始值的计算代价比较昂贵,也可以将其表达为一个函数,就只会在初次渲染时才会被执行
Vue
Vue 使用两个主要的函数来声明状态:ref 和 reactive。
ref() 返回一个反应式对象,其内部值可通过其 value 属性被访问到。可以将其用于基本类型,也可以用于对象
reactive() 只将一个对象作为其输入并返回一个对其的反应式代理
TODO ……
【参考】
Vue Composition API 和 React Hooks 对比
8.前端灰度怎么做;如果让你设计一个灰度过程的话如何设计,项目版本灰度如何来做;如何避免再次发版;
【参考】
9.说下单页应用和多页应用的区别,优缺点
【参考资料】
10.管理系统鉴权如何做的
一般我们来说主要针对RBAC鉴权模式进行控制
页面鉴权用户有没有权限访问该页面,或者有没有查看通往该页面路由的权限。(组件和路由)
组件: 一.有没有权限访问页面的时候:通过路由守卫结合后端返回来的token,进行页面跳转之前的鉴权,查看token是否过期并且是否拥有该页面的权限。
路由:判断用户是否有权限查看通过指定页面的路由(或菜单或导航): 1.纯前端处理:在写路由表的时候,我们会在每个路由下加一个meta,然后在meta里面写出可以访问或查看该路由或页面的角色信息,然后我们可以通过该meta下的信息使用addrouter控制该路由下的显隐 2.前后端配合处理:每次登陆的时候,都在后端那里获取token的路由表保存到vuex里,在通过addrouter动态渲染token下的路由及导航
UI鉴权:它的颗粒度很细,所以说难度较大,我们可以通过统一的自定义指令来进行配置。
一般来说UI鉴权指的是按钮鉴权处理UI鉴权 简单的方法是我们可以获取token下的角色信息,用v-if处理该UI的显隐,但是这种方式缺点很明显,不易于统一管理,所以我们需要集中封装一个自定义指令,在自定义指令中,集中处理鉴权的逻辑,然后分发在每个鉴权的按钮上。
Node
1.Node用过么,起一个http server的伪代码大概是?
const { createServer } = require('http');
const HOST = 'localhost';
const PORT = '8080';
const server = createServer((req, resp) => {
// the first param is status code it returns
// and the second param is response header info
resp.writeHead(200, { 'Content-Type': 'text/plain' });
console.log('server is working...');
// call end method to tell server that the request has been fulfilled
resp.end('hello nodejs http server');
});
server.listen(PORT, HOST, (error) => {
if (error) {
console.log('Something wrong: ', error);
return;
}
console.log(`server is listening on http://${HOST}:${PORT} ...`);
});
手写部分
1.括号匹配
2.数组排重
3.数组唯一出现
4.面向对象设计
5.版本号排序
6.函数柯里化
7.数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字
8.手写单例模式
9.手写快速排序
10.单向链表的反转取值,通过迭代/递归方式实现
其他
1.git merge 和 git rebase的区别是什么
- git merge会创建一个新的合并提交,保留完整的提交历史记录,但可能会导致分支历史变得复杂。
- git rebase会将提交复制到目标分支上,并重新应用目标分支上的更改,改写提交历史记录,使分支历史保持线性,但可能会丢失一些提交信息。
【参考资料】:
面试官:说说你对git rebase 和 git merge的理解?区别?
原文连接 欢迎关注
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。