25.测试题集

3/21/2023 笔记

# html、css、vue

# 1.常见的水平垂直居中实现方案

  1. flex布局
.father {
  display: flex;
  justify-content: center;
  align-items: center;
}
.son {
  ...
}
1
2
3
4
5
6
7
8
  1. 绝对定位配合margin:auto
.father {
  position: relative;
}
.son {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: auto;
}
1
2
3
4
5
6
7
8
9
10
11
  1. 绝对定位配合transform实现
.father {
  position: relative;
}
.son {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
1
2
3
4
5
6
7
8
9

# 2.BFC问题

BFC:块格式上下文,独立的渲染区域,内部元素不会影响外部的元素
一个块格式化上下文(block formatting context)是Web页面的可视化CSS渲染的一部分。它是块盒子的布局发生,浮动互相交互的区域。
触发BFC:

  • body 根元素
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
  • display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll) BFC特点:
  • 内部块级盒子垂直方向排列
  • 盒子垂直距离由margin 决定,同一个BFC 盒子的外边距会重叠
  • BFC 就是一个隔离的容器,内部子元素不会影响到外部元素
  • BFC 的区域不会与float box叠加
  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。 BFC用途:
  • 清除浮动
  • 解决外边距合并
  • 布局

解决外边距重合:

// 1.相邻兄弟元素
//HTML
<div class="up">我在上面</div>
<div class="down">我在下面</div>

//CSS
.up {
  width: 100px;
  height: 100px;
  border: 1px solid blue;
  margin: 100px;
}
.down {
  width: 100px;
  height: 100px;
  border: 1px solid red;
  margin: 100px;
  display: inline-block; // 触发BFC
}
// up和down两个元素处于不同的BFC ,外边距不重合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 2.父子元素
//HTML
<div class="parent">
  <div class="child">我是儿子</div>
