■ 符号说明
💘 主题
🌟 常见重要
🌛 需要有印象的
🆕 v3新特性
■ 杂谈
🌛 SEO优化
合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。
语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页
重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取
重要内容不要用js输出:爬虫不会执行js获取内容(查看网页源代码要有数据,有利于seo优化)
少用iframe:搜索引擎不会抓取iframe中的内容
非装饰性图片必须加alt
提高网站速度:网站速度是搜索引擎排序的一个重要指标。
🌟 渐进增强与优雅降级
渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进,达到更好的用户体验。
优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
■ HTML
🌟 H5新增API
HTML5新增了哪些内容或API,使用过哪些
音视频audio、video标签
语义化标签article、footer、header、nav、section等
表单控件date、time、color、emial、url、search等
新增图形绘制canvas标
H5存储localStorage、sessionStorage
新的技术webworker, websocket, Geolocation;
DNS预获取、FormData、FileReader、全屏API(Fullscreen API)等等
🌛 忽略电话号码
移动设备忽略将页面中的数字识别为电话号码的方法
<meta name="format-detection" content="telephone=no"> // 禁止把数字转换为拨号链接
<meta name="format-detection" content="address=no"> // 禁止把地址跳转至地图
<meta name="format-detection" content="email=no"> // 禁止识别邮箱自动发送邮件
<meta name="format-detection" content="telephone=no,adress=no,email=no">
🌟 iframe标签
iframe就是HTML中,用于网页嵌套网页的。一个网页可以嵌套到另一个网页中,可以嵌套很多层。
返回
iframe会阻塞主页面的Onload事件
搜索引擎的检索程序无法解读这种页面,不利于SEO
iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript动态给iframe添加src属性值,这样可以绕开以上两个问题
举例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe src="http://www.4399.com" frameborder="0"></iframe>
</body>
<style>
iframe{
position: fixed;
background: white;
border: none;
top: 0; right: 0;
bottom: 0; left: 0;
width: 100%;
height: 100%;
}
</style>
</html>
🌟 实战必备:favicon.ico
制作:https://tool.520101.com/diannao/ico/
作用:通过上传文件转换生成网站自定义图标
场景:打开各种网页 ,查看窗口左上角有自定义图标
🌟 性能优化:标签你不知道的属性
场景:打开淘宝、京东等网站查看标签加了一些你不知道的属性
<meta name="renderer" content="webkit" />
<title>淘宝网 - 淘!我喜欢</title>
<meta name="spm-id" content="a21bo" />
<meta name="description" content="淘宝网 - 亚洲较大的网上交易平台,提供各类服饰、美容、家居、数码、话费/点卡充值… 数亿优质商品,同时提供担保交易(先收货后付款)等安全交易保障服务,并由商家提供退货承诺、破损补寄等消费者保障服务,让你安心享受网上购物乐趣!" />
<meta name="aplus-xplug" content="NONE">
<meta name="keyword" content="淘宝,掏宝,网上购物,C2C,在线交易,交易市场,网上交易,交易市场,网上买,网上卖,购物网站,团购,网上贸易,安全购物,电子商务,放心买,供应,买卖信息,网店,一口价,拍卖,网上开店,网络购物,打折,免费开店,网购,频道,店铺" />
<link rel="dns-prefetch" href="//g.alicdn.com" />
<link rel="dns-prefetch" href="//img.alicdn.com" />
<link rel="dns-prefetch" href="//tce.alicdn.com" />
<link rel="dns-prefetch" href="//gm.mmstat.com" />
<link ref="dns-prefetch" href="//tce.taobao.com" />
<link rel="dns-prefetch" href="//log.mmstat.com" />
<link rel="dns-prefetch" href="//tui.taobao.com" />
<link rel="dns-prefetch" href="//ald.taobao.com" />
<link rel="dns-prefetch" href="//gw.alicdn.com" />
<link rel="dns-prefetch" href="//atanx.alicdn.com"/>
<link rel="dns-prefetch" href="//dfhs.tanx.com"/>
<link rel="dns-prefetch" href="//ecpm.tanx.com" />
<link rel="dns-prefetch" href="//res.mmstat.com" />
dns-prefetch
域名转化为 ip 是一个比较耗时的过程,dns-prefetch 能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns 预取。
<link rel="dns-prefetch" href=" //m.baidu.com<link rel="dns-prefetch"href="//m.baidu.com">
prefetch
prefetch 一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch 的资源。
<link rel="prefetch" href=" https://www.baidu.com/">
preload
和 prefetch 不同,prefecth 通常是加载接下来可能用到的页面资源,而 preload 是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload 不是空闲时加载,它的优先级更强,并且会占用 http 请求数量。
<link rel='preload' href='style.css' as="style" οnlοad="console.log('style loaded')"
区别:
Preload 来告诉浏览器预先请求当前页需要的资源,从而提高这些资源的请求优先级。比如,对于那些本来请求优先级较低的关键请求,我们可以通过设置 Preload 来提升这些请求的优先级。
Prefetch 来告诉浏览器用户将来可能在其他页面(非本页面)可能使用到的资源,那么浏览器会在空闲时,就去预先加载这些资源放在 http 缓存内,最常见的 dns-prefetch。比如,当我们在浏览A页面,如果会通过A页面中的链接跳转到B页面,而B页面中我们有些资源希望尽早提前加载,那么我们就可以在A页面里添加这些资源 Prefetch ,那么当浏览器空闲时,就会去加载这些资源。
所以,对于那些可能在当前页面使用到的资源可以利用 Preload,而对一些可能在将来的某些页面中被使用的资源可以利用 Prefetch。如果从加载优先级上看,Preload 会提升请求优先级;而Prefetch会把资源的优先级放在最低,当浏览器空闲时才去预加载。
defer 和 async 现在重技术的公式面试必问
//defer <script defer src="script.js"></script> //async <script async src="script.js"></script>
defer 和 async 都是异步(并行)加载资源,不同点是 async 是加载完立即执行,而 defer 是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded 事件触发之前。 因为 async 加载的资源是加载完执行,所以它比不能保证顺序,而 defer 会按顺序执行脚本。
■ CSS
🌟 CSS选择器
标签选择器,也称为元素选择器。
标签选择器的基本形式如下:tagName{property:value},其中tagName是标签名称,property是css的属性。
类选择器。
类选择器用来为一系列标签定义相同的呈现方式,常用的语法是 .classValue{property:value}。其中classValue是类选择器的名称,这是由css编写者自己命名。(所有浏览器都支持类选择器,但多类选择器(.className1.className2)不被ie6支持。)
ID选择器。
ID选择器定义的是某一个特定的html元素,一个网页中只有一个标签或元素使用某一ID的属性值。ID选择器的基本语法格式如下:#idValue{property:value}。其中idValue是ID选择器的名称,可以由CSS编写者自己编写。(所有浏览器都支持)
全局选择器。
全局选择器就是对所有的htmlz元素起作用。语法格式为: *{propery:value}。其中“*”表示对所有元素起作用,property表示css的属性,value表示属性值。
复合选择器。
将多种选择器进行搭配,可以构成一种复合选择器,也称为组合选择器。
继承选择器。
继承的规则是子标签在没有定义的情况下,继承父标签的选择器;当子标签重复定义了父标签的声明时,执行子标签选择器。
伪类选择器。
伪类选择器主要应用在标签上,它由四种状态:未访问链接(link)、已访问链接(visited)、激活链接(active)、鼠标停留在连接上(hover)。
留心,:befor(没有写错,就是比CSS3少一个e)是Css2的写法(兼容好),::before是Css3的写法(兼容相对不好)
🌛 CSS选择器优先级
权重顺序: !important > 行内样式 > ID > 类、伪类、属性 > 标签 > 通配符 > 继承 > 浏览器默认属性
权重大小
通配符0
标签 1
类/伪类/属性 10
ID 100
行内样式 1000
!important 无穷大
🌟 CSS3新特性
属性选择器
圆角
阴影
渐变
多列布局
css自定艺术性等等
🌟 定位 position
static 默认值,没有定位,元素出现在正常的文档流中;
relative 相对定位,相对位置为自身默认位置;
absolute绝对定位,相对位置为最近父非static的元素,没有就是body/可见视口/浏览器窗口;
fixed固定定位,对象位置为 body/可见视口/浏览器窗口;
sticky粘性定位,该定位基于用户滚动的位置;
🌟 定位 position场景
static 默认
relative、absolute 淘宝京东轮播图左右箭头、分页器、热卖商品、新品等等
fixed 传统网站两侧滚动广告、还有右下角客服、返回顶部等等
sticky 网站导航栏 例如 https://www.discuz.net/
🌟 谈谈你对盒模型的理解
问:什么是盒模型?
网页布局中CSS布局的思想模型
主要规定元素和元素之间的关系
例如:内容(content),内边距(padding),边框(border),外边距(margin)
问: 标准盒模型、怪异盒模型是什么
用box-sizing告诉浏览器如何计算一个元素是总宽度和总高度
标准盒模型:box-sizing: content-box
一个块的总宽度 width + margin(左右) + padding(左右) + border(左右)
IE盒模型或怪异盒模型 box-sizing: border-box
一个块的总宽度 = width(已包含padding和border) + margin(左右)
🌟 谈谈你对BFC的理解
垂直方向上的距离由margin决定,属于同一个BFC的两个相邻的块级元素会发生margin合并,不属于同一个BFC的两个相邻的块级元素不会发生margin合并(应用:防止margin重叠);
BFC的区域不会与float box重叠(应用:自适应两栏布局)
计算BFC的高度时,浮动元素也参与计算(应用:清除内部浮动)
内部的Box会在垂直方向,一个接一个地放置。
每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反);即使存在浮动也是如此。(说明:就是子盒子排列时候从父盒子左上角开始)
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
BFC是什么?
是一个独立的布局环境
BFC能干嘛?
形成一个完全独立的空间,让空间中的子元素不会影响外面的布局
BFC特性?
独立空间内元素/标签的布局规则
如何触发BFC
float:除 none 以外的值
position为 absolute、fixed
display 为 inline-block、table、table-row、table-cell、flex等等
overflow 除了 visible 以外的值 (hidden、auto、scroll)
column-count等
BFC应用
自适应两栏、三栏布局
<style>
* {padding:0px;margin:0px;}
.left {width:200px;height:100px;background:green;float: left;}
.right {height:200px;background:red;overflow: hidden; }
</style>
<div class="left"></div>
<div class="right"></div>
清清除内部浮动
<style>
* {padding:0px;margin:0px;}
/*
ul {background: green;}
*/
ul {background: green; overflow: hidden;}
li {width:100px;height:20px;background:red;float: left;}
</style>
<ul>
<li>001</li>
<li>002</li>
<li>003</li>
</ul>
利用BFC避免margin重叠(案例:京西商城后台首页头部导航
<style>
* {padding:0px;margin:0px;}
p {width:100px;height:20px;background:red;margin:10px 0px;}
</style>
<!--
<p>盒子1</p>
<p>盒子2</p>
-->
<div style="overflow:hidden;"><p>盒子1</p></div>
<p>盒子2</p>
🌛 谈谈你对栅格的理解
将网页内容划分成12等分
使用布局容器宽度变小了,依旧12等分
会发现内容变小了
不够就空着
超出就另起一行
案例:bootstrap 测试:Bootstrap可视化布局系统
🌟 自适应
rem和em区别、vw是什么?
都是相对单位
rem相对于html绕开了复杂的层级关系
em相对父麻烦
vw可视窗口1%
🌛 解决谷歌最小字体12px限制
通过transform缩放即可解决
transform: scale
🌟 CSS三角形怎么实现
宽度、高度设为0,然后设置边框样式全透明
width: 0;
height: 0;
border-top: 40px solid transparent;
border-left: 40px solid transparent;
border-right: 40px solid transparent;
border-bottom: 40px solid #ff0000;
🌟 高度塌陷解决方法
概念:父高自适应,子浮动,父高为0的表现
解决:
方法1:clear: both + 空标签(不推荐 代码冗余)
方法2:父级添加overflow属性(不推荐 超出隐藏 特殊布定位布局出问题)
方法3:伪元素清除浮动(推荐使用)
方法4:双伪元素清除浮动(推荐使用)
方法5:
/*声明清除浮动的样式*/
.clearfix:after {
content: "";
display: block;
/* 没有内容转换为快元素*/
clear: both;
/* 清除浮动*/
visibility: hidden;
/* 元素隐藏,位置保留*/
height: 0;
/* 解决IE6解析高度0不正确问题*/
overflow: hidden;
/* 解决IE6解析高度0不正确问题*/
}
.clearfix {
*zoom: 1;
/*ie6,7 专门清除浮动的样式*/
}
/*声明清除浮动的样式*/
.clearfix:before ,.clearfix:after {
content: "";
display: table;
}
.clearfix:after {
clear:both;
}
.clearfix {
*zoom: 1; /*ie6,7 专门清除浮动的样式*/
}
🌟 样式兼容
【所有】图片下方多了三像素原因及解决办法
产生原因:主要是因为图片的垂直对齐方式vertical-align引发的,默认值是baseline,默认为此值时图片下方就会多出3px
解决:display:block、vertical-align: middle;
【谷歌】表单元素获取焦点有边框线
解决:outline: none
【IE】表单元素去掉边框 border:none 不兼容IE低版本
解决:border:0;
【IE】表单元素加高度后所有浏览器默认内容垂直居中 不兼容IE低版本
解决:加行高line-height
【IE】a标签包图片会有边框
解决:图片border:0
【IE】透明语法兼容问题
【IE】双倍margin问题等等
css hack是什么?
CSS hack是通过在CSS样式中加入一些特殊的符号,让不同的浏览器识别不同的符号(什么样的浏览器识别什么样的符号是有标准的,CSS hack就是让你记住这个标准),以达到应用不同的CSS样式的目的。
选择器 {
height:32px;
background-color:#f1ee18; /*所有识别*/
.background-color:#00deff\9; /*IE6、7、8识别*/
+background-color:#a200ff; /*IE6、7识别*/
_background-color:#1e0bd1; /*IE6识别*/
}
🌛1px细线问题
说明:在移动端实现1px
解决:
方法1:伪元素 + transform
.border-1px {
position: relative;
}
.border-1px:after {
content: " ";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
border-top: 1px solid red;
color: red;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
/* 2倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 2.0) {
.border-1px:after {
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}
/* 3倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 3.0) {
.border-1px:after {
-webkit-transform: scaleY(0.33);
transform: scaleY(0.33);
}
}
方法2:通过缩放viewport实现
var scale = 1 / window.devicePixelRatio;
var viewport = document.querySelector("meta[name=viewport]")
viewport.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no')
方法3: 媒体查询利用设备像素比缩放,设置小数像素(存在兼容WWDC苹果全球开发者大会ios8+才支持小数
.border { border: 1px solid #999 }
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.border { border: 0.5px solid #999 }
}
@media screen and (-webkit-min-device-pixel-ratio: 3) {
.border { border: 0.333333px solid #999 }
}
留心:ios8- 和 安卓低版本等都不能识别小数会忽略为0px
🌛图片模糊问题
方法1
/*默认大小*/
.photo {
width:100px;height:100px;
background-image: url(image100.png);
}
/* 如果设备像素大于等于2,则用2倍图 */
@media screen and (-webkit-min-device-pixel-ratio: 2),
screen and (min--moz-device-pixel-ratio: 2) {
.photo {
background-image: url(image200.png);
background-size: 100px 100px;
}
}
/* 如果设备像素大于等于3,则用3倍图 */
@media screen and (-webkit-min-device-pixel-ratio: 3),
screen and (min--moz-device-pixel-ratio: 3) {
.photo {
background-image: url(image300.png);
background-size: 100px 100px;
}
}
方法2:
<img src="./imgs/goods.png" alt="" srcset="./imgs/goods@2.png 2x,./imgs/goods@3.png 3x">
🌟 link和@import区别
功能角度:
@import是CSS的语法规则,而<link></link>是HTML标签。
所以link不仅可以加载CSS,还可以定义RSS等,而@import只能导入CSS
加载顺序:
加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载
兼容角度:
@import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题
DOM操作:
可以通过 JS 操作 DOM ,插入link标签来改变样式;由于DOM方法是基于文档的,无法使用@import的方式插入样式。
优化角度:
link可以加一些预解析的标签,而@import没有它强
🌟 水平居中
方法1:【移动推荐】利用flex弹性布局 justify-content center
方法2:【需要宽高】利用position定位 left 50% margin-left 负自身一半
方法3:【需要宽高】利用position定位 right/left =0 然后 margin: auto
方法4:【留心兼容】利用position定位 transform translate(-50%,0)
方法5:【行内元素】text-align center
方法6:【块级元素】margin auto
🌟 垂直居中
方法1:【移动推荐】利用flex弹性布局 align-items center
方法2:【需要宽高】利用position定位 top 50% margin-top 负自身一半
方法3:【需要宽高】利用position定位 top/bottom=0 然后 margin: auto
方法4:【留心兼容】利用position定位 transform translate(0, -50%);
方法5:【文本居中】line-height
🌟 水平垂直居中
方法1:【移动推荐】利用flex弹性布局 justify-content/ align-items center
方法2:【需要宽高】利用position定位 top/left 50% margin-top/left 负自身一半
方法3:【需要宽高】利用position定位 top/right/bottom/left =0 然后 margin: auto
方法4:【留心兼容】利用position定位 transform translate(-50%,-50%)
方法5:【留心兼容】利用position定位 calc((100% - 100px) / 2);
🌟 两栏布局
方法1:左固定浮动、右触发BFC
方法2:左固定浮动、右margin-left
方法3:左固定定位、右margin-left
方法4:父弹性盒、左固定、右flex:1
<style>
*{padding:0px;margin:0px;}
.test {width:50px;height:50px;background: black;color:#fff;}
</style>
<!-- 方法1:左固定浮动、右触发BFC -->
<style>
.left1 {width:200px;height:100px;background:#ccc;float:left;}
.right1 {height:100px;background:red;overflow: hidden;}
</style>
<div class="left1">你好</div>
<div class="right1"><div class="test">中国</div></div>
<!-- 方法2:左固定浮动、右margin-left -->
<style>
.left2 {width:200px;height:100px;background:#ccc;float:left;}
.right2 {height:100px;background:red;margin-left:200px;}
</style>
<div class="left2">你好</div>
<div class="right2"><div class="test">中国</div></div>
<!-- 方法3:左固定定位、右margin-left -->
<style>
.left3 {width:200px;height:100px;background:#ccc;position:absolute;}
.right3 {height:100px;background:red;margin-left:200px;}
</style>
<div class="left3">你好</div>
<div class="right3"><div class="test">中国</div></div>
<!-- 方法4:父弹性盒、左固定、右flex:1 -->
<style>
.main {display: flex;}
.left4 {width:200px;height:100px;background:#ccc;}
.right4 {height:100px;background:red;flex:1;}
</style>
<div class="main">
<div class="left4">你好</div>
<div class="right4"><div class="test">中国</div></div>
</div>
🌟 三栏布局
方法1:左右固定浮动、中间触发BFC
方法2:左右固定浮动、中间margin-left/right后自适应
方法3:左右固定定位、中间margin-left/right后自适应
方法4:弹性盒 左固定 右侧flex:1
方法5:圣杯布局、双飞翼布局
<style>*{padding:0px;margin:0px;}</style>
<!-- 方法1:左固定浮动、右触发BFC -->
<style>
.left1{width:200px;background:green;float:left;}
.right1{width:200px;background:#ccc;float:right;}
.center1{background:red;overflow:hidden;}
</style>
<div class="left1">左侧</div>
<div class="right1">右侧</div>
<div class="center1">中间</div>
<!-- 方法2:左右固定浮动、中间margin-left/right后自适应 -->
<style>
.left2 {width:200px;background:green;float:left;}
.right2 {width:200px;background:#ccc;float:right;}
.center2 {background:red;margin:0px 200px 0px 200px}
</style>
<div class="left2">左侧</div>
<div class="right2">右侧</div>
<div class="center2">中间</div>
<!-- 方法3:左右固定定位、中间margin-left/right后自适应 -->
<style>
.box3 {position: relative;}
.left3 {width:200px;background:green;position:absolute;top:0px;left:0px;}
.right3 {width:200px;background:#ccc;position:absolute;top:0px;right:0px;}
.center3 {background:red;margin:0px 200px}
</style>
<div class="box3">
<div class="left3">左侧</div>
<div class="center3">中间</div>
<div class="right3">右侧</div>
</div>
<!-- 方法4:圣杯布局-有定位 -->
<style>
.box4{padding:0 200px;}
.content4{width:100%;background:red;float:left;}
.left4{width:200px;background:green;float:left;margin-left:-100%;position: relative;left:-200px;}
.right4{width:200px;background:#ccc;float:left;margin-left:-200px;position: relative;right:-200px;}
</style>
<div class="box4">
<div class="content4">中间</div>
<div class="left4">左侧</div>
<div class="right4">右侧</div>
</div>
<!-- 方法5:双飞翼布局-无定位 -->
<style>
.box5 {width:100%;float:left;background:red;}
.box5 .content5{padding:0 200px;}
.left5{width:200px;background:green;float:left;margin-left:-100%;}
.right5{width:200px;background:#ccc;float:left;margin-left:-200px;}
</style>
<div class="box5">
<div class="content5">中间</div>
</div>
<div class="left5">左侧</div>
<div class="right5">右侧</div>
🌟 弹性布局 flex布局(详细)
属性 | 描述 |
display | 指定 HTML 元素的盒子类型 |
flex-direction | 指定弹性盒子中子元素的排列方式 |
flex-wrap | 设置当弹性盒子的子元素超出父容器时是否换行 |
flex-flow | flex-direction 和 flex-wrap 两个属性的简写 |
justify-content | 设置弹性盒子中元素在主轴(横轴)方向上的对齐方式 |
align-items | 设置弹性盒子中元素在侧轴(纵轴)方向上的对齐方式 |
align-content | 修改 flex-wrap 属性的行为,类似 align-items,但不是设置子元素对齐,而是设置行对齐 |
order | 设置弹性盒子中子元素的排列顺序 |
align-self | 在弹性盒子的子元素上使用,用来覆盖容器的 align-items 属性 |
flex | 设置弹性盒子中子元素如何分配空间 |
flex-grow | 设置弹性盒子的扩展比率 |
flex-shrink | 设置弹性盒子的收缩比率 |
flex-basis | 设置弹性盒子伸缩基准值 |
容器属性:
flex-direction:
值 | 描述 |
row | 默认值,主轴沿水平方向从左到右 |
row-reverse | 主轴沿水平方向从右到左 |
column | 主轴沿垂直方向从上到下 |
column-reverse | 主轴沿垂直方向从下到上 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承此属性的值 |
flex-wrap:
值 | 描述 |
nowrap | 默认值,表示项目不会换行 |
wrap | 表示项目会在需要时换行 |
wrap-reverse | 表示项目会在需要时换行,但会以相反的顺序 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承属性的值 |
justify-content:
值 | 描述 |
flex-start | 默认值,左对齐 |
flex-end | 右对齐 |
center | 居中 |
space-between | 两端对齐,项目之间的间隔是相等的 |
space-around | 每个项目两侧的间隔相等 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承属性的值 |
align-items:
值 | 描述 |
stretch | 默认值,项目将被拉伸以适合容器 |
center | 项目位于容器的中央 |
flex-start | 项目位于容器的顶部 |
flex-end | 项目位于容器的底部 |
baseline | 项目与容器的基线对齐 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承属性的值 |
align-content:
值 | 描述 |
stretch | 默认值,将项目拉伸以占据剩余空间 |
center | 项目在容器内居中排布 |
flex-start | 项目在容器的顶部排列 |
flex-end | 项目在容器的底部排列 |
space-between | 多行项目均匀分布在容器中,其中第一行分布在容器的顶部,最后一行分布在容器的底部 |
space-around | 多行项目均匀分布在容器中,并且每行的间距(包括离容器边缘的间距)都相等 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承该属性的值 |
order:number(可以定义项目在容器中出现的顺序)
align-self
值 | 描述 |
auto | 默认值,表示元素将继承其父容器的 align-items 属性值,如果没有父容器,则为“stretch” |
stretch | 项目将被拉伸以适合容器 |
center | 项目位于容器的中央 |
flex-start | 项目位于容器的顶部 |
flex-end | 项目位于容器的底部 |
baseline | 项目与容器的基线对齐 |
initial | 将此属性设置为属性的默认值 |
inherit | 从父元素继承属性的值 |
flex
flex 属性是 flex-grow、flex-shrink 和 flex-basis 三个属性的简写,语法格式如下:
flex: flex-grow flex-shrink flex-basis;
参数说明如下:
flex-grow:(必填参数)一个数字,用来设置项目相对于其他项目的增长量,默认值为 0
flex-shrink:(选填参数)一个数字,用来设置项目相对于其他项目的收缩量,默认值为 1
flex-basis:(选填参数)项目的长度,合法值为 auto(默认值,表示自动)、inherit(表示从父元素继承该属性的值) 或者以具体的值加 "%"、"px"、"em" 等单位的形式
另外,flex 属性还有两个快捷值,分别为 auto(1 1 auto)和 none(0 0 auto)
🌟 谈谈你对flex:1的理解
flex:1是三个属性的连写:flex-grow、flex-shrink、flex-basis
默认为0,1,auto
快捷值,分别为 auto(1 1 auto)和 none(0 0 auto)
即,会随着页面的缩小而缩小
🌟 网格布局 Grid 布局
gird布局和flex布局的本质区别在于:flex布局是一维布局,一次只能处理一个维度上的布局,也就是行或者列;而grid布局是二维布局,它将容器划分为行和列,形成一个个单元格,然后我们就可以指定我们的 “ 项目 ” 所占的单元格,形成我们想要的布局。
Grid容器属性:
display
grid-template-rows
grid-template-columns
row-gap
column-gap
gap
grid-template-areas
grid-auto-flow
justify-items
align-items
place-items
justify-content
align-content
place-content
grid-auto-columns
grid-auto-rows
grid-template
grid
💘 JS(1)
🌟 JS数据类型有哪些
基本类型/基本类型:null、undefined、boolean、number、string、symbol、bigint
对象类型/引用类型/复杂/复合类型:
可调用/执行对象「函数」: function
标准普通对象: object
标准特殊对象: Array、Date、Math、RegExp、Error……
非标准特殊对象: Number、String、Boolean……
🌟 如何把数据强制转换为数值型
说明:面试概率低、工作概率高
语法:parseInt、parseFloat、Number
🌛 数据转换为布尔型的结果
null、undefined、0、NaN、空字符串结果是false、其他都是true
🌛 数据转换为字符串型的结果
String(内容)、内容.toString()
🌛 数据转换为数值型的结果
Number(数据)
数据 | 结果 |
null | 0 |
undefined | NaN |
true/false | 1/0 |
123/NaN | 123/NaN |
' '/'123'/'123a' | 0/123/NaN |
🌛 练习以下隐式转换(//后面为结果)
var result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false; //result = 'NaNTencentnull9false'
[] + [] // ""等于空字符串
[] + {} // "[object object]"
{} + [] // 0 对于编译器而言,代码块不会返回任何的值, 接着+[]就变成了一个强制转number的过程
true+true+true===3 // true
true-true // 0
true==1 // true
true===1 // false
8+"1" // '81'
91-"1" // 90
[]==0 // true
Number([])===0 //true
[]===0 //false
🌛 null 和 undefined 有什么区别?
undefined 表示未定义,新定义的变量没有赋值就是undefined
null表示清空,当一个变量不用的时候,除了等待网页关闭销毁,也可以把它赋值为null。此时游览器会进行一个回收也就是清空内存。(比如定时器常常初始化为null)
补充:
typeof(null)会是一个object。最初这么设计的原因为:通常用作一个空引用一个空对象的预期,就像一个占位符。typeof的这种行为已经被确认为一个错误,虽然提出了修正,出于后兼容的目的,这一点已经保持不变。
null == undefined // true null === undefined // false
🌛 为什么 0.1 + 0.2 !== 0.3? 你如何解决这个问题?
因为 0.1 这样的数值用二进制表示你就会发现无法整除,
最后算下来会是0.0001100110011001...由于存储空间有限,位数是无限的,只能取近似。
代码:0.1 + 0.2 == 0.3 // false
代码:0.625 + 0.625 == 1.25 // true
进制转换规则: 十进制小数转换为二进制小数
十进制整数 -> 转二进制
举例:(5).toString(2) // 结果 101 转换:除2反向取余 2| 5 --- 余1 ------ 2| 2 --- 余0 ------ 1 举例:(13).toString(2) // 结果 1101 转换:除2反向取余 2| 13 --- 余1 ------ 2| 6 --- 余0 ------ 2| 3 --- 余1 ------ 1 核对: ASCII表
十进制小数 -> 转二进制
举例:(0.625).toString(2) // 结果 0.101 转换:乘2正向取整 0.625*2=1.25======取出整数部分1 0.25*2=0.5========取出整数部分0 0.5*2=1==========取出整数部分1 举例:(0.7).toString(2) // 结果 0.101100110011001100110011 0011 ... 0.7*2=1.4========取出整数部分1 0.4*2=0.8========取出整数部分0 0.8*2=1.6========取出整数部分1 0.6*2=1.2========取出整数部分1 0.2*2=0.4========取出整数部分0 0.4*2=0.8========取出整数部分0 0.8*2=1.6========取出整数部分1 0.6*2=1.2========取出整数部分1 0.2*2=0.4========取出整数部分0 ...
🌟 实战购物车小数计算精度丢失如何解决?
推荐解决: 浮点数转化成整数
实战使用:购物车结算时,商品价格(33.01)-优惠券价格(5),本应该是28.01,但是实际的结果是28.009999999999998
解决方法一:减数和被减数同乘除100
33.01 - 5 = 28.009999999999998
(33.01*100 - 5*100) / 100 = 28.01
解决方法二:在Number.EPSILON误差范围内判断
es6 提供了 Number.EPSILON,这个值等于 2^-52 ,这个值非常非常小,在底层计算机已经帮我们运算好,并且无限接近 0,但不等于 0,这个时候我们只要判断误差值在这个范围内就可以说明是相等的。
function numbersequal(a,b){
return Math.abs(a-b)<Number.EPSILON
}
var a = 0.1 + 0.2;
var b = 0.3;
console.log(numbersequal(a, b))
// true
解决方法三:四舍五入
采用四舍五入方法,取了一个 10 位小数
function numTofixed(num) {
if (typeof num == 'number') {
num = parseFloat(num.toFixed(10))
}
return num;
}
numTofixed(0.1 + 0.2);
// 0.3
//使用第三方库
//math.js
//bignumber.js
💘 JS(2)
🌟 说出变量在内存中的分配
栈:基本类型【名字】&【数据】、对象类型的【名字】&【堆地址】
堆:对象类型数据
🌟 说出变量赋值内存分配
基本类型:变量赋值,栈开辟内存直接存数据 -> 数据互不影响
对象类型:变量赋值,栈开辟内存,存放堆地址 -> 数据相互影响
切记切记切记:形参也会出现传值、传地址问题
var num = 100
function fn(n) {
// var 形参 = 实参
// var n = num也就是100 基本类型 传值 不影响
n = 200
console.log(n) // 打印: 200
}
fn(num)
console.log(num) // 打印: 100
// --------------------------
var obj = { name: 'Jack' }
function fn(o) {
// var 形参 = 实参
// var o = obj 对象类型赋值 传地址 相互影响
o.name = 'Rose' //
console.log(o.name) // 打印:Rose
}
fn(obj)
console.log(obj.name) // 打印:Rose
🌟 判断是否是数组? var a=[]
方法1:通过语法 Array.isArray()
Array.isArray(a) === true
方法2:instanceof
a instanceof Array === true
方法3:原型链(constructor)
a.constructor === Array
方法4:Object.prototype.toString.call()
Object.prototype.toString.call(a) === '[object Array]'
🌛 如何交换两个变量的值
临时变量法
var a = 1
var b = 2;
var temp = a
a = b
b = temp
加减法
var a = 1
var b = 2;
a = a+b // a = 1+2 = 3
b = a-b // b = 3-2 = 1
a = a-b // a = 3-1 = 2
解构赋值法
var a = 1
var b = 2;
[a, b] = [b, a]
数组法
var a = 1
var b = 2
a = [a, b]
b = a[0]
a = a[1]
对象法
var a = 1
var b = 2
a = { a: b, b: a }
b = a.b
a = a.a
等等
🌟 说出数组有哪些方法(有疑问可百度或者私信我)
·数据操作:shift/unshift/pop/push/slice
遍历数据: forEach/map/filter/find/findIndex/some/every
工作常用:concat/join/indexOf/includes/reduce/Array.from()/fill()/isArray()
学习常用:reverse/splice
了解:sort
🌟 说出数组哪些方法会改变原数据
数据操作:shift/unshift/pop/push
学习常用:reverse/splice
了解:sort
🌟 如何实现数组去重
方法1:通过es6新增的Set数据结构、和展开运算符去重 [...new Set(重复数组)]
var arr = [1,2,3,2,3]
console.log([...new Set(arr)])
方法2:通过filter配合indexOf实现数组去重
var arr = [1,2,3,2,3]
var newarr = arr.filter(function(item, i) {
// i=0 item值是1 arr.indexOf(item) 返回0 真
// i=1 item值是2 arr.indexOf(item) 返回1 真
// i=2 item值是3 arr.indexOf(item) 返回2 真
// i=3 item值是2 arr.indexOf(item) 返回1 假
// i=4 item值是3 arr.indexOf(item) 返回2 假
return i === arr.indexOf(item);
})
console.log(newarr)
方法3:定义空数组,通过forEach遍历重复的数组,通过indexOf判断当前值是否在数组中,不在就push
var newarr = []
var arr = [1,2,3,2,3]
arr.forEach(function(item, i) {
if (newarr.indexOf(item) === -1)
{
newarr.push(item)
}
})
console.log(newarr)
方法4:定义空数组,通过forEach遍历重复的数组,通过includes判断当前值是否在数组中,不在就push
var newarr = []
var arr = [1,2,3,2,3]
arr.forEach(function(item, i) {
if (!newarr.includes(item))
{
newarr.push(item)
}
})
console.log(newarr)
方法5:利用对象的属性去重
var arr = [1,2,3,2,3]
var newarr = []
var obj = {}
arr.forEach(function(item) {
if (!obj[item]) {
obj[item] = item // 出现记录
newarr.push(item)
}
})
🌟 说出字符串常用方法
工作常用:数组、查找、替换、截取、大小、空格、截取
.split()、find()、replace()、substr()、toUpperCase()/toLowerCase()、trim()、slice()
学习:.length、lastIndexOf、repeat
🌛 JS如何去空格
说明: 通过 trim 去左右空格
留心:中间不能去
追问:JS如何去所有空格
解决:通过2021年 ES12新增语法replaceAll来解决
' a b c '.replaceAll(' ', '')
解决:通过正则
' a b c '.replace(/\s/g, '')
🌛 伪数组
长得像数组,能用少部分的属性 例如length 但是所有方法都不可以使用,在js中常见的伪数组有
document.querySelectAll()、arguments等等
🌟 谈谈你对this指向的理解
面试: 调用当前方法的对象 也就是谁调用的就指向谁
举例:
function fn1() {}
fn1() // 最大全局变量window调用的
标签对象.事件类型 = 处理函数 // 标签对象触发后调用的
学习
普通函数 指向window
对象函数 指向对象本身
事件函数 指向事件源
定时器函数 指向window
箭头函数 指向父function中的this 父没有function就是window
自定义 根据 call/apply/bind来确定指向
构造函数 this === 实例化对象 === 公共空间/原型对象上方法中的this
其他:事件、构造函数、公共空间/原型对象上方法-优先使用普通函数写法,当需要明确改变this指向的时候再换箭头函数,其它全不优先使用箭头函数。
🌟 图片懒加载原理
好处:减少HTTP请求,加快网页响应速度,减少服务器压力,增加用户浏览体验
原理:
监控滚动条滚动
获取总可视内容高度(可见视口高度+滚动条滚动高度)
获取所有图片
遍历步骤3(或这说:遍历伪数组)
在步骤4中判断,图片.offsetTop <= 步骤2 成立-修改src属性为data-src、失败-不管
节流防抖优化
留心:有时候图片高度怎么获取不到?
这是因为在某些特殊情况下需要页面加载的时候就获取到图片的高度,通过高度去做相关的操作,但是由于首次加载需要请求,图片还没完全加载回来的时候高度就显示为0
这个时候可以使用图片预加载,先保证图片加载回来了再进行下一步操作
具体操作如下:
(1)new一个图片实例对象
(2)给创建出来的图片对象的src赋值(img对象有src之后会自动请求图片资源,<img>标签同理)
(3)使用定时器定时获取图片的高度或者宽度:
a.如果值为0说明还没加载回来,不执行任何操作,让定时器再次执行
b.如果值不为0说明已经加载回来了,清除定时器,然后执行接下来的操作
示例代码:
let timer = ''
const i = new Image()
i.src = 'https://gimg2.baidu.com/image_search/src=http://img2.niutuku.com/desk/1208/1300/ntk-1300-31979.jpg&refer=http://img2.niutuku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1666936809&t=77a5b58fd5e3d5571a35f45d5c064492'
timer = setInterval(() => {
console.log(1);
if (i.height) {
clearInterval(timer)
timer = null
console.log('获取到了',i.height,i.width)
}
},1)
获取屏幕宽度:
<!-- 195x265 -->
<br />
<img src="./imgs/1.jpg" alt="">
<script>
var imgObj = document.querySelector('img')
console.log(imgObj.offsetWidth); // 0 部分属性必须再图片加载完毕后才能获取到其值
console.log(imgObj.offsetLeft); // 8
imgObj.onload = function() {
console.log(imgObj.offsetWidth); // 195
console.log(imgObj.offsetLeft); // 8
}
</script>
🌟 性能优化:网站首屏加载过慢如何解决
function lazyload()
{
// 1 每次获取最新的 可见视口 + 滚动的高度
var temp1 = window.innerHeight || document.documentElement.clientHeight // 兼容ie
var temp2 = document.body.scrollTop || document.documentElement.scrollTop // 兼容doctype
var maxShowHeight = temp1 + temp2
// 2 获取所有图片
var imgs = document.querySelectorAll('img')
// 3 遍历
imgs.forEach(function(item, index) { // item就是每一个图片标签对象
// 4 判断:当前图片的.offsetTop < 步骤1(可见视口 + 滚动的高度)
// 不成立-不管
// 成立-修改图片的src地址 改成真实的
// console.log(item, item.offsetTop , maxShowHeight)
if (item.offsetTop < maxShowHeight)
{
// item.src = item.src-real 切记非标签默认属性 不能直接点
item.src = item.getAttribute('src-real')
}
})
}
// TODO: 待后续进一步优化
// 首次
lazyload()
// 后续
// window.onscroll = lazyload() 错误 undefined
window.onscroll = lazyload
💘 JS(3)
🌛 对事件流机制的理解
术语:事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程即DOM事件流
学习:多个标签嵌套,事件触发-先检查当前标签,然后继续向上挨个检查祖先标签有没有事件 有就触发的现象就是事件流
DOM事件流分为三个阶段,分别为:
捕获阶段:事件从Document节点自上而下向目标节点传播的阶段;
目标阶段:真正的目标节点正在处理事件的阶段;
冒泡阶段:事件从目标节点自下而上向Document节点传播的阶段。
🌛 对事件委托的理解
概念:利用事件冒泡机制处理指定一个事件处理程序,来管理某一类型的所有事件。
事件委托的好处:
利用冒泡的原理,将事件加到父级身上,这样只在内存中开辟一块空间,既节省资源又减少DOM操作,提高性能
可以为动态添加的元素绑定事件
🌛 正则
匹配手机号:/^1\d{10}$/.test(数据)
匹配邮箱:/^\w{2,20}@\w{2, 20}.(com|org|cn|edu)$/.test(数据)
匹配中文:/^[\u4e00-\u9fa5]+$/.test(数据)
去所有空格:str.replaceAll(' ', '') 或 str.replace(/\s/g, '')
购物车去非数字:str.replace(/\D/g, '')
🌟 es6新增语法
# 2015年 ES6
修饰符:let、const
解构赋值:let {键:变量名=默认值,...,键:变量名=默认值} = {键:值,....,键:值} !!!
字符串相关扩展:模板字符串
函数相关扩展:箭头函数!!!、函数形参默认值、rest参数 arguments
对象相关扩展:对象简写!!!、链判断操作符 ?. 空判断操作符??
数组相关扩展:find/findIndex/indexOf/includes
新增基本类型:symbol、bigint
-----------
// 重要 中等 中等 重要 重要
其他新增:展开运算符、新增数据结构、循环forof、module模块化、class类等等
# 2016年 ES7:Array.prototype.includes
# 2017年 ES8:Async functions
# 2018年 ES9:Promise.prototype.finally
# 2019年 ES10
# 2020年 ES11:String.prototype.matchAll、import()、BigInt、Promise.allSettled、
# 2021年 ES12:String.prototype.replaceAll、Promise.any、
# 2022年 ES13:
🌟 es6常见问题汇总
新增了哪些数据类型:symbol、bigInt(在整数后加n或者调用BigInt()生成一个bigint类型数据)
新增的数据类型属于哪一类:基本类型
数组如何去重:Set、filter+indexOf、forEach+indexOf、forEach+includes、对象
新增数据类型有哪些分别有什么用
Set数组去重
Map对象的键不限于字符串
🌟 let、const
面试题1:let和const区别
相同点-都是块级作用域/不能重复定义
不同点-let修改/const不能修改
面试题2:const真的不能修改吗
基本类型:不可以
对象类型:可以
原理:const不可以修改栈里面的地址,但是可以修改堆里面的数据
面试题3:暂时性死区
let前面的区域就是暂时性死区,必须先定义再使用
面试题4:let和const如何选
普遍用let、明确后期不改用const 例如Date、xhr、promise对象等
const d = new Date()
面试题5:var和let区别
var 函数作用域 可以重复定义 可以修改
let 块级作用域 不能重复定义 可以修改
🌟 bind 和 call/apply 的区别
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
是否立刻执行
call/apply 改变了函数的 this 上下文后 马上 执行该函数。
bind 则是返回改变了上下文后的函数, 不执行该函数 。
返回值的区别
call/apply 返回 fun 的执行结果。
bind 返回 fun 的拷贝,并指定了 fun 的 this 指向,保存了 fun 的参数。
🌛 手写bind/call/apply 原理
call
<script>
Function.prototype.myCall = function (context, ...args) {
context = context ? Object(context) : window
context.fn = this
let res = context.fn(...args)
return res
}
function testFn(data1, data2) {
console.log('this:', this)
console.log('args:', data1, data2)
}
let obj = { a: 1, b: 2 }
testFn.call(obj, 11, 22)
testFn.myCall(obj, 11, 22)
</script>
apply
<script>
Function.prototype.myApply = function (context, args) {
context = context ? Object(context) : window
context.fn = this
let res = context.fn(...args)
return res
}
function testFn(data1, data2) {
console.log('this:', this)
console.log('args:', data1, data2)
}
let obj = { a: 1, b: 2 }
testFn.apply(obj, [11, 22])
testFn.myApply(obj, [11, 22])
</script>
bind
<script>
Function.prototype.myBind = function (context, ...args1) {
context = context ? Object(context) : window
return (...args2) => {
let args = args1.length ? args1 : args2
context.fn = this
let res = context.fn(...args)
delete context.fn
return res
}
}
function testFn(data1, data2) {
console.log('this:', this)
console.log('args:', data1, data2)
}
let obj = { a: 1, b: 2 }
let aFn = testFn.bind(obj, 11, 22)
let bFn = testFn.myBind(obj, 11, 22)
aFn()
// aFn(111,222)
bFn()
// bFn(111, 222)
</script>
💘 JS(4)
🌟 原型、原型链概念
原型:js给每个函数分配的公共空间,减少内存占用
原型链:多个原型的集合,当调用对象的属性或方法时,先自身找,找不到去原型链上找,一直找到Object构造函数的原型链
🌟 构造函数new干了啥
1 给obj创建一个空对象
2 给obj增加__proto__属性 并且指向 构造函数的.prototype(原型链继承)
3 将 构造函数this中的数据 全部放到obj上(构造函数继承)
4 返回obj实例对象
// let Sym = fn
function Sym() {
this.a = 1;
this.b = 2;
this.c = 3;
}
// 第一种使用方式:当变量赋值用
let data1 = Sym;
console.log(data1); // 打印:函数本身
// 第二种使用方法:当函数用
let data2 = Sym();
console.log(data2); // 打印:undefined 但是window多了a、b、c三个属性
// 第三种使用方法:new关键词
let data3 = new Sym();
console.log("new关键词:", data3); // 打印:对象
/*
栈 堆
data1: 地址1 地址1:function Sym() { this.a = 1; this.b = 2; this.c = 3; }
data2:undefined 地址2:{a:1,b:2,c:3, __proto__:}
data3:地址2
*/
function _new(当前new的构造函数) {
// let obj = {};
// obj.__proto__ = 当前new的构造函数.prototype;
let obj = Object.create(当前new的构造函数.prototype); // !!!!
// obj.a = 1;
// obj.b = 2;
// obj.c = 3;
当前new的构造函数.call(obj);
return obj;
}
let data4 = _new(Sym);
// 需求:封装_new普通函数仿写new关键词效果
// 分析:_new普通函数里面做哪些时
// - 必须返回一个对象
console.log("通过_new仿写new:", data4);
🌛 箭头函数与普通函数区别?能不能作为构造函数
语法更加简洁、清晰,(没有箭头和function关键字,括号也可以省略)
箭头函数不会创建自己的this
箭头函数继承而来的this指向永远不变
.call()/.apply()/.bind()无法改变箭头函数中this的指向
箭头函数不能作为构造函数使用,也就是不能使用new关键字
箭头函数没有自己的arguments
箭头函数没有原型prototype
箭头函数不能用作Generator函数,不能使用yeild关键字
🌛 更精确判断数据类型
// typeof 基本类型:除了null是object其他都是自身
console.log(typeof null); // 'object'
console.log(typeof undefined);// 'undefined'
console.log(typeof true);// 'boolean'
console.log(typeof 123);// 'number'
console.log(typeof "sdafadsf");// 'string'
console.log(typeof Symbol());// 'symbol'
console.log(typeof 1111n);// 'bigint'
// typeof 对象类型:除了function是function其他都是object
// 问题:你就无法根据不同类型做不同逻辑处理
// 比如:判断是数组 判断是null
// 解决:Array.isArray() 等等单独处理
// 然后:也可以统一处理
// 语法:Object.prototype.toString.call(数据)
console.log(Object.prototype.toString.call(null));// '[object Null]'
console.log(Object.prototype.toString.call(undefined));// '[object Undefined]'
console.log(Object.prototype.toString.call(true));// '[object Boolean]'
console.log(Object.prototype.toString.call(123));// '[object Number]'
console.log(Object.prototype.toString.call("sdfdsf"));// '[object String]'
console.log(Object.prototype.toString.call(Symbol()));// '[object Symbol]'
console.log(Object.prototype.toString.call(1111n));// '[object BigInt]'
console.log(Object.prototype.toString.call(function () {}));// '[object Function]'
console.log(Object.prototype.toString.call({}));// '[object Object]'
console.log(Object.prototype.toString.call([]));// '[object Array]'
//下面封装一个精确判断数据类型的方法
function getType(data) {
let result = Object.prototype.toString.call(data);
// console.log(result.substr(8, result.length - 1 - 8));
console.log(result.slice(8, -1));
}
//调用该函数实例化
getType(null);
getType(undefined);
getType(true);
getType(123);
getType("sdfdsf");
getType(Symbol());
getType(1111n);
getType(function () {});
getType({});
getType([]);
🌛 Object构造函数上有哪些语法
重要
Object.defineProperty() vue双向绑定原理
Object.keys() 获取对象的所有键 返回数组
Object.values() 获取对象的所有值 返回数据
Object.create() 创建对象(特色 基于指定原型造对象 场景1:new原理优化,场景2:vue )
Object.assign() 合并对象
Object.prototype.constructor 所属构造函数
Object.prototype.toString() 转字符串
💘 JS(5) 高频
🌟 输入网址浏览器做了什么(简单版)
打开浏览器输入网址回车 -> 去DNS服务器找网址对应的IP地址 -> 找不到【无法访问此网站】 找到了【根据ip地址去请求服务器】 -> 服务器返回数据 -> 【浏览器解析】
脚下留心:浏览器其实返回的是index.html的数据,然后在解析的过程中,遇到link、script、img等 再次发送请求拿数据然后解析
🌟 输入网址浏览器做了什么(完整版)
域名解析,浏览器查找该域名的IP地址
浏览器根据解析得到的IP地址向WEB服务器发送HTTP请求
服务器收到请求并进行处理
服务器返回一个响应
浏览器对该响应进行解码,渲染显示
页面显示完成后,浏览器发送异步请求
整个过程结束之后,浏览器关闭TCP连接。
详细解释点这儿
🌟 对HTTP理解
面试:HTTP是超文本传输协议,规定了客户端和服务端如何通信,然后由两个部分组成分别是请求、 响应
学习:就是一个规则,你必须按照这个规则才可以和后端交互拿数据
概念:超文本传输协议
作用:规定客户端和服务端通信技术
场景:网页、APP等
响应组成:响应行(同上)、响应头(暂略)、响应体(接口数据调试错误)
请求组成:请求行(地址/状态码/请求方式)、请求头(ua、content-type、token、cookie)、请 求体(接口参数)
🌟 HTTP周边:HTTP动词 状态码 请求头参数
HTTP动词(请求方式 method)
明确:form标签也就是w3c就是遵循http规则设计的 但是它的请求方式仅仅只有两种get、post 其 它不支持
但是:我们目前主流用get、post ,但是还有很多其它的请求方式
种类:常用的get查询、post增加数据、put修改数据、delete删除数据
实际:java工程师就用get查询 增删改都用post
状态码
2xx 200 成功 201 成功并且服务器创建了新数据
3xx 301 站内跳转 302 站外跳转 304 浏览器缓存
4xx 400 你传递给后端的参数 401 密码错误 403 没有权限 404文件不存在 405 请求方式有误
5xx 500 服务器有误
请求头参数
ua、content-type、token、cookie
🌟 HTTP周边:强制缓存、协商缓存
强制缓存:就是文件直接从本地缓存中获取,不需要发送请求。
响应头 Cache-Control : 86400
expires
协商缓存/对比缓存
在响应头部 Response Headers 中, 有两种资源标识:
Last-Modified 资源的最后修改时间,对应请求头为 If-Modified-Since ;
Etag 资源的唯一标识,所谓唯一,可以想象成时人类的指纹,具有唯一性;但 Etag 的本质是一个字符串;对应请求头为 If-None-Match 。
Last-Modified 和 Etag
当响应头部 Response Headers 同时存在 Last-Modified 和 Etag 的值时,会优先使用 Etag ;
Last-Modified 只能精确到秒级;
如果资源被重复生成,而内容不变,则 Etag 更精确。
请求行/响应行:method、url、status
请求头:headers、cookie、content-type、ua、if-none-match、if-modified-since
请求体:请求参数
响应头:content-type 告诉浏览器如何解析数 、etag、last-modified、 Cache-Control...
响应体:响应的数据
🌟 get和post有什么区别
GET在浏览器回退时是无害的,而POST会再次提交请求。
GET产生的URL地址可以被Bookmark,而POST不可以。
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
GET请求只能进行url编码,而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
GET请求在URL中传送的参数是有长度限制的,而POST么有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
GET参数通过URL传递,POST放在Request body中。
上传图片:2M 一般只能上传png、jpg gif不允许
很生动形象的解释了get和post的区别
🌟 谈谈你对http、https的理解,有什么区别
http超文本通讯协议 默认端口80
https也是超文本通讯协议 相对http更加安全 默认端口443
🌟 谈谈你对骨架屏的理解
骨架屏就是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。常用于文章列表、动态列表页等相对比较规则的列表页面。 很多项目中都有应用:ex:饿了么h5版本,知乎,facebook等网站中都有应用。
效果参考
🌟 谈谈你对节流防抖的理解
回答:节流防抖都是用来进行项目优化,减少代码触发的频率,同时又不影响实际效果
节流:触发事件之后如果用户在设定时间内再次触发,则刷新触发事件,只执行最后一次
防抖:用户点击之后一直点击不能触发,必须执行完每次事件并且等待时间结束才能继续执行
//思路:事件中代码用定时器包起来,然后再前面加判断
问题:输入教主会执行 jiaozhu教主 这么多次
标签对象.oninput = function() {
console.log(1)
}
节流:一段时间内 1次
let t
标签对象.oninput = function() {
if (t) return
t = setTimeout(() => {
console.log(1)
t = null
}, 3000)
}
防抖:一段时间内可重复执行,但是要把之前的取消掉
let t
标签对象.oninput = function() {
if (t) clearTimeout(t)
t = setTimeout(() => {
console.log(1)
t = null
}, 3000)
}
🌟 谈谈你对高阶函数的理解
简而言之,高阶函数是那些将其他函数作为参数或返回其他函数的函数。在高阶函数中作为参数传递的函数被称为回调。
高阶函数的优势:
它们可以帮助我们写出简洁的代码。
由于是简洁的代码,调试工作会更加容易。
现在 JavaScript 有一些内置的高阶函数,你可能已经在不知不觉中就使用它们了,例如 filter()、reduce()、sort() 和 forEach()。
🌟 function+ajax+callback
/**
* 发送get异步请求
* @param {stirng} url 请求地址
* @param {object} params 请求参数,格式 {参数名:数据,....,参数名:数据}
* @param {function} callback 回调函数
* @param {object} headers 自定义请求头 {键:值, token: '数据', 'content-type': '数据'}
*/
function get(url, params, callback, headers = {}) {
// 一、创建xhr对象
const xhr = new XMLHttpRequest();
// 二、设置请求方式、请求地址
let temp = [];
for (let key in params) {
temp.push(`${key}=${params[key]}`);
}
xhr.open("get", `${url}?${temp.join("&")}`);
// 三、监听请求状态
xhr.onreadystatechange = () => {
// 判断返回
if (xhr.readyState === 4) {
// 判断状态
if (xhr.status === 200) {
// 获取数据
let res = JSON.parse(xhr.responseText);
// 逻辑处理
// console.log(res);
callback(res);
} else {
console.log(xhr.status);
}
}
};
// 四、发送
for (let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
xhr.send();
}
🌟 function+ajax+promise
/**
* 发送get请求
* @param {stirng} url 请求地址
* @param {object} params 请求参数
* @param {object} headers 自定义请求头
* @returns
*/
function get(url, params, headers = {})
{
const p = new Promise((resolve, reject) => {
// 一、创建xhr对象
const xhr = new XMLHttpRequest
// 二、设置请求方式、请求地址
let temp = []
for (let key in params) {
temp.push(`${key}=${params[key]}`)
}
xhr.open('get', `${url}?${temp.join('&')}`)
// 三、监控请求状态
xhr.onreadystatechange = () => {
// 返回
if (xhr.readyState === 4)
{
// 状态码
if (xhr.status === 200)
{
// 获取数据
let res = JSON.parse(xhr.responseText)
// 逻辑处理
// console.log(res)
resolve(res)
} else {
// console.log(xhr.status)
reject(xhr.status)
}
}
}
// 四、发送请求
for (let key in headers) {
xhr.setRequestHeader(key, headers[key])
}
xhr.send()
})
return p // 后期谁调用get就会拿到promise对象 通过then就可以获取数据 实现不同的业务逻辑
}
🌟 谈谈你对promise的理解
概念:ES6异步编程解决方案
作用:常用于封装ajax异步请求,解决回调地狱
🌟 说一下promise原理
底层创建了Promise构造函数,并且给该构造函数绑定了then、catch、finally等原型方法,和reject、resolve、all、race等静态方法。
🌟 说一下promise几个状态
进行中(pending)、成功了(reslove)、失败了(reject)
const p = new Promise((resolve, reject) => {
// 发送异步请求
// 默认触发的 所以是进行中状态
})
追问:为什么状态不可逆
回答:底层Promise构造函数中会判断当前是否是pending进行中状态,不是就会终止代码执行 所以不可逆
// 明确:底层Promise源码大概是这么写的
function Promise(callback) {
this.PromiseState = 'pending'
this.PromiseResult = undefined
const resolve = data => {
if (this.PromiseState != 'pending') return
this.PromiseState = 'fulfilled'
this.PromiseResult = data
}
const reject = error => {
if (this.PromiseState != 'pending') return
this.PromiseState = 'rejected'
this.PromiseResult = error
}
try {
// callback(参数1, 参数2)
// callback(() => {}, () => {})
callback(resolve, reject)
} catch(error) {
reject(error.toString())
}
}
// 然后:你写
const p = new Promise((resolve, reject) => {
resolve(数据1)
reject(数据2)
})
追问:状态之间怎么转换?
回答:通过promise的then机制,来实现状态切换
const p = new Promise((resolve, reject) => {
resolve(数据1)
})
p
.then(res => {
return Promise.resolve('失败的')
})
🌟 Promise.all、Promise.allSettled区别
Promise.all( 数组里面是一个个Promise对象 ) 有一个失败就走失败
Promise.allSettled( 数组里面是一个个Promise对象 ) 没有失败
🌟 Promise.race、Promise.any区别
Promise.race( 数组里面是一个个Promise对象 ) 根据第一个最快返回的决定状态
Promise.any( 数组里面是一个个Promise对象 ) 有一个成功就是then 都失败 才是catch
🌟 跨域相关
跨域导致原因
概念:当请求一个url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
原因:浏览器安全策略/同源策略(浏览器安全行为)
后果:不能跨网站操作发送ajax请求、WEB存储等
跨域解决方案
前端
前端代理 http-proxy-middleware
谷歌插件
谷歌命令
jsonp
websocket
postMessage
留心:上述不管哪种方式仅自己可以使用
最终:开发就选上面其中一种,最终后端:cors法 原理响应头告诉浏览器任何人都可以使用;nginx 反向代理
🌛对同步异步的理解
会被加入到浏览器队列的代码称之为异步代码,例如 ajax、setTimeout/setInterval、Promise.then 等等,他们不按书写顺序执行打印结果的代码
按照书写顺序执行打印的代码称之为同步代码
🌟 对async,await的理解
async、await是generator的语法糖,通过async修饰function、await修饰promise,
底层将await后面的表达式会先执行一遍,再将await下一行代码加入到微任务中.换言之,需要等到await请求的数据回来才能执行后面的请求
🌛 对generator的理解
es6新增的语法,通过*号修饰函数,当调用函数的时候返回一个generator对象,通过next函数迭代获取函数内部的数据,当遇到yield就会暂停,再次写next才会继续
🌟 浏览器运行机制
浏览器主进程,负责创建和销毁tab进程、负责交互前进后退、负责网页文件下载等
渲染进程:每个tab对应一个渲染进程,下面有GUI渲染线程、JS引擎线程、事件线程、定时器线程、异步请求线程
GPU进程:负责3D图绘制
第三方插件进程:负责第三方插件处理,例如跨域、广告拦截插件等
🌛 JS为什么是单线程
先看一个比喻
进程就是一个公司,每个公司都有自己的资源可以调度;公司之间是相互独立的;而线程就是公司中的每个员工(你,我,他),多个员工一起合作,完成任务,公司可以有一名员工或多个,员工之间共享公司的空间
进程:是cpu分配资源的最小单位;(是能拥有资源和独立运行的最小单位)
线程:是cpu调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
浏览器是多进程的
放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。
js为什么要设计成 单线程?
这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变.
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
🌛 说出JS是单线程 为什么不存在执行效率问题
JS是单线程执行程序代码,形成一个执行栈,挨个处理;
但是遇到特别耗费时间的代码 ,例如异步请求,事件等,
不会堵塞等待执行,而是交给浏览器其他线程处理后,再丢到执行栈中处理,从而保证执行效率
🌟 谈谈你对Event Loop的理解
Event Loop即事件循环
是指浏览器或Node的一种确保javaScript单线程运行时不会阻塞的一种机制,
也就是我们经常使用 异步的原理。
种类:浏览器的Event Loop、Node.js中的Event Loop
🌟 谈谈你对浏览器的Event Loop理解
浏览器输入网址服务器响应数据后,
浏览器会通过render进程开始解析工作
GUI线程负责页面渲染
JS引擎线程负责执行JS代码
遇到异步代码会交给其他线程处理,然后放到队列中,
事件循环主要是从队列中取出代码放到执行栈中交给js引擎线程处理
🌟 说出宏任务、微任务各有哪些
单词含义:I input 输入、 O output 输出
用户角度IO操作:鼠标键盘-是计算机输入信息,显示器-是输出设备
电脑角度IO操作:CPU、内存与其他设备之间数据转移过程就是IO操作,例如数据从磁盘读到内存,或者内存写到磁盘
编程角度IO操作:进程读取数据操作
宏任务:
整体代码(script)
定时器(setTimeout、setInterval)
I/O操作(DOM事件、AJAX异步请求)
setImmediate(node环境)
requestAnimationFrame(浏览器环境)
微任务
Promise.then catch finally
async/await(底层还是promise)
process.nextTick(node环境)
MutationObserver(浏览器环境)
🌟 说出先执行宏任务还是微任务
算整体代码script:1宏n微
不算整体代码script:先n微,再1宏 -> n微,再1宏 -> n微
🌛 前端存储有几种
常用的2种,主要是cookie、h5存储;浏览器其实还有web sql、indexdb
H5
cookie
web sql
indexdb
🌟cookie和h5的区别
性能角度:相对而言H5存储性能比COOKIE高
存储空间:H5单条数据5M左右、COOKIE单条数据4KB
生命周期: cookie自己设置,如果不设置浏览器关闭销毁 h5 localStorage 永久 h5 sessionStorage 窗口
🌟如何实现localStorage7天过期
回答3:
//cookie. 1*1000*60*60*24*7
//login.html
//步骤1:存数据的时候 也额外存一个键叫 expires 记录时间
localStorage.setItem('uname', 'value')
localStorage.setItem('expires', (new Date).getTime() + 1*1000*60*60*24*7 )
//member.html
//步骤2:使用的时候增加判断
公式:当前使用时间 > 存储时间 (过期了)
举例:比如你是2月1号存
然后:你加了7天 2月8号过期
最后: 2月5号 > 2月8号 不成立 没过期
最后: 2月11号 > 2月8号 成立 过期
🌟如何实现七天免登录
登录的时候存一个时间 当前时间+7天时间戳
localStorage.setItem('uname', 'value')
localStorage.setItem('expires', (new Date).getTime() + 1*1000*60*60*24*7 )
后期用的时候判断是否过期
<h1>会员中心</h1>
<div></div>
<script>
// 获取
// let localUname = localStorage.getItem('uname')
// 明确:上一行代码有瑕疵
// 因为:永久
// 解决:加时间判断
// let localUname = 当前时间>存储时间 ? 过期了null : 没过期使用
// 分析:
// 存储时间:今天8.1登录 存7天 8.8不可以
// 当前时间:8.1 8.3 8.6 8.9
let localUname = (new Date).getTime()>localStorage.getItem('expires') ? null : localStorage.getItem('uname')
let sessionUname = sessionStorage.getItem('uname')
let uname = localUname || sessionUname
// 判断
if (uname)
{
document.querySelector('div').innerHTML = `<h1>${uname}</h1><a href="./logout.html">退出</a>`
} else {
document.querySelector('div').innerHTML = `<a href="./login.html">登录</a>`
}
</script>
🌟登录是如何实现的
步骤1:登录按钮绑定点击事件
步骤2:事件处理函数中 获取表单数据,请求接口
步骤3:失败-弹框提示,成功-提示、存储、跳转
示例代码(vue3):
<template>
<div class="wrap">
<div class="container">
<h2>登录</h2>
<div class="ipt-item">
<label for="username">账户</label>
<input
type="text"
v-model="username"
id="username"
placeholder="输入账号"
/>
</div>
<div class="ipt-item">
<label for="password">密码</label>
<input
type="password"
v-model="password"
id="password"
placeholder="输入密码"
/>
</div>
<van-checkbox v-model="remeber" checked-color="#e42929">是否记住账号密码?</van-checkbox>
<van-checkbox v-model="autoLogin" checked-color="#e42929">是否免登录?</van-checkbox>
<van-button block @click="loginEvt">登录</van-button>
<div class="pass" @click="toForget"> 忘记密码?</div>
<p>登录即代表阅读并同意<span>服务条款</span></p>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from 'vue-router';
import { deCodeApi, enCodeApi,MD5 } from '../../utils';
import { loginApi, userInfoApi, userRoleApi } from '../../apis/loginApi'
import { commonDefinition } from '../../store'
import {Toast } from 'vant'
const router = useRouter()
const username = ref("");
const password = ref("");
const remeber = ref(false)
const autoLogin = ref(false)
/**
* 获取记住账号的缓存
* 如果有记住账号,需要给输入框赋值,并且把记住账号的标识勾选上
*/
let _username =localStorage.getItem('user-name')
let _password =localStorage.getItem('password')
if(_username&&_password){
try{
_username=deCodeApi(_username)
_password = deCodeApi(_password)
username.value = _username
password.value = _password
remeber.value = true
}catch{}
}
// onMounted(function(){
// let userInfo = userInfoApi('admin79')
// console.log('-----用户信息',userInfo)
// })
function toForget(){
router.push('/forget')
}
async function loginEvt() {
debugger
let result = await loginApi({
id:username.value,
password:MD5(password.value).toString(),
})
// console.log(result)
if(result.code!==200){
Toast.fail('登录失败,原因:' + result.message)
return
}
let str = enCodeApi(result.data.token+ '---' + Date.now())
sessionStorage.setItem('login-flag',str)
commonDefinition().setToken(result.data.token)
//获取用户信息
let userInfo = await userInfoApi(result.data.id)
commonDefinition().setUserInfo(userInfo.data)
//获取用户权限
let userRole = await userRoleApi(userInfo.data.role)
commonDefinition().setUserRole(userRole.data)
/** 判断用户是否同意免登录和记住手机号,同意需要加密缓存,不同意需要删除之前保留的信息 */
if (autoLogin.value) {
localStorage.setItem('auto-login', enCodeApi(result.data.token + '---' + Date.now()))
} else {
localStorage.removeItem('auto-login')
}
if (remeber.value) {
localStorage.setItem('user-name', enCodeApi(username.value))
localStorage.setItem('password', enCodeApi(password.value))
} else {
localStorage.removeItem('user-name')
localStorage.removeItem('password')
}
router.push('/home')
}
</script>
💘 JS(6)低频
🌛 xhr、$.get、$.ajax区别
XMLHttprequest和JQ代码的异同点?
相同点:都可以发送异步请求
不同点:JQ是基于XMLHttprequest封装了 1 更简单、 2 解决兼容问题等
$.get、$.ajax区别?
一样,唯一区别$.ajax比$.get语法上更强
🌛 Jquery一些基本语法
获取标签:CSS选择器、筛选选择器、过滤选择器等
操作标签:样式css方法、类addClass、属性attr/prop、内容val/html、节点append/remove
事件:$().事件类型() $().on(事件类型,子,处理函数)
异步请求:$.get、$.post、$.ajax
动画:$.animate()
网页加载完毕:$(function(){}) (留心:和window.onload区别
对象相互转换:$()[索引] 或者 $(JS标签对象
💘 JS(7)高频
🌟 几个常用的git命令
配置SSH ssh-keygen
用户配置 git config global user.name/email ''
获取代码
git clone 仓库地址
git init
git remote add origin 添加仓库地址
git remote remove origin 删除仓库地址
增删改:git add . / commit / push
周边(忽略文件) .gitignore
周边(代码冲突) git pull
查看文件状态 git status
周边(日志回滚 git log 、 git reset git reflog
周边(分支) git branch / git checkout ...
周边(其他) git stash、git tag、git pull、git fetc、git merge
🌟 说出git工作流,说一下你们日常开发工作流
git flow
功能分支 -> 合并到dev分支
dev分支 -> 合并到release分支
release分支 -> 合并到master分支
🌟 说下git如何解决代码冲突
1 pull
2 status、手动解决
3 重新add/commit/push
通过本地IDE解决,通过git提示代码解决,再重新git add. git commit -m'describle' git push
🌟 说一下你在开发登陆时,线上出现bug了如何维护
基于master创建hotfix分支,
解决了再 push 并 merge 到 master、dev 上
🌟 克隆获取的是默认分支代码,如何获取其他分支代码
git checkout -b 分支名 分支名
🌟 用过哪些git图形化可视软件
编辑器
sourcetruee
TortoiseGit
等等
🌟 说下git和svn区别
git是分布式、svn是集中式
svn相对容易冲突
等
■ Vue基本语法
💘 主题
Vue基础(基本语法:简介、模板语法、列表渲染、条件渲染、事件处理)
🆕 先学vue3基本语法
<div id="root">
<h1>{{msg}}</h1>
<input type="text" v-model="content" />
<button @click="addFn">添加</button>
<ul>
<li v-for="item in todos">{{item.id}} {{item.title}}</li>
</ul>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 以前:new Vue({el,data,methods....})
// 以前:new Vue({data,methods....}).$mount('#root')
// 现在:Vue.createApp({data,methods}).mount('#root')
Vue.createApp({
data() {
return {
msg: "hello vue3",
content: '',
todos: [
{id:1, title: '吃饭'},
{id:2, title: '睡觉'},
{id:3, title: '挤痘痘'},
]
}
},
methods: {
addFn() {
this.todos.push({
id: this.todos.length,
title: this.content
})
}
}
}).mount('#root')
</script>
🌟 说出vue常用的指令
{{}} 两个大括号 双向绑定值语法
v-text 编译成文字段落
v-html 把html代码段编译成元素
v-bind 简写 : 绑定值成变量
v-for 循环
v-if、v-else-if、v-else 条件渲染(不占位)
v-show 条件渲染(占位)
v-on 简写 @ 一般用来绑定事件
v-pre 跳过编译(几乎99%不会用)
v-once 仅渲染一次(几乎99%不会用)
v-cloak 插值闪烁问题
v-model 双向绑定值,利用Object.defineProperty
🌟 MVVM、MVC面试题
谈谈你对MVC的理解
MVC是软件开发中常见的开发模式,主要应用于后端,将程序划分为M模型、V视图、C控制器从而便于团队协作开发,减少代码冗余
谈谈你对MVVM理解
MVVM是Model-View-ViewModel缩写,也就是将MVC中的Controller演变成ViewModel
Model层代表数据模型、
View层代表UI组件
ViewModel是Model、View层的桥梁,数据会绑定到ViewModel并自动将数据渲染到页面,视图变化会通知ViewModel层更新数据。
或者
随着移动互联网的发展,MVVM思想借鉴MVC、MVP思想演变而来,M模型负责数据维护,V视图负责数据展示,VM则是M和V的桥梁,监控M模型数据变化自动更新V视图,从而解决传统前后端分离JQ架构弊端
开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。
大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。
谈谈MVVM和MVC区别
相同点:都是软件开发常见的开发模式或者开发思想
不同点:
1- MVC后端居多,MVVM前端
2- MVC单向通信 目的将M和V代码分离,MVVM则是双向通信,不需要手动操作DOM
或
最初MVC最早出现在后端 M代表模型负责数据处理、V代表视图负责数据战士、C代表控制器负责调度
后来前端也有了MVC库,最早实现的就是backbone.js 但是V和M并没有很好的解耦
因此出现了MVVM模式,
MVVM是Model-View-ViewModel缩写,也就是将MVC中的Controller演变成ViewModel
Model层代表数据模型、
View层代表UI组件
ViewModel是Model、View层的桥梁,数据会绑定到ViewModel并自动将数据渲染到页面,视图变化会通知ViewModel层更新数据。
多说一嘴:VUE不是纯MVVM框架
https://cn.vuejs.org/v2/guide/instance.html#创建一个-Vue-实例
虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
🌟 说一下v-show、v-if的区别
相同点:都可以用户判断控制元素隐藏显示
不同点:1-v-if语法更强、2-v-if控制DOM、v-show控制CSS
如何选:高频切换例如二维码、登录弹框、提示框、删除提示框、tab选项卡,推荐使用v-show 来减少DOM频繁删除创建所产生的额外性能开销
高逼格
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
🆕 判断循环v-if、v-for优先级
手册
https://cn.vuejs.org/v2/guide/list.html#v-for-与-v-if-一同使用
https://cn.vuejs.org/v2/guide/conditional.html#v-if-与-v-for-一起使用
在 vue 2.x 中,在一个元素上同时使用 v-if 和 v-for 时, v-for 会优先作用。
在 vue 3.x 中, v-if 总是优先于 v-for 生效。
vue2
<div id="root">
<h1>vue2 v-for>v-if(仅仅指同一个标签上)</h1>
<!-- 判断假的,压根不会遍历 -->
<div v-if="state">
<p v-for="item in todos">{{item.title}}</p>
</div>
<!-- 同一个标签先v-for走三次生成3个p 然后挨个判断都隐藏删掉 脱裤子放屁的感觉 -->
<p v-for="item in todos" v-if="state">{{item.title}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#root",
data: {
state: false,
todos: [
{id:1, title:'a'},
{id:2, title:'b'},
{id:3, title:'c'},
]
}
})
</script>
vue3 优化
<div id="root2">
<h1>vue3 v-if > v-for</h1>
<!-- 不循环 -->
<p v-for="item in todos" v-if="state">{{item.title}}</p>
<hr>
<!-- 不循环 -->
<div v-if="state">
<p v-for="item in todos">{{item.title}}</p>
</div>
<hr>
<!--
多学一招:为了避免指令写在同一个标签上可读性查,因此代码提升/指令提升 也就是将v-if写在父级标签上
但是影响布局,
解决用template标签 不会
-->
<template v-if="state">
<p v-for="item in todos">{{item.title}}</p>
</template>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
state: true,
todos: [
{id:1, title:'a', show: true},
{id:2, title:'b', show: false},
{id:3, title:'c', show: true},
]
}
}
}).mount('#root2')
</script>
💘 常问
Vue基础(花式思想:Class与Style绑定、计算属性、侦听器、过滤器、自定义指令、ref属性、混入)
🌛 Class与Style绑定工作有用过吗
:class="item.icon"
:style="{ width: item.payload ? item.payload.width : '100%' }"
:style="{ width: item.width || '70px' }"
1-后台管理系统菜单 v-bidn:style="{width: 模型数据}"
2-主题色切换
3-tab选项卡
4-其他
案例1:https://vant-contrib.gitee.io/vant/#/zh-CN/tab
源码1:https://unpkg.com/browse/vant@1.0.0/packages/tab/index.vue
案例3:https://element.eleme.io/#/zh-CN/component/tabs
源码3:https://unpkg.com/browse/element-ui@2.15.5/packages/tabs/src/tab-bar.vue
案例3:https://element.eleme.io/#/zh-CN/component/button
源码3:https://unpkg.com/browse/element-ui@2.15.5/packages/button/src/button.vue
🌟 计算属性和侦听器区别、使用场景
计算属性:计算属性有缓存、并且是响应式依赖缓存,调用不加小括号
侦听器:侦听器无缓存,侦听模型数据变化,不能调用
计算属性:一个数据, 依赖另外一些数据 “计算” 而来的结果
利用vuex辅助函数,结合计算属性去显示数据 项目中大量使用
树型分类数据
等等复杂的逻辑,存在性能问题、或者避免重复调用存在性能问题的场景都可以使用计算属性。
侦听器:当需要监控模型数据变化时候
网站搜索
监控弹框显示二维码
模糊筛选、关键词筛选
日期筛选、下拉筛选
全选、全不选等
项目中监控路由变化显示对应面包屑
等等
相同点
语法角度( a 从思想上都是普通方法升级版 b 都可以写函数或对象
研发角度:计算属性-减少冗余、缓存提升性能、侦听器-减少DOM操作 符合VUE响应式思想
不同点
从语法角度:调用时,计算属性调用不加小括号,侦听器不能调用
从功能角度:计算属性有缓存、响应式依赖,侦听器没有缓存常用于搜索、监控数据变化、代替事件等
🌟 watch监控失效场景&解决方案
针对于对象类型的数据,需要加deep属性深度监听/侦听
🌟 watch两大属性应用场景
deep是什么哪里用
a-项目中:监控路由变化,通过meta路由元信息重置面包屑 🍞
// watch: { // // 针对于复杂类型的深度监听(注:深度监听场景) // // params(newData) { // // console.log(newData); // // }, // params: { // deep: true, // handler(newData) { // 也可以直接用日期组件的change事件 // console.log(newData); // this.params.start_time = newData.date[0]; // this.params.end_time = newData.date[1]; // }, // }, // },
b-项目中:给角色分配权限 重置树型控件
editAuth.vue // 获取所有权限 当前行数据变化获取最新的权限数据 watch: { row: { deep: true, handler() { getAuthsApi().then((res) => { this.authsData = res.data; }); }, }, },
C-项目中:基于elementui二次封装的form编辑默认显示数据要监控row变化
watch: { // qf-form封装的表单组件中,编辑传递的row当前行数据变化同步更改 // row(newData) { // console.log("watch", newData); // }, row: { handler(newData) { if (!newData) return; this.formData = newData; }, immediate: true, }, },
immediate是什么哪里用
a-项目中:meat路由元信息【首次】重置面包屑 🍞
watch: { // 监控路由变化同步面包屑 // $route(newData) { // // console.log(newData); // this.name1 = newData.meta.name1; // this.name2 = newData.meta.name2; // }, $route: { handler(newData) { this.name1 = newData.meta.name1; this.name2 = newData.meta.name2; }, immediate: true, }, },
b-项目中:基于elementui二次封装的form编辑默认显示数据要监控row变化
watch: { // qf-form封装的表单组件中,编辑传递的row当前行数据变化同步更改 // row(newData) { // console.log("watch", newData); // }, row: { handler(newData) { if (!newData) return; this.formData = newData; }, immediate: true, }, },
🌟 谈谈你对过滤器的理解有没有用过
作用:项目用来过滤数据,便于维护
语法:Vue.filter()
场景:订单状态、商品状态、性别、支付状态、发货状态。
🌟 谈谈你对混入的理解有没有用过
作用:复用组件里面的逻辑层,减少冗余便于维护
语法:Vue.mixin({data,methods....})
场景:项目中确认删除、接口操作后的提示重定向、jump重定向封装等等
场景:跳转封装this.$router.push 也是为了避免重复点击报错;也可以通过重置路由模块原型
🌛 自定义指令有没有用过
用过
全屏、复制剪切板、dialog对话框拖拽等等
■ 组件编程
💘 总结
Vue基础(组件编程:组件化开发思想、组件封装、props、$emit、组件通信、插槽slot、仿写UI组件库、动态组件等)
🌟 为啥data要写函数里面返回对象
说明1
一个组件被复用多次,也就会创建多个实例,本质上都是基于同一个构造函数,如果data直接是对象,因为对象是引用类型,所以会影响到所有实例。 因此:为了保证组件不同的实例之间data不冲突,data必须是一个函数
或说法2
当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象
或说法3
因为组件是用来复用的,且 JS 里对象是引用关系, 如果组件中 data 是一个对象,那么这样作用域没有隔离, 子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数, 那么每个实例可以维护一份被返回对象的独立的拷贝, 组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例, 是不会被复用的,因此不存在引用对象的问题。 function Component() {} Component.prototype.data = { name: "jack", age: 22, }; var componentA = new Component(); var componentB = new Component(); componentA.data.age = 55; // componentA 和 componentB data之间指向了同一个内存地址, // 相互污染 console.log(componentA, componentB); function Component() { this.data = this.data(); } Component.prototype.data = function () { return { name: "jack", age: 22, }; }; var componentA = new Component(); var componentB = new Component(); componentA.data.age = 55; // 互补影响 console.log(componentA, componentB); ps. let a = {} let b = {} 互不影响 ---------- let a = {} let b = a 相互影响(一个对象赋值给另一个对象 才会相互影响
🌛谈谈你对单向数据流的理解
单向数据流指在组件化思想,开发的项目中,数据由根或者父组件传递给子组件,禁止🈲子组件中直接更改,而是由父更改后重新传递给子数据使用
在vue中 只能父数据传递给子 父修改后会自动同步到子
不允许子修改父
代码中:父给你就用,你自己直接改 容易后期搞懵逼谁串改了数据
🌟 如何实现组件通信?
常用:状态管理工具 vuex
常用:父传子 props、子传父$emit、兄弟 eventBus
常用:slot插槽
概率:通过组件实例 ref获取、 =》 $parent获取、$root获取、$children获取
概率:v-model
了解:利用provide、inject
🌟 事件.native作用
概念:直接在组件上绑定原生事件
举例:使用elemenui、vantui封装的弹框、input等组件,需要使用官方原生事件
🌟 在组件上写原生事件失效解决方案
方法1:通过自定义事件重写原生事件
方法2:加.native修饰符
🌟 在组件上使用v-model原理
工作极少
但是笔试可能需要手写
<组件名 v-model="data中的键"></组件名>
<组件名 :value="data中的键" @input="data => data中的键 = data"></组件名>
🌛 修饰符.sync原理
<组件名 v-bind:属性名.sync="data中的键"></组件名>
<组件名 v-bind:属性名="data中的键" @update:必须一样的属性名="data => data中的键 = data"></组件名>
■ 剩余知识:生命周期、keep-alive等等
💘 总结
Vue基础(剩余知识:虚拟DOM、浏览器运行机制、回流重绘、生命周期、keep-alive、transition过渡
🌟 说出浏览器运行机制
浏览器主进程,负责创建和销毁tab进程、负责交互前进后退、负责网页文件下载等
渲染进程:每个tab对应一个渲染进程,下面有GUI渲染线程、JS引擎线程、事件线程、定时器线程、异步请求线程
GPU进程:负责3D图绘制
第三方插件进程:负责第三方插件处理,例如跨域、广告拦截插件等
🌟 说出浏览器输入网址干了啥
浏览器输入网址回车
去DNS服务器找网址对应的IP地址
根据IP地址加端口访问服务器软件
服务器返回数据
浏览器通过renderer是渲染进程处理,
其中GUI线程主要负责页面布局,解析HTML、CSS构建DOM树、CSS规则树、然后结合成渲染树、最终绘制显示
🌛 说出JS为什么是单线程
先看一个比喻
进程就是一个公司,每个公司都有自己的资源可以调度;公司之间是相互独立的;而线程就是公司中的每个员工(你,我,他),多个员工一起合作,完成任务,公司可以有一名员工或多个,员工之间共享公司的空间
什么是进程?
进程:是cpu分配资源的最小单位;(是能拥有资源和独立运行的最小单位)
什么是线程?
线程:是cpu调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
浏览器是多进程的
放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。
大家都在说js是单线程的,但是为什么要设计成单线程?
这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变.
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
🌛 说出JS是单线程 为什么不存在执行效率问题
JS是单线程执行程序代码,形成一个执行栈,挨个处理;
但是遇到特别耗费时间的代码 ,例如异步请求,事件等,
不会堵塞等待执行,而是交给浏览器其他线程处理后,再丢到执行栈中处理,从而保证还行效率
🌟 谈谈你对回流重绘的理解
回流/重排:页面布局流发生改变就叫做回流
重绘:重绘元素自身的样式发生改变但是不会影响布局流
🌟 哪些属性导致回流、哪些属性导致重绘
例如:width、height、border、top等
例如:color、background、box-shadow等
1、添加或删除可见的DOM元素
2、元素的位置发生变化
3、元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
4、内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
5、页面一开始渲染的时候(这肯定避免不了)
6、浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
而重绘是指在布局不变得情况下,比如background-color,或者改动一下字体颜色的color等。
🌟 如何避免或减少回流重绘
JavaScript优化法
(1)避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
(2)避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中,也就是虚拟DOM
(3)避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
CSS优化法
(1)使用 transform 替代 top
(2)使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局 (3)避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局。
(4)尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。
(5)避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
(6)将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。
(7)避免使用CSS表达式,可能会引发回流。
(8)将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。
(9)CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
🌟 谈谈你对虚拟DOM的理解
通过js对象来描述真实的DOM,从而减少回流重绘
减少了同一时间内的页面多处内容修改所触发的浏览器reflow和repaint的次数,可能把多个不同的DOM操作集中减少到了几次甚至一次,优化了触发浏览器reflow和repaint的次数。
🌟 说出VUE有哪些生命周期并说出应用场景
before开头的实际不用
非before开头
created 异步请求
mounted 异步请求、DOM操作(swiper、echarts、聊天默认滚到底部)
updated 监控数据变化进一步DOM操作,例如聊天窗口到底部、订单可视化图表重置等等
destroyed 清理非vue资源防止内存泄露,例如登陆倒计时定时器
剩余
activated 组件被keep-alive缓存后,执行场景场景数据,例如添加后列表更新最新数据
deactivated 组件被keep-alive缓存后代替destroyed工作
errorCaptured 当页面发生错误是优雅降级,显示友好提示页面,react框架提示就是参考这一做法
渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进,达到更好的用户体验。
优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
面试追问
问1:created、和mounted区别
答1:created不可以dom操作
问2:真的不可以吗
答2:可以通过 this.$nextTick()
问3:父子组件嵌套、生命周期执行顺序 created、mounted
答2:大方向 父created、子created、子mounted、父mounted
🌟 created里面可以操作DOM吗
通过ref不行,原生JS可以,因为该钩子函数触发还没有编译(不推荐
非要通过ref操作,可以写this.$nextTick来实现
<div id="root">
<button ref="btn">a</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#root",
data: {},
created() {
let btnObj = document.querySelector("button");
console.log("js可以", btnObj);
console.log("ref不行🚫", this.$refs.btn);
this.$nextTick(() => {
console.log("ref可以", this.$refs.btn);
});
},
});
</script>
追问1:created 定时器打印1 this.$nextTick 打印2 定时器打印3
回答1: 2、1、3
追问2:created Promise.then打印1 this.$nextTick 打印2 定时器打印3
回答2:1、 2、 3
// 追问1:created 定时器打印1 this.$nextTick 打印2 定时器打印3
// 回答1: 2、1、3
// setTimeout(() => console.log(1), 1000)
// // Promise.resolve().then(() => console.log(2))
// this.$nextTick(() => console.log(2))
// setTimeout(() => console.log(3), 1000)
// 追问2:created Promise.then打印1 this.$nextTick 打印2 定时器打印3
// 回答2:1、 2、 3
// Promise.resolve().then(() => console.log(1))
// this.$nextTick(() => console.log(2))
// setTimeout(() => console.log(3), 1000)
🌛 watch与created() 哪个先执行?
watch 中的 immediate 会让监听在初始值声明的时候去执行监听计算,否则就是 created 先执行
<div id="root">
<h1>{{msg}}</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#root",
data: {
msg: "hello",
},
created() {
console.log("后created");
},
watch: {
msg: {
handler() {
console.log("先watch");
},
immediate: true,
},
},
});
</script>
🌛数据可视化 echarts
在项目中的使用细节,数据的使用,修改,销毁?
细节1:首次实例化渲染,mounted中注意dom是否挂载,特别是操作子组件
细节2:重置echarts数据,如果页面没有遍历显示数据明细,无法在updated钩子函数监控
解决:watch或者更新后直接调用methods方法
细节3:重置echarts数据,得重新实例化才能显示最新数据
细节4:离开组件手动关闭连接 (注:如果定时器写的要清除定时器,在销毁生命周期里写)
🌟 谈谈你对keep-alive的理解,并说出应用场景
vue中内置组件,主要将组件相关数据缓存到内存中,避免重复挂载卸载产生的性能开销
场景:后台管理系统数据、移动端长列表、tab选项卡等
周边问题
1-缓存组件使用哪个属性?
:include、:exclude、:max
2-此时多的哪两个钩子函数?
activated 、deactivated
3-页面加载刷新的时候,被缓存的组件,会执行其中的方法吗?
刷新就相当于首次打开 所以created、activated又重新出发
🌟 谈谈你对$nextTick的理解,并说出应用场景
理解:vue中用来确保,在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
场景:对话框放登录二维码,放表单选项获取数据、获取焦点,点击事件修改对话框状态后开始加载DOM,为确保能获取并操作DOM使用$nextTick包起来。
语法:
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
vue确保模型数据更新完毕之后执行$nextTick里面的callback代码
🌟 说出$nextTick原理
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
Vue底层监控的数据更新会开启一个队列进行优化处理,然后在下一个的事件循环中去触发nextTick的callback执行,也就是会把nextTick的callback依次尝试放到用原生的 Promise.then、MutationObserver 和 setImmediate、setTimeout中,底层核心代码大致思路如下
1. 把回调函数放入callbacks等待执行
2. 将执行函数放到微任务或者宏任务中
3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => { // 将拿到的回调函数存放到数组中
if (cb) {
try { // 错误捕获
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // 如果当前没有在pending的时候,就会执行timeFunc
pending = true
timerFunc() // 多次执行nextTick只会执行一次,timerFunc就是一个异步方法
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
//
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
//判断1:是否原生支持Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
//判断2:是否原生支持MutationObserver
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
■ Vue原理
💘 主题
Vue原理(语法原理:v-model语法原理、响应式原理、Vue源码分析、自定义Vue库)
🌟 说一下v-model原理
v-model其实是个语法糖底层是基于【:value】和【@input】 封装
<input type="text" :value="msg" @input="msg = $event.target.value">
🌟 VUE2响应式原理(中级)
Vue2在初始化数据时,会使用Object.defineProperty语法对data中的所有属性进行数据劫持,如果属性发生变化就会通知进行更新操作
🌟 VUE2响应式原理(高级)
底层通过Object.defineProperty进行数据劫持,然后通过发布订阅通知视图更新。或者说vue在初始化数据时,会使用Object.defineProperty重新定义data中所有属性,当页面使用对应属性时,首先会进行依赖收集(watcher,如果属性发生变化就会通知相关依赖进行更新操作(发布订阅。)并且底层针对于对象、数组做了特殊处理,对象类型多次递归,数组类型重写数组方法
🌟 VUE2响应式数据无法劫持原因、和解决方案
原因:是因为原生Object.defineProperty针对复杂数据修改无法劫持,因此无法通知视图更新
解决:
自己解决:递归全搞定
VUE解决:
1-对象递归
2-数组重写(因为深度监听需要递归到底,而数组相对数据很多,一次性计算量大 所以改用重写方式)
3-增加额外api
this.$forceUpdate()
this.$set(this.data数据, 要劫持得数组索引或对象键, 默认值)
🌟 $forceUpdate 原理
notify强制视图所有数据更新
🌟 $set 原理
他是vue原型上的一个方法
主要两大核心
(1)通过defineReactive进行数据劫持
(2)通过notify进行视图更新
🌛 $delete 原理
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
语法:this/Vue.$delete(target,propertyName/index)
他是vue原型上的一个方法
主要两大核心
(1)通过delete 删除对象的属性
(2)通过notify进行视图更新
🌟 Vue.set/delete 原理
import { set } from '../observer/index'
...
Vue.set = set
...
import { set } from '../observer/index'
...
Vue.prototype.$set = set
...
都是从 ../observer/index 文件中导出的
区别在于Vue.set()是将set函数绑定在Vue构造函数上,this.$set()是将set函数绑定在Vue原型上。
🆕 响应式原理
为啥VUE3要选择proxy和reflect
- Object.defineProperty 拦截的是对象的属性,会改变原对象。 proxy 是拦截整个对象,通过 new 生成一个新对象,不会改变原对象。
- proxy 语法更强,拦截方式除了上面的 get 和 set ,还有 11 种,以前就6个
- proxy 特性更强,可以监听未定义的,针对于N层则get时判断递归添加proxy拦截即可
不用reflect可以吗
- 简单场景可以,复杂场景不行
- 举例:https://blog.csdn.net/qq_34629352/article/details/114210386 存在BUG
- 其次:用了更方便推荐结合用 例如 has、deleteProperty、defineProperty友好提示等
■ 路由相关
💘 总结
Vue进阶(项目准备:路由、重定向、路由模式、路由原理、路由参数、路由传参、嵌套路由、命名视图、导航守卫、路由addRoute)
🌛 路由-对象里面的键
{
path,
component,
name,
redirect,
alias
children,
components
meta
....
}
🌟 路由-你说下vue路由模式有几种?
常用路由模式有2个,分别为hash和history 直接修改路由构造函数加个mode键即可
准确说有3个,hash/history用于客户端,abstract用户服务端
🌟 路由-你说下vue路由原理?
首先vue路由是基于SPA单页面应用思想去开发的
利用BOM API 来使用
hash模式 通过 BOM location对象的hash属性来改变路由 window.onhashchange
history模式 通过BOM history对象的pushState方法来改变路由(service Worker) window.onpopstate
🌟 路由-history有什么问题,如何解决?
刷新无法加载网页问题
可以通过服务器配置来解决
🌟 路由-什么是单页面应用SPA优缺点,如何选择
SPA优点:减少HTTP请求、加载响应数据、提高用户体验度,方便增加动画
SPA缺点:首屏加载过慢、不利于SEO优化(就是百度可以搜到你)
如何选择
根据项目需求,老板没有明确说就不管,
但是老板说需要seo优化则通过:Vue.js 服务器端渲染(nuxt.js)
tips
1-为什么单页面(SPA)网站无法被seo?:
搜索引擎百度的爬虫通常爬的是静态页面,将整个静态页面保存起来,然后进行词法分析(分析内容、关键词,是否有其他链接继续进行爬取),并不会执行其中的js,因此若用js追到到页面中的内容,并不会被百度抓取。
2-多页面应用(MPA)也是不利于seo优化,最终是否利于seo主要还是看页面数据是直接和html一起返回的、还是要重新发送ajax请求的。
3-vue中想利于seo则通过nuxt.js技术,react则通过next.js
🌟 路由-参数周边种类、方式
参数种类:query、params
传参方式
query、path
this.$router.push( {query, path} ) 定义路由时【不用管】刷新【不丢失】
params、name 写冒号
this.$router.push( {params, name} ) 定义路由时【需要管】刷新【不丢失】
params、name 不写冒号
this.$router.push( {params, name} ) 定义路由时【不用管】刷新【丢失】
🌟 路由-谈谈你对编程式导航的理解
作用:就是利用js跳转网页
留心:声明式就是用a标签跳转
场景:登陆、添加按钮、删除按钮等
语法
this.$router.push( { path:'/路径', query: {参数名:值} } )
this.$router.push( { name:'名称', params: {参数名:值} } )
获取:this.$route.query/params.参数名
🌟 路由-说出嵌套路由&命名视图的场景
嵌套路由
概念:一个路由,显示多个组件,并且有父子关系所以通过children来定义
语法:子路由通过children来定义,然后父的组件内容通过router-view来显示匹配的子组件
场景:后台管理系统经典两栏布局 点击左侧菜单,右侧显示子组件内容
命名视图
概念:一个路由,显示多个组件,并且有兄弟关系所以component改为components
语法:定义路由把component改为components,然后视图给router-view 加name属性
场景:移动端navBar、tabBar
🌟 路由-全局导航守卫登陆鉴权
作用:没登陆不可以访问会员中心、后台首页
语法:router.beforeEach 判断h5 next
// 全局前置守卫
// 导航守卫:导航/地址栏门卫,监控路由变化
router.beforeEach((to, from, next) => {
// ...
// to 存放新的路由数据
// from 存放旧的路由数据
// next() 写- 允许后续代码执行,可以看到组件内容
// next() 不写- 阻止后续代码执行,因此组件不渲染看不到内容
// next({path: '/login'}) 跳转到login页面
// console.log(to);
// 检查当前访问的路径
// 在数组中就直接next 不用检查是否登录
// 白名单
if (["/login", "/404"].includes(to.path)) {
next();
} else {
// 上述白名单直接忽略不用判断
// 但是其他得判断
let token = localStorage.getItem("token");
if (token) {
next();
} else {
next({ path: "/login" });
}
}
🌟 路由-导航守卫种类&作用
种类:全局、路由、组件
全局:beforeEach 登录状态、网页加载进度条
全局:beforeResolve 不用(路由、组件守卫被解析也就是被调用后)
全局:afterEach 关闭网页加载进度条 NProgress
路由:beforeEnter 不用
组件:beforeRouteEnter 组件被创建前(注:不能用this)
组件:beforeRouteUpdate 动态路由匹配/动态获取路由参数
组件:beforeRouteLeave 未保存离开组件/清除定时器/切换组件保存数据等
全局
全局前置守卫 beforeEach
场景1:登录验证,判断用户是否登录,登录-next(),没登录-重定向到登录页 next({path:'/login'})
场景2:显示网页加载进度条
// 导航守卫(监控路由变化)
// to 路由对象(新页面路由对象 to.path 可以获取当前新页面路由路径
// form 路由对象(旧页面路由对象 form.path 可以获取当前旧页面路由路径
// next 函数 可以控制页面正常访问或者重定向
import store from "@/store";
import router from "@/router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
const whiteList = ["/login", "/404", "/login/sms", "/login/token"];
router.beforeEach((to, from, next) => {
NProgress.start();
// ...
// 1 判断你访问的是什么页面
if (whiteList.indexOf(to.path) != -1) {
next();
} else {
// 登录相关的页面、404 直接next
// 其他 判断token是否存在
// let token = this.$store.state.login.token
let token = store.state.login.token;
// - 存在 next({})
// - 不存在 next({path: '/login'})
if (token) {
if (store.state.auths.menus.length <= 0) {
console.log("重新获取权限菜单");
store.dispatch("auths/FETCH_MENUS");
}
next();
} else {
next({ path: "/login" });
}
}
});
router.afterEach(() => {
NProgress.done();
});
全局解析守卫beforeResolve 2.5.0+
场景:不用 触发:路由、组件守卫被解析也就是被调用后
全局后置钩子afterEach
场景:关闭网页加载进度条 NProgress
触发:最后、特色没有next不会改变导航本身:
router.afterEach(() => { NProgress.done(); });
路由
{
path,
component,
...
beforeEnter: (to, from, next) => {
// ...
}
}
组件
Vue.component(组件名, {
beforeRouteEnter 组件被创建前(注:不能用this)
beforeRouteUpdate (2.2 新增) 动态路由匹配/动态获取路由参数
beforeRouteLeave 未保存离开组件/清除定时器/切换组件保存数据等
})
beforeRouteLeave (to, from, next) {
// 禁止用户在还未保存修改前突然离开
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
或
beforeRouteLeave (to, from, next) {
window.clearInterval(this.timer) //清楚定时器
next()
}
或
beforeRouteLeave (to, from, next) {
// 当用户需要关闭页面时, 可以将公用的信息保存到session或Vuex中
localStorage.setItem(name, content); //保存到localStorage中
next()
}
🌛 路由-导航守卫执行顺序
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
🌟 路由-动态路由如何实现
面试提问
如何实现权限控制
如何实现菜单权限
如何实现动态路由
如何动态添加路由规则
项目是前端路由还是后端路由
作用&语法
作用:不同角色,权限菜单看到的不一样
步骤1:页面菜单导航根据接口数据渲染
步骤2:路由不能全部写死,而是全部注释掉,利用addRoutes或addRoute动态添加路由规则
举个例子:商品列表、用户列表、权限列表、订单列表等等
1、api目录定义接口导出
2、组件内导入
3、最后组件内的created钩子函数中调用
动态路由特殊:
1-首先api目录定义接口导出
2-接着在vuex auth文件中去定义actions获取权限菜单数据 然后再全局导航守卫beforeEach中判断用户登录了,但是没有菜单数据 就通过dispatch触发actions获取权限菜单数据
目的1:登录页 跳转到 欢迎页 beforeEach 发现vuex中没有菜单数据 请求接口保存到vuex中
目的2:后期页面刷新vuex就丢了,没事 刷新页面也就是路由变化了 beforeEach 也会触发 发现vuex中没有菜单数据 请求接口保存到vuex中
或
1、在vuex中定义actions获取权限菜单数据 actions拿到数据后 触发mutations添加动态路由addRoute并且保存到模型中
2、在全局导航守卫中,判断vuex中是否有权限菜单数据 ,没有就触发actions 去获取权限菜单数据 并保存到模型中
🌟 路由-元信息有啥用
meta 存放面包屑、也可以加标识控制是否缓存组件
实现1:页面菜单导航根据接口数据渲染
实现2:路由不能全部写死,利用addRoutes或addRoute动态添加路由规则
router.addRoute("admin", {
path: twoMenu.url,
component: () => import("@/views/" + twoMenu.component),
meta: {
name1: twoMenu.auth_pname,
name2: twoMenu.auth_name,
keep_alive: twoMenu.keep_alive,
},
});
🌛 路由-过渡特效场景
项目中会用到
<transition class-enter-class="animated 类名">
<router-view></router-view>
</transition>
■ 脚手架相关
💘 总结
Vue进阶(项目准备:脚手架、ToDoList案例、组件封装、全局组件、Vue.config.js配置)
🌟 说出框架中做了哪些配置vue.config.js
别名
跨域(最终上线还得后端或运维处理
移出console
图片压缩
引入外部CDN
等等
面试追问:为啥要引入外部cdn
回答:减轻应用/代码服务器压力/并发,请求负载均衡/分发到其他服务器(注:银行原来有一个取款机,现在有n个)
🌟 说出跨域的解决方案
常用的
谷歌命令
谷歌插件
JSONP
http-proxy-middleware
等等
追问:项目中如何配置
vue中配置 devServer 配置就可以了
追问:在哪个文件中
vue.config.js 中
module.exports = {
// ...
devServer: {}
// ...
}
🌟 说出Vue.use原理
Vue.use主要用来在框架中全局注册插件,例如注册全局的组件等,
然后底层源码大致做了两件事
1 检查传递的插件有没有重复注册
2 判断plugin.install是不是函数或plugin是不是函数,然后分别用apply调用让函数执行,注册,
Vue.component(组件名, 单文件组件)
export function initUse (Vue: GlobalAPI) {
// 接受一个plugin参数,限制为 Function | Object两种类型
Vue.use = function (plugin: Function | Object) {
// _installedPlugins 存储所有注册过的 plugin
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 保存注册组件的数组,不存在则创建,存在则直接返回,不允许重复注册
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
// 将传入的参数转换成数组
const args = toArray(arguments, 1)
// 将Vue对象拼接到数组头部
args.unshift(this)
// 如果提供了 install 方法,则直接调用
if (typeof plugin.install === 'function') {
// 如果组件是对象,且提供install方法,调用install方法将参数数组传入,改变`this`指针为该组件
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
// 否则直接执行
plugin.apply(null, args)
}
// 将plugin存储到 installedPlugins,表示y已经注册过
installedPlugins.push(plugin)
return this
}
}
/**
* Convert an Array-like object to a real Array.
*/
export function toArray (list: any, start?: number): Array<any> {
start = start || 0
let i = list.length - start
const ret: Array<any> = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
■ UI组件库
💘 总结
Vue进阶am(项目准备:UI组件库ElemenUI、IView、AntdV、Vant、Mint等、自研UI组件库)
🌛 说出UI组件库/UI框架原理
步骤1:首选通过vue单页面应用定义好公共组件
步骤2:定义所有UI组件导出入口文件 src/components/index.js
import Kuangkuang from '@/components/kuangkuang/Index.vue'
import Page from '@/components/page/Index.vue'
export default Vue => {
// 语法:Vue.component(后期调用的组件名, 单文件组件)
// 注册:后期就可以直接调用,不需要单独导入
// Vue.component('qfui-kuangkuang', Kuangkuang)
// Vue.component('qfui-page', Page)
// Vue.component(Kuangkuang.name, Kuangkuang)
// Vue.component(Page.name, Page)
const coms = [Kuangkuang, Page]
coms.forEach(com => {
Vue.component(com.name, com)
})
}
步骤3:接着打包
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
打包后文件名 打包后存的目录 打包的组件在哪
"build:ui": "vue-cli-service build --target lib --name qf-ui --dest dist2 ./src/components/index.js"
},
步骤4:最后按照node包发布就可以使用
■ Vue项目布局
🌟 项目开发中有没有封装过组件
有或者用语言:开发过
🌟 项目中封装了哪些组件
有很多,
比如封装了公共的table、form等组件,
还有很多页面组件,例如例如编辑用户、分配角色,分配权限、门店创建、分类编辑等都有封装提取页面组件,从而便于后期维护
🌟 组件如何封装的
页面组件:首先在页面中完成功能,然后再views/模块名/components中定义单文件组件,然后导入使用,从而让组件更便于后期维护
概念:将组件中不同逻辑的代码提取封装,便于后期维护
公共组件:首先在src/components定义form公共组件,然后全局注册导入使用,最终根据需求利用props接受参数&创建自定义事件
🌟 样式相关
题目1:如何防止样式的污染?
scoped
题目2:scoped解决样式污染原理
说明:vue在编译的时候通过在DOM元素以及css样式上加上唯一标记,实现样式私有化,不污染全局样式。
举例:
<div class="my-class"></div>
编译为
<div class="my-class" data-v-56e7f952></div>
对应的样式
.my-class编译为
.my-class[data-v-56e7f952]
原理:利用前端自动化构建工具webpack结合PostCSS模块编译实现css模块化
题目3:项目中开启样式私有化后,无法修改子组件例如elementui、antdv如何解决
通过【深度作用选择器】,你可以使用 >>> 操作符;有些像 Sass 之类的预处理器无法正确解析 >>>。这种情况下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作。
🌟 用户角色权限三者是如何关联的
首先在权限管理可以添加删除权限,实现后端路由
接着可以添加删除角色,然后给角色分配权限
最后在用户管理给用户分配角色,
后期不同用户登录就可以看到不同点权限菜单/导航
■ 异步请求
💘 总结
Vue基础(项目准备:axios、fetch数据请求)
🌟 谈谈你对HTTP理解
超文本传输协议、规定客户端和服务端如何通信,
他是由请求行,响应行,请求头,响应头,请求体,响应体组成,
之前我做项目的回收 请求行你们主要查看请求地址、请求状态、请求方式、请求体头里面主要放cookie、token、content-type等,请求体主要看参数有没有传递给后端、响应体后端返回的数据进行项目调试。
🌟 谈谈你对状态码的理解
2xx 200成功 201成功并创建新资源
3xx 301永久重定向 302临时重定向 304浏览器缓存
4xx 400参数有误 401密码错误 403无权访问 404文件不存在 405请求方式有误
5xx 500服务器错误
🌟 post、get区别
安全角度 post对象安全 get地址栏 后期可以通过历史记录查看登陆密码
数据角度 get地址栏 不同浏览器地址栏长度限制 post后端规定 2M 8M
🌟 xhr、fetch区别
xhr、fetch
相同点:1-都可以发送异步请求、2-都是ECMA定义的
不同点:前者异步回调地狱,后者promise
其他了解
最初ajax(XMLHttpRequest ECMA组织)
瑕疵:1-语法麻烦太多,2-异步回调地狱,3-语法有兼容性问题
后台jq(第三方作者)
明确:JQ里面的$.ajax/get/post是基于XMLHttpRequest封装的 function + ajax + callback
好处:语法更简单、解决很多兼容性问题
瑕疵:异步回调地狱还在
解决:通过promise技术
最后fetch(ECMA组织) :结合promise技术而生,代替传统xhr
🌟 fetch、axios区别
相同点:1 都可以发送异步请求,2 都是promise
不同点:1 fetch官方、axios社区,2 axios更强并发、拦截器等
🌟 axios之前有没有封装过
- 封装axios导出request实例对象(timeout、baseURL、headers content-type、.env
- 请求拦截器(开启Loading、token、CancelToken
- 响应拦截器-成功(关闭Loading、res.data.data过滤 、 接口权限、TOKEN过期
- 响应拦截器-失败(关闭Loading、timeout处理 、404、canceled、邮件报警捕捉前端异常
🌟 axios原理
基于xmlhttprequest构造函数、和node中的http模块封装实现,动态判断浏览器环境、还是服务端环境,去选择对应语法返回promise对象
■ 状态管理vuex相关
💘 总结
Vue进阶(项目准备:VUEX单库、VUEX数据持久化b、VUEX单库模块化、VUEX框架模块化b)
🌟 说出vuex有哪些键
state、getters、mutations、actions、plugins、modules
🌟 说出vuex工作流
state存放数据、
getters过滤
mutations更新
actions异步请求
🌟 说出vuex数据持久化
H5存储
第三方模块(vux-persistedstate)
🌟 面试问答
问1:vuex工作流
答1:通过state定义状态、通过getters过滤状态、通过mutations更新状态、通过actions异步请求
- 追问:里面具体怎么写、或者面试官问的是vuex具体怎么写的?
- 回答:state是对对象 里面写键:值,getter是对象 里面写函数 返回过滤的数据,mutations 是对象 里面写函数 形参是state。
- 追问:vuex在项目中怎么用?
- 回答:store文件中通过module模块化定义状态,然后通过辅助函数mapState、mapMutations去获取
- 追问:具体场景呢?
- 回答:比如我之前做xxx项目,登录成功之后用户名、角色、token、权限菜单等等就是存在vuex中,然后通过辅助函数获取显示;
- 追问:怎么获取state的数据怎么取?
- 回答:可以通过 this.$store.state.键获取 但是我之前做xxx项目主要使用vuex的辅助函数mapState
- 追问:vuex属性之间的联系,还有辅助函数
- 回答:getter过滤state里面,mutations去更新state里面,actions主要发送异步请求然后触发mutations更新state里面的数据;
- 追问:为什么在action里面写异步请求,而不会mutation
- 回答:首先mutation也可以写异步请求,但是官方推荐在action里面写,从而确保拿到数据之后再更新状态,避免mutation写异步请求存在同步异步问题,也就是拿到数据【之前】更新了状态。
问2:vuex有哪些键
vuex中的键,和其他工作流,具体mutation和action的相同点,怎么用
问3:vuex怎么用
问4:vuex数据是永久的还是临时的
■ Vue项目接口
🌟 说一下登录如何实现的
1、通过elementui布局 表单验证
2、给登录绑定点击事件,...
3、然后通过vuex辅助函数 调用 actions 去请求接口
4、失败弹框提示,成功将token、uname、roleName存储vuex中,然后提示登录成功&&重定向
1 vuex周边问题 比如数据持久化、比如有哪些键
2 axios周边问题
3 token周边问题(过期问题)
🌟 如何判断用户是否登录
sessionStorage,cookie,localStorage
🌟 登录是如何鉴权的
通过全局导航守卫判断,白名单直接next、非白名单判断h5或vuex中是否存在 不存在重定向到 登录页即可
🌟 说一下token过期机制
纯前端处理
1 登录既要存token 又要额外存一个时间
2 axios请求拦截器里面 判断是否过期 提示token过期,然后就重定向到登录页
前后端结合处理
1 登录就存token
2 请求拦截器每次请求携带token
3 相应拦截器判断是否过期 提示token过期,过期就重定向登录页
🌟 项目中是前端路由还是后端路由
后端路由是url地址映射到服务器上的某些资源
前端路由是url地址映射到浏览器上的某些资源
🌟 权限菜单如何实现的
后端路由
追问:后端路由如何实现的?
回答:几个关键词 请求接口、addRoute、菜单还得遍历搞出来、vuex
1、首先通过vuex定义actions获取当前用户的权限数据,然后触发mutations、保存到state中、并且通过addRoute添加动态路由
2、在导航守卫中判断store中的权限数据是否存在,存在next不存在全局导航守卫beforeEach触发action获取
3、后台首页layout下面的menu中通过辅助函数获取遍历显示
其他备注:一般token有效期7200s 也就是2小时 具体看公司
🌟 项目做了哪些优化
交互角度
animate.css
vue-lazyload
nprogress
loading
preventReClick
等
代码角度
混入
过滤器
页面组件提取封装
公共组件提取封装
axios封装
全局配置文件 process.env
等等
性能角度 webpack
关闭prefetch预加载
图片压缩 原理:image-webpack-loader
引入外部cdn 原理:configureWebpack -> externals
UI框架按需加载 原理:babel-plugin-import 实现自动按需引入
路由/组件懒加载 原理:es6 import动态加载 + webapck文件分割
等等
性能优化 web server 或 code
nginx服务器配置:开启强制缓存expires缓存(注:这个单词cookie中碰到过)
nginx服务器配置:开启gzip压缩
keep-alive组件缓存
等等
🌟 说出路由懒加载原理
es6 动态导入 + webpack文件分割
普通导入:import 组件名 from 路径及文件名 打开就触发
动态导入:component: () => import(路径及文件名) 访问才触发
🌟 周边:async、await具体怎么用的
就是钩子函数中、或者vuex的actions中不经常需要写api请求接口吗
然后通过async、await来修饰完成功能
举例:比如权限菜单、各种登录咱们都用了
■ 查缺补漏
🌟 说一下Diff算法
算法规则
步骤一:用JS对象模拟DOM树
步骤二:比较两棵虚拟DOM树的差异(切记切记切记:一层一层比)
步骤三:把差异应用到真正的DOM树上
步骤四:在页面展示
虚拟DOM介绍:https://www.jianshu.com/p/616999666920
如何实现一个Virtual DOM 算法:https://github.com/livoras/blog/issues/13
深入理解Diff算法:https://blog.csdn.net/lunahaijiao/article/details/86741739
<div id="root">
<div v-for="item in todos">
<input type="checkbox" name="" id="">
{{item.title}}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#root",
data: {
todos: [
{id:1, title:'吃饭'},
{id:2, title:'睡觉'},
{id:3, title:'挤痘痘'},
]
}
})
</script>
步骤1:通过浏览器运行 ,给第二个checkbox打钩
步骤2:在控制台 vm.todos.splice(1,1)
什么是虚拟DOM:就是使用javascript的对象来描述了DOM结构
为什么要虚拟DOM:提升性能(因为回流、重绘 浏览器工作机制)
如何更新DOM数据:通过diff算法(同层比较)
步骤一:用JS对象模拟DOM树
步骤二:比较两棵虚拟DOM树的差异(注:后期如果写写遍历 加上:key 提升性能的)
步骤三:把差异应用到真正的DOM树上
切记:当遍历数据的时候要写key 提升性能、也是避免BUG
■ 周边
🌟 【事件循环】谈谈你对Event Loop的理解
Event Loop即事件循环,
是指浏览器或Node的一种确保javaScript单线程运行时不会阻塞的一种机制,
也就是我们经常使用 异步的原理。
种类:浏览器的Event Loop、Node.js中的Event Loop
🌟 【事件循环】谈谈你对浏览器的Event Loop理解
浏览器输入网址服务器响应数据后,
浏览器会通过render进程开始解析工作
GUI线程负责页面渲染
JS引擎线程负责执行JS代码
遇到异步代码会交给其他线程处理,然后放到队列中,
事件循环主要是从队列中取出代码放到执行栈中交给js引擎线程处理
🌛【事件循环】谈谈你对Node.js中的Event Loop理解
libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
idle, prepare 阶段:仅node内部使用
poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里(轮训阶段)
check 阶段:执行 setImmediate() 的回调
close callbacks 阶段:执行 socket 的 close 事件回调
🌟 【事件循环】说出宏任务、微任务各有哪些
单词含义:I input 输入、 O output 输出
用户角度IO操作:鼠标键盘-是计算机输入信息,显示器-是输出设备
电脑角度IO操作:CPU、内存与其他设备之间数据转移过程就是IO操作,例如数据从磁盘读到内存,或者内存写到磁盘
编程角度IO操作:进程读取数据操作
宏任务:
整体代码(script)
定时器(setTimeout、setInterval)
I/O操作(DOM事件、AJAX异步请求)
setImmediate(node环境)
requestAnimationFrame(浏览器环境)
微任务
Promise.then catch finally
async/await(底层还是promise)
process.nextTick(node环境)
MutationObserver(浏览器环境)
🌟 【事件循环】说出先执行宏任务还是微任务
算整体代码script:1宏n微
不算整体代码script:先n微,再1宏 -> n微,再1宏
🌟 【事件循环】浏览器和node中event loop区别
浏览器:一个宏走完清空所有微任务
node:一个阶段走完 再清空所有微任务