HTML
常见块元素、行内元素、行内块元素,及其特点
(1)块元素
常见的有h1、p、div等。
特点:
- 独占一行
- 宽、高、外边距、内边距都可以控制
- 宽度默认为容器的100%
- 内部可以放行内/块元素
(2)行内元素
常见的有a、span、em等
特点:
- 不独占一行
- 宽、高设置无效
- 默认宽度是本身内容的宽度
- 行内元素内部,只能放文本和其他行内元素
(3)行内块元素
常见的有img、input等
特点:
- 不独占一行,一行可以显示多个
- 宽、高、外边距、内边距都可以控制
- 默认宽度是本身内容的宽度
localStorage、sessionStorage、cookie 三者明细区别
| cookie | localStorage | sessionStorage | |
|---|---|---|---|
| 存储大小 | 4kb | 5M | 5M |
| 通讯相关 | 随请求携带 | 不会自动携带 | 不会自动携带 |
| 存储格式 | 字符串 | 键值对 | 键值对 |
| 时效相关 | 可以设置 | 永久 | 会话级别,窗口关闭后清除 |
| 跨页面通讯 | 可以 | 只在本页面有效 |
网站TDK三大标签以及SEO优化
TDK:是网站标题title、描述description、关键词keywords的缩写,主要对当前网页进行总结和概述。
SEO:搜索引擎优化,目的是在搜索引擎上提升网站的排名,提高网站的知名度。必须要有三大标签。
img标签的title属性与alt属性的区别
- alt是给搜索引擎识别,在图像无法显示时的替代文本
- title属性是关于元素的注释信息,主要是给用户解读
- 在定义 img 对象时,将 alt 和 title 属性写全,可以保证在各种浏览器中都能正常使用
src 和 href 的区别
src和href都是HTML中特定元素的属性,都可以用来引入外部的资源。
- src:全称source,它通常用于img、video、audio、script元素,通过src指向请求外部资源的来源地址,指向的内容会嵌入到文档中当前标签所在位置,在请求src资源时,它会将资源下载并应用到文档内,比如说:js脚本、img图片、frame等元素。当浏览器解析到该元素时,会暂停其它资源下载,直到将该资源加载、编译、执行完毕。这也是为什么将js脚本放在底部而不是头部的原因。
- href:全称hyper reference,意味着超链接,指向网络资源,当浏览器识别到它指向的⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理,通常用于a、link元素。
title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别
title与h1的区别
- 从网站角度看,title 更重于网站信息。title可以直接告诉搜索引擎和用户这个网站是关于什么主题和内容的。
- 从文章角度看,h1则是用于概括文章主题。
- 一个网站可以有多个title,最好一个单页用一个title,以便突出网站页面主体信息,从seo看,title权重比h1高,适用性比h1广。
- 标记了h1的文字页面给予的权重会比页面内其他权重高很多。一个好的网站是h1和title并存,既突出h1文章主题,又突出网站主题和关键字。达到双重优化网站的效果。
b与strong 的区别
- b 是只是对文本的简单加粗,strong 是一个语义化标签,对相关文本具有强调作用
- b 标签只是侧重于字体加粗,strong标签加强字体的语气都是通过粗体来实现的,相比之下,搜索引擎更喜欢侧重于strong标签
- strong标签更注重于内容上的应用,在html中,对关键词的标明,然而还有一些网站上,也有使用strong标签登对小标题进行强调,但是在页面中,如果出现过多的strong标签,可能会对排名不利。
i 与 em 的区别
- i(italic)是实体标签,用来使字符倾斜,em(emphasis)是逻辑标签,作用是强调文本内容
- i标签只是斜体的样式,没有实际含义,常用来表达无强调或着重意味的斜体,比如生物学名、术语、外来语;
- em表示标签内字符重要,用以强调,其默认格式是斜体,但是可以通过CSS添加样式。
- 建议:为了符合CSS3的规范,i 标签应尽量少用而应改用 em
iframe的基本介绍与使用
介绍:iframe(内嵌框架)是 HTML 中一种用于将一个网页嵌入到另一个网页中的标签,它可以在一个页面中显示来自其他页面的内容。在网页中,使用iframe标签可以将一个网页嵌套在另一个网页中,实现网页间的互联互通。
优点:
- iframe能够原封不动的把嵌入的网页展现出来。
- 如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
- 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
- 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。
- 重载页面时不需要重载整个页面,只需要重载页面中的一个框架页(减少了数据的传输,增加了网页下载速度)
缺点:
- 页面样式调试麻烦,出现多个滚动条;
- 浏览器的后退按钮失效;
- 过多会增加服务器的HTTP请求;
- 小型的移动设备无法完全显示框架;
- 产生多个页面,不易管理;
- 不容易打印;
- 代码复杂,无法被一些搜索引擎解读。
- 搜索引擎的检索程序无法解读这种页面,不利于SEO;
场景总结:
- 运维性网站或继承性开发的网站,可以使用iframe
- 销售内,官网、展示性网站等建议不使用iframe
- 标准的网页设计是不使用iframe的
- 嵌入的网页必须与主页面同源,否则会受到浏览器的安全限制
- 嵌入的网页可能会影响页面性能和加载速度,特别是当嵌入的网页包含大量的资源(如图片、CSS 和 JavaScript)时
- 嵌入的网页可能会被恶意攻击者用于钓鱼或注入恶意代码的攻击,因此需要谨慎使用
浏览器渲染流程
- 解析html(parse),会生成dom树和cssOM树
- 样式计算(style),得到一颗带有样式的DOM树
- 布局(layout),得到布局树
- 分层(layer)
- 绘制(paint)
- 。。
css
link 与 @import 的区别和用法
<link href="styles.css" type="text/css" />
<style type="text/css">
@import url("styles.css");
</style>- 适用范围不同:import可以在网页中使用,也可以在css中使用,但link只能在网页中使用来引入css
- 加载顺序不同:页面被加载的时候,link引用的CSS会同时被加载,而@import引用的CSS会等到页面全部被下载完再被加载。所以有时候浏览@import加载CSS的页面时开始会没有样式(就是闪烁)
- 使用DOM控制样式时的差别:当使用JavaScript控制DOM去改变样式的时候,只能使用link标签,因为@import不是DOM可以控制的。
rgba和opacity的透明效果有什么不同
- opacity是一个属性。opacity属性的值,可以被其子元素继承,给父级div设置opacity属性,那么所有子元素都会继承这个属性,并且,该元素及其继承该属性的所有子元素的所有内容透明度都会改变。
- rgba是一个属性值。rgba设置的元素,只对该元素的背景色有改变,并且,该元素的后代不会继承该属性。
- 补充:rgba只是一个属性值,在background 里用改变背景色,在color里是改字体颜色,shadow里是改阴影色,不止是能够改元素的背景色,要看具体是在哪个属性上用
display:none与visibility:hidden的区别
这两个属性都是让元素隐藏,不可见。
区别:
- 渲染树:display: none 会让元素完全从渲染树中消失,渲染时不会占据任何空间;visibility: hidden 不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。
- 是否可继承:display: none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点的属性也无法显示;visibility: hidden 是继承属性,子孙节点消失是由于继承了hidden,通过设置visibility: visible可以让子孙节点显示。
- 重排重绘:修改常规文档流中元素的 display 通常会造成文档的重排,但是修改visibility属性只会造成本元素的重绘。
- 文档布局:display:none 隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,就当他从来不存在。visibility:hidden 隐藏对应的元素,但是在文档布局中仍保留原来的空间。
定位布局 position中的relative、absolute、fixed、sticky它们之间的区别
- static:默认值(静态定位),没有定位,元素出现在正常的文档流中。
- relative:相对定位,相对于自己本身在正常文档流中的位置进行定位。
- absolute:生成绝对定位,相对于最近一级定位不为static的父元素进行定位。会脱离标准文档流。
- fixed:生成固定定位,相对于浏览器窗口进行定位。和父元素没有关系,也会脱离文档流。
- sticky:生成粘性定位的元素,容器的位置根据正常文档流计算得出。
如何用CSS3画一条0.5px的直线
height: 1px;
transform: scale(0.5);如何用CSS3画一个三角形
思路:div宽高为0,设置border宽度,一边有颜色,其他为transparent透明。
<style>
.up{
width:0;
height:0;
border: 100px solid transparent;
border-top: 100px solid red;/*红色*/
}
</style>
<body>
<div class="up"></div>
</body>CSS3盒子模型:标准盒模型、怪异盒模型
盒子由内容区域content + 内边距padding + 边框border + 外边距margin组成
- 标准盒模型下,盒子的宽高指的是内容区域content的宽高,那么盒子的大小 = content + padding + border + margin
- 怪异盒模型下,盒子的宽高指的是内容、边框、内边距总的宽高(content + border + padding),那么怪异盒模型下盒子的大小 = width(content + border + padding)+ margin
可以通过属性 box-sizing 来设置盒子模型的解析模式:
{
box-sizing: content-box; // 标准盒模型
box-sizing: border-box; // 怪异盒模型
}问题1:相邻块元素垂直外边距的合并
解决方案:尽量只给一个盒子添加margin值
问题2:嵌套块元素垂直外边距的塌陷
解决方案:1、给父元素添加边框;2、给父元素添加内边距padding;3、给父元素设置overflow: hidden.
浮动和清除浮动
浮动:元素会脱离标准文档流,移动到指定位置,并且不再保留原先位置。
清除浮动:
- 清除浮动的本质是清除浮动元素造成的影响:父盒子没高度,子盒子浮动了,就会影响后面盒子的布局,就会有影响。
- 如果父盒子本身有高度,则不需要清除浮动。
- 清除浮动之后,父级就会根据浮动的子盒子自动检测高度,父级有了高度之后,就不会影响后面元素的布局。
清除浮动的方法:
- 额外标签法:官方推荐的做法,在浮动元素末尾添加一个额外的标签,并加上
clear: both;的样式。优:通俗易懂,书写方便。劣:添加一个额外的无意义标签。 - 父级添加overflow:给父级元素添加overflow样式,值为hidden、auto、scroll。优:代码简洁。劣:无法显示溢出的部分。
- 伪元素法:添加after伪元素。优:代码简洁、不添加额外元素。
.clearfix:after{
content: '';
display: block;
clear: both;
visibility: hidden;
height: 0;
}flex布局
flex布局父级相关的css属性:
- display: flex,将元素设置为flex布局。
- flex-direction,设置flex布局的方向,值为row、column、row-reverse、column-reverse。
- flex-wrap,设置flex布局是否换行,默认值为nowrap不换行,wrap换行。
- justify-content,设置主轴的对齐方式,值为flex-start、flex-end、center、space-around、space-between、space-evenly。
- align-items,设置交叉轴的对齐方式,值为flex-start、flex-end、center、stretch。
- align-content,设置多行下,交叉轴的对齐方式。
flex布局子元素相关的css属性:
- flex-shrink,设置是否可以缩小,0表示不能缩小。
- flex-grow,设置是否可以放大,0表示不能放大。
- flex-basis,设置项目的大小。
- flex,flex-grow、flex-shrink、flex-basis的简写形式。
transform 属性
- 水平位移:translate
- 缩放:scale
- 翻转:rotate
transition
语法:transition:[属性名] [持续时间] [速度曲线] [延迟时间]
示例:
transition:height 2s ease 0.5s;盒子居中的几种方法
- 利用定位(子绝父相)、margin-left、margin-top实现
- 利用定位(子绝父相)、transform属性实现
- 利用flex布局实现盒子居中
CSS3选择器及其优先级
选择器:
- 标签选择器、伪元素选择器,优先级1。
- 类选择器、伪类选择器、属性选择器,优先级10。
- id选择器,优先级100。
- 内联样式:1000。
注意:
- !important的优先级最高。
- 如果优先级相同,则最后出现的样式生效。
通用选择器(*)、子选择器(>)和相邻同胞选择器(+)并不在这四个等级中,所以它们的权值都为 0- 样式表的来源不同时,优先级顺序为:内联样式 > 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式。
结构伪类选择器&伪元素选择器
结构伪类选择器:可方便的选取一个或多个特定的元素
- :first-child 选取属于其父元素的首个子元素
- :last-child 选取属于其父元素的最后一个子元素
- :nth-child(n) 选择第n个子元素,n=even / 2n:选取偶数孩子;n=odd / 2n+1:选取奇数孩子
- first-of-type
- last-of-type
- nth-of-type(n)
伪元素选择器:
- ::first-letter / line: 文本第一个单词 / 第一行
- ::selection: 改变选中文本的样式
- ::before & ::after
属性选择器:
div[class=xx]: 选择类名为xx的divdiv[class^=xx]: 选择以类名为xx开头的divdiv[class$=xx]: 选择类名是以xx结束的divdiv[class*=xx]: 选择类名带有xx的div
display的block、inline和inline-block的区别
定位堆叠顺序z-index
在使用定位布局时,可能会出现盒子重叠的情况。此时,可以使用 z-index 来控制盒子的前后次序 (z轴)。
如果属性值相同,则按照书写顺序,后来居上。
实现双飞翼(圣杯)布局
- 利用定位实现两侧固定中间自适应。父盒子设置左右 padding 值,给左右盒子的 width 设置父盒子的 padding 值,然后分别定位到 padding 处,中间盒子自适应。
- 利用 flex 布局实现两侧固定中间自适应。父盒子设置 display:flex,左右盒子设置固定宽高,中间盒子flex:1。
实现图片懒加载的原理
图片懒加载的原理很简单,就是我们先设置图片的 data-set 属性(当然也可以是其他任意的,只要不会发送 http 请求就行了,作用就是为了存取值)值为其图片路径,由于不是 src,所 以不会发送 http 请求。然后我们计算出页面 scrollTop 的高度和浏览器的高度之和,如果 图片距离页面顶端的坐标 Y(相对于整个页面,而不是浏览器窗口)小于前两者之和,就说明图片就要显示出来了(合适的时机,当然也可以是 其他情况),这时候我们再将data-set 属性替换为 src 属性即可。
BFC 是什么
定义:BFC (Block formatting context) 直译为 “块级格式化上下文”。它是一个独立的渲染区域,只有 Block-level box 参与,它规定了内部的 Block-level Box 如何布局,并且与这个区域外部毫不相干。
布局规则:
- 内部的 Box 会在垂直方向,一个接一个地放置
- BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此
- Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin会发生重叠
- 每个元素的 margin box 的左边,与包含块 border box 的左边相接触(对于从左往 右的格式化,否则相反)。即使存在浮动也是如此
- BFC 的区域不会与 float box 重叠
- 计算 BFC 的高度时,浮动元素也参与计算
哪些元素会生成 BFC:
- 根元素
- float 属性不为 none
- overflow 不为 visible
- position 为 absolute 或 fixed
- display 为 inline-block,table-cell,table-caption,flex,inline-flex
js
JS基础类型和复杂类型
基础类型:string、number、boolean、undefined、null、symbol
复杂类型(引用类型):Object、Array、Function
箭头函数与普通函数的区别
- 代码更加简洁:无参数可以直接写空括号,只有一个参数可以省略括号,函数体只有一个return语句的话可以省略大括号。
- 箭头函数没有自己的this:函数内部不会创建自己的this,它只会访问自己作用域的上一层的this,本质是通过闭包来访问的。
- 箭头函数继承来的this指向永远不会改变
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
- 箭头函数不能作为构造函数使用:由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
- 箭头函数没有自己的arguments对象:在箭头函数中访问arguments实际上获得的是它外层函数的arguments值(也是闭包)。
- 箭头函数没有prototype。
JS中null和undefined的判断方法和区别
undefined表示缺少值,即此处应该有值,但是还没有定义。null表示一个值被定义了,但是这个值是空值。
typeof null; // 'object'
typeof undefined; // 'undefined'
Number(null); // 0
Number(undefined); // NaN 原型链和继承
基础定义:函数的原型对象prototype、原型对象的constructor、对象的原型__proto__。
原型链:访问对象属性时,如果这个对象上本身没有这个属性时,它就会去它的原型__proto__上去找,如果还找不到,就去原型的原型上去找…一直直到找到最顶层(Object.prototype)为止,Object.prototype对象也有__proto__属性,值为null。
原型链的作用:实现继承。
继承的几种方式:原型链继承、构造函数继承、组合继承、寄生组合继承。
this指向
函数中的this指向,是取决于如何调用的:
- 通过new调用,this指向新对象
- 直接调用,this指向全局对象,浏览器是window,node是global
- 通过对象调用,this指向前面的对象
- 通过call、apply、bind,this指向第一个参数。(特殊的,如果bind返回的函数通过new调用,则this指向新创建的对象)
理解箭头函数中的this:
箭头函数中的this的指向,取决于箭头函数定义的位置,而不是运行的位置。这是因为箭头函数中没有定义this,会基于闭包(词法作用域)从外层寻找this。
闭包
概念:有权访问另一个函数内部变量的函数。
闭包用处:1、读取另一个函数内部的变量。2、这些变量的值始终会保持在内存中,不会在外层函数调用后被自动清除。
优点:1、变量会一直在内存中,不会被清除;2、可以视为私有属性,不会被外部访问;3、避免全局变量的污染(三方库的封装)。
闭包缺点:变量长期储存在内存中,会增大内存的使用量,使用不当会造成内存泄露
内存泄漏怎么解决:首先避免闭包的使用,其次的话就是变量执行完以后,可以让它赋值为null,最后利用JS的一个垃圾回收机制进行回收。
- 解释一下作用域链是如何产生的。
- 解释一下js执行上下文的创建、执行过程。
- 解释一下闭包所产生的变量放在哪了。
垃圾回收
引用计数(老版本,无法解决循环引用的问题)、标记清除(现代浏览器)
V8引擎中新生代和老生代的垃圾回收机制。
标记清除的工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记。
- 再被加上标记的会被视为准备删除的变量。
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
js列举和数组操作相关的方法
- 添加元素:push、unshift
- 删除元素:pop、shift、splice
- 查找元素:indexOf、lastIndexOf、find、findIndex
- 排序和翻转:sort、reverse
- 遍历和过滤:forEach、map、filter、reduce
- 归并数组:concat、slice
- 其它:join、split等等
js数组去重的方法
- Set
- Map
- array.filter + indexof
- array.reduce
typeof和instanceof的区别是什么
JS中 "=="和"==="的区别详解
"==":两个等号我们称为等值符,当等号两边的值为相同类型时比较值是否相同,类型不同时会发生类型的自动转换,转换为相同的类型后再作比较。也就是说两个等号只要值相等就可以。"===":三个等号我们称为等同符,当等号两边的值为相同类型的时候,直接比较等号两边 的值,值相同则返回 true,若等号两边的值类型不同时直接返回 false。也就是说三个等号既要判断值也要判断类型是否相等。
如何用原生 JS给一个按钮绑定两个 onclick 事件?
使用addEventListener
var、let和const的区别
- 全局污染
- 块级作用域
- 暂时性死区
- 重复声明
call、apply、bind的区别
相同点:都是改变this的指向,指向第一个参数。
不同点:
- call,接收的是参数列表,返回函数执行的结果。
- apply,接收的是数组,返回函数执行的结果。
- bind,不会立即执行函数,而是返回一个新的函数。作为构造函数时,绑定的this会失效,指向新对象。
Function.prototype.myCall = function (ctx, ...args) {
// 参数归一化
ctx = ctx === undefined || ctx === null ? globalThis : Object(ctx)
// 拿到要执行的函数
const fn = this
// 绑定函数到ctx,(优化:不能被遍历到)
const key = Symbol("function_call")
Object.defineProperty(ctx, key, {
value: fn,
enumerable: false,
})
// 执行函数并返回对应的结果
const result = ctx[key](...args)
delete ctx[key]
return result
}
function test(a, b) {
console.log(this, a, b, this.name)
return a + b
}
console.log(test.myCall({ name: "tttt" }, 1, 3))
Function.prototype.myBind = function (ctx, ...args) {
// 要执行的函数
const fn = this
return function A(...rest) {
// 如果是 new 调用的,保留new的行为
if (Object.getPrototypeOf(this) === A.prototype) {
// 或者 this instanceof A
return new fn(...args, ...rest)
}
// 执行函数并返回结果
return fn.apply(ctx, [...args, ...rest])
}
}
function test(a, b, c, d) {
console.log(this, a, b, c, d)
return a + b
}
const newFn = test.myBind({ name: "aa" }, 1, 2)
console.log(newFn(3, 4))
console.log(new newFn(3, 4))
栈溢出及解决方法
缓冲区溢出是由于C语言系列设有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。栈溢出就是缓冲区溢出的一种。
栈溢出的解决方法:
- 减少栈空间的需求,不要定义占用内存较多的auto变量,应该将此类变量修改成指针,从堆空间分配内存。
- 函数参数中不要传递大型结构/联合/对象,应该使用引用或指针作为函数参数。
- 减少函数调用层次,慎用递归函数,例如A→B→C→A环式调用。
JS如何实现多线程
概念:JavaScript本身是单线程的,但是可以通过实现多线程来提高性能和用户体验。多线程允许JavaScript在等待用户交互或网络请求时,执行其他任务,从而提高页面加载速度和响应速度。
实现多线程的方式:Web Workers、SharedArrayBuffer、WebAssembly等。其中,Web Workers允许在后台线程中运行JavaScript代码,而SharedArrayBuffer和BufferSource API则允许在多个线程之间共享数据。
浅拷贝和深拷贝
浅拷贝:Object.assign()、array.slice()、es6扩展运算符
深拷贝:JSON.parse(JSON.stringify(obj))、三方库如lodash等
事件循环
简答:如何理解js的异步
js是一门单线程的语言,这是因为他运行在浏览器的渲染主线程中,而渲染主线程只有一个。 而渲染主线程承担着很多工作,渲染页面、执行js都在其中。 如果使用同步的方式,就会导致渲染主线程产生阻塞,从而导致消息队列中的其他任务无法得到执行。这样的话,一方面会导致繁忙的主线程白白消耗时间,另一方面页面无法及时更新,给用户造成卡死的现象。 所以浏览器采用异步的方式来避免。具体做法是当某任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即执行任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器可以实现永不阻塞,从而最大限度的保证了渲染主线程的流畅运行。 总结:单线程是异步产生的原因,事件循环是异步的实现方式。
简答:阐述浏览器的事件循环
事件循环又称消息循环,是浏览器渲染主线程的工作方式。 在chrome的实现中,会开启一个无限循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候,将任务加入到队列末尾即可。 过去把消息队列分为宏队列和微队列,目前已不适用,取而代之的是一种更灵活多变的处理方式。 根据w3c的官方解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务执行。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
js操作数组、对象、Map的复杂度
数组:
- 访问元素:O(1),原因:数组是基于连续内存的,通过索引可以直接计算出内存地址。
- 末尾插入/删除元素,arr.push() / arr.pop(),O(1),原因:动态数组在末尾插入/删除元素不需要移动其他元素。
- 开头插入或删除元素,arr.unshift() / arr.shift(),O(n),原因:需要移动所有元素以腾出空间或填补空缺。
- 查找/遍历元素:arr.indexOf() / arr.includes() / arr.find() / arr.filter() / arr.forEach() / arr.map() / arr.reduce(),O(n)
- 排序:arr.sort(),O(n log n),现代 JavaScript 引擎通常使用高效的排序算法(如快速排序或归并排序)。
Map:JavaScript 的 Map 是基于哈希表实现的,类似于其他语言中的 HashMap。
- 插入/删除元素:map.set / map.delete,O(1)
- 查找元素:map.get、map.has,O(1)
- 遍历:map.forEach() / map.keys() / map.values() / map.entries(),O(n)
对象:
- 属性查找:O(1)
注意事项:
- 哈希冲突:Map 的复杂度在哈希冲突较多时可能退化为 O(n),可以采用拉链法解决冲突。
- 动态数组扩容:数组在容量不足时需要扩容,扩容操作的时间复杂度为 O(n),但均摊到每次插入操作后仍然是 O(1)。
- 引擎实现:不同 JavaScript 引擎(如 V8、SpiderMonkey)可能对数组和 Map 的实现有细微差异,但时间复杂度通常符合上述分析。
0.1 + 0.2
JavaScript 中的浮点数:都是以 双精度浮点数(double-precision floating-point format)来存储的。这是根据 IEEE 754 标准定义的,意味着它遵循与其他编程语言类似的浮点数表示规则。然而,双精度浮点数在表示某些十进制数时也存在精度问题。就像我们在前面提到的 0.1 和 0.2,这些数字在二进制浮点数中无法精确表示。
0.1 和 0.2 在二进制浮点数中无法被精确表示,它们是循环小数。当你执行 0.1 + 0.2 时,计算机在存储这些值时会做出一定的舍入,从而导致最终的计算结果不等于你期望的 0.3。
解决方式:建议使用专门的计算库Decimal.js
ts
interface (接口) 和 type (类型别名) 有什么区别?应该如何选择?
- 共同点:都可以定义对象形状。
- interface 特点:声明合并、可继承 (extends)、可被类实现 (implements)。主要用于定义对象/类结构契约。
- type 特点:更灵活,可以为任何类型创建别名(联合、交叉、元组、原始类型等),不能声明合并,通常用于定义联合/交叉类型或使用高级类型。
- 选择:定义公共 API 或期望被扩展用 interface,定义联合/交叉类型或简单类型别名用 type,应用内部对象两者皆可,看团队规范。
any, unknown
- any:任意类型,关闭类型检查(应避免使用)。
- unknown:类型未知,是 any 的类型安全版本,使用前需要进行类型检查或断言。
vue
v-show 与 v-if 的区别
- v-show指令是通过修改元素的display的CSS属性让其显示或者隐藏;
- v-if指令是直接 销毁 和 重建 DOM达到让元素显示和隐藏的效果;
- 使用 v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。
keep-alive 的作用是什么
官网解释:包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。
作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。需要缓存组件 频繁切换,不需要重复渲染。
场景:tabs标签页、后台导航、vue性能优化。
原理:Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。
nextTick的实现
- nextTick是Vue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM。
- Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中1次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用
- 比如,我在干什么的时候就会使用nextTick,传一个回调函数进去,在里面执行dom操作即可。简单了解nextTick的实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。
- 实现原理:在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
Vue 组件之间的通信方式
- 父组件向子组件传值:props
- 子组件向父组件传值:emit事件
- $ref获取子组件的实例
- provide、inject:实现多层级注入数据
- event-bus事件总线
Vuex的理解及使用场景
Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。
Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation,这样使得我们可以方便地跟踪每一个状态的变化。
Vuex主要包括以下几个核心模块:
- State:定义了应用的状态数据
- Getter:在 store 中定义”getter”(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
vue 的生命周期
- beforeCreate:在实例创建之间执行,数据是未加载状态。创建一个Vue实例,此时实例上只有一些生命周期函数和默认的事件。此时data computed watch methods上的方法和数据均不能访问。
- created:在实例创建、数据加载后,能初始化数据,DOM渲染之前执行。可以对data数据进行操作,可进行一些请求,请求不易过多,避免白屏时间太长。
- beforeMount:虚拟DOM已创建完成,在数据渲染前最后一次更改数据。el未挂载。判断el的挂载方式。判断是否有template设置。将template进行渲染保存到内存当中,还未挂载在页面上。
- mounted:页面、数据渲染完成。el挂载完毕。可以访问DOM节点。将内存中的模版挂载到页面上。此时可以操作页面上的DOM节点。此时组件从创建阶段进入运行阶段
- beforeUpdate:重新渲染之前触发。不会造成重渲染。页面显示的数据是旧的,此时data里面的数据是最新,页面数据和data数据暂未同步。
- updated:数据已经更新完成,DOM也重新render完成,更改数据会陷入死循环。根据data里的最新数据渲染出最新的DOM树,然后将最新的DOM挂载到页面。此时data和页面数据一致,都是最新的。
- beforeDestroy:实例销毁前执行,实例仍然完全可用。此时组件从运行阶段进入到销毁阶段。组件上的data和methods以及过滤器等都处于可用状态,销毁还未执行。
- destroyed:实例销毁后执行,这时候只剩下DOM空壳。组件已经被完全销毁,组件中所有的数据、方法、指令、过滤器等,都已不可用。
简述Vue每个周期具体适合哪些场景
- beforeCreate:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法
- created:data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作
- beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的
- mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。如果我们想要通过插件操作页面上的DOM节点,最早可以在和这个阶段中进行
- beforeUpdate:当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的,页面还没有和最新的数据保持同步
- updated:页面显示的数据和data中的数据已经保持同步了,都是最新的
- beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods,指令,过滤器 ……都是处于可用状态。还没有真正被销毁
- destroyed:这个时候上所有的 data 和 methods,指令,过滤器 ……都是处于不可用状态。组件已经被销毁了。
简述MVVM 和MVC的原理以及区别
MVVM视图模型双向绑定,是Model-View-ViewModel的缩写。MVVM的优点:
- 低耦合。视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上,当View变化的时候Model可以不变化,当Model变化的时候View也可以不变。
- 可重用性。你可以把一些视图逻辑放在一个Model里面,让很多View重用这段视图逻辑。
- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
- 可测试。
MVC是应用最广泛的软件架构之一,一般MVC分为:Model(模型),View(视图),Controller(控制器)。这主要是基于分层的目的,让彼此的职责分开.View一般用过Controller来和Model进行联系。Controller是Model和View的协调者,View和Model不直接联系。基本都是单向联系。M和V指的意思和MVVM中的M和V意思一样。C即Controller指的是页面业务逻辑。MVC是单向通信。也就是View跟Model,必须通过Controller来承上启下。
MVC与MVVM的区别?
- MVC和MVVM的区别并不是VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。也就是说MVVM实现的是业务逻辑组件的重用。
- MVC中Controller演变成MVVM中的ViewModel
- MVVM通过数据来显示视图层而不是节点操作
- MVVM主要解决了MVC中大量的dom操作使页面渲染性能降低,加载速度变慢,影响用户体验
vue常见指令
- v-model 多用于表单元素实现双向数据绑定
- v-bind:简写为冒号:”:“,动态绑定一些元素的属性,类型可以是:字符串、对象或数组。
- v-on:click 给标签绑定函数,可以缩写为:”@“,例如绑定一个点击函数 函数必须写在methods里面
- v-for 格式:v-for=“字段名 in(of) 数组json” 循环数组或json,记得加上key
- v-show 显示内容
- v-if 指令:取值为true/false,控制元素是否需要被渲染
- v-text 解析文本
- v-html 解析html标签
vue中的data为什么是一个函数?起到什么作用?
- 在Vue组件中,data选项必须是一个函数,而不能直接是一个对象。这是因为Vue组件可以同时存在多个实例,如果直接使用对象形式的data选项,那么所有的实例将会共享同一个data对象,这样就会造成数据互相干扰的问题。
- 因此,将data选项设置为函数可以让每个实例都拥有自己独立的data对象。当组件被创建多次时,每个实例都会调用该函数并返回一个新的data对象,从而保证了数据的隔离性。
- 另外,data选项作为一个函数还具有一个重要的特性,就是它可以接收一个参数,这个参数是组件实例本身。这个特性在一些场景下非常有用,例如在定义组件时需要使用组件实例的一些属性或方法来计算初始数据。
- 因此,为了避免数据共享和保证数据隔离性,以及方便使用组件实例的属性和方法,Vue组件中的data选项必须是一个函数。
vue中ref的作用
- 获取DOM元素的引用
- 获取子组件的引用
- 利用 v-for 和 ref 获取一组数组或者dom 节点
vue中hash和history的区别
Vue-router的路由分为hash和history模式
hash模式:
- 当#后面的url地址发生变化时,浏览器不会向服务器发送请求,故不会刷新页面
- 当#后面的url地址发生变化时,会触发hashChange(hash模式得核心实现原理)事件,从而,我们可以通过监听hashChange事件来知道路由发生变化,从而进一步去更新我们的页面
- 只可修改hash部分
- 当浏览器刷新时,浏览器只会向服务器去请求# 前面的域名服务器下根目录下的index.html文件
history模式
- history模式得路由和域名之间直接通过/连接,无#符分隔,就是普通url形式
- history模式当发生路由跳转时,通过HTML5的history.pushState()方法或者history.replaceState() 方法改变地址栏地址,并将地址的改变记录到浏览器访问栈中。(这里有一点需要注意,它只改变了浏览器地址栏中的地址,但并不会像服务器去发送请求)
- 当浏览器前进,后台,或者调用back(),forward(), go()等方法时,会触发popstate事件。故,我们可以通过监听popstate事件来获取最新的路由地址,从而更新页面
- 通过pushstate() 修改的url可以是与当前url同源的任意url。
- 需要和服务器配合使用,否则容易出现页面404的情况
Vue3
Vue2.0和Vue3.0的区别
vue经历从2.0到3.0更新之后,简⽽⾔之就是变得更轻,更快,使⽤起来更加⽅便,每⼀次的版本迭代都是对上⼀个版本的升级优化,不管 是对于我们开发者还是对于⽤户体验都是不断地在越来越⽅便,接下来我会着重于开发者来说⼀下两个不同版本的区别。
- 工程化有差别:打包工具Vite替代了webpack;更适合typescript。
- 在开发过程中两个版本的使⽤⽅法虽然在表⾯上没有太⼤的⼀个区别,但是在他的底层⽅⾯去看的话区别还是很⼤的,其中就包括渲染⽅式,数据监听,双向绑定,⽣命周期,vue3更精准变更通知,这⾥着重说⼀下关于双向绑定的更新。 - vue2 的双向数据绑定是利⽤ES5的⼀个 API,Object.definePropert()对数据进⾏劫持 结合发布订阅模式的⽅式来实现的。 - vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽实现对数据的监控。
Vue3带来了什么改变
- 性能的提升:打包大小减少41%,初次渲染快55%, 更新渲染快133%,内存减少54%。
- 源码的升级:使用Proxy代替defineProperty实现响应式,重写虚拟DOM的实现和Tree-Shaking。
- 拥抱TypeScript:Vue3可以更好的支持TypeScript。
- 新特性:Composition API(组合API),setup、ref与reactive、watch与watchEffect、provide/inject
- 新的内置组件:Fragment、Teleport、Suspense
- 生命周期重命名:beforeDestroy改名为 beforeUnmount,destroyed改名为 unmounted
Vue3.0中的响应式原理是什么?vue2的响应式原理是什么?
vue2.x的响应式
实现原理:
- 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。
- 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
存在问题:
- 新增属性、删除属性, 界面不会更新。
- 直接通过下标修改数组, 界面不会自动更新。
Vue3.0的响应式
实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'vue3响应式数据的判断:
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
- isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
- isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
Proxy的优势:
- 可以对整个对象进行监听,而无需遍历属性。
- 添加和删除对象的属性时,也支持响应式。
vue3的常用 Composition API有哪些?
setup:Vue3.0中一个新的配置项,值为一个函数。
- 执行时机在beforeCreate之前,this是undefined。
- setup的参数,props、context、attrs、slots、emit
- 返回值:返回值可以是一个对象(即模版中可以使用的数据),也可以是一个渲染函数(则可以自定义渲染内容)。
ref:定义一个响应式的数据
- 语法:
const xxx = ref(initValue) - 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。
- JS中操作数据:xxx.value
- 模板中读取数据: 不需要.value,直接:
<div>{ {xxx}}</div>
diff算法
参考:https://blog.csdn.net/qq_36384657/article/details/137630990
vue3 diff算法做了哪些优化:
- 静态树提升,Vue3使用静态树提升技术,将静态内容从动态内容中分离出来,并在渲染时只更新动态内容,从而减少不必要的更新操作,提高性能。
- 优化的算法逻辑:Vue3对diff算法的逻辑进行了优化,进行前缀后缀处理,并引入最长递增子序列,减少元素移动次数。使得在更新过程中能够更快地定位变化并进行更新,减少不必要的操作。
diff算法比较流程
- 比较是否是相同节点;相同节点比较属性,并复用老节点;
- 如果是相同节点,考虑老节点和新节点的儿子节点情况: - 老的没有儿子,新的有儿子,将新的儿子节点挂载给老节点; - 老的有儿子,新的没儿子。删除页面节点; - 老的有儿子,新的有儿子,但都是文本节点,直接更新文本节点; - 老的儿子是一个列表,新的儿子也是一个列表。核心diff,双端比较。
vue的patch
当组件中数据更新时,会通过调用 render 方法为组件生成一个新的 VNode,将新的 VNode 与 旧的 VNode 通过 diff 算法进行比较,查找本次需要更新的内容,接着执行 DOM 操作去更新对应节点.
updateComponent = () => {
// 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode
vm._update(vm._render(), hydrating)
}首先会执行 vm._render() 得到组件的 VNode,接着将 VNode 传递给 vm._update 方法,就正式开始 patch 阶段.
Vue 中的 patch 都做了些什么?
Vue 的 patch 负责组件的 首次渲染、后续更新、销毁组件
- 如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模版节点
- 如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode - 首先是全量更新所有的属性 - 如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程 - 老的 VNode 没孩子,如果新的 VNode 有孩子,则新增这些新孩子节点 - 如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点 - 如果不符合上面的几种,说明属于更新文本节点
- 如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy 销毁老节点
React
JSX的理解
当 Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babel和webpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一起了。
- 可以将HTML语言直接写在JavaScript语言之中,不加任何引号,这就是JSX的语法,它允许HTML与JavaScript的混写。
- JSX允许直接在模板插入JavaScript变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
- 防注入攻击:React DOM在渲染之前默认会过滤所有传入的值。它可以确保应用不会被注入攻击。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止XSS(跨站脚本攻击)。
- Babel转译器会把JSX转换成一个名为React.createElement()的方法调用。
React 的生命周期方法有哪些
可分为三个阶段:挂载阶段、更新阶段和卸载阶段。同时,React还提供了钩子函数的方式(如useEffect钩子)来完成与生命周期相关的操作。
挂载阶段:
- constructor,组件实例化时调用,用于初始化状态和绑定方法。
- static getDerivedStateFromProps,在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
- render,渲染组件的内容。
- componentDidMount,组件挂载后调用,可以进行异步操作、订阅事件等。
更新阶段:
- static getDerivedStateFromProps,在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
- shouldComponentUpdate,在渲染之前调用,用于决定是否重新渲染组件,默认返回true。
- render:渲染组件的内容。
- getSnapshotBeforeUpdate:在最终将内容渲染到DOM之前调用,用于获取DOM更新前的快照。
- componentDidUpdate:组件更新后调用,可以进行DOM操作、发起网络请求等。
卸载阶段:
- componentWillUnmount:组件卸载前调用,可以进行清理操作,如取消订阅、清除定时器等。
其他:
- static getDerivedStateFromError:在子组件渲染过程中,如果发生错误,会调用该方法,返回一个新的状态。
- componentDidCatch,在子组件渲染过程中,如果发生错误,会调用该方法,用于记录错误信息或上报错误。
React的Hooks详解
Hook 是一些可以让你在函数组件里”钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用——这使得你不使用 class 也能使用 React。React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。
React提供了多个常用的Hooks,用于在函数组件中管理状态、处理副作用和访问React的上下文等。
- useState:用于在函数组件中添加状态。
- useEffect:用于在函数组件中执行副作用操作。
- useContext:用于在函数组件中访问React的上下文。
- useReducer:用于在函数组件中使用Reducer模式来管理状态。
- useRef:用于在函数组件中创建可变的引用。
- useMemo:用于在函数组件中缓存计算的值。
- useCallback:用于在函数组件中缓存函数。
- useLayoutEffect:类似于useEffect,但在DOM更新之后同步执行。
- useImperativeHandle:用于在函数组件中自定义外部组件实例的暴露。
- useDebugValue:用于在自定义Hooks中显示自定义的调试值。
React和Vue.js的相似性和差异性是什么
相似性:
- 组件化:React和Vue都推崇组件化的开发方式,允许开发者将UI拆分成独立、可复用的组件。
- 虚拟DOM:两者都使用了虚拟DOM来提高渲染性能,通过最小化需要实际更新的DOM元素来提升性能。
- 响应式数据绑定:虽然实现方式不同,但React和Vue都提供了响应式数据绑定的机制,使得数据和UI能够自动同步。
- 单向数据流:React和Vue都倾向于使用单向数据流(尽管Vue在某些情况下也支持双向绑定)。
区别:
- 性能优化:React:通过Fiber架构改进了渲染性能,使得React在处理大型应用时更加高效。Vue:通过虚拟DOM的优化和一些内置的指令(如v-once)提供了一些性能优化手段。
- react数据不可变,Vue主要是基于是数据可变的响应式思想,当属性变化的时候,响应式的更新对应的虚拟dom。
- 编译&写法:React:思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等。Vue:把html,css,js组合到一起,用各自的处理方式,Vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
React的一些主要优点
- 提高了应用程序的性能
- 可以方便地在客户端和服务器端使用
- 由于有了JSX,代码的可读性提高了
- 使用React,编写UI测试用例变得非常容易
如何理解React State不可变性的原则
在 React 中,不可变性是指数据一旦被创建,就不能被修改。React 推崇使用不可变数据的原则,这意味着在更新数据时,应该创建新的数据对象而不是直接修改现有的数据。
- 数据一旦创建就不能被修改:在 React 中,组件的状态(state)和属性(props)应该被视为不可变的。一旦创建了状态或属性对象,就不应该直接修改它们的值。这样可以确保组件的数据在更新时是不可变的,从而避免意外的数据改变和副作用。
- 创建新的数据对象:当需要更新状态或属性时,应该创建新的数据对象。这可以通过使用对象展开运算符、数组的 concat()、slice() 等方法,或者使用不可变数据库(如Immutable.js、Immer 等)来创建新的数据副本。
- 比较数据变化:React 使用 Virtual DOM 来比较前后两个状态树的差异,并仅更新需要更新的部分。通过使用不可变数据,React 可以更高效地进行比较,因为它可以简单地比较对象引用是否相等,而不必逐个比较对象的属性。
- 性能优化:使用不可变数据可以带来性能上的优势。由于 React 可以更轻松地比较前后状态的差异,可以减少不必要的重新渲染和组件更新,提高应用的性能和响应性。
不可变性的原则在 React 中有以下好处:
- 简化数据变更追踪:由于数据不可变,可以更轻松地追踪数据的变化。这样可以更好地理解代码的行为和数据的流动。
- 避免副作用:可变数据容易引发副作用和难以追踪的 bug。通过使用不可变数据,可以避免许多与副作用相关的问题。
- 方便的历史记录和回滚:不可变数据使得记录和回滚应用状态的历史变得更容易。可以在不改变原始数据的情况下,创建和保存不同时间点的数据快照
React Fiber
是React 16版本中引入的一种新的协调引擎,旨在解决旧版React在处理大型应用时的性能和灵活性问题。Fiber的核心设计理念包括增量渲染、可中断更新和优先级控制,这些特性使得React能够更好地管理组件树的更新和渲染过程。
Fiber的设计理念和核心特性:
- 增量渲染:Fiber将渲染任务拆分成多个小任务,每次处理一部分任务,完成后将控制权交回浏览器,再继续处理剩余任务。这种方式避免了主线程的长时间占用,提升了页面的响应速度。
- 可中断更新:Fiber允许渲染过程在必要时被中断,以便优先处理高优先级的任务(如用户输入)。这通过将渲染工作分解为多个小任务并在主线程空闲时执行这些任务来实现。
- 优先级控制:Fiber为每个更新任务分配优先级,确保高优先级的任务能够快速得到处理,而不会被低优先级的任务所阻塞。这通过React内部的”优先级队列”来实现。
Fiber的工作原理:Fiber通过任务切片(Time Slicing)将渲染任务分解为多个小任务,这些小任务在浏览器空闲时逐步执行。使用requestIdleCallback或scheduler调度渲染任务,确保高优先级的任务能够优先处理。此外,Fiber使用双缓冲机制(Double Buffering),维护两棵Fiber树:当前树和工作进行树,更新完成后交换角色,确保渲染的连贯性。
将当前时间分片(5ms)结束后,若任务还没执行完,将任务暂时挂起到任务队列(记录当前fiber节点的位置),让出主线程去执行用户输入/ui渲染/网络等高优先级操作,如果高优先级操作产生高优先级的任务,此时需要马上响应,将高优先级放到任务队列顶层,等到下一次事件循环时取出顶层任务(高优先级任务or挂起的任务)继续执行。按以上机制循环往复,直到执行完毕。(每个任务都有自己的过期时间,如果到了过期时间他也会变成一个类似高优先级任务,在下一个时间片(5ms)优先执行。
diff算法
React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另一棵在React状态变更将要重新渲染时生成。React通过比较这两棵树的差异,决定是否需要修改DOM结构,以及如何修改。这种算法称作Diff算法。
Diff算法会对新旧两棵树做深度优先遍历,遍历到一个节点,就将新旧两棵树作比较,并且只对同一级别的元素进行比较。
React diff算法具体策略:
- tree diff:tree diff主要针对的是React dom节点跨层级的操作。由于跨层级的DOM移动操作较少,所以React diff算法的tree diff没有针对此种操作进行深入比较,只是简单进行了删除和创建操作。
- component diff:component diff是专门针对更新前后的同一层级间的React组件比较的diff 算法。 - 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树(例如继续比较组件props和组件里的子节点及其属性)即可。 - 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创建新组件。 - 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要进行 diff 算法分析。
- element diff:element diff是专门针对同一层级的所有节点(包括元素节点和组件节点)的diff算法。当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
key机制:
- key的作用:当同一层级的某个节点添加了对于其他同级节点唯一的key属性,当它在当前层级的位置发生了变化后。react diff算法通过新旧节点比较后,如果发现了key值相同的新旧节点,就会执行移动操作(然后依然按原策略深入节点内部的差异对比更新),而不会执行原策略的删除旧节点,创建新节点的操作。这无疑大大提高了React性能和渲染效率。
- key的工作流程:首先,对新集合中的节点进行循环遍历 for (name in nextChildren),通过唯一的 key 判断新旧集合中是否存在相同的节点 if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在旧集合中的位置与 lastIndex 进行比较 if (child.mountIndex < lastIndex),否则不执行该操作。
key使用注意事项:
- 如果遍历的列表子节是作为纯展示,而不涉及到列表元素顺序的动态变更,那使用index作为key还是没有问题的。
- key只是针对同一层级的节点进行了diff比较优化,而跨层级的节点互相之间的key值没有影响。
- 大部分情况下,通过遍历的同一层级的使用了key属性的元素节点其节点类型是相同的(比如都是span元素或者同一个组件)。如果存在新旧集合中,相同的key值所对应的节点类型不同(比如从span变成div),这相当于完全替换了旧节点,删除了旧节点,创建了新节点。
- key值在比较之前都会被执行toString()操作,所以尽量不要使用object类型的值作为key,会导致同一层级出现key值相同的节点。key值重复的同一类型的节点或组件很可能出现拷贝重复内部子元素的问题。
React和Vue的异同点
相同点:
- 组件化
- 函数式编程
- 虚拟dom等
不同点:
- 设计理念,React组件就是函数、编写组件的语法是JSX,本质就是Javascript;Vue内置了很多黑魔法,比如单文件组件、指令等。
- 组件存在的形式
- 数据可变性
- diff优化策略
webpack
谈谈你对Webpack的理解
- Webpack是一个模块打包工具,可以使用它管理项目中的模块依赖,并编译输出模块所需的静态文件。
- 它可以很好地管理、打包开发中所用到的HTML,CSS,JavaScript和静态文件(图片,字体)等,让开发更高效。
- 对于不同类型的依赖,Webpack有对应的模块加载器,而且会分析模块间的依赖关系,最后合并生成优化的静态资源。
Webpack的基本功能有哪些
- 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等等
- 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等
- 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
- 模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件
- 自动刷新:监听本地源代码的变化,自动构建,刷新浏览器
- 代码校验:在代码被提交到仓库前需要检测代码是否符合规范,以及单元测试是否通过
- 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
Webpack构建过程
- 初始化编译参数:从配置文件和shell命令中读取与合并参数
- 开始编译:根据上一步得到的参数初始化Compiler对象,加载所有配置的Plugin,执行对象的 run 方法开始执行编译。
- 确定入口:从entry里配置的module开始递归解析entry依赖的所有module
- 编译模块:从入口文件触发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,然后递归本步骤直到所有入口依赖的文件都进行翻译。
- 完成模块编译:在经过上一步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系图。
- 输出资源:根据依赖关系图,组装成一个个包含多个模块的Chunk,再把每个Chunk转化成一个单独的文件加入到输出列表,根据配置确定输出的路径和文件名输出。
- 在整个流程中Webpack会在恰当的时机执行plugin里定义的逻辑。
loader的作用
- Loader 是webpack中提供了一种处理多种文件格式的机制,因为webpack只认识JS和JSON,所以Loader相当于翻译官,将其他类型资源进行预处理。即对模块的”源代码”进行转换。
- loader支持链式调用,原型调用的顺序是从右往左。原型链中的每个loader会处理之前已处理过的资源,最终变为js代码。
- 可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。
有哪些常见的Loader
- optimize-css-assets-plugin:压缩css;
- file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
- url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
- style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
- json-loader: 加载 JSON 文件(默认包含)
- babel-loader:把 ES6 转换成 ES5
- ts-loader: 将 TypeScript 转换成 JavaScript
- less-loader:将less代码转换成CSS
- eslint-loader:通过 ESLint 检查 JavaScript 代码
- vue-loader:加载 Vue单文件组件
Plugin的作用
- Plugin功能更强大,主要目的就是解决loader 无法实现的事情,比如打包优化和代码压缩等。
- Plugin加载后,在webpack构建的某个时间节点就会触发plugin定义的功能,帮助webpack做一些事情。实现对webpack的功能扩展。
有哪些常见的Plugin
html-webpack-plugin
用途:自动创建一个 HTML 文件,并将打包好的 JavaScript 文件插入到这个 HTML 文件中。
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html', // 使用现有的 HTML 文件作为模板
filename: 'index.html', // 输出的 HTML 文件名
inject: 'body'
})
]
};uglifyjs-webpack-plugin
用途:压缩 JavaScript 文件。需要注意的是,UglifyJS 不支持 ES6+ 代码压缩。如果你需要压缩 ES6+ 代码,可以考虑使用 terser-webpack-plugin
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new UglifyJsPlugin()]
}
};mini-css-extract-plugin
用途: 在构建过程中将 CSS 代码从 JavaScript 打包文件中抽离出来,单独生成一个或多个 CSS 文件。这样可以提高页面加载性能,因为浏览器可以并行加载 CSS 和 JavaScript 文件。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css'
})
]
};clean-webpack-plugin
用途: 在每次构建之前清理输出目录,删除整个输出文件夹下的内容。这有助于避免旧的文件干扰新的构建结果,确保每次构建都是基于最新的源代码。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
};copy-webpack-plugin
用途: 用于将项目中的文件或文件夹复制到构建输出目录中。
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: 'assets' } // 将 src/assets 文件夹复制到输出目录的 assets 文件夹中
]
})
]
};webpack-bundle-analyzer
用途: 可视化 Webpack 输出文件的体积,帮助你分析业务组件和依赖的第三方模块的大小。
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};optimize-css-assets-plugin
用途: 压缩 CSS 文件,减少文件大小。
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin()]
}
};Loader和Plugin的区别
- Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
- Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
- Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
- Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
Webpack如何配置压缩代码?压缩了什么?
- js压缩:利用terser-webpack-plugin
- css压缩:利用了optimize-css-assets-webpack-plugin 插件
- 删除了console、注释、空格、换行、没有使用的css代码等
如何优化 Webpack 的构建速度
- 使用高版本的 Webpack 和 Node.js
- 压缩代码:通过 uglifyjs-webpack-plugin 压缩JS代码,通过 mini-css-extract-plugin 提取 chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
- 多线程/多进程构建:thread-loader, HappyPack
- 压缩图片: image-webpack-loader
- 缩小打包作用域:exclude/include、resolve.modules、resolve.mainFields、resolve.extensions、noParse、ignorePlugin
Webpack 的热更新原理
- Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
- HMR的核心就是客户端从服务端拉去更新后的文件,准确地说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
- 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
什么是bundle?什么是chunk?什么是module?
- bundle:是webpack打包后的一个文件;
- chunk:代码块,一个chunk 可能有很多的模块组成,用于合并和分割代码;
- module:模块,在webpack中,一切都是模块,一个文件就是一个模块,她从入口开始查找webpack依赖的所有模块
package.json中sideEffects的作用
sideEffects和 Tree Shaking实际上二者没什么因果关系。
sideEffects用于告知打包工具(如 Webpack、Rollup 等)哪些模块或文件可能存在副作用(side effects),以便在进行 Tree Shaking(摇树优化)时做出正确决策。
Tree Shaking 是一种优化技术,旨在在打包过程中去除那些在最终代码中未被引用的模块,以减小打包后的文件大小。它依赖于 ES6 模块的静态导入导出特性,能够分析模块间的依赖关系,丢弃未使用的代码。
然而,某些模块可能包含副作用,即除了导出可供外部使用的值之外,还在模块内部执行了其他操作,如全局变量赋值、注册事件监听器、修改外部状态等。这些副作用可能在模块被导入时触发,即使导入者并未直接使用模块导出的任何值。为了防止误删此类模块,需要在 sideEffects 字段中明确指出。
网络
https://www.cnblogs.com/junun/p/18543421
http无状态
无状态是指协议对于事物没有记忆功能。假如后面的处理需要前面的信息,则前面的信息必须重传,这可能导致每次连接传送数据量增大。另一方面,服务器不需要前面信息时,应答就较快。直观的说,就是每个请求都是独立的,与前面的请求和后面的请求都是没有直接联系的。
实现无状态:web=http协议+状态机制+其他机制,可以通过Cookie、Session和Application实现Web的状态连接
三次握手和四次挥手过程
三次握手的过程:
- 客户端发送 SYN 包:客户端选择一个初始序列号 x,发送 SYN 包给服务器(SYN=1, seq=x)。
- 服务器响应 SYN-ACK 包:服务器收到 SYN 包后,发送一个 SYN-ACK 包作为响应(SYN=1, ACK=1, seq=y, ack=x+1)。
- 客户端发送 ACK 包:客户端收到 SYN-ACK 包后,发送 ACK 包确认连接建立(ACK=1, seq=x+1, ack=y+1)。
四次挥手过程,因为 TCP 连接是全双工的,所以每个方向的关闭都需要单独的步骤
- 主动方发送 FIN 包:主动关闭方发送 FIN 包,请求关闭连接(FIN=1, seq=u)。
- 被动方响应 ACK 包:被动关闭方收到 FIN 包后,发送 ACK 包作为确认(ACK=1, seq=v, ack=u+1)。
- 被动方发送 FIN 包:被动关闭方准备关闭连接时,发送 FIN 包(FIN=1, seq=w)。
- 主动方响应 ACK 包:主动关闭方收到 FIN 包后,发送最后的 ACK 包来关闭连接(ACK=1, seq=u+1, ack=w+1)。
HTTP 状态码有哪些?
- 200 OK:请求成功,服务器已正常处理请求。
- 301 Moved Permanently:永久重定向,资源的 URL 已永久更改。
- 302 Found:临时重定向,资源的 URL 已临时更改。
- 304 Not Modified:未修改,请求的资源未发生变化,可以使用缓存的版本。
- 400 Bad Request:客户端请求有语法错误,服务器无法理解。
- 401 Unauthorized:请求需要用户的身份认证。
- 403 Forbidden:服务器理解请求但拒绝执行,可能是权限问题。
- 404 Not Found:服务器上未找到请求的资源。
- 500 Internal Server Error:服务器内部错误,无法完成请求。
http常见请求头/响应头
- 缓存控制:cache-control、expires、Last-Modify/If-Modify-Since、Etag/If-None-Match
- 跨域控制:access-control-allow-origin、allow-methods、allow-headers
- accept:accept-encoding、accept-language
- content-type、content-length
- cookie、user-agent
- origin、refer
在交互过程中如果数据传送完了,还不想断开连接怎么办,怎么维持?
- 发送心跳包:定期发送小的数据包,以保持连接活跃。心跳包可以是一个空的数据包或者特定的心跳消息。
- 使用 HTTP 持久连接(HTTP Persistent Connections):在 HTTP/1.1 中,默认开启持久连接,使用Connection: keep-alive头部。
http2.0的优势
提升性能:
- 多路复用:可以同时处理多个请求和响应,大大减少了连接建立的次数,提高了网络传输效率。
- 头部压缩:压缩头部信息降低数据量。
- 服务器推送:允许服务器在客户端明确请求前,主动发送为完成客户端请求所需的资源。这意味着服务器可以在发送 HTML 页面响应时,预测客户端需要的其他资源(如CSS文件、JavaScript脚本等)并主动推送它们,消除了客户端等待这些资源的时间。
HTTP 和 HTTPS 的区别是什么?
强缓存和协商缓存
强缓存
强缓存也就是本地缓存。
浏览器首次请求资源后,需要再次请求时,浏览器会首先获取该资源缓存的header信息,然后根据Cache-Control和expires来判断该资源在本地缓存否过期。若没过期则直接从本地缓存中获取资源信息,浏览器就不再向服务器重新请求资源,如过期则需重新发送请求,重新缓存资源,更新缓存时间。
- Expires,绝对时间,http1.0的规范
- Cache-Control,相对时间,http1.1的规范
- 可以同时存在,Cache-Control的优先级更高
协商缓存
协商缓存是询问服务器资源是否过期。
因为浏览器需要向服务器确认缓存资源是否可用,二者要进行通信,而通信的过程就是发送请求,所以在header中就需要有专门的标识来让服务器确认请求资源是否可以缓存访问。
- Last-Modify/If-Modify-Since: - 当浏览器第一次向服务器请求资源时,服务器会在该资源的请求头中加上Last-Modify,就是该资源在服务器的最新修改时间。 - 然后当浏览器再次请求这个资源时,会在请求报文中带上If-Modify-Since,值是浏览器上一次请求该资源时返回的Last-Modify时间。 - 当服务器收到If-Modify-Since时间后,会判断这个资源的当前最新修改时间和If-Modify-Since时间是否相等,相等则说明浏览器缓存的资源已经是最新的了,服务器返回304状态码告诉浏览器资源已是最新不用更新。不相等则说明在浏览器没有请求的这段时间,这个资源已经进行了修改、更新,浏览器本地缓存的这个资源已经不是最新的了,这时候服务器重新返回该资源的最新版以及最新的Last-Modify时间。
- Etag/If-None-Match:和上面的逻辑处理一致,但是Last-Modified标注只能精确到秒级,如果某些资源在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间。Etag是服务器自动生成或者由开发者生成的资源在服务器的唯一标识符,能够更加准确的控制缓存。优先级更高。
跨域
同源策略:协议、端口号、域名不一致
跨域解决方法:
- CORS跨域资源共享:需要在服务器实现CORS,设置跨域的请求头:Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers这些
- 服务端代理
- Jsonp:
利用<script>标签可以跨域加载资源的特性,通过添加一个查询参数并处理回调函数来实现跨域数据传输。
DNS预解析
DNS 预解析(也称为 DNS 预取或 DNS 预加载)是一种技术,它允许浏览器在实际需要之前就开始解析域名。这通常用于提高 Web 页面的加载时间,通过减少页面加载过程中的 DNS 查询延迟。
- 工作原理:浏览器在解析 HTML 文档时遇到带有
<link rel="prefetch">或<link rel="dns-prefetch">的标签时,会触发 DNS 预解析。当实际请求资源时,DNS 查询已经完成。 - 减少延迟:通过提前进行 DNS 查询,可以减少页面加载过程中的延迟,因为当实际请求资源时,DNS 查询已经完成。
安全
XSS(跨站脚本攻击)
原理:恶意攻击者往web页面里插入恶意的Script代码,当用户浏览该页面时,嵌入web里面的恶意Script代码会被执行,从而达到恶意攻击用户的目的。XSS攻击针对的是用户层面的攻击。
XSS分为:存储型、反射型和DOM型
- 反射型XSS。又称非持久型XSS。之所以称为反射型XSS,是因为这种攻击方式的注入代码是从目标服务器通过错误的信息、搜索结果等等方式”反射”回来的:发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,XSS代码随响应内容一起传回给浏览器,最后浏览器解析执行XSS代码。这个过程像一次反射,故叫反射型XSS。而称为非持久型XSS,则是因为这种攻击方式具有一次性,由于代码注入的是一个动态产生的页面而不是永久的页面,因此这种攻击方式只在点击链接的时候才产生作用。
- 存储型XSS。存储型XSS,又称持久型XSS,他和反射型XSS最大的不同就是,攻击脚本将被永久地存放在目标服务器端(数据库,内存,文件系统等),下次请求目标页面时不用再提交XSS代码。
- DOM型。客户端的脚本程序可以动态地检查和修改页面内容,而不依赖于服务器端的数据。例如客户端如从 URL 中提取数据并在本地执行,如果用户在客户端输入的数据包含了恶意的 JavaScript 脚本,而这些脚本没有经过适当的过滤和消毒,那么应用程序就可能受到 DOM-based XSS 攻击。需要特别注意以下的用户输入源 document.URL、location.hash、location.search、document.referrer 等。DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS的防御:
- 对用户的输入(和URL参数)进行过滤,对输出进行html编码。也就是对用户提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。
- CSP(Content Security Policy)内容安全策略,以限制页面上可以加载的资源和内容,从而防止跨站脚本攻击。在 HTTP 响应头中设置 CSP 策略,告知浏览器允许加载哪些内容,哪些不允许加载。
CSRF(Cross-site request forgery)跨站请求伪造