</div>
//CSS
.parent {
  width: 100px;
  height: 200px;
  background: red;
  margin-top: 50px;
  overflow: hidden; // 触发父元素FBFC,取消上边距合并
}
.child {
  width: 50px;
  height: 50px;
  margin-top: 100px;
  border: 1px solid blue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3.felx:1; 是哪些属性的缩写,对应的属性代表什么含义

flex: 1;在浏览器中查看分别是flex-grow(设置了对应元素的增长系数)、flex-shrink(指定了对应元素的收缩规则,只有在所有元素的默认宽度之和大于容器宽度时才会触发)、flex-basis(指定了对应元素在主轴上的大小)

# 4.隐藏元素的属性有哪些

  • display: none;
  • opacity: 0;
  • visibility: hidden;

# 5.请解释两种盒模型之间的区别?以及如何用 CSS 设置?

// 1.W3C标准盒模型
box-sizing="content-box";
// width不计算padding和border
// 设置的宽高是对实际内容content的宽高进行设置,内容周围的border和padding另外设置;

// 2.怪异盒模型(IE盒模型)
box-sizing="border-box";
// 计算padding 和border
// width【height】= 设置的content的宽【高】 + padding + border
1
2
3
4
5
6
7
8
9

# 外边距合并

所谓外边距合并,指的是margin合并。块的顶部外边距和底部外边距有时被组合(折叠)为单个外边距,其大小是组合到其中的最大外边距,这种行为称为外边距合并。
要注意的是,外边距合并只针对块级元素,而且是顶部或底部的外边距。

// 1.相邻兄弟元素
//HTML
<div class="up">我在上面</div>
<div class="down">我在下面</div>

//CSS
.up {
  width: 100px;
  height: 100px;
  border: 1px solid blue;
  margin: 100px;
}
.down {
  width: 100px;
  height: 100px;
  border: 1px solid red;
  margin: 100px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 2.父子元素
// 如果在块级父元素中,不存在上边框、上内补、inline content、清除浮动这四个属性,(对于不存在上边框和上内补,也可以理解为上边框和上内补宽度为0),那么这个块级元素和其第一个子元素的存在外边距合并,也就是上边距”挨到一起“,那么此时父元素展现出来的外边距,将会是子元素的margin-top 和父元素的margin-top 的较大值。
//HTML
<div class="parent">
  <div class="child">我是儿子</div>
</div>
//CSS
.parent {
  width: 100px;
  height: 200px;
  background: red;
  margin-top: 50px;
}
.child {
  width: 50px;
  height: 50px;
  margin-top: 100px;
  border: 1px solid blue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 6.列举出你所知道的CSS选择器

  1. 常用选择器:
  • 标签选择器
  • 类选择器
  • id选择器
  • 伪类选择器
:link :选择未被访问的链接
:visited:选取已被访问的链接
:active:选择活动链接
:hover :鼠标指针浮动在上面的元素
:focus :选择具有焦点的
:first-child:父元素的首个子元素
:first-of-type 表示一组同级元素中其类型的第一个元素
:last-of-type 表示一组同级元素中其类型的最后一个元素
:only-of-type 表示没有同类型兄弟元素的元素
:only-child 表示没有任何兄弟的元素
:nth-child(n) 根据元素在一组同级中的位置匹配元素
:nth-last-of-type(n) 匹配给定类型的元素,基于它们在一组兄弟元素中的位置,从末尾开始计数
:last-child 表示一组兄弟元素中的最后一个元素
:root 设置HTML文档
:empty 指定空的元素
:enabled 选择可用元素
:disabled 选择被禁用元素
:checked 选择选中的元素
:not(selector) 选择与 <selector> 不匹配的所有元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 伪元素选择器
:first-letter :用于选取指定选择器的首字母
:first-line :选取指定选择器的首行
:before : 选择器在被选元素的内容前面插入内容
:after : 选择器在被选元素的内容后面插入内容
1
2
3
4
  1. 通用选择器
* 将匹配文档的所有元素
1
  1. 高级选择器
  • 子选择器
	ul > li
1
  • 相邻同胞选择器

1
  • 属性选择器
[attribute] 选择带有attribute属性的元素
[attribute=value] 选择所有使用attribute=value的元素
[attribute~=value] 选择attribute属性包含value的元素
[attribute|=value]:选择attribute属性以value开头的元素

/* 元素名[属性名] 或 *[属性名] */
/* 选择带有alt属性的所有img元素: */
img[alt] { ...}  
/* 时拥有href和title的a元素 */
a[href][title] { ...}
1
2
3
4
5
6
7
8
9
10

# 7.在什么情况下会出现浮动元素?以及用什么方法清除浮动

# 7.1什么是浮动

浮动最开始出现的意义是为了让文字环绕图片,后来发展着发现浮动可以让多个块级元素并排显示,这样子很方便只需要一个属性。到这里肯定有人会问了,那我把display属性设置为inline-block不也能达到效果,为啥还用浮动。那有没有考虑过我就要这些元素从右往左排列呢哈哈哈,所以浮动可以控制居左还是居右排列。

# 7.2如何实现浮动

其实想实现浮动很容易,给想让他浮动的元素设置属性float:left/right就可以了,浮动造成的结果就是让设置浮动的元素脱离文档流,达到按方向排成一排的效果,也同时会带来一个问题-------父元素高度塌陷。

<div class="box">
  <div class="div"></div>
  <div class="div"></div>
</div>

.box{
    border: 1px solid;
    width: 100%;
  }
.div{
   float: left;
   border: 1px solid;
   width: 100px;
   height: 100px;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3清除浮动

这里的清除浮动不是清除已经浮动的元素,而是解决由于浮动所带来的的问题(父元素高度塌陷)来清除浮动

  1. 额外标签法:插入多余元素并设置其属性clear:both
    原理:父元素和冗余元素的高度都为0,并且浮动元素会盖在其上方,当设置clear:both时,冗余元素会躲开浮动元素直到不被其盖住,而父元素为了包裹住他自然就撑开了。
<div class="box">
  <div class="div"></div>
  <div class="div"></div>
  <div class="div3"></div>
</div>
.box{
 border: 1px solid;
 width: 100%;
}
.div{
 float: left;
 border: 1px solid;
 width: 100px;
 height: 100px;
}
.div3{
  clear: both;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

缺点:在页面中添加冗余元素太麻烦,且不符合语义化 2. 采用伪元素:after,给父元素新增一个clearfix类,为其增加伪元素

<div class="box clearfix">
  <div class="div"></div>
  <div class="div"></div>
</div>
.box{
  border: 1px solid;
  width: 100%;
}
.div{
  float: left;
  border: 1px solid;
  width: 100px;
  height: 100px;
}
.clearfix::after{
  content: " ";
  display: table; /*采用此方法可以有效避免浏览器兼容问题*/
  clear: both;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

缺点:伪元素存在兼容性问题 3. 创建BFC 必须定义width或zoom:1,同时不能定义height,使用overflow:hidden时,浏览器会自动检查浮动区域的高度
原理:因为BFC有隔离作用,BFC内的元素无论怎么排布都不会影响BFC,这样也可以达到清除浮动的效果。

# 8.display:none和visibility:hidden的区别

  1. 是否占据空间 display:none,该元素不占据任何空间,在文档渲染时,该元素如同不存在(但依然存在文档对象模型树中)。 visibility:hidden,该元素空间依旧存在。
  2. 是否渲染 display:none,会触发reflow(回流),进行渲染。 visibility:hidden,只会触发repaint(重绘),因为没有发现位置变化,不进行渲染。
  3. 是否继承属性 display:none,display不是继承属性,元素及其子元素都会消失。 visibility:hidden,visibility是继承属性,若子元素使用了visibility:visible,则不继承,这个子孙元素又会显现出来。

# js

# 1.Js的基础类型,typeof和instanceof的区别

基础类型有:boolean、string、number、bigint、undefined、symbol、null
typeof能识别所有的值类型,识别函数,能区分是否是引用类型。

const a = "str";
console.log("typeof a :>> ", typeof a); // typeof a :>>  string

const b = 999;
console.log("typeof b :>> ", typeof b); // typeof b :>>  number

const c = BigInt(9007199254740991);
console.log("typeof c :>> ", typeof c); // typeof c :>>  bigint

const d = false;
console.log("typeof d :>> ", typeof d); // typeof d :>>  boolean

const e = undefined;
console.log("typeof e :>> ", typeof e); // typeof e :>>  undefined

const f = Symbol("f");
console.log("typeof f :>> ", typeof f); // typeof f :>>  symbol

const g = null;
console.log("typeof g :>> ", typeof g); // typeof g :>>  object

const h = () => {};
console.log("typeof h :>> ", typeof h); // typeof h :>>  function

const i = [];
console.log("typeof i :>> ", typeof i); // typeof i :>>  object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

# 2.数组的forEach和map方法有哪些区别?常用哪些方法去对数组进行增、删、改

  • forEach是对数组的每一个元素执行一次给定的函数。
  • map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回的值。
  • pop():删除数组后面的最后一个元素,返回值为被删除的那个元素。
  • push():将一个元素或多个元素添加到数组末尾,并返回新的长度。
  • shift():删除数组中的第一个元素,并返回被删除元素的值。
  • unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度。
  • splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
  • reverse(): 反转数组。改变原数组,返回原数组。
const arr = [1, 2, 3, 4, 5, 6];

arr.forEach(x => {
  x = x + 1;
  console.log("x :>> ", x);
});
// x :>>  2
// x :>>  3
// x :>>  4
// x :>>  5
// x :>>  6
// x :>>  7

console.log("arr :>> ", arr); // arr :>>  [ 1, 2, 3, 4, 5, 6 ]

const mapArr = arr.map(x => {
  x = x * 2;
  return x;
});
console.log("mapArr :>> ", mapArr); // mapArr :>>  [ 2, 4, 6, 8, 10, 12 ]
console.log("arr :>> ", arr); // arr :>>  [ 1, 2, 3, 4, 5, 6 ]

const popArr = arr.pop();
console.log("popArr :>> ", popArr); // popArr :>>  6
console.log("arr :>> ", arr); // arr :>>  [ 1, 2, 3, 4, 5 ]

const pushArr = arr.push("a");
console.log("pushArr :>> ", pushArr); // pushArr :>>  6
console.log("arr :>> ", arr); // arr :>>  [ 1, 2, 3, 4, 5, 'a' ]

const shiftArr = arr.shift();
console.log("shiftArr :>> ", shiftArr); // shiftArr :>>  1
console.log("arr :>> ", arr); // arr :>>  [ 2, 3, 4, 5, 'a' ]

const unshiftArr = arr.unshift("b", "c");
console.log("unshiftArr :>> ", unshiftArr); // unshiftArr :>>  7
console.log("arr :>> ", arr); // arr :>>  ['b', 'c', 2,3,4,5,'a']

const spliceArr = arr.splice(2, 4, "d", "e");
console.log("spliceArr :>> ", spliceArr); // spliceArr :>>  [ 2, 3, 4, 5 ]
console.log("arr :>> ", arr); // arr :>>  [ 'b', 'c', 'd', 'e', 'a' ]

const reverseArr = arr.reverse();
console.log("reverseArr :>> ", reverseArr); // reverseArr :>>  [ 'a', 'e', 'd', 'c', 'b' ]
console.log("arr :>> ", arr); // arr :>>  [ 'a', 'e', 'd', 'c', 'b' ]
console.log("reverseArr === arr :>> ", reverseArr === arr); // reverseArr === arr :>>  true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 3.闭包和作用域

闭包是作用域应用的特殊场景。 js中常见的作用域包括全局作用域、函数作用域、块级作用域。
闭包:能够访问另一个函数作用域变量的函数
闭包形成的原因:内部的函数存在外部作用域的引用就会导致闭包

var a = 0
function foo(){
    var b =14
    function fo(){
        console.log(a, b)
    }
    fo()
}
foo()
// 这里的子函数 fo 内存就存在外部作用域的引用 a, b,所以这就会产生闭包
1
2
3
4
5
6
7
8
9
10

闭包中的变量存储的位置是堆内存(假如闭包中的变量存储在栈内存中,那么栈的回收 会把处于栈顶的变量自动回收。所以闭包中的变量如果处于栈中那么变量被销毁后,闭包中的变量就没有了。所以闭包引用的变量是出于堆内存中的。)
闭包的作用:

  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化 闭包经典使用场景:
  1. return 回一个函数
var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    return f
}

// 这里的 return f, f()就是一个闭包,存在上级作用域的引用。
var x = fn()
x() // 21
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 函数作为参数
var a = '林一一'
function foo(){
    var a = 'foo'
    function fo(){
        console.log(a)
    }
    return fo
}

function f(p){
    var a = 'f'
    p()
}
f(foo())
/* 输出
*   foo
/ 
// 使用 return fo 返回回来,fo() 就是闭包,f(foo()) 执行的参数就是函数 fo,因为 fo() 中的 a 的上级作用域就是函数foo(),所以输出就是foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 计时器的一个应用
for (var i = 0; 1 < 10; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j);
    }, 1000 * j)
  })(i)
}
1
2
3
4
5
6
7
  1. 埋点计数器
function count() {
	var num = 0;
	return function() {
		return ++num
	}
}
var getNum = count();  // 第一个需要统计的地方
var getNewNum = count(); //第二个需要统计的地方
 // 如果我们统计的是两个button的点击次数
document.querySelectorAll('button')[0].onclick = function() {
 	console.log('点击按钮1的次数:'+getNum());
}
document.querySelectorAll('button')[0].onclick = function() {
 	console.log('点击按钮2的次数:'+getNewNum());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 柯里化:把一个多参数的函数转化成单参数函数的方法,使得更灵活方便
// 原函数 用来检验文本是否符合规范
// reg 传入的正则表达式  txt 需要被检测的文本
function check(reg,txt){
	return reg.test(txt)
}
console.log(check(电话号码的正则,13923456789));
console.log(check(邮箱的正则,youxiang@163.com));

// 现如今
function nowCheck(reg){
	return function(txt){
		return reg.test(txt)
	}
}
var isPhone = nowCheck(电话号码的正则)
console.log(isPhone('13923456789'))
var isEmail = nowCheck(邮箱的正则)
console.log(isEmail('youxiang@163.com'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用闭包需要注意什么:容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
为什么还要使用闭包:在某些情况下,希望某些函数内的变量在函数执行后不被销毁 经典面试题1:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]()
/* 输出
    3
    3
    3
/
// 这里的 i 是全局下的 i,共用一个作用域,当函数被执行的时候这时的 i=3,导致输出的结构都是3。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

写法1:使用闭包改善上面的写法达到预期效果,写法1:自执行函数和闭包

var data = [];

for (var i = 0; i < 3; i++) {
    (function(j){
      setTimeout( data[j] = function () {
        console.log(j);
      }, 0)
    })(i)
}

data[0]();
data[1]();
data[2]()
1
2
3
4
5
6
7
8
9
10
11
12
13

写法2:使用let

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]()
// let 具有块级作用域,形成的3个私有作用域都是互不干扰的
1
2
3
4
5
6
7
8
9
10
11
12

js 内存泄漏场景、如何监控以及分析 (opens new window)

# 4.箭头函数与普通函数的区别

  • 在普通函数中,this关键字是在函数调用时动态绑定的。在函数体内部,this指向调用函数的对象。而在箭头函数中,this关键字指向定义函数时的作用域
  • 在普通函数中,可以使用 arguments 对象来获取所有传递给函数的参数。但在箭头函数中,arguments 对象不可用,没有自身的prototype
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

# 构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么

不可以

  • 没有自己的this,无法调用call,apply
  • 没有prototype属性,而new命令在执行时需要将构造函数的prototype赋值给新的对象的 proto

# 5.深拷贝、浅拷贝

基本数据类型赋值传递的是存放在栈内的数据,而引用类型赋值传递的是他们存放在栈内的地址,他们的数据存放在这个地址指向的堆内存里
所以引用类型赋值传递的是存放在栈内的地址,当对新对象数据进行修改时,改变的是这个地址指向的堆内存里的数据
因为新旧对象使用的是相同的地址,地址指向的数据改变后,旧对象的值也就随之改变了。如果不想改变旧对象的值,这时候就用到了深浅拷贝

# 浅拷贝

浅拷贝创建一个新的对象,只拷贝一层,即拷贝对象里第一层基本数据类型的值和引用类型的地址
如果修改新对象里第一层基本数据类型的值,不会对旧对象产生影响,但如果修改第一层的引用类型的值,仍会对旧对象产生影响(因为虽然新旧对象不再使用同一地址,但第一层的引用类型的地址仍是相同的)

let obj_old = {
  name: 'Tom',
  age: 15,
  favorite: {
    food: 'bread',
    drink: 'milk'
  }
}
// Object.assign()是一种浅拷贝的方法
let obj_new = Object.assign({}, obj_old)
console.log(obj_old === obj_new)  // false
console.log(obj_old.name === obj_new.name)  // true
console.log(obj_old.favorite === obj_new.favorite)  // true
// obj_old === obj_new是false,这说明新旧对象的地址已经不一样了
// obj_old.name === obj_new.name是true,这个是基本数据类型的值是相同的,没有问题
// obj_old.favorite === obj_new.favorite也是true,这里的favorite也是对象,但是他们比较出来的结果不是false而是true,说明这个对象的地址已经是相同的了,它们共享同一块内存空间

// 修改新对象的第一层基本数据类型时,新旧对象互不影响
obj_new.name = 'Jerry'
console.log(obj_old)
console.log(obj_new)

// 修改新对象第一层的引用数据类型时,也就是修改obj_new第一层的favorite对象里的属性值时,obj_old里的favorite相应的属性值也随之改变了,新旧对象相互影响
obj_new.favorite.food = 'cheese'
console.log(obj_old)
console.log(obj_new)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 浅拷贝的方法

  1. Object.assign()

语法:Object.assign(target, ...sources) target 目标对象,接收源对象属性的对象,也是修改后的返回值。 sources 源对象,包含将被合并的属性。

let obj_old = {
    name: 'Tom',
    age: 15,
    favorite: {
        food: 'bread',
        drink: 'milk'
    }
}
let obj_new = Object.assign({}, obj_old)
console.log(obj_old === obj_new)  // false
console.log(obj_old.name === obj_new.name)  // true
console.log(obj_old.favorite === obj_new.favorite)  // true
1
2
3
4
5
6
7
8
9
10
11
12
  1. 展开运算符 Spread ...

语法:{...sources} sources 源对象,包含将被合并的属性。

let obj_old = {
    name: 'Tom',
    age: 15,
    favorite: {
        food: 'bread',
        drink: 'milk'
    }
}
let obj_new = {...obj_old}
console.log(obj_old === obj_new)  // false
console.log(obj_old.name === obj_new.name)  // true
console.log(obj_old.favorite === obj_new.favorite)  // true
1
2
3
4
5
6
7
8
9
10
11
12
  1. Array.prototype.concat()

语法:arr.concat(value0, /* … ,*/ valueN) 注:如果省略了所有 valueN 参数,则 concat 会返回调用此方法的现存数组的一个浅拷贝。

let arr_old = [1, 2, {name: 'Tom'}]
let arr_new = arr_old.concat()
console.log(arr_old === arr_new)  // false
console.log(arr_old[2] === arr_new[2])  // true
1
2
3
4
  1. Array.prototype.slice()

语法:arr.slice(begin, end) 注:如果省略了 begin, end 参数,则 slice 会返回调用此方法的现存数组的一个浅拷贝。

let arr_old = [1, 2, {name: 'Tom'}]
let arr_new = arr_old.slice()
console.log(arr_old === arr_new)  // false
console.log(arr_old[2] === arr_new[2])  // true
1
2
3
4

# 深拷贝

深拷贝就是在堆内存中开辟一个新的空间存放新对象,拷贝原对象的所有属性,拷贝前后的两个对象互不影响。深拷贝的新旧对象不共享内存。
这时我们去修改新对象中的任意层级的任意属性值,都不会对原对象产生影响,原对象依然保持不变

let obj_old = {
  name: 'Tom',
  age: 15,
  hobby: ['eat', 'game'],
  favorite: {
      food: 'bread',
      drink: {
        dname: 'milk',
        color: 'white',
      },
  }
}
let obj_new = _.cloneDeep(obj_old)
console.log(obj_old)
console.log(obj_new)
console.log(obj_old.name === obj_new.name) // true
console.log(obj_old.favorite === obj_new.favorite) // false
console.log(obj_old.favorite.drink === obj_new.favorite.drink) // false
// 这里我们使用了lodash工具库提供的_.cloneDeep深拷贝方法,来看一下新旧对象的对比
// obj_old.name === obj_new.name为true,这个是基本数据类型,完全相同,没有问题
// obj_old.favorite === obj_new.favorite,这里为false,到这里就和浅拷贝不同了,浅拷贝到这一层的时候,对象属性的地址是相同的,而深拷贝是完全拷贝出一个新的对象,所以不管是哪一层,对象属性的地址都是不同的
// obj_old.favorite.drink === obj_new.favorite.drink为false,再往深一层仍然是false,即地址不相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 深拷贝的方法

  1. 递归实现 使用递归实现原对象每一层的拷贝,当遇到基本数据类型时,直接拷贝,遇到引用数据类型时,递归拷贝它的每个属性
const obj_old = {
  name: 'Tom',
  age: 15,
  hobby: ['eat', 'game'],
  favorite: {
      food: 'bread',
      drink: {
        dname: 'milk',
        color: 'white',
      },
  }
}
const obj_new = {}

function deepClone(newObj, oldObj){
  for (const key in oldObj) {
    if (oldObj[key] instanceof Object) {
      newObj[key] = {}
      deepClone(newObj[key], oldObj[key])
    } else if (oldObj[key] instanceof Array) {
      newObj[key] = []
      deepClone(newObj[key], oldObj[key])
    } else {
      newObj[key] = oldObj[key]
    }
  }
}

deepClone(obj_new, obj_old)

console.log(obj_old)
console.log(obj_new)
console.log(obj_old.name === obj_new.name) // true
console.log(obj_old.favorite === obj_new.favorite) // false
console.log(obj_old.favorite.drink === obj_new.favorite.drink) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  1. lodash工具包 lodash中文文档 (opens new window) 引入成功后,我们直接使用lodash提供给我们的函数_.cloneDeep就行
let obj_old = {
  name: 'Tom',
  age: 15,
  hobby: ['eat', 'game'],
  favorite: {
      food: 'bread',
      drink: {
        dname: 'milk',
        color: 'white',
      },
  }
}
let obj_new = _.cloneDeep(obj_old)

console.log(obj_old)
console.log(obj_new)
console.log(obj_old.name === obj_new.name) // true
console.log(obj_old.favorite === obj_new.favorite) // false
console.log(obj_old.favorite.drink === obj_new.favorite.drink) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. JSON.parse(JSON.stringify(obj))
  • JSON.stringify() 将JSON格式的对象转为字符串
  • JSON.parse() 将JSON格式的字符串转为对象
const obj_old = {
  name: 'Tom',
  age: 15,
  hobby: ['eat', 'game'],
  favorite: {
      food: 'bread',
      drink: {
        dname: 'milk',
        color: 'white',
      },
  }
}
const obj_new = JSON.parse(JSON.stringify(obj_old))

console.log(obj_old)
console.log(obj_new)
console.log(obj_old.name === obj_new.name) // true
console.log(obj_old.favorite === obj_new.favorite) // false
console.log(obj_old.favorite.drink === obj_new.favorite.drink) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

虽然这个方法最简单,代码行数最少,但是它也有一定的缺陷:

  • 拷贝对象的值中如果有‘函数’,‘undefined’,‘symbol’ JSON.stringify()序列化后,键值对丢失
  • 拷贝RegExp会变成空对象{}
  • 对象中含有‘NaN’,‘Infinity’会变成null
  • 拷贝Date会变成字符串
-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

# 6.事件循环机制

JavaScript事件循环机制是一种用于处理异步任务的机制。
执行流程:主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去任务队列中取下一个任务执行。在每次事件循环中,JavaScript 会先执行所有微任务,再执行一个宏任务。
异步任务的进一步细分---宏任务与微任务:

  • 触发宏任务的方式:script 中的代码块、setTimeout()、setInterval()、setImmediate() (非标准,IE 和 Node.js 中支持)、注册事件
  • 触发微任务的方式:promise.then()、await、MutationObserver、queueMicrotask()
setTimeout(() => {
  console.log(222)
}, 
2000)  // 2秒后, 同步代码未完成, 于是去排队

setTimeout(() => {
  console.log(111)
}, 
1000)  // 1秒后, 同步代码未完成, 于是去排除

let start = Date.now()
while(Date.now() - start < 3000) {  // 卡3秒 

}
console.log(333)  // 3秒后立即执行
// 333
// 111
// 222
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种
// await相当于promise then 是微任务
async function fun() {
  await 1  // await后面的语句相当于在一个then里,相当于又是一个微任务
  console.log('didi')
}
fun()
console.log('123')
// 123
// didi


setTimeout(function(){   
  console.log('1') 
}); 
new Promise(function(resolve){ 
    console.log('2'); 
    for(var i = 0; i < 10000; i++) {     
      i == 99 && resolve();  
    } })
    .then(function(){   
      console.log('3') 
    });  
console.log('4');
// 2
// 4
// 3
// 1
// new Promise是一个构造函数,是同步任务,先同步原则,先输出2 ,4 同步任务执行完后,setTimeout的回调再宏任务队列,promise的then方法再微任务队列,秉承先微再宏原则,再输出3 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 要注意的是如果await的是promise对象,await会暂停async函数内后面的代码,先执行async函数外的同步代码(注意,promise内的同步代码会先执行),等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果返回后,再继续执行async函数内后面的代码。
// 若await是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
// 就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。
async function async1() {
  console.log('async1 start')
  await async2();  
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)

async1();

new Promise(
  function (resolve) {
    console.log('promise1')
    resolve()
  })
  .then(
  function () {
      console.log('promise2')
  })
console.log('script end')
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
// 解析:先同步任务:script start、async1 start。setTimeout加入到宏任务队列。await相当于promise then是微任务,执行async输出async2。这里注意:await的是promise对象,所以先不执行async1函数内的其他内容,要完成其他所有的同步任务,那就接着输出:promise1、script end。在执行async1函数的其他内容,输出async1 end。紧接着执行promise的then方法输出promise2最后执行setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Promise.resolve().then(()=>{
  console.log('第一个回调函数:微任务1')  
  setTimeout(()=>{
    console.log('第三个回调函数:宏任务2')
  },0)
});
setTimeout(()=>{
  console.log('第二个回调函数:宏任务1')
  Promise.resolve().then(()=>{
    console.log('第四个回调函数:微任务2')   
  })
},0)
// 第一个回调函数:微任务1
// 第二个回调函数:宏任务1
// 第四个回调函数:微任务2
// 第三个回调函数:宏任务2
// 解析:这一堆js代码会作为宏任务执行,Promise作为p1压入微任务队列,setTimeout作为s1压入宏任务队列,此时代码都执行完毕。执行宏任务队列之前会清空微任务队列,执行p1:打印"第一个回调函数:微任务1",然后把setTimeout作为s2压入宏任务队列。然后接着执行宏任务s1:打印"第二个回调函数:宏任务1",然后把Promise作为p2压入微任务队列。此时一个循环结束,执行下一个宏任务,先把微任务队列清空,打印:"第四个回调函数:微任务2"。接着执行宏任务队列,打印:"第三个回调函数:宏任务2"。
// 事件循环的标志是宏任务队列结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function wait() {
	return new Promise(resolve =>
		setTimeout(resolve, 10 * 1000)
	)
}

async function main() {
	console.time();
	const x = await wait(); // 每个都是都执行完才结,包括setTimeout(10*1000)的执行时间
	const y = await wait(); // 执行顺序 x->y->z 同步执行,x 与 setTimeout 属于同步执行
	const z = await wait();
	console.timeEnd(); // default: 30099.47705078125ms
	
	console.time();
	const x1 = wait(); // x1,y1,z1 同时异步执行, 包括setTimeout(10*1000)的执行时间。
	const y1 = wait(); // x1 与 setTimeout 属于同步执行
	const z1 = wait(); // wait()方法赋值给x1,y1,z1时,函数被调用并开始执行
	await x1;
	await y1;
	await z1;
	console.timeEnd(); // default: 10000.67822265625ms
	
	console.time();
	const x2 = wait(); // x2,y2,z2 同步执行,但是不包括setTimeout(10*1000)的执行时间
	const y2 = wait(); // x2 与 setTimeout 属于异步执行
	const z2 = wait();
	x2,y2,z2;
	console.timeEnd(); // default: 0.065185546875ms
}
main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 7.从输入一个URL地址到浏览器完成渲染的整个过程

  1. 浏览器地址栏输入URL并回车
  2. 浏览器查找当前URL是否存在缓存,并比较缓存是否过期
  3. DNS解析URL对应的IP
  4. 根据IP建立TCP连接(三次握手)
  5. 发送http请求
  6. 服务器处理请求,浏览器接受HTTP响应
  7. 浏览器解析并渲染页面
  8. 关闭TCP连接(四次握手) 详情可参考:从输入URL到看到页面发生了什么? (opens new window)

# 8.如何阻止事件冒泡和阻止默认事件

# 8.1 阻止原生事件

  • 例如a链接的跳转,form标签的提交等等。
  • 阻止默认事件使用preventDefault()函数,或者在js中return false也可以。
<a href="https://www.baidu.com">百度</a>
<form action="https://www.baidu.com">
    <input type="submit" value="提交" name="sub" id="submit">
</form>

 <script>
    let a = document.querySelector('a')
    let input = document.getElementById('submit')
    a.onclick = function (e) {
        return false
    }
    input.addEventListener('click', function (e) {
        e.preventDefault();
    })
    
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 8.2 阻止事件冒泡

阻止事件冒泡则使用stopPropagation()函数

<div id="a">
  <ul id="b">
      <li id="c">222222222</li>
  </ul>
</div>
<script>
  let c = document.querySelector('#c')
  let b = document.querySelector('#b')
  let a = document.querySelector('#a')
  
  c.addEventListener('click', function (e) {
      e.stopPropagation()
  })
  a.addEventListener('click', function () {
      console.log(`div触发`);
  })
  b.addEventListener('click', function () {
      console.log(`ul被触发`);
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 9.如何实现继承?

# 9.1继承是什么

继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
继承的优点:

  • 继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
  • 在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

# 9.2常见的继承方式

  1. 原型链继承 原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针
function Parent() {
  this.name = 'zhangsan';
  this.children = ['A', 'B'];
}

Parent.prototype.getChildren = function() {
  console.log(this.children);
}

function Child() {

}

Child.prototype = new Parent();

var child1 = new Child();
child1.children.push('child1')
console.log(child1.getChildren()); // Array ["A", "B", "child1"]

var child2 = new Child();
child2.children.push('child2')
console.log(child2.getChildren()); // Array ["A", "B", "child1", "child2"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 借用构造函数 为解决原型中包含引用类型值所带来的问题,人们开始用一种叫做借用构造函数的技术来实现继承。这种技术的基本思想非常简单,即在子类构造函数内部调用超类构造函数
function Parent(age) {
  this.names = ['lucy', 'dom'];
  this.age = age;
  this.getName = function() {
    return this.name;
  }
  this.getAge = function() {
    return this.age;
  }
}
ß
function Child(age) {
  Parent.call(this, age);
}

var child1 = new Child(18);
child1.names.push('child1');
console.log(child1.names); // [ 'lucy', 'dom', 'child1' ]

var child2 = new Child(20);
child2.names.push('child2');
console.log(child2.names); // [ 'lucy', 'dom', 'child2' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

优点: 避免了引用类型的属性被所有实例共享; -可以直接在Child中向Parent传参; 缺点: 方法都在构造函数中定义了,每次创建实例都会创建一遍方法,函数复用就无从谈起了

  1. 组合继承 组合继承就是将原型链和借用构造函数的技术结合到一起,发挥二者长处的一种继承模式,背后思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。这样,既能够保证能够通过原型定义的方法实现函数复用,又能够保证每个实例有自己的属性。
function Parent(name, age) {
  this.name = name;
  this.age = age;
  this.colors = ['red', 'green']
  console.log('parent')
}

Parent.prototype.getColors = function() {
  console.log(this.colors);
}

function Child(name, age, grade) {
  Parent.call(this, name, age);// 创建子类实例时会执行一次
  this.grade = grade;
}

Child.prototype = new Parent(); // 指定子类原型会执行一次
Child.prototype.constructor = Child;// 校正构造函数
Child.prototype.getName = function() {
  console.log(this.name)
}

var c = new Child('alice', 10, 4)
console.log(c.getName())

> "parent"
> "parent"
> "alice"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

参考:https://juejin.cn/post/6844904161071333384

# 10.如何解决浏览器中的跨域问题?请举例说明

浏览器有一个很重的安全策略,称之为安全策略
安全策略:同源策略,即协议、域名、端口号一致。不一致称之为跨源或跨域

# 常见的跨域解决方法

  • 代理,常用
  • CORS,常用
  • JSONP
  1. 代理 对于前端开发而言,大部分的跨域问题,都是通过代理解决的
    可以设置一个代理服务器,用于在服务器端进行跨域请求。前端代码将请求发送给代理服务器,然后由代理服务器将请求转发到目标服务器,并将响应返回给前端。由于同源策略仅适用于浏览器,代理服务器可以绕过这种限制
    场景:生产环境不发生跨域,但开发环境发生跨域(因此,只需要在开发环境使用代理解决跨域即可,这种代理又称之为开发代理)
// vue 的开发服务器代理配置
// vue.config.js
module.exports = {
  devServer: { // 配置开发服务器
    proxy: { // 配置代理
      "/api": { // 若请求路径以 /api 开头
        target: "http://dev.taobao.com", // 将其转发到 http://dev.taobao.com
      },
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
  1. JSONP 当需要跨域请求时,不使用AJAX,转而生成一个script元素去请求服务器,由于浏览器并不阻止script元素的请求,这样请求可以到达服务器。服务器拿到请求后,响应一段JS代码,这段代码实际上是一个函数调用,调用的是客户端预先生成好的函数,并把浏览器需要的数据作为参数传递到函数中,从而间接的把数据传递给客户端
    JSONP有着明显的缺点,即其只能支持GET请求
  2. CORS CORS是基于http1.1的一种跨域解决方案,它的全称是Cross-Origin Resource Sharing,跨域资源共享。
    如果浏览器要跨域访问服务器的资源,需要获得服务器的允许
    CORS 是一种基于 HTTP 头部的机制,用于授权浏览器允许特定的跨域请求。在服务端设置合适的响应头信息,允许特定的源访问资源。例如,在服务器端响应中添加以下头部信息:
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
// 这样就允许来自 http://example.com 域的 GET、POST 和 PUT 请求访问资源
1
2
3
4

# 11.你有哪些性能优化和方法,最好写出实际场景?

大致可以分为一下几个方向:

  • 网络优化
  • 页面渲染优化
  • JS优化
  • 图片优化
  • webpack打包优化
  • React优化
  • Vue优化 参考:https://juejin.cn/post/7194400984490049573

# 12.请列出几种你所知道的设计模式,以及对应的使用场景?

TODO:

# 13.实现回文字符串

function checkStr(str) {
  return str = str.split('').reverse().join('')
}
checkStr('123')
1
2
3
4

# 14.场景题:一个公告栏,每天都展示,当用户点击关闭后今天不再展示,明天(过了今天零点)还会显示

思路:把关闭的时间戳存储在localStorage中,再次进入页面判断是否存在

  • 如果不存在,则显示广告栏
  • 如果存在,判断时间戳和当前时间是否是同一天,如果是同一天不显示,不是同一天就显示
getTime(data, type){  //data时间戳,type返回的类型默认Y,可传参Y和H
  let time = new Date(data);    
  let Y = time.getFullYear();//获得年
  let Mon = time.getMonth() + 1; //月
  let Day = time.getDate();//日
  let H = time.getHours();
  let Min = time.getMinutes();
  let S = time.getSeconds();
  //自定义选择想要返回的类型
  if(type === "Y") {   //返回年月日2020-10-10
    return `${Y}-${Mon}-${Day}`
  } else if(type === "H") {  //返回时分秒20:10:10
    return `${H}:${Min}:${S}`
  } else {  //返回年月日时分秒2020-10-10 10:26:38
    return `${Y}-${Mon}-${Day} ${H}:${Min}:${S}`
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 15.命名方式中划线改小驼峰

// hi-lily --> hiLily
function camelCase(str){
  if(typeof str !== 'string') {
    return str
  }

  return str.replace(/-[a-zA-Z]/g,match=>match.replace('-','').toUpperCase())
}
1
2
3
4
5
6
7
8

# 16.命名方式小驼峰改中划线

// hiLily --> hi-lily
function camelCase(str){

  if(typeof str !== 'string') {
    return str
  }

  return str.replace( /[A-Z]/g, match=>'-'+match.toLowerCase())
}
1
2
3
4
5
6
7
8
9

# 17.计算数组平均值

const average = (arr) => arr.reduce((a, b) => a+b) / arr.length
average([1, 2, 3]) // 2
1
2

# 18.数组随机排序

  1. 方法一:时间复杂度 O(n^2)
  // 随机抽取法
  const getRandomRes = (arr) => {
    var stack = []
    while(arr.length) {
        var index = parseInt(Math.random() * arr.length) // 利用数组长度生成随机索引值
        stack.push(arr[index]) // 将随机索引值对应的数组元素添加到新的数组中
        arr.splice(index, 1) // 删除原数组中随机索引值对应的元素
    }
    return stack
  } 
  var arr1 = [1, 2, 3, 4, 5]
  console.log('---', getRandomRes(arr1)) 
1
2
3
4
5
6
7
8
9
10
11
12
  1. 方法二:sort对数组排序
//  利用Math.random()-0.5,这个运算结果要么大于0(不交换位置),要么小于0(交换位置),如果传入参数是0(两个数位置不变)
const getSortArray1 = (arr) => {
 return arr.sort(() => Math.random() - 0.5) 
}

var arr = [1, 2, 3, 4]
getSortArray1(arr)
1
2
3
4
5
6
7
  1. 方法三:时间复杂度 O(n)
function randomSortArray2(arr) {
   var len = arr.length;
   //首先从最大的数开始遍历,之后递减
   for(var i = len - 1; i >= 0; i--) {
       var randomIndex = Math.floor(Math.random() * (i + 1));  //随机索引值randomIndex是从0-arr.length中随机抽取的,因为Math.floor()方法是向下取整的,所以这里是i+1
       //下面三句相当于把从数组中随机抽取到的值与当前遍历的值互换位置
       var temp = arr[randomIndex];
       arr[randomIndex] = arr[i];
       arr[i] = temp;
   }
   //每一次的遍历都相当于把从数组中随机抽取(不重复)的一个元素放到数组的最后面
   return arr;
}
var arr = [1, 2, 3, 4, 5, 6];
var res = randomSortArray2(arr);
console.log(res);  // [ 1, 3, 5, 2, 4, 6 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# vue

# 1. MVC和MVVM区别

  1. MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
    Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
    View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
    Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
    MVC MVC思想:controller负责将model的数据用view显示出来。换句话说,就是在controller里面把model的数据赋值给view
  2. MVVM MVVM新增了VM类
    MVC ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
    MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
    整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性

注意:vue并没有完全遵循MVVM的思想,这一点官网自己也有说明 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

# 2.为什么data是一个函数

在vue2.x中组件中的data是一个对象或者是返回对象的函数,用于定义组件的初始数据。
在vue3.x中组件中的data必须是一个返回对象的函数。 这个是出于性能和响应式系统的考虑。在vue2.x中,组件的data对象是被复制的,这样就会出现多个组件实例共享同一个data对象的情况,当其中一个组件实例的data发生改变时,其他共享同一data对象的组件实例的data也会随之改变,这样就导致了数据不可预测的情况。而在vue3.x中,每个组件实例都会拥有一个独立的data对象,这样就避免了数据不可预测的情况。

在Vue2.x中,组件的data对象是被复制的。当一个组件实例被创建时,Vue会将该组件的data对象复制一份,然后将复制后的对象作为该组件实例的数据对象(即该组件实例的 $data 属性)。这样,不同组件的data对象就可以互相独立地操作,而不会相互影响。

# 3.computed和watch有什么区别

  • computed是根据依赖的数据自动计算并返回一个值。computed是具有缓存功能的,只有依赖的数据发生变化时才会重新计算值,可以设置getter、setter
  • watch则是手动定义需要监听的数据,当这些数据发生变化时,执行特定的操作。而watch没有缓存功能,每次数据变化都会执行特定的操作
    使用场景:
  1. computed一般适用于模板渲染时,莫个值是依赖于其他响应式对象甚至是计算型计算而来。
  2. 监听器适用于监听某个值变化去完成一段复杂的数据逻辑

# 4.vue中的路由有哪些方式

有两种方式:hash模式和history模式

  • hash模式基于URL的hash值来实现路由,URL中的#符号用于分隔路由路径和查询参数,不会触发浏览器的刷新,适用于单页应用。
  • history模式使用HTML5的history API实现路由,可以通过pushState和replaceState方法修改URL,可以支持前进和后退,需要服务器配置支持

# 5.vue2.0响应式数据的原理

整体思路是数据劫持 + 观察者模式
对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都有自己的dep属性,存放他所依赖的watcher(依赖收集),当属性变化时会通知自己对应的watcher去更新(派发更新)

class Observer {
  // 观测值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");

      //需要做依赖收集过程 这里代码没写出来
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("设置值");
      //需要做派发更新过程 这里代码没写出来
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果传过来的是对象或者数组 进行属性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    return new Observer(value);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 6.vue2如何监听数组变化

不是因为Object.defineProperty检测不到数组变化,因为对于数组的每次变化都可能影响它索引key的变动,从而重新遍历,添加劫持,数据量大时非常影响性能。
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)
所以在vue2中修改数组的索引、长度是无法检测到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新

// src/obserber/array.js
// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "reverse",
  "sort",
];
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    //   这里保留原型方法的执行结果
    const result = arrayProto[method].apply(this, args);
    // 这句话是关键
    // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)  this就是a  ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
    const ob = this.__ob__;

    // 这里的标志就是代表数组有新增操作
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
    if (inserted) ob.observeArray(inserted);
    // 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
    return result;
  };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 7.vue2.0和3.0响应式原理的区别

vue3.x使用proxy代替Object.defineProperty。因为可以直接检测对象、数组的变化,并且有多达13中拦截方法

import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
  // 根据不同参数创建不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
  if (!isObject(target)) {
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
  return function get(target, key, receiver) {
    // 对获取的值进行放射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回当前对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {
      console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {
      console.log("属性值被修改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此方法
  set, // 当修改属性时调用此方法
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 8.vue父子组件生命周期钩子函数执行顺序

【setup是在props解析之后,beforeCreate执行之前进行调用】

  • 加载渲染过程【子组件先挂载,然后父组件】
    (父created -> 子created -> 子mounted -> 父mounted)
    父组件beforeCreate-父组件created-父组件beforeMounted-子组件beforeCreate-子组件created-子组件beforeMounted-子组件mounted-父组件mounted
  • 子组件更新过程【父更新导致子更新,子更新完成后父】
    父组件beforeUpdate-子组件beforeUpdate-子组件updated-父组件updated
  • 父组件更新过程
    父组件beforeUpdate-父组件updated
  • 销毁过程 【先父后子,完成顺序:先子后父】
    父组件beforeDestroy-子组件beforeDestroy-子组件destroyed-父组件destroyed
    原因:
  • Vue创建过程是一个递归过程,先创建父组件,
  • 有子组件就会创建子组件,因此创建时先有父组件再有子组件;
  • 子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,
  • 可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

# 9.虚拟DOM是什么?有什么优缺点?

vue2的VirtualDOM借鉴了开源库snabbdom的实现。虚拟DOM本质上就是一个原生的js对象用来去描述一个真实的DOM节点(描述了真实DOM树的属性和结构),是对真实DOM的一层抽象。可以提高vue应用的性能和可维护性。
当数据发生变化时,vue会重新渲染虚拟DOM,与之前的虚拟DOM进行比较,并找出变化的部分,只将这部分更新到真实DOM上,从而避免了无效的dom操作。
虚拟DOM的优点:

  • 提供性能:通过虚拟DOM,避免了大量的DOM操作,减少了页面渲染的开销,从而提高了应用的性能
  • 提供可维护性:由于虚拟DOM是轻量级的js对象,可以方便的进行组建的复用和维护,从而提高了应用的可维护性
  • 无需手动操作DOM:不需要在手动操作DOM,只要写好View-Model的代码逻辑,框架会根据虚拟DOM和数据双向绑定,帮我们以可预期的方式更新视图,帮我们极大地提高了开发效率
  • 实现跨平台:由于虚拟DOM是独立于平台的,可以在不同的平台上使用,例如web端、移动端
    虚拟DOM的缺点:
  • 首次渲染开销较大:首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
  • 内存占用较大:由于需要维护虚拟DOM,因此会占用一定的内存空间,如果虚拟DOM层次较深,会导致内存占用较大。

# 10.使用过 Vue SSR 吗?说说 SSR

  • 在服务器端使用Vue SSR插件,将Vue组件渲染成HTML字符串。
  • 在服务器端使用Node.js等后端框架,将HTML字符串返回给客户端。
  • 在客户端使用Vue客户端插件,将服务器端渲染的HTML字符串与客户端生成的JavaScript代码进行激活,生成完整的Vue组件。
    主要解决两个问题:
  1. 更好的SEO
  • SPA页面的内容是ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再进行爬取页面内容,所以在SPA页面是抓取不到页面通过Ajax获取到的内容,爬虫获取的html,是一个没有数据的空壳子。
  • 而SSR,是服务端直接将数据处理好,再拼接组装好,返回一个已经渲染好的页面(数据已经包含在页面中),所以爬虫可以爬取渲染好的页面。
  1. 首屏渲染更快
  • SPA页面,需要等所有的Vue编译后的js文件全部下载完成之后,才开始进行页面的渲染,文件下载需要一定的时间,所以首屏渲染需要一定的时间。
  • SSR直接有服务端渲染好页面直接返回显示,不需要等待下载js文件后再去渲染,所以SSR能解决SPA页面首屏渲染时间太长的问题。
    优点:
  • 提高SEO:由于SSR可以在服务器端生成HTML,搜索引擎可以直接爬取到完整的HTML,提高了SEO优化效果。
  • 提高性能:由于SSR可以在服务器端生成HTML,可以减少客户端渲染的时间,提高了页面的加载速度。
  • 提高可维护性:由于SSR可以将Vue组件在服务器端进行渲染,可以使得前后端工程师共同维护组件,提高了项目的可维护性。
    缺点:
  • 开发条件会受到限制,服务端只支持beforeCreate、created两个钩子函数,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。
    总结:
  • 并不是所有网站都需要SEO,比如企业内部管理网站等,不惜要SEO,所以不需要使用SSR,直接使用SPA更好。
  • 有些项目首屏,可以借用路由懒加载、预加载来解决

# 11.SPA(单页应用)

SPA是指在Web应用程序中使用单个HTML页面作为应用程序的容器,动态地更新该页面的部分内容,不需要重新加载整个页面,以提供更流畅、更快速的用户体验。SPA通常使用Ajax或WebSocket技术来异步加载数据,不需要刷新整个页面。Vue.js可以很好地支持SPA模式,它使用Vue Router管理不同页面之间的切换,并使用Vue.js的组件化开发模式来构建整个应用程序。

# 12.nextTick使用场景和原理

nextTick是用于下一次DOM更新循环结束之后执行延迟回调的函数。
主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
使用场景:

  1. 获取元素的尺寸 在组件的 mounted 钩子函数中获取某个元素的宽度或高度通常是不准确的,因为此时元素可能还没有被渲染。此时可以使用 nextTick等待组件更新后再获取元素的尺寸。
  2. 在更新后执行某些动画效果 在更新数据后,需要等待Vue实例更新后才能执行某些动画效果。此时可以使用nextTick来延迟执行动画效果,确保动画效果能够正确地应用到更新后的DOM上。

# 13.通信方式

  1. props
  • 使用场景:父向子传值(自定义属性)
// section 父组件
<template>
  <div class="section">
    <com-article :articles="articleList"></com-article>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {  
  name: 'HelloWorld',  
  components: { comArticle },  
  data() {    
    return {      
      articleList: ['红楼梦', '西游记', '三国演义']    
    }  
  }
}
</script>

// 子组件 article.vue
<template>
  <div>
    <span v-for="(item, index) in articles" :key="index">{{item}}</span>
  </div>
</template>

<script>
export default {  
  props: ['articles']
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  1. emits
  • 使用场景:子向父传值(自定义事件)
<!-- // Child.vue 派发 -->
<template>
    // 写法一
    <button @click="emit('myClick')">按钮</buttom>
    // 写法二
    <button @click="handleClick">按钮</buttom>
</template>
<script setup>
    
    // 方法一 适用于Vue3.2版本 不需要引入
    // import { defineEmits } from "vue"
    // 对应写法一
    const emit = defineEmits(["myClick","myClick2"])
    // 对应写法二
    const handleClick = ()=>{
        emit("myClick", "这是发送给父组件的信息")
    }
    
    // 方法二 不适用于 Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const { emit } = useContext()
    const handleClick = ()=>{
        emit("myClick", "这是发送给父组件的信息")
    }
</script>

// Parent.vue 响应
<template>
    <child @myClick="onMyClick"></child>
</template>
<script setup>
    import child from "./child.vue"
    const onMyClick = (msg) => {
        console.log(msg) // 这是父组件收到的信息
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  1. $ref、defineExpose
    在vue3中,ref函数可以用于定义一个响应式的变量或引用之外,还可以获取DOM组件实例
    而 defineExpose 是用于将组件内部的属性和方法暴露给父组件或其他组件使用。通过这种方式,我们可以定义哪些部分可以从组件的外部访问和调用。
// 子组件 Child.vue
<script setup>
  // 引入 ref 函数,用于定义响应式数据
  import { ref } from 'vue';

  // 定义变量和方法
  const msg = ref('我是子组件中的数据');
  const childMethod = () => {
    console.log('我是子组件中的方法');
  }

  // defineExpose 对外暴露组件内部的属性和方法,不需要引入,直接使用
  // 将属性 msg 和方法 childMethod 暴露给父组件
  defineExpose({
    msg,
    childMethod
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 父组件
<script setup>
  // 引入响应式ref
  import { ref } from 'vue';
  // 引入子组件 Child.vue
  import Child from './Child.vue';

  // 获取子组件DOM实例
  const childRef = ref();

  // 该方法用于获取子组件对外暴露的属性和方法
  const getChildPropertyAndMethod = () => {
    // 获取子组件对外暴露的属性
    console.log(childRef.value.msg);
    // 调用子组件对外暴露的方法
    childRef.value.childMethod();
  }
</script>

<template>
  <div id="parent">
    <Child ref="childRef"/>
    <button @click="getChildPropertyAndMethod">获取子组件对外暴露的属性和方法</button>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  1. 插槽slot
// 子组件:Child.vue
<template>
  <div>
    <slot name='header' :title="'标题1'">
  </div>
</template>

// 父组件
<template>
  <Child>
    <template #header={ title }>
      <p>{{ title }}</p>
    </template>
  </Child>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. provide、inject(依赖注入)
// 父传孙
// 父组件
const colorVal = ref('#fff')
provide('color', colorVal)
// 孙组件
const color = inject('color')


// 孙传父:父组件传递函数从而更新value【这里和react子组件更新父组件的值做法类似,父组件传递函数更新值,子组件调用props传进来的函数】
// 父组件
// 子孙组件向父组件传值,父组件中使用该方法接收
let newValue = ''
const onChange = (value) => {
  console.log("子孙组件向父组件传的值是", value)
  newValue = value
};
provide("onChangeValue", onChange);

// 孙组件
const changeValue = inject('onChangeValue')
const getValue = (value) => {
	changeValue(value)  // 传值
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. EventBus
    mitt事件总线库(第三方库)
    自定义中央事件
  2. vuex

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

# 14.生命周期

  1. 定义:vue实例创建到销毁的过程
  2. 8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后。表格包含一些特殊场景的生命周期
生命周期 描述
beforeCreate 组件实例被创建之初
created 组件实例已经完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上之后
beforeUpdate 组件数据发生变化,更新之前
updated 组件数据更新之后
beforeDestroy 组件实例销毁之前
destroyed 组件实例销毁之后
activated keep-alive 缓存的组件激活时
deactivated keep-alive 缓存的组件停用时调用
errorCaptured 捕获一个来自子孙组件的错误时被调用
  1. 使用场景分析:
生命周期 描述
beforeCreate 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created 组件初始化完毕,各种数据可以使用,常用于异步数据获取(获取接口数据)
beforeMount 未执行渲染、更新,dom未创建
mounted 初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate 更新前,可用于获取更新前各种状态
updated 更新后,所有状态已是最新
beforeDestroy 销毁前,可用于一些定时器或订阅的取消
destroyed 组件已销毁,作用同上
  1. 数据请求在created和mounted的区别 created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成;mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,两者的相同点:都能拿到实例对象的属性和方法。 讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议数据请求放在created生命周期当中

# vue-router

# 1.vue-router中常用的路由模式实现原理

  1. hash模式
    URL的路径会添加一个#符号,例如:http://example.com/#/user/profile。vue-router利用浏览器的hashchange事件来监控URL的变化,从而更新路由状态
  2. history模式
    vue-router利用HTML5 History API中的pushState和replaceState方法来操作浏览器历史记录,并通过popstate事件来处理路由状态的更新。
    总的来说,vue-router利用浏览器、HTML5 History提供的API来实现路由状态的监听和管理,在用户操作浏览器时,动态更新页面内容,从而实现单页应用程序的效果

# 2.vue-router和window.location跳转的区别(对浏览器来讲)

  1. vue-router使用pushState进行路由更新,静态跳转,页面不会更新。location.href会触发浏览器页面会重新加载一次
  2. vue-router使用diff算法,实现按需加载,减少DOM操作
  3. vue-router是路由跳转或同一个页面跳转,location.href是不同页面间跳转
  4. vue-router是异步加载this.$nextTick(()=>{获取url});location.href是同步加载

# 3.路由懒加载

# 定义:

在传统的vue系统中,所有的页面组件都会在应用初始化的时候一次性加载,这会导致应用的初次加载时间过长,影响用户体验。而路由懒加载是一种优化技术,可以将页面组件按需加载,只有当用户访问到某个路由时才加载对应的页面组件,从而提升应用的初始加载速。

# 路由懒加载的原理

使用webpack打包器:利用Webpack的代码分割功能(Code Splitting),将页面组件打包成单独的文件,当需要加载某个路由时,在动态的加载对应的文件,仅在当前路由被访问时才加载组件,避免了在初次渲染时加载所有的组件,从而提高应用程序的效率。

注意: 不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

异步组件: 在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了defineAsyncComponent方法来实现此功能:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
1
2
3
4
5
6
7
8
9

如你所见,defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)
1
2
3
4
5

最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

与普通组件一样,异步组件可以使用 app.component() 全局注册:

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))
1
2
3

# 如何实现路由懒加载

  1. 使用import函数
    使用import函数可以实现路由懒加载的原因是因为import函数是模块加载语法,动态import是ECMAScript 2020标准中引入的特性,它支持动态加载模块。在Vue中,我们可以将页面组件作为模块来处理,通过import函数动态地加载对应的页面组件。

当使用**import()**加载一个模块时,Webpack会将该模块单独打包成一个文件,并在需要加载该模块时才进行请求和加载。这样就实现了按需加载页面组件的效果,从而提升了应用的初始加载速度。

当用户访问到对应的路由时,Vue会动态地执行这个函数,从而触发Webpack加载对应的组件文件。这样就实现了页面组件的按需加载。

// Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [
    { path: '/users/:id', component: UserDetails }
    // 或在路由定义里直接使用它
    { path: '/users/:id', component: () => import('./views/UserDetails.vue') },
  ],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 使用Babel插件
    @babel/plugin-syntax-dynamic-import是一个Babel插件,用于解析并转换动态导入语法。这个插件允许我们在Babel编译过程中使用动态导入语法,即使当前的环境不支持该语法。

动态import是ECMAScript 2020标准中引入的特性,可以在运行时动态地加载模块。这种语法允许我们按需加载模块,从而提高应用的性能和用户体验。使用动态导入语法可以将模块的导入延迟到需要的时候再进行,而不是在应用初始化时就加载所有模块。

(1)安装

npm install --save-dev @babel/plugin-syntax-dynamic-import

yarn add --dev @babel/plugin-syntax-dynamic-import

pnpm add --save-dev @babel/plugin-syntax-dynamic-import
1
2
3
4
5

(2)配置 在Babel配置中添加插件

// babel.config.json
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
1
2
3
4

(3)使用

// 在路由配置中使用动态import
const routes = [
  {
    path: '/about',
    name: 'About',
    component: () => import('./views/About.vue') // 直接使用动态 import
  },
  // 其他路由...
];
1
2
3
4
5
6
7
8
9

# 把组件按组分块

(1)使用webpack
有事我们想把某个路由下的所有组件都打包在同个异步块(chunk)中。只需要使用命名chunk,一个特殊的注释语法来提供chunk name(需要Webpack > 2.4)

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
1
2
3
4
5
6

Webpack会将任何一个异步模块与相同的块名称组合到相同的异步块中
(2)使用vite
在vite中,你可以在rollupOptions下定义分块:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
      },
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 优势和注意事项

(1)优势

  • 减少初始加载时间,提升用户体验
  • 按需加载页面组件,减少资源浪费
  • 优化代码分割,提高应用性能
    (2)注意事项
  • 尽量将页面组件拆分成较小的模块,以提高加载速度
  • 避免过度使用路由懒加载,合理分化模块边界
  • 注意处理懒加载过程中的错误和加载状态

# $ref有哪些弊端(vue2角度)

  1. this.$refs如果要在mounted中使用,必须要在this.$nextTick(() => {})这里面实现,要是找不到ref,原因是mounted之后,BOM节点还没有完全挂在上,于是找不到定义的ref。
  2. 可以直接在update的生命周期函数中使用,不用写this.$nextTick(() => {})
  3. 在methods方法中,也需要使用this.$nextTick(() => {})等在页面完全渲染完毕之后在调用即可

# 首屏加载优化

SSR 路由懒加载 图片懒加载 gzip

# 动态组件

最近更新时间: 3/11/2024, 2:03:51 AM
강남역 4번 출구
Plastic / Fallin` Dild