HTML
HTML5 中 meta 标签作用?
HTML 5 中的 meta 标签是一个非常常用的标签,它可以用来描述一个 HTML 文档的一些基本信息与配置,包括字符编码、页面关键词、作者、视口大小等。具体来说,meta 标签可用于以下几个方面:
- 描述文档内容:通过设置 meta 标签中的一些属性,可以描述文档的主体内容、作者、关键词和摘要等信息,以便搜索引擎索引和显示。
- 控制页面行为:指定 meta 标签中的属性值可以控制页面的默认行为,比如设置视口大小可以实现响应式设计。
- 声明字符编码:通过设置 meta 标签中的 charset 属性值,可以声明文档中使用的字符编码格式,帮助浏览器正确地解读页面内容。
- 防止 XSS 攻击:设置 meta 标签的 http-equiv 属性为 content-security-policy,可以提高页面的安全性,保护页面免受跨站脚本攻击(XSS)。
- 提供缓存机制:设置一些 meta 标签属性(如 cache-control、expires、pragma),可以控制浏览器缓存页面内容的时间和方式。
页面导入样式时,使用 link 和@import 有什么区别?
- 从属关系:@import 只能导入样式表,link 还可以定义 RSS、rel 连接器等
- 加载顺序:加载页面时,link 标签引入的 CSS 会同时加载,@import 导入的样式会在 CSS 文件被解析式顺序加载
- 兼容性:link 是标签,浏览器几乎都支持;@import 在低版本浏览器可能存在兼容性问题
简述前端性能优化?
页面内容方面
- 通过文件合并、css 雪碧图、使用 base64 等方式来减少 HTTP 请求数,避免过多的请求造成等待的情况
- 通过 DNS 缓存等机制来减少 DNS 的查询次数
- 通过设置缓存策略,对常用不变的资源进行缓存
- 通过延迟加载的方式,来减少页面首屏加载时需要请求的资源,延迟加载的资源当用户需要访问时,再去请求加载
- 通过用户行为,对某些资源使用预加载的方式,来提高用户需要访问资源时的响应速度
服务器方面
- 使用 CDN 服务,来提高用户对于资源请求时的响应速度
- 服务器端自用 Gzip、Deflate 等方式对于传输的资源进行压缩,减少传输文件的体积
- 尽可能减小 cookie 的大小,并且通过将静态资源分配到其他域名下,来避免对静态资源请求时携带不必要的 cookie
CSS 和 JavaScript 方面
- 把样式表放在页面的 head 标签中,减少页面的首次渲染的时间
- 避免使用 @import 标签
- 尽量把 js 脚本放在页面底部或者使用 defer 或 async 属性,避免脚本的加载和执行阻塞页面的渲染。
什么是 webp?
WebP 是谷歌开发的一种新图片格式,它是支持有损和无损两种压缩方式的使用直接色的点阵图。使用 webp 格式的最大优点是是,在相同质量的文件下,它拥有更小的文件体积。因此它非常适合于网络图片的传输,因为图片体积的减少,意味着请求时间的减少,这样会提高用户的体验
浏览器如何判断是否支持 webp 格式图片? 通过创建 Image 对象,将其 src 属性设置为 webp 格式的图片,然后在 onload 事件中获取图片的宽高,如果能够获取,则说明浏览器支持 webp 格式图片。如果不能获取或者处罚了 onerror 函数,那么就说明浏览器不支持 webp 格式的图片
浏览器渲染机制?
- 浏览器采用流式布局模型(Flow Based Layout)
- 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就产生了渲染树(Render Tree)
- 有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上
- 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一;
介绍下重绘和回流(Repaint & Reflow),以及如何进行优化?
重绘:由于节点的集合属性发生改变或者由于样式改变而不会影响布局 回流:布局或者几何属性需要改变
减少重绘和回流:
- 使用 transform 代替 top
- 使用 visibility 替换 display: none,前者引起重绘,后者引发回流
- 避免使用 table 布局
- 尽可能在 DOM 树的最末端改变 class
- 避免使用 CSS 表达式,可能会引发回流
- 避免频繁操作样式,修改 class 最好
- 避免频繁操作 DOM,合并多次修改为一次
- 避免频繁读取会引发回流/重绘的属性,将结果缓存
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流
CSS
谈谈 flex 布局
Flex 布局(弹性布局)是 CSS3 中的一种布局模式,它使得在容器中分配空间和对齐元素变得更加简单和高效 Flex 布局的核心思想是将容器的子元素(称为 flex 项)进行灵活的排列和对齐,而不需要使用传统的浮动或定位方式
flex:1 是哪些属性的缩写?
- flex: 1 是 flex-grow: 1, flex-shrink: 1, flex-basis: 0 的合并写法
- flex-grow:定义项目的放大比例。设置为 1 表示该项目可以占据可用空间的比例为 1。如果所有项目的 flex-grow 都设置为 1,那么它们将均分可用空间
- flex-shrink:定义项目的缩小比例。设置为 1 表示该项目可以缩小以适应容器的大小。如果容器的空间不足,所有项目会按比例缩小
- flex-basis:定义项目在分配多余空间之前占据的主轴空间。设置为 0 表示在计算可用空间时,项目的基础大小为 0,这样项目会根据 flex-grow 的值来决定其最终大小
display: none; 与 visibility: hidden; 的区别?
- display:none ;会让元素完全从渲染树中消失, 渲染的时候不占据任何空间;visibility: hidden ;不会让元素从渲染树消失, 渲染师元素继续占据空间,只是内容不可⻅
- display: none ;是⾮继承属性, ⼦孙节点消失由于元素从渲染树消失造成,通过修改⼦孙节点属性⽆法显示 ;visibility: hidden; 是继承属性,⼦孙节点消失由于继承了 hidden , 通过设置 visibility: visible; 可以让⼦孙节点显式
- 修改常规流中元素的 display 通常会造成⽂档重排 。修改 visibility 属性只会造成本元素的重绘
- 读屏器不会读取 display: none ;元素内容;会读取 visibility:
JavaScript
JavaScript 的数据类型?存储区别?
- 基本类型:Number、String、Boolean、undefined、null、symbol
- 引用类型:Object、Function、Array、Date、RegExp 等
区别
- 基本类型的值存放在栈中
- 引用类型的值存储在堆中,在栈中存放的是指向堆内存的地址
如何判断数据类型?有哪些方法?区别?
- typeof 返回基本数据类型,instanceof 返回布尔值
- typeof:判断基础类型(null 除外),引用类型除了 function,其他都为 object
- instanceof:可以判断引用数据类型,不能判断基本类型
- 通用检测数据类型,Object.prototype.toString.call(),返回字符串 “[object Xxx]”
简单说下 ES6 新特性?
- let/const
- 箭头函数
const func = ()=>{} - 模板字符串
const word ={age}岁`` - 解构赋值
- 扩展运算符
- 类和继承 class extends
- Symbol
- Map/Set Map 对象用于保存键值对;Set 类似数组,但 Set 里的元素是不重复的
- 数组新方法
Array.from() includes()map() filter() forEach() find() some() every() reduce() - Promise 和 Proxy
- 模块化 通过
import xx form xx导入模块export 或 export default导出模块
JaveScript 原型,原型链?有什么特点?
- 原型:每个对象都有拥有一个原型对象,每个函数都有特殊属性原型 prototype,原型对象有一个自有属性 constructor,指向函数本身。
- 原型链:当访问一个对象的属性时,不仅在该对象上寻找,还会在该对象的原型以及原型的原型上找,依次层层向上搜索,直到找到匹配的属性或原型链的末尾,这种关系被称为原型链。
- 每个对象的proto指向他构造函数的 prototype
person1.__proto__ === Person.prototype - 构造函数是一个函数对象,通过 Function 构造器生成的
Person.__proto__=== Function.prototype - 原型对象本身是一个对象,对象的构造函数都是 Object
Person.prototype.__proto__=== Object.prototype - 所有的构造器都是函数对象,函数对象都是 Function 构造产生
Object.__proto__ === Function.prototype - Object 的原型对象也有proto属性,指向 null,null 是原型链的顶端
Object.prototype.__proto__=== null
对作用域链的理解?
- 作用域:变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合,作用域决定了代码区块中变量和其他资源的可见性
- 全局作用域:任何不在函数内部或大括号中声明的变量都是在全局作用域下,可以在任意位置访问
- 函数作用域:也叫局部作用域,即变量在函数内部声明,只能在函数内部访问,不能在函数外访问(闭包除外)
- 块级作用域:在大括号中使用 let 或者 const 声明的变量处于块级作用域,大括号之外不能访问
- 作用域链:使用一个变量时,首先会在当前作用域下寻找,如果没找到,再到上级作用域寻找,以此类推,直到找到该变量
对事件循环 EVENT-LOOP 的理解?
- JavaScript 是一门单线程语言,同一时间只能做一件事,事件循环可实现单线程非阻塞
- 同步任务:立即执行的任务,直接进入到主线程中执行
- 异步任务:异步执行的任务
- 微任务:需要异步执行的函数,执行时机在主函数执行结束之后,当前宏任务前: Promise.then,MutationObject、process.nextTick()
- 宏任务:script,etTimeout ,setInterval,setImmediate,I/O,UI renderin
- 事件循环流程:
- 执⾏同步代码, 这属于宏任务
- 执⾏栈为空,查询是否有微任务需要执⾏
- 执⾏所有微任务
- 然后开始下⼀轮 Event loop ,执⾏宏任务中的异步代码
setTimeout 倒计时误差?
JS 是单线程的,所以 setTimeout 的误差其实是无法被完全解决的,原因有很多, 可能是回调中的,有可能是浏览器中的各种事件导致。这也是为什么页面开久了,定时器会不准的原因, 当然我们可以通过⼀定的办法去减少这个误差
JaveScript 本地存储的方式有哪些?区别和应用场景?
区别
- 存储大小:cookie 大小不能超过 4k.sessionStorage 和 localStaorage 大小限制为 5M 左右
- 有效时间:localStorage 存储时间持久,浏览器关闭数据不丢失除非手动清除;sessionStorage 数据在当前窗口关闭后自动清除;cookie 在过期时间前一直有效
- 数据与服务器交互方式:cookie 数据会自动传递到服务器,服务器也可以写 cookie 到客户端;sessionStorage 和 localStorage 不会自动把数据发给服务器,尽在本地保存
使用场景
- 标记用户与跟踪用户行为,使用 cookie
- 适合长期保存在本地的数据,使用 localStoage
- 敏感账号一次性登录,使用 sessionStorage
- 存储大量数据情况,在线文档保存编辑历史,使用 indexedDB
箭头函数与普通函数的区别,什么情况不能用箭头函数?
- 语法上的区别:箭头函数使用箭头(=>)来定义函数,而普通函数使用 function 关键字来定义函数
- this 指向的区别:在普通函数中,this 指向的是调用该函数的对象,也就是函数的执行上下文。而在箭头函数中,this 指向的是定义该箭头函数时所在的上下文,也就是箭头函数所在的函数或者全局对象
- arguments 对象的区别:在普通函数中,arguments 对象是一个类数组对象,包含了函数所有的参数;而在箭头函数中,arguments 对象是不存在的,需要使用 Rest 参数来获取所有的参数
- 箭头函数不能用作构造函数
JavaScript 实现继承的方式?
- 继承是面向对象软件技术中的一个概念,继承可以使得子类具有父类的各类属性和方法,不需要再次编写相同的代码
原型链继承
function Parent() {
this.name = "parent1";
this.play = [1, 2, 3];
}
function Child() {
this.type = "child2";
}
Child.prototype = new Parent();
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
// 改变s1的属性,s2也跟着变化,因为两个示例用的同一个原型对象,内存空间共享
构造函数继承
function Parent2() {
this.parent = "parent2";
}
Parent.prototype.getName = function () {
return this.name;
};
function Child2() {
Parent2.call(this);
this.type = "child2";
}
let child2 = new Child2();
console.log(child2.getName()); // 会报错
// 只能继承父类的实例属性和方法,不能继承父类原型属性和方法
组合式继承
function Parent3() {
this.name = "parent3";
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
};
function Child3() {
Parent3.call(this);
this.type = "child3";
}
Child3.prototype = new Parent3();
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]
console.log(s3.getName(), s4.getNmae()); // parent3
// 问题1,2解决,但是parent3执行了2次,增加了性能开销
原型式继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
},
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
// Object.create()方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能性
寄生式继承
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
},
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function () {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"],存在篡改的可能性
寄生组合式继承
function clone(parent, child) {
// Object.create
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = "parent6";
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
};
function Child6() {
Parent6.call(this);
this.friends = "child5";
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
};
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
var,let,const 的区别?
- var 存在变量提升,声明前可以调用,值为 undefined;let 和 const 不存在变量提升,声明前使用会报错
- var 不存在暂时性死区,let 和 const 存在,只有等到声明变量的那一行代码出现,才可以获取和使用变量
- let 和 const 存在块级作用域,var 没有
- var 允许重复声明,let 和 const 在同一作用域下不允许重复声明
- var 和 let 可以修改声明的变量,const 声明的常量不能被修改
defer 和 async 的区别?
- defer 并行加载 js 文件,会按照页面上 script 标签的顺序执行
- async 并行加载 js 文件,下载完成立即执行,不会按照页面上 script 标签的顺序执行
介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set:
- 成员不能重复
- 只有键值,没有键名,有点类似数组
- 可以遍历,方法有 add、delete、has
WeakSet:
- 成员都是对象(引用)
- 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露
- 不能遍历,方法有 add、delete、has;
Map:
- 本质上是键值对的集合,类似集合
- 可以遍历,方法很多,可以跟各种数据格式转换
WeakMap:
- 只接收对象为键名(null 除外),不接受其他类型的值作为键名
- 键名指向的对象,不计入垃圾回收机制
- 不能遍历,方法同 get、set、has、delete
React
对 React 的理解?有哪些特性?
React 用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案 遵循组件设计模式,声明式编程范式和函数式编程 使用虚拟 DOM 有效的操作 DOM
- 特性:JSX 语法、单向数据流、虚拟 DOM、声明式编程、组件化
- 优势:高效灵活、声明式的设计,使用简单、组件化开发,提高代码复用率
React18 有哪些更新?
- setState 自动批处理
- 引入了 createRoot API
- flushSync,可以退出批量更新
- react 组件返回值更新,可返回 null 或者 undefined
为什么虚拟 dom 会提高性能?
虚拟 dom 相当于在 js 和真实 dom 中间加了⼀个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能 具体实现步骤如下:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建⼀个真正的 DOM 树,插到⽂档当中
- 当状态变更的时候, 重新构造⼀棵新的对象树 。然后用新的树和旧的树进行比较,记录两棵树差异
- 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新
为什么 react 组件,都必须要申明一个 importReact from 'react'?
JSX 语法不能直接被浏览器解析和运行,因此需要插件 @babel/plugin-transform-react-jsx 来转换语法,使之能够在浏览器或任何 JavaScript 环境中执行。所以 React 组件需要引入 React 的一个主要原因是:在组件中使用 JSX 时,JSX 语法最终会被 Babel 编译成使用 React.createElement 方法的 JavaScript 代码。也就是说,任何使用 JSX 的 React 组件的背后都隐含了 React.createElement 的调用
为什么 React 自定义组件首字母要大写?
jsx 通过 babel 转义时,调用了 React.createElement 函数,它接收三个参数,分别是 type 元素类型,props 元素属性,children 子元素。如果组件首字母为小写,它会被当成字符串进行传递,在创建虚拟 DOM 的时候,就会把它当成一个 html 标签,如果没有这个标签,就会报错。组件首字母为大写,它会当成一个变量进行传递,React 知道它是个自定义组件就不会报错了
概述下 React 中的事件处理逻辑?
为了解决跨浏览器兼容性问题, React 会将浏览器原生事件( BrowserNative Event ) 封装为合成事件 ( SyntheticEvent ) 传⼊设置的事件处理器中 。这里的合成事件提供了与原生事件相同的接⼝,不过它们屏蔽了底层浏览器的细节差异,保证了⾏为的⼀致性 。另外有意思的是,React 并没有直接将事件附着到⼦元素上, 而是以单⼀事件监听器的⽅式将所有的事件发送到顶层进⾏处理 。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器, 最终达到优化性能的目的
说说 React 的事件机制?
React 基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件合成、事件冒泡、事件派发等,在 React 中这套事件机制称为合成事件 合成事件(SyntheticEvent):React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器,根据 W3C 规范来定义合成事件,兼容所有浏览器,拥有与原生事件相同的接口
-
React 所有的事件都挂载到 document 上
-
当真实 DOM 元素触发事件,会冒泡到 document 对象上,再处理 React 事件
-
先执行原生事件,再处理 React 事件
-
最后真正执行 document 上挂载的事件
-
阻止合成事件的冒泡,用 e.stopPropagation()
-
阻止合成事件与最外层 document 上事件的冒泡,用 e.nativeEvent.stopImmediatePropagation()
-
阻止合成事件与最外层 document 上的原生事件上的冒泡,通过判断 e.target 来避免
React 的生命周期及方法?
- 创建阶段
- constructor
- getDerviedStateFromProps
- render
- componentDidMount
- 更新阶段
- getDerviedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 卸载阶段
- componentWillUnmount
在生命周期中的哪⼀步你应该发起 AJAX 请求?
我们应当将 AJAX 请求放到 componentDidMount 函数中执⾏,主要原因有下:
- React 下⼀代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数 。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定, React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易⻅其会被触发多次,自然也就不是好的选择。
- 如果我们将 AJAX 请求放置在生命周期的其他函数中, 我们并不能保证请求仅在组件挂载完毕后才会要求响应 。如果我们的数据请求在组件挂载之前就完成,并且调用了 setState 函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进⾏ AJAX 请求则能有效避免这个问题
setState 的执行机制?
- 在组件生命周期或 React 合成事件中,是异步的
- 在 setTimeout 或者 dom 原生事件中,是同步的
- 对同一值进行多次 setState,setState 的批量更新策略会对其进行覆盖,取最后一次执行结果
说说真实 DOM 和虚拟 DOM 的区别?优缺点?
真实 DOM(real dom):文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个节点都是真实 DOM 结构 虚拟 DOM(virtual dom):以 JavaScript 对象形式存在的对 DOM 的描述
两者的区别如下:
- 虚拟 DOM 不会进行重排重绘,而真实 DOM 会频繁的重排重绘
- 虚拟 DOM 总消耗:虚拟 DOM 增删改+真实 DOM 差异增删改+重排重绘,真实 DOM 总消耗:真实 DOM 完全增删改+重排重绘 真实 DOM 优缺点: 优势:易用 缺点:效率低,解析慢,内存占用高;性能差,频繁操作真实 DOM,容易导致重绘重排 虚拟 DOM 优缺点 优势:简单方便;避免 DOM 的频繁更新,提高性能;跨平台 缺点:在一些性能要求极致的情况下,虚拟 DOM 无法进行针对性的极致优化;首次渲染大量 DOM 时,多了一层虚拟 DOM 的计算,速度比正常稍慢
对 Fiber 架构的理解?解决了什么问题?
- React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
- 为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
- 所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
- 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
- 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。
- 核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
JavaScript 引擎和渲染引擎两个线程是互斥的,如果 JavaScript 线程长时间占用主线程,那么渲染层的更新就会长时间的等待,界面长时间不更新,导致页面响应速度变差,造成卡顿 React Filber 做了一下操作:
- 为每个任务增加了优先级,优先级高的任务可以中断优先级低的任务
- 增加了异步任务,调用 requestIdleCallback(),浏览器空闲时执行
- dom diff 变成了链表,一个 dom 对应两个 Fiber,对应两个队列
- 从架构看,Fiber 是对 React 核心算法调和的重写
React 中 key 有什么作用?
用于判断元素是新创建还是还是被移除,从而减少不必要的渲染
注意事项:
- key 应该要唯一
- key 不要使用随机值(随机数在下一次 render 时,会重新生成一个数字)
- 使用 index 作为 key 值,对性能没有优化
说说 React diff 原理是什么?
虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,**将更新补丁作用于真实 DOM 当组件状态发生变化时,React 会生成新的虚拟 DOM,新旧虚拟 DOM 进行比较(diff),只比较同一层级的节点, 三个策略:
-
把树形结构按照层级分解, 只比较同级元素
-
给列表结构的每个单元添加唯⼀的 key 属性, ⽅便比较。
-
React 只会匹配相同 class 的 component ( 这里面的 class 指的是组件的名字)
-
合并操作,调用 component 的 setState ⽅法的时候, React 将其标记为- dirty. 到每⼀个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
-
选择性⼦树渲染 。开发⼈员可以重写 shouldComponentUpdate 提高 diff 的性能
或者
- 树:只对同一层级的节点进行比较
- 组件:class 一致,则默认为相似的树结构;不是则直接放入补丁中。只要父组件类型不同,就会被重新渲染。
- 元素/节点:通过标记的 key 值进行对比。
React 性能优化的手段?
- 避免使用内联函数
- 使用 React.Fagments 避免额外标记
- 使用 Immutable
- 懒加载组件
- 事件绑定方式
- 服务的渲染
React 常用 hooks?
- useState:用于在函数组件中管理状态
- useEffect:用于处理副作用,比如数据获取、订阅或手动操作 DOM。它可以设置清理函数,确保在组件卸载时清理副作用
- useContext:用于在组件树中共享状态,避免通过 props 层层传递。它需要与
React.createContext一起使用 - useReducer:用于管理复杂状态,类似于 Redux 的 reducer。它返回当前状态和一个 dispatch 函数
- useRef:用于访问 DOM 元素或存储可变的值而不引起重新渲染。它返回一个可变的 ref 对象
- useMemo:用于在函数组件中缓存计算结果
- useCallback:缓存函数引用,优化性能。它可以防止因为组件重新渲染,导致方法被重新创建,起到缓存作用
memo、useMemo 和 useCallback 的用法 (react 函数式组件如何避免子组件渲染)?
- React.memo:函数组件的记忆化
- 用途:用于函数组件,通过对比组件的 props 变化来避免不必要的重新渲染
- 工作原理:React.memo 对比组件接收到的 props 是否发生变化。仅当 props 发生变化时,才会重新渲染组件,否则会使用上一次的渲染结果
function memo<P extends object>(
Component: FunctionComponent<P>,
propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean,
): NamedExoticComponent<P>;
- React.useMemo
- 用途:用于记忆化计算结果,避免在每次渲染时都重新计算。
- 工作原理:useMemo 接收一个计算函数和一个依赖项数组。它仅在依赖项数组中的值发生变化时,才会重新计算,并返回记忆化的值。
const useMemoValue = useMemo(() => computeExpensiveValue(dep1, dep2), [dep1, dep2]);
- React.useCallback
- 用途:用于记忆化回调函数,避免在每次渲染时都重新创建回调
- 工作原理:useCallback 接收一个回调函数和一个依赖项数组。它仅在依赖项数组中的值发生变化时,才会返回上一次的记忆化的回调函数。
const useCallbackValue = useCallback(() => {// 回调逻辑}, [dep1, dep2]);
说说你用 react 有什么坑点?
- JSX 做表达式判断时候,需要强转为 boolean 类型
- 尽量不要在 componentWillReviceProps 里使用 setState,如果⼀定要使用,那么需要判断结束条件,不然会出现无限重渲染,导致页面崩溃
VUE
Vue 组件 data 为什么必须是函数?
- 组件复用性:你在 Vue 中定义一个组件时,可能会在多个地方使用这个组件。如果 data 是一个对象,那么所有的组件实例都会共享这个对象的引用。这意味着如果一个组件实例修改了 data 中的属性,其他实例的 data 也会受到影响,从而导致不可预期的错误。通过将 data 定义为一个函数,每个组件实例都会调用这个函数并返回一个新的对象。这确保了每个实例都有自己独立的 data ,避免了状态之间的干扰。
- 状态隔离:使用函数定义 data 可以确保每个组件实例的状态是隔离的。这样可以更好地控制组件的行为,确保它们之间不会相互影响。这对于复杂的应用程序尤其重要,能够减少状态管理的复杂性
nextTick 的作用是什么?他的实现原理是什么?
nextTick 是 Vue.js 中的一个重要 API,主要用于在下次 DOM 更新循环结束后执行延迟回调。这在需要在数据更改后立即访问更新后的 DOM 状态时非常有用
- 确保 DOM 更新:当你在 Vue 组件中修改数据后,Vue 并不会立即更新 DOM,而是将更新的操作放入一个队列中,等到下一个“tick”时再统一进行更新。使用 nextTick 可以确保在 DOM 更新后执行某些操作,例如获取更新后的 DOM 元素的大小或位置
- 异步操作:在某些情况下,你可能需要在数据变化后立即执行某些逻辑,但又需要确保 DOM 已经更新。 nextTick 提供了一种简单的方式来处理这种需求
实现原理:
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用 setTimeout
Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
- Object.defineProperty 无法低耗费的监听到数组下标的变化,导致通过数组下标添加元素,不能实时响应
- Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历。如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象, 并返回一个新的对象
- Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性
Vue 双向数据绑定原理?
vue 通过双向数据绑定,来实现了 View 和 Model 的同步更新。vue 的双向数据绑定主要是通过数据劫持和发布订阅者模式来实现的。首先我们通过 Object.defineProperty() 方法来对 Model 数据各个属性添加访问器属性,以此来实现数据的劫持,因此当 Model 中的数据发生变化的时候,我们可以通过配置的 setter 和 getter 方法来实现对 View 层数据更新的通知。 对于文本节点的更新,我们使用了发布订阅者模式,属性作为一个主题,我们为这个节点设置一个订阅者对象,将这个订阅者对象加入这个属性主题的订阅者列表中。当 Model 层数据发生改变的时候,Model 作为发布者向主题发出通知,主题收到通知再向它的所有订阅者推送,订阅者收到通知后更改自己的数据。
vue3 的 diff 算法是什么,简单介绍一下
Vue3 的 diff 算法是一种用于比较虚拟 DOM 树之间差异的算法。它用于确定需要更新的部分,以便最小化对实际 DOM 的操作,从而提高性能。 Vue3 的 diff 算法采用了一种称为"逐层比较"的策略,即从根节点开始逐层比较虚拟 DOM 树的节点。具体的比较过程如下:
-
对比两棵虚拟 DOM 树的根节点,判断它们是否相同。如果不相同,则直接替换整个根节点及其子节点, 无需进一步比较。
-
如果根节点相同,则对比它们的子节点。这里采用了一种称为"双端比较"的策略,即同时从两棵树的头部和尾部开始比较子节点。
-
从头部开始,依次对比两棵树的相同位置的子节点。如果两个子节点相同,则继续比较它们的子节点。4. 如果两个子节点不同,根据一些启发式规则(如节点类型、key 值等),判断是否需要替换、删除或插入子节点。
-
继续比较下一个位置的子节点,直到两棵树的所有子节点都被比较完。
通过逐层比较和双端比较的策略,Vue3 的 diff 算法能够高效地找到虚拟 DOM 树之间的差异,并只对需要更新的部分进行操作,从而减少了对实际 DOM 的操作次数,提高了性能。 值得注意的是,Vue3 还引入了一种称为"静态标记"的优化策略,用于在编译阶段将一些静态节点标记出来,从而在 diff 算法中更快地跳过这些静态节点的比较,进一步提升性能。这一优化策略在处理大型列表、静态内容等场景下特别有效。
VUE2 和 VUE3 的区别?
- 支持 Composition API:基于函数的 API,更加灵活组织组件逻辑(vue2 用的是 options api)
- 响应式系统提升:Vue3 中响应式数据原理改成 proxy,可监听动态新增删除属性,以及数组变化
- 编译优化:vue2 通过标记静态根节点优化 diff,Vue3 标记和提升所有静态根节点,diff 的时候只需要对比动态节点内容
- 打包体积优化:移除了一些不常用的 api(inline-template、filter)
- 生命周期的变化:使用 setup 代替了之前的 beforeCreate 和 created
- Vue3 的 template 模板支持多个根标签
- Vuex 状态管理:创建实例的方式改变,Vue2 为 new Store , Vue3 为 createStore
Vue3.0 的设计⽬标是什么?做了哪些优化?
设计⽬标
- 随着功能的增⻓,复杂组件的代码变得越来越难以维护
- 缺少⼀种⽐较「⼲净」的在多个组件之间提取和复⽤逻辑的机制
- 类型推断不够友好
- bundle 的时间太久了
Vue3 优势:
- 更⼩更快
- TypeScript ⽀持
- API 设计⼀致性
- 提⾼⾃身可维护性
- 开放更多底层功能
浏览器
从输入 URL 到看到页面发生了什么?
- 浏览器地址栏输入 URL 并回车
- 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期
- DNS 解析 URL 对应的 IP
- 根据 IP 建立 TCP 连接(三次握手)
- 发送 http 请求
- 服务器处理请求,浏览器接受 HTTP 响应
- 浏览器解析并渲染页面
- 关闭 TCP 连接(四次握手)
导致页面加载白屏时间长的原因有哪些,怎么进行优化?
白屏时间:即用户点击一个链接或打开浏览器输入 URL 地址后,从屏幕空白到显示第一个画面的时间。 当用户点开一个链接或者是直接在浏览器中输入 URL 开始进行访问时,就开始等待页面的展示。页面渲染的时间越短,用户等待的时间就越短,用户感知到页面的速度就越快。这样可以极大的提升用户的体验,减少用户的跳出,提升页面的留存率
从输入 url,到页面的画面展示的过程
- 首先,在浏览器地址栏中输入 url
- 浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。
- 在发送 http 请求前,需要域名解析(DNS 解析),解析获取相应的 IP 地址。 浏览器向服务器发起 tcp 连接,与浏览器建立 tcp 三次握手。
- 握手成功后,浏览器向服务器发送 http 请求,请求数据包。
- 服务器处理收到的请求,将数据返回至浏览器
- 浏览器收到 HTTP 响应
- 读取页面内容,浏览器渲染,解析 html 源码
- 生成 Dom 树、解析 css 样式、js 交互,渲染显示页面
DNS 解析优化
- 针对 DNS Lookup 环节,我们可以针对性的进行 DNS 解析优化。
- DNS 缓存优化
- DNS 预加载策略
- 稳定可靠的 DNS 服务器
TCP 网络链路优化
服务端的处理优化,是一个非常庞大的话题,会涉及到如 Redis 缓存、数据库存储优化或是系统内的各种中间件以及 Gzip 压缩等
浏览器下载、解析、渲染页面优化
- 尽可能的精简 HTML 的代码和结构
- 尽可能的优化 CSS 文件和结构
- 一定要合理的放置 JS 代码,尽量不要使用内联的 JS 代码
- 将渲染首屏内容所需的关键 CSS 内联到 HTML 中,能使 CSS 更快速地下载。在 HTML 下载完成之后就能渲染了,页面渲染的时间提前,从而缩短首屏渲染时间;
- 延迟首屏不需要的图片加载,而优先加载首屏所需图片
什么是跨域?为什么浏览器要使用同源策略?你有几种方式可以解决跨域问题?
跨域:因为浏览器出于安全考虑,有同源策略 。也就是说, 如果协议、域名或者端口有⼀个不同就是跨域 解决跨域方式:
- JSONP:利用
<script>标签没有跨域限制的漏洞。通过<script>标签指向⼀个需要访问的地址并提供⼀个回调函数来接收数据,JSONP 使用简单且兼容性不错,但是只限于 get 请求 - CORS:需要浏览器和后端同时支持
- postMessage
浏览器缓存?
-
强缓存
- Expires ( 该字段是 http1.0 时的规范,值为⼀个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
- Cache-Control:max-age ( 该字段是 http1.1 的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期, 它的值单位为秒)
-
协商缓存
- Last-Modified ( 值为资源最后更新时间, 随服务器 response 返回)
- If-Modified-Since ( 通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)
- ETag (表示资源内容的唯⼀标识, 随服务器 response 返回)
- If-None-Match ( 服务器通过比较请求头部的 If-None-Match 与当前资源的 ETag 是否⼀致来判断资源是否在两次请求之间有过修改, 如果没有修改,则命中协商缓存)
webpack
webpack 中 loader 和 plugin 的区别是什么?
loader:loader 是一个转换器,将 A 文件进行编译成 B 文件,属于单纯的文件转换过程; plugin:plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。
webpack 的 module、bundle、chunk 分别指的是什么?
- Module(模块):module 指的是 Webpack 处理的代码的单个文件。这可以是 JavaScript、CSS、图片或其他类型的文件。在 Webpack 中,每个文件都被视为一个独立的模块,它们可以通过 import、require 等方式引入和导出。模块可以包含代码、依赖关系和其他相关资源,它们通常用于组织和管理应用程序的各个部分。
- Bundle(捆绑包):bundle 是由 Webpack 根据模块之间的依赖关系生成的最终输出文件。它将多个模块打包成一个或多个捆绑包。在开发过程中,Webpack 会根据入口文件(entry)和模块之间的依赖关系,递归地构建一个或多个捆绑包。捆绑包通常是用于在浏览器中加载和执行的最终文件,包含了应用程序所需的所有代码和资源。
- Chunk(代码块):chunk 是 Webpack 在构建过程中生成的代码块,它是一种逻辑上的概念,表示一组相互依赖的模块。
- module 是 Webpack 处理的单个文件,代表了应用程序的组成部分
- bundle 是由 Webpack 生成的最终输出文件,它包含了所有模块的代码和资源
- chunk 是逻辑上的代码块,表示一组相互依赖的模块。它可以根据需要进行拆分和加载。
说说你对前端工程化的理解
前端工程化是指将前端开发中的设计、开发、测试和部署等环节进行标准化和自动化,以提高开发效率和代码质量,并降低维护成本。 具体而言,前端工程化包括以下方面:
- 模块化:使用模块化思想可以将复杂的代码拆分成小的可重用的模块,并且使得不同模块之间的依赖关系更加清晰。
- 自动化构建:通过使用构建工具(如 Gulp、Webpack、Rollup 等),可以自动化地完成代码编译、压缩、打包、转换、优化等任务,从而提高开发效率。
- 自动化测试:通过使用自动化测试框架和工具(如 Jest、Mocha、Chai、Selenium 等),可以自动化地完成单元测试、集成测试、UI 测试等任务,从而提高代码质量并减少故障。
- 自动化部署:通过使用自动化部署工具(如 Jenkins、Travis CI、GitLabCI/CD 等),可以自动化地完成代码上传、服务器部署、数据库更新等任务,从而减少手动操作产生的错误和漏洞。
- 规范化管理:通过使用代码规范(如 ESLint、Stylelint、Prettier 等)和版本控制系统(如 Git),可以规范开发流程和代码风格,提高代码可读性和可维护性。前端工程化是将前端开发中的设计、开发、测试和部署等环节进行标准化和自动化,以提高开发效率和代码质量,并降低维护成本。它是一种现代化的开发方式,适用于各种大小项目的开发,并且可以在不断变化的技术环境中保持竞争力。
如何减少打包后的代码体积
- 代码分割(Code Splitting):将应用程序的代码划分为多个代码块,按需加载
- Tree Shaking:配置 Webpack 的 Tree Shaking 机制,去除未使用的代码
- 压缩代码:使用工具如 UglifyJS 或 Terser 来压缩 JavaScript 代码
- 使用生产模式:在 Webpack 中使用生产模式,通过设置 mode: 'production'来启用优化
- 使用压缩工具:使用现代的压缩工具,如 Brotli 和 Gzip,来对静态资源进行压缩
- 利用 CDN 加速:将项目中引用的静态资源路径修改为 CDN 上的路径,减少图片、字体等静态资源等打包
如何提高 webpack 的打包速度
- 利用缓存:利用 Webpack 的持久缓存功能,避免重复构建没有变化的代码
- 使用多进程/多线程构建 :使用 thread-loader、happypack 等插件可以将构建过程分解为多个进程或线程
- 使用 DllPlugin 和 HardSourceWebpackPlugin: DllPlugin 可以将第三方库预先打包成单独的文件,减少构建时间。HardSourceWebpackPlugin 可以缓存中间文件,加速后续构建过程
- 使用 Tree Shaking: 配置 Webpack 的 Tree Shaking 机制,去除未使用的代码,减小生成的文件体积
- 移除不必要的插件: 移除不必要的插件和配置,避免不必要的复杂性和性能开销
说⼀下 webpack 的⼀些 plugin ,怎么使用 webpack 对项目进行优化?
- 减少编译体积: ContextReplacementPugin 、 IgnorePlugin 、babel-pluginimport 、 babel-plugin-transform-runtime
- 并行编译: happypack 、 thread-loader 、 uglifyjsWebpackPlugin 开启并行
- 缓存: cache-loader 、 hard-source-webpack-plugin 、uglifyjsWebpackPlugin 开启缓存 、 babel-loader 开启缓存
- 预编译: dllWebpackPlugin && DllReferencePlugin 、 auto-dll-webapck-plugin
性能优化:
- 减少编译体积 Tree-shaking 、 Scope Hositing
- hash 缓存 webpack-md5-plugin
- 拆包 splitChunksPlugin 、 import() 、 require.ensure
什么是 Webpack 的热更新(Hot Module Replacement)?原理是什么?
Webpack 的热更新,在不刷新页面的前提下,将新代码替换掉旧代码。 HRM 的原理实际上是 webpack-dev-server(WDS)和浏览器之间维护了一个 websocket 服务。当本地资源发生变化后,webpack 会先将打包生成新的模块代码放入内存中,然后 WDS 向浏览器推送更新,并附带上构建时的 hash,让客户端和上一次资源进行对比.
Webpack 的 Tree Shaking 原理
Webpack 的 Tree Shaking 是一个利用 ES6 模块静态结构特性来去除生产环境下不必要代码的优化过程。其工作原理在于:
- 当 Webpack 分析代码时,它会标记出所有的 import 语句和 export 语句。
- 然后,当 Webpack 确定某个模块没有被导入时,它会在生成的 bundle 中排除这个模块的代码。
- 同时,Webpack 还会进行递归的标记清理,以确保所有未使用的依赖项都不会出现在最终的 bundle 中。
vite 比 webpack 快在哪里?
- 开发模式的差异:在开发环境中,Webpack 是先打包再启动开发服务器,而 Vite 则是直接启动,然后再按需编译依赖文件
- 对 ES Modules 的支持:现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。Vite 充分利用了这一点,将开发环境下的模块文件直接作为浏览器要执行的文件,而不是像 Webpack 那样先打包,再交给浏览器执行。这种方式减少了中间环节,提高了效率
- 底层语言的差异:Webpack 是基于 Node.js 构建的,而 Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,Go 语言是纳秒级别的,而 Node.js 是毫秒级别的。因此,Vite 在打包速度上相比 Webpack 有 10-100 倍的提升。
- 热更新的处理:在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块;在 Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间
场景题
H5 如何解决移动端适配问题?
- viewport 标签:通过设置 viewport 标签的 meta 属性,来控制⻚⾯的缩放⽐例和宽度,以适配不同的设备
<meta name="viewport" content="width=device-width, initial-scale=1.0" - CSS3 的媒体查询:根据不同的设备宽度设置不同的样式,以适配不同的设备
web ⽹⻚如何禁⽌别⼈移除⽔印?
- 可以通过监听 DOM 的变化来检测是否有⼈删除⽔印,可以使⽤ MutationObserver API
// ⽬标节点
const targetNode = document.body;
// 创建 MutationObserver 实例
const observer = newMutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
// 检查是否有⼦节点被删除
if (mutation.removedNodes.length > 0) {
// 在此处判断是否有⽔印被删除// 如果⽔印被删除,则重新插⼊⽔印的 DOM 元素到⽬标节点
// 例如:
targetNode.appendChild(watermarkElement);
}
}
});
// 配置 MutationObserver
const config = { childList: true, subtree: true };
// 开始观察⽬标节点
observer.observe(targetNode, config);
⽤⼾访问⻚⾯⽩屏了, 原因是啥, 如何排查?
- ⽹络问题:⽤⼾的⽹络连接可能存在问题,⽆法正确加载⻚⾯内容。可以要求⽤⼾检查⽹络连接, 或者⾃⼰尝试在不同⽹络环境下测试⻚⾯的加载情况。
- 服务端问题:服务器未正确响应⽤⼾请求,导致⻚⾯⽆法加载。可以检查服务器的状态、⽇志和错误信息,查看是否有任何异常。同时,可以确认服务器上的相关服务是否正常运⾏。
- 前端代码问题:⻚⾯的前端代码可能存在错误或异常,导致⻚⾯⽆法正常渲染。可以检查浏览器的开发者⼯具,查看是否有任何错误信息或警告。同时,可以尝试将⻚⾯的 JavaScript、CSS 和 HTML 代码分离出来进⾏单独测试,以确定具体的问题所在。
- 浏览器兼容性问题:不同浏览器对于某些代码的⽀持可能不⼀致,导致⻚⾯在某些浏览器中⽆法正常加载。可以尝试在不同浏览器中测试⻚⾯的加载情况,同时使⽤浏览器的开发者⼯具检查是否有 任何错误或警告。
- 第三⽅资源加载问题:⻚⾯可能依赖于某些第三⽅资源(如外部脚本、样式表等),如果这些资源⽆法加载,可能导致⻚⾯⽩屏。可以检查⽹络请求是否正常,是否有任何资源加载失败的情况。
- 缓存问题:浏览器可能在缓存中保存了旧版本的⻚⾯或资源,导致新版本⽆法加载。可以尝试清除浏览器缓存,或者通过添加随机参数或修改⽂件名的⽅式强制浏览器重新加载⻚⾯和资源。
- 其他可能原因:⻚⾯⽩屏问题还可能由于安全策略(如 CSP、CORS 等)限制、跨域问题、DNS 解析问题等引起。可以使⽤浏览器的开发者⼯具检查⽹络请求和错误信息,查找可能的问题。
JS 中如何实现⼤对象深度对⽐?
function deepEqual(obj1, obj2) {
// 检查类型是否相同
if (typeof obj1 !== typeofobj2) {
return false;
}
// 检查是否是对象或数组
if (typeof obj1 === "object" && obj1 !== null && obj2 !== null) {
// 检查对象或数组⻓度是否相同
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (let key in obj1) {
// 递归⽐较每个属性的值
if (!deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
// ⽐较基本类型的值
return obj1 === obj2;
}
介绍⼀下 requestIdleCallback api?
requestIdleCallback 是⼀个 Web API,它允许开发者请求浏览器在主线程空闲时执⾏⼀些低优先级的后台任务,这对于执⾏如分析、整理状态和数据等不紧急的任务是理想的。这种⽅法可以提 ⾼⽤⼾的响应性和⻚⾯的整体性能。
-
何时使⽤ requestIdleCallback? requestIdleCallback 特别适合那些不直接关联⽤⼾交互及响应的任务,这些任务可以延后执⾏⽽不会明显影响⽤⼾体验。例如:
- 清理⼯作:如标记的 DOM 节点删除、数据的本地存储同步等。
- ⾮关键的解析:如解析⼤量数据。
- 状态更新:如发送不紧急的状态变更
-
如何使⽤ requestIdleCallback? 使⽤ requestIdleCallback ,你需要传递⼀个回调函数给它,此函数会在浏览器的空闲时间调⽤。你可以指定⼀个超时参数,它定义了浏览器在“空闲期”最多可以花费的时间来执⾏你的回调
requestIdleCallback(myNonCriticalFunction, { timeout: 5000 });- myNonCriticalFunction: 这是你想要浏览器在空闲时间执⾏的函数
- timeout: ⼀个可选的参数,表⽰回调执⾏时间的上限(以毫秒为单位)。如果超时,浏览器可能在下次空闲机会进⾏执⾏。
-
回调函数参数:你的回调函数会接收到⼀个 IdleDeadline 对象作为参数,通常命名为 deadline 。这个对象包含两个属性
- didTimeout ⼀个布尔值,如果超时已经被触发为 true
- timeRemaining 返回当前空闲阶段剩余时间的函数,单位是毫秒
function myNonCriticalFunction(deadline) { while ( (deadline.timeRemaining() > 0 || deadline.didTimeout) && someCondition() ) { // 执⾏⼯作直到时间⽤完或下次更新不是必要的 } // 如果还有未完成的⼯作,可以请求下⼀次空闲周期 if (someCondition()) { requestIdleCallback(myNonCriticalFunction); } } -
注意事项
- requestIdleCallback 不保证你的回调会在⼀个特定的时刻被调⽤,它只在浏览器需要的时候调⽤
- 执⾏低优先级任务时,不应该太过频繁或执⾏时间太⻓,以免影响⻚⾯性能
- 这个 API 为了最⼤化性能优化,会强制性地结束你的任务,在不迟于指定的超时时⻓执⾏结束
如何⼀次性渲染⼗万条数据还能保证⻚⾯不卡顿?
requestAnimationFrame + fragment(时间分⽚):requestAnimationFrame 也是个定时器,不同于 setTimeout ,它的时间不需要我们⼈为指定,这个时间取决于当前电脑的刷新率,如果是 60Hz ,那么就是 16.7ms 执⾏⼀次,如果是 120Hz 那就是 8.3ms 执⾏⼀次
const total = 100000;
let ul = document.getElementById("container");
let once = 20;
let page = total / once;
function loop(curTotal) {
if (curTotal <= 0) return;
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(() => {
let fragment = document.createDocumentFragment(); // 创建⼀个虚拟⽂档碎⽚
for (let i = 0; i < pageCount; i++) {
let li = document.createElement("li");
li.innerHTML = ~~(Math.random() * total);
fragment.appendChild(li); // 挂到fragment上
}
ul.appendChild(fragment); // 现在才回流
loop(curTotal - pageCount);
});
}
loop(total);
你认为组件封装的⼀些基本准则是什么?
- 单⼀职责原则:⼀个组件应该具有单⼀的功能,并且只负责完成该功能,避免组件过于庞⼤和复杂
- ⾼内聚低耦合:组件内部的各个部分之间应该紧密相关,组件与其他组件之间应该尽量解耦,减少对外部的依赖
- 易⽤性:组件应该易于使⽤,提供清晰的接⼝和⽂档,使⽤⼾能够⽅便地使⽤组件
- 可扩展性:组件应该具有良好的扩展性,能够⽅便地添加新的功能或进⾏修改,同时不影响已有的功能
- 可重⽤性:组件应该是可重⽤的,能够在多个项⽬中使⽤,减少重复开发的⼯作量
- ⾼效性:组件应该具有⾼性能和低资源消耗的特点,不会成为整个系统的性能瓶颈
- 安全性:组件应该具有安全性,能够防⽌恶意使⽤或攻击
- 可测试性:组件应该容易进⾏单元测试和集成测试,以保证组件的质量和稳定性
SPA ⾸屏加载速度慢的怎么解决?
- 影响因素:
- ⽹络延时问题
- 资源⽂件体积是否过⼤
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
- 解决方案:
- 减⼩⼊⼝⽂件积
- 静态资源本地缓存
- UI 框架按需加载
- 图⽚资源的压缩
- 组件重复打包
- 开启 GZip 压缩
- 使⽤ SSR
- 启⽤ CDN 加速
前端有哪些跨⻚⾯通信⽅式?
- 使⽤ URL 参数:可以通过 URL 参数在不同⻚⾯之间传递数据。例如,可以在 URL 中添加查询字符串参数来传递数据,并通过解析 URL 参数来获取传递的数据。
- 使⽤ localStorage 或 sessionStorage:可以使⽤浏览器的本地存储(localStorage 或 sessionStorage)在不同⻚⾯之间共享数据。⼀个⻚⾯可以将数据存储在本地存储中,另⼀个⻚⾯ 可以读取该数据。
- 使⽤ Cookies:可以使⽤ Cookies 在不同⻚⾯之间共享数据。⼀个⻚⾯可以将数据存储在 Cookie 中,另⼀个⻚⾯可以读取该 Cookie。
- 使⽤ postMessage API:postMessage API 允许不同窗⼝或 iframe 之间进⾏跨⻚⾯通信。可以使⽤ postMessage 发送消息,接收⽅可以通过监听 message 事件来接收消息。
- 使⽤ Broadcast Channel API:Broadcast Channel API 允许不同⻚⾯或不同浏览器标签之间进⾏⼴ 播式的消息传递。可以使⽤ Broadcast Channel 发送消息,其他订阅同⼀频道的⻚⾯都可以接收到消息。
- 使⽤ Shared Worker:Shared Worker 是⼀种特殊的 Web Worker,可以在多个⻚⾯之间共享。可以通过 Shared Worker 进⾏通信和共享数据。
- 使⽤ WebSocket:WebSocket 是⼀种双向通信协议,可以在不同⻚⾯之间建⽴持久的连接,实现实时的跨⻚⾯通信。
其他
说说设计模式?
- 单例模式:是一种只允许创建一个实例的模式。在前端开发中,常用于创建全局唯一的对象,例如全局的状态管理器、日志记录器等。单例模式可以保证全局只有一个实例,避免了重复创建和资源浪费的问题
- 工厂模式:是一种根据参数的不同创建不同对象的模式。在前端开发中,常用于创建不同类型的组件、插件等。工厂模式可以将对象的创建和使用分离,提高代码的灵活性和可维护性
- 观察者模式:是一种对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动更新。在前端开发中,常用于实现事件监听和消息订阅等。观察者模式可以降低对象间的耦合度,提高代码的可读性和可复用性
- 装饰器模式:是一种在不改变对象自身的基础上,动态地给对象增加新的功能的模式。在前端开发中,常用于实现组件的复用和功能的增强等。装饰器模式可以避免类的继承带来的复杂性和耦合度,提高代码的灵活性和可维护性
- 构造器模式:抽象了对象实例的变与不变(变的是属性值,不变的是属性名)
- 工厂模式:为创建一组相关或相互依赖的对象提供一个接口,且无须指定它们的具体类
- 观察者模式:当一个属性发生变化时,观察者会连续引发所有的相关状态变更
git reset 三种模式?
- soft:git reset --soft 版本号 :回退到指定版本,保留工作目录的内容,并把因为保留工作目录内容所带来的新的文件差异放进暂存区
- hard:git reset --hard 版本号 : 回退到指定版本,重置工作区和暂存区,只有指定版本中含有的内容(commited)。换句话说,就是没有 commit 的修改会被全部擦掉
- mixed:git reset --mixed 版本号 /默认模式 :回退到指定版本,保留工作区内容,暂存区会删掉。。简而言之,就是「把所有差异都混合(mixed)放在更改中」
git hotfix 流程?
- 确认问题:确认生产环境中存在的问题,并收集足够的信息以便进行修复
- 切换到主分支:
git checkout master - 拉取最新代码:
git pull - 创建 hotfix 分支:
git checkout -b hotfix_name - 进行 bug 修复
- 提交更改:
git add.git commit -m "fix:xxxxxx"git push - 合并 hotfix 分支到主分支:
git pullgit checkout mastergit merge hotfix - 提交:
git push
什么样的前端代码是好的?
- 高复用低耦合, 这样⽂件⼩, 好维护, 而且好扩展
- 具有可用性 、健壮性 、可靠性 、宽容性等特点
- 遵循设计模式的六大原则