从上图能够看出,要完毕一次CSRF攻击,受害者必须依次完毕两个步骤:
- 登录受信任站点A,并在本地生成Cookie(目标站点一定要有 CSRF 漏洞;)
- 在不登出A的情况下,访问危险站点B
常见的攻击类型:
- GET类型的CSRF
- POST类型的CSRF,通常使用的是一个自动提交的表单。
- 未进行token校验
防御CSRF:
- 提交验证码,通过强制用户和应用进行交互,来有效地遏制CSRF攻击。
- 验证请求的来源站点,origin、referer
- token验证,可以在表单内/请求头内。
csrf-token的理解:
- 第一步、在浏览器向服务器发起请求时,服务器生成一个 CSRF Token,前端保存起来。
- 第二步,在浏览器端如果要发起请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法获取到 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。
微前端(qiankun)
介绍微前端
可以帮助我们将多个独立的前端应用整合成一个整体,并且可以独立开发、独立部署、独立运行。
- 主应用:整个微前端应用的入口,负责加载和管理子应用。
- 子应用:独立的前端应用,可以独立开发、独立部署、独立运行。
- 生命周期:主应用和子应用之间的生命周期钩子,用于控制应用的加载、启动、卸载等过程。
- 沙箱:用于隔离子应用的 JavaScript 执行环境,防止子应用之间的冲突和污染。
- 应用间通信:主应用和子应用之间的通信机制,用于实现数据共享和事件传递。
优劣势
优势:
- 可以将多个独立的前端应用整合成一个整体,提高了代码复用性和可维护性。
- 技术兼容性好,各个子应用可以基于不同的技术架构。
- 耦合性更低,可以独立开发、独立部署、独立运行,提高了开发效率和部署灵活性。
- 代码库更小、内聚性更强。
- 可以实现子应用之间的通信和数据共享,提高了应用之间的协作能力。
- 可以实现子应用的按需加载和动态卸载,提高了应用的性能和用户体验。
缺点:
- 子应用间的资源共享能力较差,使得项目总体积变大。
- 需要对现有代码进行改造(指的是未按照微前端形式编写的旧工程)。
实现原理
一般来说,微前端需要解决的问题分为两大类:
- 应用的加载与切换
- 应用的隔离与通信
应用的加载与切换需要解决的问题包括:路由问题、应用入口、应用加载;应用的隔离与通信需要解决的问题包括:js隔离、css样式隔离、应用间通信。
single-spa很好地解决了路由和应用入口两个问题,但并没有解决应用加载问题,而是将该问题暴露出来由使用者实现(一般可以用system.js或原生script标签来实现);qiankun借助import-html-entry实现了应用加载,并给出了js隔离、css样式隔离和应用间通信三个问题的解决方案。
路由问题
single-spa是通过监听hashChange和popState这两个原生事件来检测路由变化的,它会根据路由的变化来加载对应的应用。
应用入口
采用的是协议入口,即只要实现了single-spa的入口协议规范,它就是可加载的应用。暴露bootstrap、mount、unmount三个函数。
应用加载
qiankun使用了一个更完整的应用加载方案:import-html-entry。该方案的主要思路是允许以html文件为应用入口,然后通过一个html解析器从文件中提取js和css依赖,并通过fetch下载依赖。将外联样式处理成内部样式表,并且构造了一个可以在隔离环境下执行应用脚本的方法execScripts。
js隔离
import-html-entry构造了一个可以在隔离环境下执行应用脚本的方法execScripts,qiankun要做的就只需要在加载一个应用时为其初始化一个proxy传递进来即可。
export default class ProxySandbox implements SandBox {
...
constructor(name: string) {
...
const proxy = new Proxy(fakeWindow, {
set () { ... },
get () { ... }
}
}
}css隔离
目前qiankun主要提供了两种样式隔离方案,一种是基于shadowDom的;另一种则是实验性的,思路类似于Vue中的scoped属性,给每个子应用的根节点添加一个特殊属性,用作对所有css选择器的约束。
- strictStyleIsolation,基于shadowDom。但是某些UI框架可能会生成一些弹出框直接挂载到document.body下,此时由于脱离了shadow tree,所以它的样式仍然会对全局造成污染。
- experimentalStyleIsolation,类似于scoped属性。但是不支持@ keyframes,@ font-face,@ import,@ page
应用通信
一般来说,微前端中各个应用之前的通信应该是尽量少的,而这依赖于应用的合理拆分。反过来说,如果你发现两个应用间存在极其频繁的通信,那么一般是拆分不合理造成的,这时往往需要将它们合并成一个应用。
当然了,应用间存在少量的通信是难免的。qiankun官方提供了一个简要的方案,思路是基于一个全局的globalState对象。这个对象由基座应用负责创建,内部包含一组用于通信的变量,以及两个分别用于修改变量值和监听变量变化的方法:setGlobalState和onGlobalStateChange。
子应用如何实现按需加载
- 子应用可以通过导出一个异步加载函数来实现按需加载:bootstrap、mount、unmount三个钩子函数。
- 主应用中可以通过loadMicroApp动态加载子应用,也可以匹配路由的方式加载子应用。
子应用与主应用的通信
- 子应用:
window.parent.postMessage - 主应用:可以通过
onGlobalStateChange方法来监听子应用的状态变化
子应用与其他子应用的通信
- 子应用:可以通过 window.dispatchEvent 方法来触发自定义事件
- 其他子应用:可以通过 window.addEventListener 方法来监听自定义事件
子应用如何实现路由跳转
子应用可以通过 history.pushState 方法来实现路由跳转。
子应用如何实现样式隔离
可以通过 CSS Modules 或 CSS-in-JS 等技术来实现样式隔离。
子应用共享工具库的思考
- 为什么不通过主应用分发:各子应用,依赖的组件库/工具库版本不一致,如果全部升级的话,影响面不可控。
- 可以通过webpack分包的方式,优化加载
使用中的问题
弹窗组件样式失效
弹窗的dom挂载到了body上,应该挂载到子应用上。
子应用资源跨域
子应用静态资源一定要支持跨域。由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域。
子应用加载的资源会 404
需要使用 webpack 运行时 publicPath 配置:
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;子应用切换时视觉问题
不要单独打出来 css chunk,直接把 css 插到 dom 中。
styleLoader: {
injectType: 'singletonStyleTag'
}ssr服务端渲染
基本概念和优势
- SSR是指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
- SSR的优点包括提升首屏加载速度、改善SEO。
- 挑战:处理服务器压力、增加开发复杂度、处理服务器端和客户端的渲染差异等
工作流程
- 服务器请求:当用户访问应用时,浏览器向服务器发送一个请求。
- 服务器渲染:服务器接收到请求后,使用renderToString,结合请求的url生成页面的HTML表示。
- 客户端激活:浏览器接收到HTML内容后,会拉取/执行js脚本,绑定数据和事件。
性能优化
性能监控
- Lighthouse 评分(Chrome DevTools):可以给Performance、Accessibility、SEO等进行打分。
- LCP (Largest Contentful Paint):最大内容渲染时间
- FCP(First Contentful Paint):初次内容渲染时间
- FID (First Input Delay):首次输入延迟
- CLS (Cumulative Layout Shift):布局偏移量
网络层面
- 减少 HTTP 请求:合并文件/图标
- 压缩资源:gzip压缩、代码压缩(webpack压缩css、js代码)
- 缓存策略:强缓存、协商缓存
- CDN加速:静态资源(JS/CSS/图片)部署到 CDN、使用 HTTP/2 多路复用降低延迟
- http2.0:多路复用、压缩头部
- 预加载关键资源:preload、prefetch
- ssr服务端渲染
渲染层优化(交互)
- 路由懒加载
- 减少重排(Reflow)与重绘(Repaint),避免频繁操作 DOM。
- 虚拟列表优化长列表
- 防抖(Debounce)与节流(Throttle)
- Web Worker 处理 CPU 密集型任务,比如大文件上传
- 避免内存泄漏:未移除的事件监听器、未清理的定时器/异步任务、闭包保留外部引用等等
- react相关:useMemo、useMemoizedFn、React.memo等等
打包优化
- Tree Shaking:移除未使用的代码(需 ES Module 语法)
- 按需加载第三方库
- 代码分割
// Webpack 配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 包括异步和非异步的chunks
minSize: 20000, // 模块的最小体积,单位是字节(byte)
maxSize: 0, // 模块的最大体积,设置为0表示不限制大小,单位是字节(byte)
minChunks: 1, // 被拆分前必须共享模块的最小块数
maxAsyncRequests: 30, // 按需加载时的最大并行请求数
maxInitialRequests: 30, // 入口点的最大并行请求数
automaticNameDelimiter: '~', // 命名连接符
name: true, // 使用模块的id作为拆分出的chunk的名字,也可以指定一个函数来自定义命名方式,或者使用true启用Heuristic命名方式(基于模块路径和请求的模块路径)
cacheGroups: { // 用于指定缓存组来优化chunk的拆分结果,可以继承minSize、maxSize、minChunks等选项来进一步拆分代码块。例如:将所有node_modules中的包拆分出来:
vendors: { // 组名(或者说缓存组的名称)
test: /[\\/]node_modules[\\/]/, // 正则匹配规则,匹配到node_modules目录下的模块才会被拆分出来。这里使用了转义字符[\\/]来匹配路径分隔符,因为在Windows系统中路径分隔符是\,而在类Unix系统中是/。为了兼容性,这里使用了[\\/]来表示路径分隔符的正则表达式。实际上,你也可以直接使用/[\\/]/来匹配路径分隔符,因为在大多数情况下,路径分隔符只会是\或/中的一个。但在某些特殊情况下,例如在某些特定的正则表达式引擎中,可能需要显式地指定路径分隔符的正则表达式。因此,为了更好的兼容性和清晰性,建议使用[\\/]来表示路径分隔符的正则表达式。但是,如果你的代码只在类Unix系统上临时提问
准备的几个问题:
- 入职后是否有产品培训和技能培训,是否有新人引导
- 公司的技术栈是什么
- 这个职位的话,有什么晋升机制
- 整个面试流程有几轮面试,接下来的流程有哪些
- 团队成员有哪些,大家的分工和负责的核心业务是哪些
- 我如果进入公司之后,负责的日常工作是什么,需要准备哪些技能