一、HTML
- src 和 href 的区别:
(1)src:当浏览器解析到该元素时,会暂停其他资源的下载和处理,优先加载src,影响了其他资源的下载,所以js脚本才放在底部。
(2)href:当浏览器识别该文件为css时,会并行下载其资源,不会停止对当前文档的处理,这就是为什么要用link的方式来加载css,而不是用@import的方式。
总结:
①src用于img、input、style、script、iframe src 同步加载执行
②href用于link a 标签 href 异步
- <!DOCTYPE html> 的作用:
(1)告诉浏览器(解析器)应该以什么样的文档类型来解析文档。
(2)该声明必须在html文件的第一行。
浏览器渲染页面的两种方式:
- 标准模式:w3c最高标准。
- 怪异模式:比较宽松的向后兼容。
- HTML5的新内容:
(1)新增语义化标签: nav、 header、 footer、aside、section、article
(2)音频、视频标签: audio、video
(3)数据存储: localStorage、sessionStorage
(4)canvas (画布)、Geolocation (地理定位) 、websocket (通信协议)
(5)input标签新增属性: placeholder、 autocomplete、 autofocus、 required
(6)history APl: go、forward、back、pushstate
- 行内元素有哪些? 块级元素有哪些? 空(void)元素有那些?
(1)行内元素有:a b span img input select strong ;
(2)块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p ;
(3)空元素,即没有内容的HTML元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签
常见的有:<br> 、<hr>、<img>、<input>、<link>、<meta>
鲜见的有:<area>、<base>、<col>、<colgroup> 、<command>、<embed>、<keygen>、<param>、
<source> 、<track> 、 <wbr> 。
二、CSS
1. CSS选择器及其优先级
继承或通配符* 0
元素选择器 1
类(伪类选择器) 10
id选择器 100
行内选择器 1000
!important 无穷大
其他的全为0
- 有继承性的属性
- 字体系列属性
- 文本距离系列属性
- 元素可见性:visibility 控制元素的显示隐藏
- 列表布局属性:list-style等
- 光标属性:cursor
- 隐藏元素的方法
1.display:none
2.visibility:hidden (隐藏但是,还占位置)
3.opacity:0
4.position:absolute
5.z-index:负值
6.clip/clip-path: 元素裁剪
7.transform:scale(0,0) 缩放为0
1 2 6 7 不会响应绑定的监听事件。
- 字体小于12px
这个是Chrome 这款任性的浏览器做了如下限制:
1. font-size 有一个最小值 12px(不同操作系统、不同语言可能限制不一样),低于 12px 的,一律按 12px 显示。理由是 Chrome 认为低于 12px 的中文对人类是不友好的。
2. 但是允许你把 font-size 设置为 0.
3. 这个 12px 的限制用户是可以自行调整的,进入 chrome://settings/fonts 设置,滚动到最下方你就可以调整 12px 为其他值。
但我要真想在做css时设置小于12px的字体怎么办?
1.Chrome 29 版本之前,你可以使用 `-webkit-text-size-adjust: none; 来解除这个限制。29 版本后,就不能这样做了。
2.你可以先设置 12px,然后使用 transform: scale(0.833333) 将元素缩小,效果跟 10px 很接近。不过要注意的是,transform: scale 出了缩小 font-size,也会缩小其他一些属性,需要多测试。(为了避免缩小其他属性,一般单独用一个span放要调整的字体,然后调span的样式就行,其他的不会乱)
- link和@import的区别:
两者都是外部引用CSS的方式,他们的区别如下:
1.link是XHTML标签,除了加载CSS外,还可以定义RSS(一种基于XML的标准)等其他事务;@import属于CSS范畴,只能加载CSS。
2.link引用CSS时,在页面载入时加载;@import需要页面网页全载入以后加载。
3.link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
4.link支持使用JavaScript控制DOM去改变样式;而@import不支持。
- display:none与visibility:hidden的区别
这两个属性都是让元素隐藏,不可见。两者区别如下:
- 在染树中
- display:none 会让元素完全从渲染树中消失,渲染时不会占据任何空间;
- visibility:hidden 不会让元素从染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。
- 是否是继承属性
- display:none 是非继承属性,子孙节点会随着父节点从染树消失,通过修改子孙节点的属性也无法显示;
- visibility:hidden 是继承属性,子孙节点消失是由于继承了 hidden,通过设置 visibility:visible 可以让子孙节点显示;
- 修改常规文档流中元素的 display 通常会造成文档的重排(回流),但是修改 visibility 属性只会造成本元素的重绘;
- 如果使用读屏器,设置为 display:none 的内容不会被读取,设置为 visibility:hidden 的内容会被读取。
- 为什么有时候用translate来改变位置而不是定位?
translate 是 transform 属性的一个值。改变transform或opacity不会触发浏览器重新布局(reflow)(回流) 或重绘(repaint),只会触发复合 (compositions) 。而改变绝对定位会触发重新布局,进而触发重绘和复合。transform使浏览器为元素创建一个 GPU 图层,但改变绝对定位会使用到 CPU。因此translate()更高效,可以缩短平滑动画的绘制时间。而translate改变位置时,元素依然会占据其原始空间,绝对定位就不会发生这种情况。
- z-index属性在什么情况下会失效
通常 z-index 的使用是在有两个重叠的标签,在一定的情况下控制其中一个在另一个的上方或者下方出现。z-index值越大就越是在上层。Z-index元素的position属性需要是relative,absolute或是fixed。
z-index属性在下列情况下会失效:
- 父元素position为relative时,子元素的z-index失效。解决: 父元素position改为absolute或static
- 元素没有设置position属性为非static属性。解决: 设置该元素的position属性为relative,absolute或是fixed中的一种
- 元素在设置z-index的同时还设置了float浮动。解决: float去除,改为display: inline-block
- 解决浮动塌陷
- 给浮动元素的父辈添加高度(最简单,但不灵活)
- 开启BFC
- 在浮动元素后添加元素,并设置其clear属性
- br元素的clear属性
- after伪类【各大公司推荐】
- 对BFC的理解,如何创建BFC
块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。
通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
创建BFC的条件:
- 根元素: body
- 元素设置浮动:float 除 none 以外的值
- 元素设置绝对定位:position (absolute、fixed)
- display 值为:inline-block、table-cell、 table-caption、 flex等
- overflow 值为:hidden、auto、 scroll
自己总结BFC开启条件:
- 浮动
- 定位
- overflow:hidden
- display:flex 、inline-block
BFC的特点:
- 垂直方向上,自上而下排列,和文档流的排列方式一致
- 在BFC中上下相邻的两个容器的margin会重叠
- 计算BFC的高度时,需要计算浮动元素的高度
- BFC区域不会与浮动的容器发生重叠
- BFC是独立的容器,容器内部元素不会影响外部元素
- 每个元素的左margin值和容器的左border相接触
BFC的作用:
- 解决margin塌陷问题:由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin重叠的问题。
- 解决浮动高度塌陷的问题:在对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变为0。解决这个问题,只需要把父元素变成一个BFC。常用的办法是给父元素设置 overflow:hidden 。
- 创建自适应两栏布局:可以用来创建自适应两栏布局: 左边的宽度固定,右边的宽度自适应。左侧设置 float:left ,右侧设置 overflow: hidden 。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局。
- vm、vh、rem
(1)rem:
rem单位是相对单位,是相对于HTML标签的字号计算结果
1rem = 1HTML字号大小
(2)vm/vh
vw是viewport width,1vw = 1/100视口宽度
vh是viewport height,1vh = 1/100视口高度
(现在一般都用vm/vh)
- 伪元素和伪类
- 伪类
伪类是以冒号:为前缀,可被添加到一个选择器的末尾的关键字。
它用于让样式在元素的特定状态下才被应用到实际的元素上。比如::checked、:hover、:disabled、:first-child、:nth-child等。
(注:伪类,虽然是写法比较特殊,但css选择器的权重和类一致)
- 伪元素
:before / :after
伪元素用于创建一些并不在DOM树中的元素,为其添加样式。伪元素的语法和伪类类似,可以一个冒号或者两个冒号为前缀。
并且,虽然用户可以看到这些文本,但其实他们并不在DOM树中。
(注:伪元素是无法注册时间的,所以不用通过js来控制伪元素)
(3)两者的区别
- 伪类是用来匹配元素的特殊状态的
- 伪元素是用来匹配元素的隶属元素的,这些隶属元素可以在界面中展示,但在DOM中不体现
- Flex的常用属性
- flex-direction 主轴的方向
flex-direction: row | row-reverse | column | column-reverse
- flex-wrap
flex-wrap: nowrap | wrap | wrap-reverse;
nowrap(默认)不换行
wrap 换行
wrap-reverse 换行,第一行在下方
- flex-flow
是flex-direction和flex-wrap的简写形式
flex-flow: column wrap;
- justify-content 主轴对齐方式
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
space-between:两端对齐,项目之间的间隔都相等。
space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
space-evenly:每个间隔都相等。
- align-items 侧轴对齐方式
align-items: flex-start | flex-end | center | baseline | stretch;
baseline: 项目的第一行文字的基线对齐。
- align-content
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly |stretch
- 瀑布流的实现
(1)flex实现
Flex布局,按列排序,换行,侧轴内容开始
display: flex;
flex-flow:column wrap;
align-content: flex-start;
- 图片下方的小缝隙
原因:
因为图片是行内元素,默认的图片对齐方式为vertical-align:baseline 基线对齐。baseline对其方式导致图片并不是与div的真实底部对齐,而是与文基线对齐。文基线就是字母x的下边缘,即使没有文字,也会有空隙。
- 使图片变为块级元素
给img(图片)设置display:block; 因为vertical-align对块元素不起作用, 所以让vertical-align失效。但是这样会导致图片独占一行,文字会跑到下面一行。
- 给img(图片)设置vertical-align: bottom;
调整行内元素的元素对其方式为底部对其。
- 修改line-height值(div的line-height)
图片下面的空隙高度,实际上是文字计算后的行高值和字母x下边缘的距离。因此,只要行高足够小,实际文字占据的高度的底部就会在x的上面,下面没有了高度区域支撑,自然,图片就会有容器底边贴合在一起了。
- div的font-size=0
让div的font-size=0,本质上还是改变line-height的值,但是div中的文字就显示不出来了
- Element-ui的覆盖方式:
(1)v-deep样式穿透
::v-deep .样式class {
}
(2)引入外部样式
可以在vue项目中public文件夹下的index.html中引入一个外部样式,进行覆盖
三、JS
1. JS基础+ES6
1. JS中有哪些数据类型,它们的区别?
JS中共有8种数据类型:undefined、string、symbol、object、null、number、boolean、BigInt
辅助记忆: you so NB
u undefined
s string symbol
o object
n null number
b boolean bigInt
number和bigInt的对比:
number :1. 取值范围是: ±2^53-1 2. 可以使用Math对象 3. 无需担心精确度
bigInt :1. 取值范围是: 无穷 2. 不能使用Math对象 3. bigInt转number时可能会丢失精确度
Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题
这些数据可以分为原始数据类型和引用数据类型:
栈: 原始数据类型 (Undefined、Null、Boolean、Number、String)
堆: 引用数据类型 (对象、数组和函数)
2. let const var 的区别
(1)块级作用域: 块作用域由 {} 包括起来,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
。内层变量可能覆盖外层变量
。用来计数的循环变量泄露为全局变量
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
- 初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
- 指针指向:let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
3. 箭头函数和普通函数之间的区别:
- 箭头函数比普通函数简洁
- 箭头函数没有自己的this,只能继承作用域上一层的this
- 箭头函数中的this无法改变指向,不管是call、apply、bind都无法改变
- 箭头函数没有自己的arguments
- 箭头函数没有prototype
- 箭头函数不能用做Generator函数,不能使用yeild关键字
4. 什么是 BOM 和 DOM:
- DOM是 文档对象模型, (document)
DOM 管:标签等网页内容。
- BOM 是 浏览器对象模型, (window) document是window的一个子对象,
window既是js访问浏览器窗口的一个接口,又是一个Global(全局)对象
Window对象含有:location、screen、navigate、history、document等
BOM 管:浏览器中的东西:滚动条、右键菜单栏、地址栏、书签栏等浏览器内容。
5. 对JSON 的理解:
首先,JSON是一种基于文本的轻量级数据交换格式,可以被任何一种语言读取和作为数据格式来传递。
在项目开发中,一般使用JSON作为前后端数据交换的方式。
JSON的数据格式要求很严格,比如:JSON中的属性值不能为函数,不能出现NaN这样的属性值。
JSON中的两个函数:
(1)JSON.stringify 将数据结构转为JSON格式
(2)JSON.parse 将JSON转为js的数据结构
6. 数组的常用方法:
(1)增:push unshift
(2)删:pop shift
(3)改(删): splice
(4)查:(从头查: )find findIndex indexOf (从尾查: ) findLast findIndexLast lastIndexOf
(5)迭代:every some filter map forEach reduce
(6)合并:concat
(7)包含:includes
(8)排序:sort (a-b是从小到大) (b-a是从大到小)
Sort是快速排序 时间复杂度O(n^2) 空间复杂度O(n)
(9)反转:reverse
(10)填充:fill() 使用方法: Array(5).fill(true)
(11)数组转字符串:join
(12)伪数组转为真数组:Array.from(伪数组)
7. string的常用方法:
(1)长度:length
(2)字符串转数组:split(字符)
(3)字符串截取:substring(开始索引,结束索引)
(4)检测以什么开头/结尾:startsWith('开头字符') endsWith('结尾字符')
(5)包含:includes
(6)去除两端空格:trim
(7)转大小写:toUpperCase() tolowerCase()
(8)替换:replace('替换谁','替换成谁')
(9)查找(返回索引):indexof
8. || 和 && 操作符的返回值:
(1) || :第一个值为true时,返回第一个值,否则返回第二个值
(2) &&:第一个值为true时,返回第二个值,否则返回第一个值
9. 如何判断一个对象是空对象
(1)使用JSON自带的.stringify 方法来判断:
JSON.stringify(obj) == ‘{}’
(2)使用ES6新增的方法Object.keys() 来判断
Object.keys(obj).length < 0
10. null 和 undefined 的区别
undefined和null的区别:
● undefined 表示一个变量没有被声明,或者被声明了但没有被赋值(未初始化),一个没有传入实参的形参变量的值为undefined,如果一个函数什么都不返回,则该函数默认返回undefined。null 则表示"什么都没有",即"空值"。
● JavaScript将未赋值的变量默认值设为 undefined ;JavaScript从来不会将变量设为 null。它是用来让程序员表明某个用var声明的变量时没有值的。
- for..of 和 for..in 的区别
- for...in可以遍历对象和数组,for...of不能遍历对象
- for...in 循环不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的
- for...in遍历的索引为字符串类型
- for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合,但是不能遍历对象
- for...of与forEach()不同的是,它可以正确响应break、continue和return语句
- 具有迭代器对象才可以使用for...of
总结一句话就是遍历数组使用for...of,遍历对象使用for...in。
自己总结一下:
in对象 of数组
- Of适用于遍历 数/数组/字符串/map等拥有迭代对象的集合,但不能遍历对象。for...of...与forEach不同的是,其可以正确响应break、continue、和return语句。
- In对象数组都可以遍历。不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的。
- 对eventLoop的理解?
1. 首先 js 是单线程,为了防止代码阻塞,我们把代码(任务)分为:同步和异步。
2. 同步代码直接给 js 引擎去执行,异步代码交给宿主环境(浏览器、node)去执行,因为他们是多线程。
3. 同步代码直接放入执行栈中,异步代码等待时机成熟后送到任务队列中排队。
4. 执行栈执行完毕后,会去任务队列看是否有异步任务,有就送到执行栈中执行,反复循环查看执行,这个过程就是事件循环(eventLoop)。
13. 同步、异步(宏任务、微任务)
同步任务:可以立即执行不需要等待
异步任务:不能立即执行,需要等待一段时间后才能执行,例如网络请求和setTimeout定时器。
异步任务又分为:宏任务和微任务
微任务:promise.then 和 nextTick 就是典型的微任务。
宏任务:其余的都是宏任务。
执行流程如下:
同步代码直接放入执行栈中,异步代码分为宏任务和微任务,分别放进宏任务和微任务队列中。等待执行栈中的同步任务执行完毕后,查看微任务队列中有无微任务,有的话将微任务放到执行栈中执行,执行完毕后,再将宏任务队列中的代码放入执行栈中执行。
14. 闭包
什么是闭包:
存在函数嵌套,且内部函数调用了外部函数的变量,并执行了外部函数就叫闭包。
闭包的作用:
- 延长了局部变量的生命周期
- 让函数外部可以操作到内部函数的数据
闭包的缺点:
- 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
- 容易造成内存泄露
解决方法:
- 能不用就不用
- 及时释放( 变量 = null )
闭包的使用场景:
节流防抖函数
- 深浅拷贝
- 本地存储的三种方式和区别
- 类型检测方式
- Typeof
用法:
- Instanceof
其内部运行机制是判断在其原型链中能否找到该类型的原型。(能检测复杂数据类型)
用法:
- Constructor
用法:
- Object.prototype.toString.call()
用法:
- 作用域、作用域链
- 作用域
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。作用域最大的用处就是隔离变量,不同作用域下的同名变量不会有冲突。
-
- 全局作用域
- 函数作用域
- 块级作用域
- 作用域链
访问变量时,自己的作用域中没有,一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃,这种一层一层的关系,就是作用域链。
- 原型链
-
- 显式原型
每个类(构造函数)都有一个显式原型 prototype(本质就是个对象)
-
- 隐式原型
每一个实例都有一个隐式原型__proto__
-
- 显式原型与隐式原型的关系
类显式原型的prototype等于其创建的实例的隐式原型__proto__
-
- 原型链
查找对象实例的方法和属性时,先在自身找,找不到则沿着__proto__向上查找,我们把__proto__形成的链条关系称为原型链(实现了js继承)
总结过程:
- 访问对象的一个属性,先在自身查找,如果没有,会访问对象的__proto__,沿着原型链查找,一直找到Object.prototype.__proto__。
- 每个函数都有prototype属性,会指向函数的原型对象。
- 所有函数的原型对象的__proto__,会指向Object.prototype。
- 原型链的尽头是Object.prototype.__proto__,为null。
- ES6的map和set
map和set都是集合类型
(1)set
① 使用
new Set()
.add .delete .has .clear .size
② 应用
数组、字符串去重,数组求交集、并集等。
③ 如何遍历
For of forEach (set没有索引)
(2)map
键值对数据集合的特点:键不可重复
有一些数据,可能随意增加,删除,多一个少一个没问题,适合map
① 使用
new Map()
.size .set(键,值) get(键) has(键) delete(键) clear()
② 如何遍历
for-of ,每次迭代得到一个长度为2的数组
forEach,通过回调函数遍历
参数1:每一项的值
参数2:每一项的键
参数3:所遍历的map本身
③ 应用
用于存储多个键值对数据。在map出现之前使用对象存储键值对,但是有几个问题,键名只能是字符串,获取数据的数量不方便。键名容易和原型上的名称冲突。
当描述一个整体,且属性不能随意添加删除时,适合用对象,当存储一些数据可以随意添加删除时可以用map。
- 字符串反转
先转数组,反转,再转为字符串。
Str.split(‘’).reverse().join(‘’)
- 数组判断相等
- 循环比较
- 转字符串比较
toString
JSON.stringify
- Lodash.isEqual比较
- JS精度问题怎么造成的,如何解决
- JS精度问题的造成
因为JS遵循IEEE754标准,使用64位固定长度来表示数据,是标准的double双精度浮点数。
eg. 0.1 + 0.2 在转2进制表示时,得到如下数据:
0.1 -> 0.0001 1001 1001 1001...(1100循环)
0.2 -> 0.0011 0011 0011 0011...(0011循环)
0.0100110011001100110011001100110011001100110011001100
二进制再转为十进制时就成了0.30000000000000004,所以在使用时会有误差。
- 解决
- toFixed(n)
直接转成几位小数,但在使用时,会存在一些问题,toFixed会有一些不精准。
- 化整数运算
先乘再除。
- 第三方组件库
Bignumber.js
使用:
x = new BigNumber(0.1)
y = x.plus(0.2) // '0.3'
BigNumber(0.7).plus(x).plus(y) // '1'
x.plus('0.1', 8) // '0.225' // 第二个参数是进制数
plus加 minus减 multipliedBy/times乘 dividedBy/div除
- Object的常用方法:
- Assign 浅拷贝
- Values 对象的值
- Keys 对象的键
- DefineProperty 添加、配置属性
- Is 比较是否相等
- Typeof NaN的结果
typeof NaN; // ‘number’
NaN 指“不是一个数字”(not anumber),NaN 是一个“警戒值’(sentinel value,有特殊用途的常规5值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
NaN 是一个特殊值,它和自身不相等,是唯-一个非自反(自反,reflexive,即x===x不成立)的值。而 NaN !== NaN 为 true。
- Map和object的区别
2. 垃圾回收 与 内存泄露
1. 垃圾回收的概念 和 回收机制:
垃圾回收:
JS代码运行时,需要分配内存空间来存储变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- JS具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JS中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;局部变量声明在函数中,当函数执行完毕后,局部变量不再使用,占用空间就会被释放。
- 特殊情况,局部变量被外部函数引用(闭包),此时局部变量仍然在使用,不会被回收。
2. 垃圾回收的方式:
- 标记清除
标记清除法是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记该变量“进入环境”,被标记“进入环境”的变量是不能被回收的。当变量离开环境时,就会被标记为“离开环境”,被标记“离开环境”的变量会被内存释放。
垃圾收集器在运行时会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及标记。在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
- 引用计数
这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值。因此,在下次运行回收时,这个变量就会被释放。
这种方法会引起循环引用的问题,例如:Obj1.a = Obj2; Obj2.a = Obj1; 两个对象相互引用,引用次数都是2,因此他们的引用次数永远都不会是0,就会引起循环引用。
3. 减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大所以应该尽量减少垃圾回收。
- 对数组进行优化:在清空一个数组时,用简单的方法就是给其赋值为[],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以次来达到清空数组的目的。
- 对Object进行优化:对象尽量复用,用于不再使用的对象,就将其设置为null,尽快被回收。
- 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外边。
4. 哪些情况会导致内存泄露:
- 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数:设置了setInterval 定时器,而忘记取消,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离DOM的引用:获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
3. this/call/apply/bind
1. 对this对象的理解:
this是执行上下文中的一个属性,它指向最后一次调用的这个方法的对象。在实际开发中,this的指向可以通过四种调用模式来判断。
- 函数调用模式。当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象。
- 方法调用模式。如果一个函数作为一个对象的方法来调用时,this指向这个对象。
- 构造器调用模式。如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。
- apply、call、bind调用模式。这三个方法都可以显示指定调用函数的 this 指向。其中 apply方法接收两个参数: 一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式然后是函数调用模式。
2. call() 、bind() 和 apply() 的比较
(1)三者接受的第一个参数都是this的指向。
(2)第二个参数apply接受的是数组形式,bind和call都是普通参数形式。
(3)call和apply可以调用函数,function.call()和function.apply(),bind不行。
除了改变this指向的其他使用场景:
(1)apply:求数组的最大值/最小值 Math.max.apply(null, [8, 2, 3])
(2)call:检测数据类型,因为typeof 检测数据类型不够精确,typeof null 为object,Object.prototype.toString.call(null) 结果为[object Null]
4.异步编程
1. 异步编程的实现方式?
JavaScript中的异步机制可以分为以下几种:
- 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种万法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
2 . 对promise的理解
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了函数回调地狱,它比传统的解决方案回调函数和事件更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用司样的方法进行处理。
(1) Promise的实例有三个状态:
- Pending (进行中)
- Fulfilled (已完成)
- Rejected (已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
(2) Promise的实例有两个过程
- pending -> fulfilled : Resolved (已完成)
- pending -> rejected: Rejected (已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了
Promise的特点:
- 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态, pending (进行中)、 fulfilled (已成功)、 rejected (已失败) 。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来一“承诺”。
- 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能: 从 pending 变为 fulfilled ,从 pending 变为 rejected 。这时就称为 resolved(已定型) 。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event) 完全不同,事件的特点是: 如果你错过了它,再去监听是得不到结果的。
Promise的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段 (刚刚开始还是即将完成)
总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变就凝固了,无法再被改变了。
状态的改变是通过 resolve 和 reject0 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:在构造 promise 的时候,构造函数内部的代码是立即执行的。
- promise的五个方法
Promise有五个常用的方法: then、catch、all、race、finally。
- then
- catch
- all
- race
- finally
- 对async和await的理解
async/await其实是 Gnerator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中。
这里函数return出的是一个promise对象。
所以,async 函数返回的是一个 Promise 对象。async 函数 (包含函数语、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return一个直接量,async 会把这个直接量通过 Promise.resolve()封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层如果不用 await 获取其返回值,那么可以用原来的方式: then() 链来处理这个 Promise 对象。
那如果 async 函数没有返回值,又该如何? 很容易想到,它会返回 Promise.resolve(undefined)。
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
注意::Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
- await 到底在等啥?
一般来说,都认为 await 是在等待一个async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换话说,就是没有特殊限定)。
因为 async 函数返回一个Promise 对象,所以 await 可以用于等待一个async 函数的返回值一这也可以说是await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。
await 表达式的运算结果取决于它等的是什么:
- 如果它等到的不是一个 promise 对象,那 await 表达式的运算结果就是它等到的东西。
- 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
四、Vue
1. Vue基础
1. 介绍一下生命周期:
(1)beforeCreate (创建前)
(2)created (创建后) data、computed等配置完成,但未挂载到DOM上
(3)beforeMount (挂载前) 不能操作DOM,页面呈现未经Vue编译的DOM结构
(4)mounted (挂载后) DOM可以操作了,一般做开启定时器等初始化操作
(5)beforeUpdate (更新前) 数据已经更新,但还未渲染到页面上
(6)updated (更新后)
(7)beforeDestroy (销毁前) 一般做收尾工作,如:关闭定时器等
(8)destroyed (销毁后)
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。
- data为什么是一个函数而不是对象:
Vue组件可能存在多个组件实例,如果使用对象形式定义data,则会导他们公用一个data对象,那么状态变更将会影响所有组件实例。采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。(小闭包)
- v-if 和 v-for 哪个优先级更高?如果同时出现,应该如何优化
v-for优先于v-if被解析,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能。
要避免出现这种情况,则在外层嵌套 template ,在这一层进行v-if判断,然后在内部进行v-for循环。如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。
- 常见的vue性能优化方法:
- 路由懒加载
- keep-alive缓存页面
- 使用v-show复用DOM
- v-for遍历避免同时使用v-if
- 长列表性能优化
- 事件的销毁
- 图片懒加载
- 第三方插件按需引入
- 无状态组件标记为函数式组件
- 子组件分隔
- 变量本地化
- SSR
- Vue的优点:
- 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb
- 简单易学:国人开发,中文文档,不存在语言障碍,易于理解和学习
- 双向数据绑定:保留了angular的特点,在数据操作方面更为简单
- 组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势
- 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作
- 虚拟DOM:dom操作时非常耗费性能的,不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom,不过是换了一种方式
- 运行速度更快:相比较于react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势
- Vue子组件和父组件生命周期的执行顺序
(总结:父子子父 3 4 1)
加载渲染过程:
1.父组件 beforeCreate
2.父组件 created
3.父组件 beforeMount
4.子组件 beforeCreate
5.子组件 created
6.子组件 beforeMount
7.子组件 mounted
8.父组件 mounted
更新过程:
1.父组件 beforeUpdate
2.子组件 beforeUpdate
3.子组件 updated
4.父组件 updated
销毁过程:
1.父组件 beforeDestroy
2.子组件 beforeDestroy
3.子组件 destroyed
4.父组件 destoryed
7. created和mounted的区别
(1)created:在模版渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
(2)mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
8. 一般在哪个生命周期请求异步数据
我们可以在钩子函数created、beforeMount、mounted中进行调用,因为这三个钩子函数中,data已经创建,可以将服务器端返回的数据进行赋值。
推荐在created中调用异步请求,原因:
- 能更快获取到服务器端数据,减少页面加载时间,用户体验更好。
- SSR不支持beforeMount、mounted钩子函数,放在created中有助于一致性。
- keep-alive中的生命周期
(1)keep-alive是vue提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
(2)使用keep-alive就会多出两个生命周期:activated、deactivated。同时,组件缓存后就不会执行组件的 created, mounted, destroyed 等钩子了。
(3)activated:当组价被激活时触发 → 进入这个页面的时候触发
(4)deactivated:当组件不被使用的时候触发 → 离开这个页面的时候触发
钩子顺序: created -> mounted -> activated
10. v-model的实现原理
v-model是vue的一个语法糖,实现原理:
① :value 数据绑定
② @input input事件监听
11. 对MVVM模型的理解
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义(data)
- View代表UI视图,负责数据的展示(html视图)
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操( vue实例对象)
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会钟发View层的刷新,View中中于用户交与操作而改变的数据也会在Mode中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
12. Computed 和 Watch 的区别
对于Computed:
- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值; 在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于Watch:
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
immediate: 组件加载立即触发回调函数
deep: 深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
- 当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch.
总结:
computed 计算属性:依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器:更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
- v-if和v-show的区别:
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素,v-show是通过设置DOM元素的display样式属性控制显隐
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
- 编译条件:v-if是情性的,如果初始条件为假,则什么也不做,只有在条件第一次变为真时才开始局部编译;v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留
- 性能消耗:v-if有更高的切换消耗,v-show有更高的初始渲染消耗
- 使用场景:v-if适合运营条件不大可能改变,v-show适合频繁切换
- 过滤器的作用,如何实现一个过滤器
根据过滤器的名称,过滤器时用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性computed,方法methods都是通过修改数据来处理格式的输出显示)。
使用场景:
格式化数据:时间、价格等
用法:
{{ price | filter_price }}
filters: {
filter_price (price){
return price ? (‘¥’ + price) : ‘--’
}
}
- $route 和 $router 的区别是什么?
$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
$router 为 VueRouter 的实例,相当于一个全局的路由器对象,里面含有很多属性和子对象,例如 history 对象,经常用的跳转链接就可以用 this.router.push 会往 history 栈中添加一个新的记录。返回上一个 history 也是使用$router.go 方法。
- Vue中给data中的对象属性添加一个新的属性时会发生什么?如何解决?
视图不会刷新,因为在Vue实例对象创建时,改属性并未声明,因此没有被Vue转换为响应式的属性。需要用 $set() 来解决。
- Vue单页面应用与多页面应用的区别
概念:
- SPA单页面应用,指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用,指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页面跳转应用,需要整页资源刷新。
区别:
- 对SSR的理解
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。
SSR的优势:
- 更好的SEO
- 首屏加载速度更快
SSR的缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子。
- 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.is的运行环境。
- 更多的服务端负载。
- v-for 的 key作用
- 用于给每个节点(组件)设置唯一的标识符
- 提高渲染效率 当 Vue 在进行虚拟 DOM 的 diff 算法比较新旧节点时,如果节点具有相同的 key,则 Vue会认为它们是相同的节点,不会进行重新渲染,从而提高渲染效率。
- 保持组件状态 在使用 v-for 指令渲染列表时,每个列表项都应该拥有唯一的 key,这样可以在列表项顺序改变时,Vue可以准确地判断哪些列表项是新添加的,哪些列表项是已存在但位置改变的,哪些列表项是被删除的,从而保持组件状态的正确性。
用index作为key可能会引发的问题:
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作。会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
- 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
开发中,有id选id,无id选索引
- Vue中的 $set
Vue会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性添加getter/setter方法,所以属性必须在data对象上存在时才能进行上述过程,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行上述过程,不是响应式的,所以会出现数据变化,页面不变的情况。此时需要用到 $set
this.$set(对象, key, value)
- 插槽
- 匿名插槽
(2)具名插槽
(3)作用域插槽
2. Vue路由
1. Vue-Router 的懒加载如何实现:
非懒加载:
import find from "@/views/Find.vue"
const router = new VueRouter({
routes: [
{ path: "/find", component: find }
],
});
- 方案一(常用):使用箭头函数 + import动态加载
const find = () => import(‘@/views/Find.vue’)
const router = new VueRouter({
routes: [
{ path: "/find", component: find }
],
});
- 方案二:使用箭头函数 + require动态加载
const router = new VueRouter({
routes: [
{ path: "/find",
component: resolve => require([‘@/views/Find.vue’], resolve)
}
],
});
- 方案三:使用webpack 的 require.ensure 技术,也可以实现按需加载。这种情况下,多个路由指定相同的chunkName,会合并打包成一个 js 文件。
// r 就是resolve
const find = r => require.ensure([], () => r(require(‘@/views/Find.vue’)), ‘find’ )
// 路由也是正常的写法 这种是官方推荐的写法,按模块划分懒加载
const router = new VueRouter({
routes: [
{ path: "/find", component: find , name:’find’ }
],
});
- $route 和 $router的区别
(1)$route 是“路由信息对象”,包括 path、params、hash、query、fullPath、matched、name等路由信息参数。
(2)$router 是“路由实例对象”,包括了路由的跳转方法,钩子函数等。
3. 路由 history 和 hash 模式的区别
(1)hash路由在地址栏URL上有#,用 window.location.hash 读取。而history路由没有会好看一点
(2)我们进行回车刷新操作,hash路由会加载到地址栏对应的页面,而history路由一般就404报错了(刷新是网络请求,没有后端准备时会报错)。
(3)hash路由支持低版本的浏览器,而history路由是HTML5新增的API。
(4)hash的特点在于它虽然出现在了URL中,但是不包括在http请求中,所以对于后端是没有一点影响的,所以改变hash不会重新加载页面,所以这也是单页面应用的必备。
(5)history运用了浏览器的历史记录栈,之前有back,forward,go方法,之后在HTML5中新增了pushState()和replaceState()方法,它们提供了对历史记录进行修改的功能,不过在进行修改时,虽然改变了当前的URL,但是浏览器不会马上向后端发送请求。
(6)history的这种模式需要后台配置支持。比如:当我们进行项目的主页的时候,一切正常,可以访问,但是当我们刷新页面或者直接访问路径的时候就会返回404,那是因为在history模式下,只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求,但是当我直接在浏览器里输入这个地址的时候,就一定要对服务器发起http请求,但是这个目标在服务器上又不存在,所以会返回404
总结:
- #
- 刷新
- 版本问题
3. 组件通信
- props / $emit 父传子/子传父
- eventBus 事件总线
- 依赖注入 provide / inject
- ref / $refs
- $parent / $children
- Vuex
- v-model
- .sync
4. Vuex
- Vuex有哪几种属性
有五种:state、getters、mutations、actions、modules
- state => 基本数据
- getters => (计算属性)
- mutations => 唯一可以修改数据的地方(同步)
- actions => (异步)
- modules => 模块化
5. 虚拟DOM
- 对虚拟DOM的理解?
从本质上来说,Vitual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。在代码染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
总结:
从本质上来说,虚拟DOM是一个JS对象,通过对象的方式来表示DOM结构。将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改真实DOM的回流重绘次数,提高性能。
在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM通过diff算法进行比较,重新渲染变化的内容,没有发生改变的通过原先的数据进行渲染。
- DIFF算法原理
DIFF算法是用来对比新旧虚拟DOM的,是深度优先算法,只会同层比较。
首先比较同层节点虚拟DOM的key值、标签名、是否有data等属性,看是否为同一类型的标签,如果是,则继续进行深层对比,如果不是则将整个节点替换为新节点。
6. Vue原理部分
1. 双向数据绑定原理(响应式原理)
Vue.js 是采用数据劫持结合发布者-订阅者模式和观察者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
- compile解析模板指令,将模板中的变量替换成数据,然后初始化染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和compile之间的通信桥梁,达到数据变化->视图更新,视图交互变化(input) -> 数据model变更的双向绑定效果。
自己总结的:
(1)概念:
Vue.js 是采用数据劫持结合发布者-订阅者模式和观察者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,用观察者模式监听数据,在数据变动时发布消息给订阅者,触发相应的回调。
(2)具体步骤:
① Observe (观察者)对数据对象进行递归遍历(因为对象中可能存在子对象)(若是数组,则重写数组方法),再用Object.defineProperty() 进行数据劫持,如果是对某个值赋值,则会触发 setter 再次 Observe 递归遍历。
② Compile 解析模板指令,将模板中的变量替换成数据,然后初始化染页面视图,并将每个指令对应的节点绑定更新函数(update),添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
③ Watcher (订阅者)作为 Observe 和 Compile 中间的桥梁,首先将自身添加到 dep (订阅器)中,再定义一个update()方法,待 dep.notice() 通知时,能够调用自身的update方法去触发 Compile 的回调。一句话概括就是(订阅Observe 属性变化的消息,触发 Compile 更新函数。)
④ MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和compile之间的通信桥梁,达到数据变化->视图更新(Compile的功能),视图交互变化(input) -> 数据model变更(Observe的功能)的双向绑定效果。
2. $nextTick的原理及作用
Vue的nextTick本质是对JavaScript执行原理EventLoop的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、 setTimeout的原生 JavaScript 方法来模拟对应的微 / 宏任务的实现,本质是为了利用JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理。
nextTick 是典型的将底层JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因:
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要。
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了 nextTick 了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在 $nextTick 中。
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在 nextTick()的回调函数中
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在 nextTick() 的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在 nextTick()的回调函数中。
自己总结:
Vue的nextTick本质是对JavaScript执行原理EventLoop的一种应用。
nextTick 的核心是利用了如 Promise 、 setTimeout等原生 JavaScript 方法来模拟对应的微 / 宏任务的实现,本质是为了利用JavaScript 来实现 Vue 框架中自己的异步回调队列。
nextTick的使用场景:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在 nextTick() 的回调函数中
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在 nextTick() 的回调函数中。
- Vue 为什么要对数组进行方法重写?
这些方法的调用会导致数组发生变化,但是由于默认情况下数组的操作不会触发getter和setter,因此Vue无法观察到这些变化。所以,当我们使用这些方法修改数组时,Vue无法自动更新视图,导致视图和实际数据不一致。
为了解决这个问题,Vue对数组的这些方法进行重写,以便在使用这些方法修改数组时,能够自动通知Vue更新视图。例如,当我们调用数组的push方法时,Vue实际上会将这个新加入的元素再进行一层劫持,以确保在这个元素上发生的变化也能够被Vue检测到。
- Vue 3部分
- Vue3.0有什么更新
- 监测机制的改变
- 3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪
- 消除了 Vue 2 当中基于 object.defineProperty 的实现所存在的很多限制
- 只能监测属性,不能监测对象
- 检测属性的添加和删除
- 检测数组索引和长度的变更
- 支持 Map、Set、WeakMap 和 WeakSet
- 模版
- 作用域插槽,2x 的机制导致作用域插槽变了,父组件会重新染,而 3.0 把作用域插槽改成了函数的方式这样只会影响子组件的重新渲染,提升了渲染的性能。
- 同时,对于render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom
- 对象式的组件声明方式
- vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦
- 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 Typescript 的结合变得很容易
(5)其他方面的更改
- 支持自定义渲染器,从而使得 weex 可以通过自定义染器的方式来扩展,而不是直接 fork 源码来改的方式。
- 支持 Fragment (多个根节点) 和 Protal (在 dom 其他部分染组建内容)组件,针对一些特殊的场景做了处理。
- 基于 tree shaking 优化,提供了更多的内置功能。
简单说一下:
- 组合式api
- 生命周期
- Proxy
- defineProperty和proxy的区别
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
Obiect.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持E8 以及更低版本浏览器的原因。
但是这样做有以下问题:
1.添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set 来调用 Object.defineProperty() 处理
2.无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为: 用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等) 。相对于 object.defineProperty(),其有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- Proxy 可以监听数组的变化。
- Vue0 为什么要用 proxy?
在 Vue2 中,object.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点:
- 不需用使用 Vue.$set 或 Vue.$delete 触发响应式。
- 全方位的数组变化检测,消除了Vue2 无效的边界情况。
- 支持 Map,Set,WeakMap 和 WeakSet。
Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异:
- get 收集依赖
- Set、delete 等发依赖
- 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑
- vue2和vue3的区别?
- vue3比vue2的优势
性能更好,打包体积更小,更好的ts支持
- vue3的生命周期
Setup取代创建的两个钩子,命名方式加on,destroy改名unmounted
- 选项式api和组合式api
组合式api的好处:
更好的代码组织,更好的逻辑复用,可维护性强。
- proxy和defineProperty的区别
数据响应式原理不同
vue2是采用Object.defineProperty()来劫持各个属性的setter和getter,是存在一些问题的。比如:初始化时要深度递归遍历待处理的对象才能完全对它进行完全拦截明,显增加了初始化的时间。还检测不到数组的变化,对数组方法进行了重写。并且不能拦截新增属性,要调用$set让它检测到。
① vue2是采用Object.defineProperty()来劫持各个属性的setter和getter,就因为是对象中的各个属性,若对象中包含子对象的话则要层层递归来进行数据劫持。而vue3则是采用proxy,可以对整个对象进行劫持,用到数据时再进行对下一层属性的劫持。
② 因为vue2无法劫持整个对象,所以对象上新增、删除的属性则无法进行响应式,需要用$set让其具有响应式。vue3的proxy则没有这种困扰。
③ vue2在劫持数组时,是对几个方法进行了重写,并没有对数组的每个索引进行劫持,数组长度变化时
Vue3是采用proxy,它解决了上述问题,它不仅可以对数组实现拦截,还能对Map、Set集合实现拦截。
- v-if和v-for优先级问题
vue2里v-for的优先级大于v-if,就导致了浪费性能的问题。
vue3修复了这个机制,v-if的优先级大于v-for。
- Vuex和pinia的区别?
1.pinia它没有mutation,它只有state,getters action[同步,异步],使用他来修改state数据2.pinia语法上比vuex更容易理解和使用,灵活
3.pinia没有modeles配置,每一个独立的仓库都是defindStore生成出来的
4.pinia完整的支持TS
5.pinia可以有多个store,每个store都是defineStore定义出来的
6.pinia不用借助MapAction、MapState等辅助函数,直接常规的函数或者值调用即可
- ref和reactive有何差异?
(1)数据类型不同
ref用于包装字符串、数字、布尔等基本数据类型,reactive用于包装对象和数组等复杂数据类型的。
(2)访问方式不同
ref的需要 .value来访问实际值,reactive直接访问即可。
- watch的使用,v2和v3里
- v2里:
watch:{
数据: {
handler(新值,老值) {
// ...
},
deep: true,
immediate: true
}
}
- v3里:
- 简单数据类型
Watch(数据,(新值,老值)=>{})
- 复杂数据类型
Watch( ()=> 数据,(新值,老值)=>{})
- watch和watchEffect区别
(1)watch
- watch显式指定依赖数据,依赖数据更新时执行回调函数
- 具有一定的惰性lazy 第一次页面展示的时候不会执行,只有数据变化的时候才会执行(设置immediate: true时可以变为非惰性,页面首次加载就会执行)
- 监视ref定义的响应式数据时可以获取到原值
- 既要指明监视的属性,也要指明监视的回调
(2)watchEffect
- watchEffect自动收集依赖数据,依赖数据更新时重新执行自身
- 立即执行,没有惰性,页面的首次加载就会执行
- 无法获取到原值,只能得到变化后的值
- 不用指明监视哪个属性,监视的回调中用到哪个属性就监视哪个属性
总结一下:
- watch可获取新值和旧值,watchEffect只能获取新值。
- watch有惰性,需要开启immediate才能立即执行。watchEffect没有惰性,首次加载就会执行。
- watch需要指明监视的属性和回调。watchEffect不需要指明,回调中用到哪个属性就监视哪个属性。
- provide/inject的响应式问题
- 组合式api中
只要provide的是ref响应式数据,那么inject得到的就是响应式数据。
- 选项式api中
正常传递就不是响应式,需要对传递的数据computed包裹。
五、小程序
1. 几种跳转方式的区别
(1)navigateTo:保留当前页面,跳转 不允许跳tabBar页面
(2)redirectTo:关闭当前页面,跳转 不允许跳tabBar页面
(3)switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
(4)navigateBack:关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层(页面栈最多十层)
用法:
wx.navigateBack({
delta: 1
})
navigateBack携带参数跳转:
先获取想要跳转到的那一页的页面栈,直接对那个page写setData就行。然后再跳。
(5)reLaunch:关闭所有页面,打开到应用内的某个页面
2. 小程序传值方式
(1)URL参数传递:通过在小程序的跳转连接中添加参数
Eg. wx.navigateTo({ url: 'url', id: '1' }) 或者 直接在navigator 标签内传值
接收:在 onLoad 钩子里(options) 接受
(2)全局变量传递
直接挂载到 wx上
(3)Storage存储传递
(4)组件传值
父传子:父直接传,子 properties 接收。
子传父:子triggerEvent,父通过方法接收就行。
- 上拉刷新,下拉加载
- 小程序有个组件 scroll-view 配置一下属性,写个函数就行
- 页面自带的事件处理函数:
下拉刷新:onPullDownRefresh()
上拉加载:onReachBottom()
- 生命周期
- 应用
- onLaunch 当整个项目启动,渲染完成时触发(全局只触发一次)
- onShow 前台显示
- onHide 后台隐藏
- 页面
- onLoad 页面加载
- onShow 页面展示
- onHide 页面隐藏
- 组件
用lifetimes包起来了
(1)created 实例创建完成
(2)attached 进入页面节点树
(3)detached 被从页面节点树移除
- 小程序分包限制
(1)整个小程序所有分包大小不超过 4M
(2)单个分包/主包大小不能超过 2M
6. uni-app的生命周期
(1)应用 小程序
(2)页面 小程序
(3)组件 Vue
7. uni-app的组件传值
(一)父传子 跟vue一样
- 子传父 跟vue一样
(三)非父子 监听 $on 发送 $emit
六、性能优化
1. 性能优化总结
(1)路由懒加载
(2)组件懒加载
Vue2:直接import导入就行
Vue.component('async-component', () => import('./MyComponent.vue'))
Vue3:用defineAsyncComponent()
const AsyncComponent = defineAsyncComponent(() => import('./MyComponent.vue'))
(3)图片懒加载
用插件vue-lazyload
用的时候直接 v-lazy=”image”
(4)打包减少体积方向
① CDN加速
② 按需导入
(5)开启Gzip压缩
(6)开启http2.0 在nginx上配置
2. 回流与重绘
- 回流与重绘的概念以及触发条件、性能
- 回流
当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。
会导致回流的操作:
- 页面的首次渲染
- 浏览器的窗口大小发生变化
- 元素的内容发生变化
- 元素的尺寸或位置发生变化
- 元素的字体大小发生变化
- 激活CSS伪类
- 查询某些属性或者调用某些方法
- 添加或删除可见的DOM元素
在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发会流失,会导致周围的DOM元素重新排列,它的影响范围有两种:
- 全局范围:从根节点开始,对整个渲染树进行重新布局
- 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局
- 重绘
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
会导致重绘的操作:
- color、background相关属性:background-color、background-image等
- outline相关属性:outline-color、outline-width、outline-decoration
- border-radius、visibility、box-shadow
注意:回流一定重绘,重绘不一定回流
(3)性能:
回流的开销较大,有时仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流,如果每个操作都去回流重绘的话,浏览器可能会受不了。
故,现在浏览器会对频繁的回流或重绘操作进行优化:
浏览器会维护一个队列,把所有引起回流和重绘的操作放入到队列中,如果队列中的任务数量或者时间间隔达到一个阈值,浏览器会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
虽然有了浏览器的优化,但有时一些代码可能会强制浏览器提前清空队列,这样浏览器的优化可能就起不到作用了。比如:
offsetTop、offsetLeft、 offsetWidth、 offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
width、height
- 如何避免回流与重绘?
减少回流与重绘的措施:
- 不要经常访问会引起浏览器清空队列的属性,如果你确实要访问,利用缓存让元素脱离动画流,减少回流的规模
- 操作DOM时,尽量在低层级的DOM节点进行操作
- 不要使用table布局,一个小的改动可能会使整个table进行重新布局
- 尽量使用CSS属性的简写,如: 用 border 代替 border-width, border-style, border-color
- 不用频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式
- 需要多次重排的元素,使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
- 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上边应用所有DOM操作,最后再把它添加到文档中
- 将元素先设置display:none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的操作不会引发回流和重绘。
- 如何优化动画?
一般情况下,动画需要频繁的操作DOM,就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。
- documentFragment是什么?用它跟直接操作DOM的区别是什么?
documentFragment的概念:
documentFragment,文档片段的接口,一个没有父对象的最小文档对象。它被作为一个轻量版的document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是documentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的重新渲染,且不会导致性能等问题。
当我们把一个documentFragment节点插入到文档树时,插入的不是documentFragment自身,而是它所有的子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入documentFragment,之后一次性将所有的子孙节点插入文档中。和直接操作DOM相比,将documentFragment节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。
3.节流与防抖
1. 对节流与防抖的理解:
(1)防抖
(回城)
函数防抖是指在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
(2)节流
(技能CD)
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在scroll函数的事件监听上,通过事件节流来降低事件调用的频率。
防抖函数的使用场景:
输入框、提交按钮
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务器验证场景:表单验证需要服务器端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似生存环境请用lodash.debounce
节流函数的使用场景:
上拉加载、下拉刷新
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器resize
- 动画场景:避免短时间内多次触发动画引起性能问题
4. 图片优化
- 如何对项目中的图片进行优化?
(1)不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替
(2)对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
(3)小图使用 base64 格式
(4)将多个图标文件整合到一张图片中 (雪碧图)
(5)选择正确的图片格式:
①对于能够显示 WebP 格式的浏览器尽量使用 Webp 格式。因为 Webp 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好。
②小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
③照片使用JPEG
- 常见的图片格式及使用场景
- BMP
BMP是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。
- GIF
GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。
- JPEG
JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。
- PNG-8
PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。
- PNG-24
PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多
- SVG
SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。
- WebP
Webp是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢? 就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据专输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持Webp格式,兼容性不太好。
在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%
在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%;, WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。
- CDN加速简单概述
- 在config 文件中配置线上cdn链接,我一般都是在bootcdn上找的。
- 判断是否是生产环境,若是则将链接挂载,在index.html上css资源以link herf的形式引入,js资源以script src形式引入。
- Gizp 压缩简单概述
- 在config文件中先关闭prefetch预加载模块(默认开启,则会提前获取用户未来可能会访问的内容,在首屏时直接把所有的路由文件全部下载了)
- 再判断是否是生产环境,若是则匹配文件后缀名(js、html、css)超过4kb则进行压缩。
- 缩短首页白屏时间(优化)
- 路由、图片、组件懒加载
- 骨架屏、loading效果(感知上的)
- 线上CDN加速
- Gzip压缩
- 长列表虚拟滚动
- 长列表、虚拟列表
- 控制请求并发数量
浏览器对同一个域的并发请求数量限制为6。
突破chrome 6个线程的限制:
方法一:让请求处于不同域,就可以打破这个限制
- Promise.all 和 promise.allSettled
- Promise.all
一组中一个请求失败就无法获取改组其他成员的返回值
- Promise.allSettled
每个请求都会返回结果,不管失败还是成功
七、计算机网络
- 三次握手
TCP 是因特网中的传输层协议,使用三次握手协议建立连接,完成三次握手,客户端与服务器开始传送数据。
第一次握手: 建立连接时,客户端发送 syn 包 (syn=j) 到服务器,并进入SYN_SENT (发送)状态等待服务器确认; SYN: 同步序列编号 (Synchronize Sequence Numbers)
第二次握手: 服务器收到 syn 包,必须确认客户的 SYN (ack=j+1),同时自己也发送一个SYN 包 (syn=k) ,即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态
第三次握手: 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED (TCP 连接成功) 状态,完成三次握手。
TCP 协议优点:
TCP 发送的包有序号,对方收到包后要给一个反馈如果超过一定时间还没收到反馈就自动执行超时重发,因此 TCP 最大的优点是可靠。
TCP协议缺点:
很简单,就是麻烦,如果数据量比较小的话建立连接的过程反而占了大头,不断地重发也会造成网络延迟,因此比如视频聊天通常就使用 UDP,因为丢失一些包也没关系,速度流畅才是重要的。
为什么是三次握手,不是两次握手:
为了防止已失效的连接请求报文段突然有送到了服务器,因而产生错误,假设两次握手时客户发出的第一个请求连接报文段在某一网络节点长时间滞留,以致延误到连接释放后才到达服务器。服务器收到失效的连接请求报文段后,认为是客户又发出一次新的连接请求于是向客户发送确认报文段,同意建立连接,此时在假定两次握手的前提下,连接建立成功。这样会导致服务器的资源白白浪费。
- 四次挥手(四次分手)
第一次分手:客户端 ----> 服务器 FIN包
第二次分手:服务器 ----> 客户端 ACK包
第三次分手:服务器 ----> 客户端 FIN包
第四次分手:客户端 ----> 服务器 ACK包
为什么是四次挥手:
TCP 协议是全双工通信,这意味着客户端和服务器端都可以向彼此发送数据,所以关闭连接是双方都需要确认的共同行为,假设是三次挥手时,首先释放了客户到服务器方向的连接此时 TCP 连接处于半关闭状态,这时客户不能向服务器发送数据,而服务器还是可以向客户发送数据。如果此时客户收到了服务器的确认报文段后,就立即发送一个确认报文段这会导致服务器向客户还在发送数据时连接就被关闭。这样会导致客户没有完整收到服务器所发的报文段
- 第三次握手失败了怎么办?
若第三次握手失败,服务器会有定时器发送第二步SYN+ACK数据包,如果客户端再次发送ACK成功,建立连接。
如果一直不成功,服务器肯定会有超时(大概64s)设置,超时之后会给客户端发RTS报文(连接重置),进入CLOSED状态,防止SYN洪泛攻击,这个时候客户端应该也会关闭连接。
- 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
- 浏览器自动补全协议、端口,自动完成url编码,并根据请求的URL交给DNS域名解析,找到真实IP,向服务器发出建立TCP连接的申请,完成三次握手后建立通道通信。若使用了HTTPS,则还会进行SSL握手,建立加密信道。
- 浏览器自动设置好cookie、请求头、协议版本等,并根据connection字段来决定是否保留信道不关闭。
- 服务器交给后台处理完成后返回数据,浏览器接收文件 ( HTML、JS、CSS 、图象等)
- 浏览器对加载到的资源 (HTML、JS、CSS 等)进行语法解析,建立相应的内部数据结构 (如 HTML 的 DOM)
- 载入解析到的资源文件,渲染页面,完成。
5. 同源和跨域策略
1. 什么是同源策略?
同源策略是 浏览器最核心最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
2. 什么是跨域?
当 协议 + 域名 + 端口 任意一个不同,即为跨域
3. 跨域解决方法?
1. 跨域资源共享(CORS) (后端设置)
CORS实现(在node中)
配置CORS中间件,配置origin请求源、headers请求头允许参数、methods请求方法
2. 代理服务器 (前端设置)
3.1 为什么服务器代理可以解决跨域问题?为什么不收同源策略的影响?
1. 服务器代理,本质上是服务器发送请求,所以不受同源策略的影响 (同源策略是浏览器的安全策略)
6. get和 post请求的区别
(1)传参方式不同(get请求参数通过url传递,post请求放在请求体request body中传递)
(2)post请求更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中,get请求的是静态资源,则会缓存,如果是数据,则不会缓存)
(3)post请求发送的数据更大(get请求有url长度限制,http协议本身不限制,请求长度限制是由浏览器和web服务器决定和设置)
(4)get在请求时,配合params使用,post配合data使用。
7. http和https的区别
- 端口号
http:80端口
https:443端口
- 加密
http:明文传输
https:SSL加密传输
- 性能
http:因为明文传输,不需要加密,所以速度会快一些
https:服务器和客户端双方都要做加密、解密处理,耗费更多服务器资源,并且在协议握手阶段要处理加密、解密,比较费时。
- webSocket
概念:
webSocket是一种支持双向通讯的网络通信协议。
特点:
- 建立在TCP协议上,还与HTTP协议有着很好的兼容性,默认端口也是80和443,并且在握手阶段采用HTTP协议
- 数据格式比较轻量,性能开销小,通信高效
- 没有同源限制,客户端可以与任意服务器通信
- 标识符是ws(如果加密,则为wss(TCP + TLS)),服务器网址就是URL
为什么要使用webSocket?
因为http 通信只能由客户端发起,服务器返回查询结果,HTTP 协议做不到服务器主动向客户端推送信息,服务器有连续的状态变化,客户端要获知就非常麻烦。
- 轮询
实现即时通讯主要有四种方式,它们分别是短轮询、长轮询(comet)、长连接(SSE)、WebSocket。
短轮询:
短轮询是浏览器每隔一段时间向服务器发送http请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。
需要不断的建立http连接,严重浪费了服务器端和客户端的资源。
长轮询:
当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回。 客户端JavaScript响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。缺点在于,连接挂起也会导致资源的浪费。
- 浏览器如何进行页面渲染?
1. 获取 HTML ⽂件并进⾏解析,生成一棵 DOM 树(DOM Tree)
2. 解析 HTML 的同时也会解析 CSS,⽣成样式规则(Style Rules)
3. 根据 DOM 树和样式规则,生成一棵渲染树(Render Tree)
4. 进行布局(Layout),即为每个节点分配⼀个在屏幕上应显示的确切坐标位置
5. 进⾏绘制(Paint),遍历渲染树节点,调用 GPU 将元素呈现出来
- 说一下你对http的了解
http本质上是一种客户端与服务器端进行数据请求和应答的标准规范属于应用层的协议,建立在tcp/ip协议上,
http简单介绍、三次握手、四次挥手、http和https的区别、http的常见请求方式、get和post请求的区别、http常见响应状态码、请求数据后浏览器如何渲染、http版本差异
- http版本差异
http 1.0:
- 无法复用链接:
- 队头阻塞:
他为每个请求单独新开一个TCP连接,也就是每一个请求都会走一遍三次握手和四次挥手,这样会造成队头阻塞、内存资源和响应时间上的浪费。
http 1.1:
- 长连接:(在一次连接中,可承载多次请求)
解决了1.0 无法复用链接的问题,在请求头中默认包含了Connection:keep-alive字段,一遍三次握手建立连接后可以将所有的请求都按顺序发送,接收完毕后再四次挥手摧毁链接。
- 管道化和队头阻塞:
用管道化的方式去建立的长连接,按顺序去发送,但也要按顺序去接收,还是有队头阻塞的问题。
http 2.0:
- 二进制分帧:
2.0可以允许以更小的单元传输数据,每个传输单元称之为帧,完整数据称之为流,每个流有自己的编号,每个帧会记录所属的流。真正的解决了队头阻塞,实现了多路复用。
不仅如此,还可以不同数据间交替传输,还可以设置传输优先级。
- 头部压缩:
因为大部分头部信息都有很多的重复。为了解决这一问题,HTTP2.0使用Huffman编码对头部进行压缩来减少消息头的体积。(Huffman编码:每个字符都有一串不同的二进制码对应,要求:任何一个字符的编码都不能是其他字符编码的前缀,如果是前缀就会有歧义)
- 服务器预推送:
2.0允许在客户端没有主动请求的情况下,服务器预先把资源推送给客户端。当客户端后续需要请求该资源时,则自动从之前推送的资源中寻找。
http 3.0:
- 抛弃了tcp拥抱了udp
- 为了保证传输的可靠性,使用了谷歌提出的QUIC协议
- 解决了tcp队头阻塞、建立时间长等问题
- CSRF和XSS攻击
CSRF:
①概念:
CSRF 是跨站请求伪造,是一种挟制用户在当前已登录的Web应用上执行非本意的操作的攻击方法。就是利用用户的cookie身份信息来伪造假请求。
②防御:
- 不使用cookie
- 为表单添加校验的 token 校验
- cookie中使用sameSite字段
XSS:
①概念:
XSS 是指跨站脚本攻击。攻击者利用站点的漏洞,在表单提交时,在表单内容中加入一些恶意脚本,当其他正常用户浏览页面,而页面中刚好出现攻击者的恶意脚本时,脚本被执行,从而使得页面遭到破坏,或者用户信息被窃取。
②防御:
- 过滤:在服务器端过滤脚本代码,将一些危险的元素和属性去掉
- 编码:对危险标签进行HTML实体编码
八、工程化
- 鉴权
- HTTP鉴权
- Session-Cookie鉴权
- Token鉴权
- 扫码登录
- 一键登录
九、综合
1. 前端优化的解决方案:
优化原则:
- 能缓存的,尽量强缓存。
- 引入外部资源时不要出现超时、404的状况。
- 减少HTTP请求数。
- 合理设置cookie的大小以及过期时间。
- 合理利用懒加载。
一、网页内容的优化:
- 懒加载数据。
首先根据标签的left和top属性判断是否显示在了屏幕中(如果显示在屏幕中,其left和top属性值应该是在0到窗口长宽之间)。
如果显示在屏幕中,则将src标签的内容替换为图片的url。
- 使用外部引入的css和js文件,并且引入的css和js越少越好(HTTP2.0不适用)。
这里可以使用webpack打包这些文件,也可以使用强缓存与协商缓存来缓存这些文件。
3、不要在中缩放图片。
img计算缩放也需要时间
4、避免重定向。
重定向会重新渲染网页。
5、尽量不要用iframe。
因为iframe会阻塞渲染。
6、使用base64编码将图片嵌入到样式表中,减少请求数(由于base64会比一般的图片大一点,只适用于那些体积比较小但是很常用的图片)。
7、使用雪碧图(精灵图):
通过使用background-position:-xpx -ypx;来调整图片的位置,不过HTTP2不适用,原因为HTTP2实际上是多路复用的,只用一个TCP连接,所以多个图片的请求也是在同一个TCP连接里,这样能省下非常多的请求时间,但坏处就是单连接开销很大,如果要传多个大文件,就很麻烦。
8、要有网站小图标favicon.ico。
如果没有小图标,会引起404,拖慢网页加载进度。
9、能使用jpeg就不要用png,能使用png8就不要用png24。
(1)色彩丰富的、大的图片切成jpg的;
(2)尺寸小的,色彩不丰富的和背景透明的切成gif或者png8的;
(3)半透明的切成png24。
10、使用canvas压缩图片。
二、css的优化:
1、避免使用@import。
使用@import相当于将引入的css放在了页面底部,因为使用@import引用的文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。因此使用@import会拖慢渲染的过程。
2、将样式表放在head中。
如果放在body中,可能出现在浏览器下载好css样式表之前,组件就已经加载好了的情况,这可能会导致重新渲染。
3、避免使用css表达式。
如:expression((new Date()).getHours()%2 ? “#B8D4FF” : “#F08A00” );
解析表达式和计算都需要时间。
三、JavaScript的优化:
1、尽量减少DOM访问。
2、使用事件代理(减少DOM操作)。
3、把脚本放在底部(加载脚本时会阻塞页面渲染)。
4、合理使用节流函数和防抖函数。
四、使用CDN优化加载速度:
CDN即内容分发网络。它依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
五、针对webpack打包优化:
针对webpack打包优化主要是减少打包后的代码体积,主要的措施有:
1、进行tree-shaking
2、使用UglifyJS等插件压缩代码
3、分割代码、按需加载
我们可以使用webpack-bundle-analyzer这个插件来查看每部分代码的加载耗时,进而分析可以优化的地方