前端面试考点
# 面试考点
# 前言
# 重要考点
- HTML和CSS
- 性能优化
- 原型,作用域,异步
- 各种手写代码
- DOM事件和Ajax
- HTTP协议
# 知识点
- CSS
- 布局
- 定位
- 移动端响应式
- ES语法
- 原型 原型链
- 作用域 闭包
- 异步 单线程
- WebAPI
- DOM BOM
- Ajax 跨域
- 事件 存储
- 开发环境
- 版本管理
- 调试抓包
- 打包构建
- 运行环境
- 页面渲染
- 性能优化
- Web安全
- 网络通讯
- headers
- Resultful API
- 缓存策略
# 简历
不要过于在意JD
- JD是hr 发布的
- hr和技术人员可能会沟通不及时
- 不能完全相信JD的要求
简历内容
- 个人信息
- 工作经历
- 教育经历
- 可以只写最高学历
- 项目经历
- 2-4个具有说服力的项目(视工作时间)
- 项目描述,技术栈,个人角色
- 技巧:可以把别人的项目写上,只要你能hold住
- 专业技能
- 表现出自己的核心竞争力
- 内容不要太多,3-5条即可
- 太基础的不要写,例如会用vscode
- 博客和开源
# HTML面试题
# 如何理解HTML语义化?
- 增加代码可读性
- SEO(搜索引擎优化)
定义
html语义化是指用合理的html标记以及其特有的属性去格式化文档内容
为什么需要语义化?
- 易读,易书写,易理解 (增加代码可读性,规范性)
- 利于SEO搜索:语义化可以和搜索引擎建立良好的联系,有利于爬虫抓取有效信息;因为爬虫依赖于语义化标签来确定上下文和各个关键字的权重。
- 易于跨设备解析,进而完成渲染网页(如屏幕阅读器、盲人阅读器、移动设备)
- 利于规范化:方便团队开发和维护,也遵循W3C规范。
语义化的基本标签
# 默认情况下,哪些HTML标签是块级元素、哪些是内联元素?
- 块级元素
display: block/table
;有div h1 h2 table ul ol p
等
- 内联元素
display: inline/inline-block
;有span img input button
等
内联元素允许其他内联元素与其位于同一行,块状元素独占一行。
宽度和高度只对块状元素起作用,内联元素不起作用
如果我们把内联元素转化成块状元素,在相应的内联元素加上一个属性display:block就可以了,反之块级元素转换为内联元素在相应的块级元素上添加 属性display:inline;就可以啦。
text-indent:2em;”属性,只能加在块状元素上面,内联元素是不起作用的。
块级元素可以包含内联元素和某些块级元素,但内联元素不能包含块级元素
块级元素不能放在p标签里面
li 标签可以包含div
块级元素可以设置margin和padding;行内元素起边距作用的只有margin-left、margin-right、padding-left、padding-right,其它属性不会起边距效果。
# CSS面试题
# 布局
# 盒子模型的宽度如何计算?
offsetWidth =(内容宽度+内边距+边框),无外边距
#div1 {
width: 100px;
padding: 10px;
border: 1px solid #ccc;
margin: 10px;
}
2
3
4
5
6
要想offsetWidth是100px,需要加上box-sizing: border-box
属性
# margin 纵向重叠的问题
- 相邻元素的margin-top和margin-bottom会发生重叠
- 空白内容的
<p></p>
也会重叠
<style type="text/css">
p {
font-size: 16px;
line-height: 1;
margin-top: 10px;
margin-bottom: 15px;
}
</style>
</head>
<body>
<p>AAA</p>
<p></p>
<p></p>
<p></p>
<p>BBB</p>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AAA和BBB之间只有15px
# margin负值的问题
- margin-top和margin-left负值,元素向上、向左移动
- margin-right负值,右侧元素左移,自身不受影响
- margin-bottom负值,下方元素上移,自身不受影响
# BFC理解和应用
- Block format context ,块级格式化上下文
- 一块独立渲染区域,内部元素的渲染不会影响边界以外的元素
形成BFC的常见条件
- float 不是none
- display是flex inline-block等
- position是absolute或fixed
- overflow不是visible
BFC的常见应用
- 清除浮动
# float 布局的问题,以及clearfix
如何实现圣杯布局和双飞翼布局
是两边宽度固定,中间自适应的三栏布局。其中,中间栏放到文档流前面,保证先行渲染。
区别:
圣杯布局是中间位置,添加左右
padding值
(padding左右值即为左右栏宽度)双飞翼布局是中间主盒子里面的内容盒子设置左右
margin
圣杯布局
1、父级盒子设置高度和宽度(100%),为了留出中间位置,添加左右padding值(padding左右值即为左右栏宽度),为了取消补白区域影响,添加box-sizing;border-box
2、父级里三栏的位置,中间栏在前,然后左右
3、中间栏宽度100%
4、左侧宽度和padding-left一致,右侧和padding-right一致
5、使左侧、右侧内容在一行内显示,为他们添加定位(父相子绝)
父元素添加
padding
预留左右的位置,中间元素宽度100%;左盒子
margin-left: -100%
,再用相对定位,相对自身移动自身的宽度右盒子
margin-right: 负自身宽度
<!DOCTYPE html>
<html>
<head>
<title>圣杯布局</title>
<style type="text/css">
body {
min-width: 550px;
}
#header {
text-align: center;
background-color: #f1f1f1;
}
/* 父元素使用padding留出左右元素对象的padding值 */
#container {
/*第一步*/
padding-left: 200px; /*左边部分的宽度*/
padding-right: 150px; /*右边部分的宽度*/
}
#container .column {
float: left;
height: 200px;
}
#center {
background-color: #ccc;
width: 100%;
}
#left {
/*第二步*/
position: relative;
background-color: yellow;
width: 200px;
margin-left: -100%; /*加了这个之后,左侧和中间的元素齐平*/
right: 200px;
}
#right {
background-color: red;
width: 150px;
/*第三步*/
margin-right: -150px;
}
#footer {
text-align: center;
background-color: #f1f1f1;
}
.clearfix:after {
clear: both;
content: '';
display: table;
}
</style>
</head>
<body>
<div id="header">this is header</div>
<div id="container" class="clearfix">
<div id="center" class="column">this is center</div>
<div id="left" class="column">this is left</div>
<div id="right" class="column">this is right</div>
</div>
<div id="footer">this is footer</div>
</body>
</html>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
双飞翼布局
1、父级盒子设置高度和宽度
2、父级里三栏的位置,中间栏在前,然后左右
3、中间盒子宽度width:100% 且独占一行
4、三个盒子设置float:left;
5、使用margin-left属性将左右两边的盒子拉回与中间盒子同一行left{margin-left:-100%};向左走一个父级盒子的宽度{margin-left:负的自身宽度}
6、中间主盒子里面的内容盒子设置左右margin,避免被遮挡内容
中间主盒子里面的内容盒子设置左右
margin
,margin值为左右盒子的值左盒子像左移动父元素的100%
右盒子像左移动自身宽度
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>双飞翼布局</title>
<style type="text/css">
body {
min-width: 550px;
}
.col {
float: left;
}
#main {
width: 100%;
height: 200px;
background-color: #ccc;
}
#main-wrap {
margin: 0 190px 0 190px;
}
#left {
width: 190px;
height: 200px;
background-color: #0000FF;
margin-left: -100%;
}
#right {
width: 190px;
height: 200px;
background-color: #FF0000;
margin-left: -190px;
}
</style>
</head>
<body>
<div id="main" class="col">
<div id="main-wrap">
this is main
</div>
</div>
<div id="left" class="col">
this is left
</div>
<div id="right" class="col">
this is right
</div>
</body>
</html>
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
48
49
50
51
52
# flex画色子
- lex-direction 主轴方向 row|row-reverse|column|column-reverse
- justify-content 主轴对齐方式 flex-start|flex-end|center|space-between|space-around
- align-items 交叉轴对齐方式 flex-start|flex-end|center|baseline|stretch
- flex-warp 换行 nowrap|wrap|wrap-reverse
- align-self 子元素在交叉轴对齐方式 auto|flex-start|flex-end|center|baseline|stretch
# 定位
# absolute和relative分别依据什么定位?
- relative依据自身定位
- absolute依据最近一层的定位元素定位(最后一层是body)
# 居中对齐有哪些实现方式?
水平居中
- inline元素:
text-align: center
- block元素:
margin: auto
- absolute元素:
left: 50% + margin-left负值
- absolute元素:
left: 50% + transform: translateX(-50%)
垂直居中
- inline元素:
line-height的值等于height值
- absolute元素:
top: 50%+ margin-top 负值
- absolute元素:
transform(-50%, -50%)
- absolute元素:
top, left, bottom, right = 0 + margin: auto
# 图文样式
# line-height的继承问题
写具体数值,如30px ,则继承该值(比较好理解)
写比例,如2/1.5 ,则继承该比例(比较好理解)
body { font-size: 20px; line-height: 1.5; } p { background-color: #ccc; font-size: 16px; }
1
2
3
4
5
6
7
8p标签的行高是
16px乘以1.5 = 24px
写百分比,如200%,则继承计算出来的值(考点)
body { font-size: 20px; line-height: 200%; } p { background-color: #ccc; font-size: 16px; }
1
2
3
4
5
6
7
8先计算在继承。20px*200% = 40px, 行高40px
# 响应式
# rem 是什么?
rem是一个长度单位
px
,绝对长度单位,最常用em
,相对长度单位,相对于父元素,不常用rem
,相对长度单位,相对于根元素,常用于响应式布局
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>rem 演示</title>
<style type="text/css">
html {
font-size: 100px;
}
div {
background-color: #ccc;
margin-top: 10px;
font-size: 0.16rem;
}
</style>
</head>
<body>
<p style="font-size: 0.1rem">rem 1</p>
<p style="font-size: 0.2rem">rem 1</p>
<p style="font-size: 0.3rem">rem 1</p>
<div style="width: 1rem;">
this is div1
</div>
<div style="width: 2rem;">
this is div2
</div>
<div style="width: 3rem;">
this is div3
</div>
</body>
</html>
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
# 如何实现响应式?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>响应式布局</title>
<style type="text/css">
@media only screen and (max-width: 374px) {
/* iphone5 或者更小的尺寸,以 iphone5 的宽度(320px)比例设置 font-size */
html {
font-size: 86px;
}
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
/* iphone6/7/8 和 iphone x */
html {
font-size: 100px;
}
}
@media only screen and (min-width: 414px) {
/* iphone6p 或者更大的尺寸,以 iphone6p 的宽度(414px)比例设置 font-size */
html {
font-size: 110px;
}
}
body {
font-size: 0.16rem;
}
#div1 {
width: 1rem;
background-color: #ccc;
}
</style>
</head>
<body>
<div id="div1">
this is div
</div>
</body>
</html>
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
# vw/vh
vh 网页视口高度的1/100
vw网页视口宽度的1/100
vmax取两者最大值;vmin 取两者最小值
相当于把网页的宽度和高度分为100份
网页视口尺寸
window.screen.height
//屏幕高度window.innerHeight
//网页视口高度document.body.clientHeight
// body 高度
#container {
background-color: red;
width: 10vw;
height: 10vh;
}
#container2 {
width: 10vmax;
height: 10vmax;
background-color: antiquewhite;
}
2
3
4
5
6
7
8
9
10
# CSS3
# CSS3动画
# JS面试题
# 变量类型和计算
# 值类型和引用类型
- typeof 能判断哪些类型
- 何时使用
===
何时使用==
- 值类型和引用类型的区别
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
# typeof 运算符
识别所有值类型
null, undefined, number, string, boolean, symbol
识别函数
function
判断是否是引用类型(不可再细分)
Object
typeof null // object
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
2
3
4
5
6
7
8
其中数组、对象、null都会被判断为object,其他判断都正确。
# 手写深拷贝
function deepClone(obj = {}) {
if(typeof obj !== 'object' || obj == null) {
// 如果传入的不是对象或者为null,则直接返回该值
return obj;
}
// 初始化返回结果
let result;
if(obj instanceof Array) {
result = []; // 初始化一个空数组
} else {
result = {}
}
for (let key in obj) {
// 保证key不是原型的属性
if(obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 变量计算-类型转换
- 字符串拼接
const a = 100 + 10 //110
const b = 100 +'10' //'10010'
const c = true + '10' // 'true10
2
3
==
运算符100 = '100' // true 0 == '' // true 0 == false // true false == '' // true null == undefined // true
1
2
3
4
5obj == null
相当于obj === null || obj === undefined
if 语句和逻辑运算
- truly变量:
!!a === true
的变量 - falsely变量:
!!a === false
的变量
以下是falsely变量。除此之外都是truly变量
!!0=== false !!NaN === false !!' '=== false !!null === false !!undefined === false !!false === false
1
2
3
4
5
6- truly变量:
逻辑判断
console. log (10 && 0) // 0 console.log ('' || 'abc' ) // 'abc ' console.log (!window.abc) // true
1
2
3||
和&&
首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。- 对于
||
来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。 &&
则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
- 对于
# class和继承
- constructor
- 属性
- 方法
继承
- extends
- super
// 父类
class People {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log(`${this.name} eat something`)
}
}
class Student extends People {
constructor(name, age, number) {
super(name, age);
this.number = number;
}
study() {
console.log(`学生: ${this.name}, 学号: ${this.number}`)
}
}
class Teacher extends People {
constructor(name, age, title) {
super(name, age);
this.title = title;
}
sayHello() {
console.log(`老师: ${this.name}, 职称: ${this.title}`)
}
}
const student = new Student('Tom', 18, 123456);
console.log(student.name)
student.eat();
student.study();
const teacher = new Teacher('Alice', 30, '教授');
console.log(teacher.name)
teacher.eat();
teacher.sayHello();
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
# 类型判断instanceof
xialuo instanceof Student // true
xialuo instanceof People // true
xialuo instanceof Object // true
[] instanceof Array // true
[] instanceof Object // true
{} instanceof Object // true
2
3
4
5
6
# 原型和原型链
// class 实际上是函数,可见是语法糖
typeof People // 'function'
typeof Student // 'function '
// 隐式原型和显示原型
console.log( xialuo.__proto__ ) // 隐式原型
console.log( Student.prototype ) // 显示原型
console.log( xialuo.__proto__ == Student.prototype )
2
3
4
5
6
7
class Student extends People {
constructor(name, age, number) {
super(name, age);
this.number = number;
}
study() {
console.log(`学生: ${this.name}, 学号: ${this.number}`)
}
}
2
3
4
5
6
7
8
9
打印student.__proto__
和Student.prototype
- 每个class都有显示原型
prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向对应class的prototype
基于原型的执行规则
- 获取属性xialuo.name或执行方法xialuo.sayhi()时
- 先在自身属性和方法寻找
- 如果找不到则自动去
__proto__
中查找
原型链
# 手写jquery
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector);
const length = result.length;
for (let i = 0; i < length; i++) {
this[i] = result[i];
}
this.length = length;
this.selector = selector;
}
get(index) {
return this[index];
}
each(fn) {
for(let i = 0; i < this.length; i++) {
const elem = this[i];
fn(elem);
}
}
on(type, fn) {
return this.each(function(elem) {
elem.addEventListener(type, fn, false);
})
}
// 其他扩展API
}
// 插件
jQuery.prototype.dialog = function(info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
const $p = new myJQuery('p');
$p.get(0).style.background = 'red';
$p.on('click', function(e) {
$p.dialog(e.target.innerHTML);
})
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
48
49
# 作用域和闭包
# 作用域和自由变量
作用域代表变量和合法使用范围
- 全局作用域
- 函数作用域
- 块级作用域(ES6新增)
自由变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域都没找到,则报错xx is not defined
# 闭包
应用:隐藏数据,只提供API
作用域应用的特殊情况,有两种表现︰
- 函数作为参数被传递
- 函数作为返回值被返回
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找不是在执行的地方!!!
// 1. 函数作为参数被传递
function print(fn) {
const a = 200;
fn();
}
let a = 100;
function fn() {
console.log(a)
}
fn() // 100 作用域查找范围是在函数定义的上级作用域中查找
// 2. 函数作为返回值被返回
function create() {
const b = 100
return function() {
console.log(b) // 100 作用域查找范围是在函数定义的上级作用域中查找
}
}
let b = 200;
let fn1 = create();
fn1(); // 100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# this
- 作为普通函数
- 使用call apply bind
- 作为对象方法被调用
- 在class方法中调用
- 箭头函数
# 手写bind函数
Function.prototype.myBind = function() {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments);
// 获取this,即要绑定的函数实例
const _this = args.shift();
// 获取到原函数的this
const self = this;
return function() {
return self.apply(_this, args);
}
}
let obj = {
a : 1
}
function fn() {
console.log(this);
}
fn() // Window
let fn1 = fn.myBind(obj);
fn1(); // obj
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 异步和单线程
setTimeout面试题
- JS是单线程语言,只能同时做一件事儿
- 浏览器和nodejs 已支持JS启动进程,如Web Worker
- JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
异步应用场景
- 网络请求,如ajax图片加载
- 定时任务,如setTimeout
# 手写Promise加载图片
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = function() {
resolve(img)
document.body.appendChild(img)
}
img.onerror = function() {
reject(new Error('图片加载失败'))
}
img.src = url
})
}
const url = 'https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
const url2 = 'https://gimg3.baidu.com/search/src=http%3A%2F%2Fpics3.baidu.com%2Ffeed%2Faec379310a55b31958049d09ebf9f42bcefc17ae.jpeg%40f_auto%3Ftoken%3Daf593bd676b37b5d30e3702ff951461d&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=f360,240&n=0&g=0n&q=75&fmt=auto?sec=1706461200&t=7d6718c24be56d9e70b2d215a07b41a3'
loadImg(url).then(img => {
return img // 返回一个普通对象
}).then(img1 => {
console.log(img1)
return loadImg(url2) // 返回一个Promise对象
}).then(img2 => {
console.log(img2)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# event loop
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
JS如何执行?
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
执行过程:
- 同步代码,一行一行放在Call Stack执行
- 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
- 时机到了,就移动到Callback Queue
- 如Call Stack 为空(即同步代码执行完)Event Loop开始工作
- 轮询查找Callback Queue ,如有则移动到Call Stack 执行
- 然后继续轮询查找(永动机一样)
DOM事件和EventLoop
- JS是单线程的
- 异步( setTimeout , ajax 等)使用回调,基于event loop
- DOM事件也使用回调,基于event loop
# promise进阶
# Promise三种状态
- pending
- resolved
- rejected
只能从pending -> resolved 或 pending -> rejected
并且状态是不可拟的
- pending 状态,不会触发then和catch
- resolved 状态,会触发后续的then回调函数
- rejected 状态,会触发后续的catch回调函数
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
console.log(p) // 输出: Promise { <pending> }
// 1秒钟后状态变为 fulfilled
console.log(p) // 输出: Promise { <fulfilled> }
const p2 = new Promise(() => {})
console.log(p2) // 输出: Promise { <pending> }
const p3 = Promise.reject();
console.log(p3) // 输出: Promise { <rejected> }
// pending状态的Promise不会触发.then和.catch
const p4 = new Promise(() => {})
.then(() => {
console.log('pending状态的Promise不会触发.then和.catch')
})
.catch(() => {
console.log('pending状态的Promise不会触发.then和.catch')
})
const p5 = new Promise((resolve, reject) => {
resolve()
})
.then(() => {
console.log('resolveed状态只会出发.then')
})
.catch(() => {
console.log('resolveed状态只会出发.then')
})
const p6 = new Promise((resolve, reject) => {
reject()
})
.then(() => {
console.log('rejected状态只会出发.catch')
})
.catch(() => {
console.log('rejected状态只会出发.catch')
})
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
// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态Promise.reject('some error')
# then和catch改变状态
- then正常返回resolved ,里面有报错则返回rejected
- catch 正常返回resolved ,里面有报错则返回rejected
const p = Promise.resolve().then(() => {
console.log(1); // then正常执行,返回resolved状态
}).then(() => {
console.log('我还会再次触发')
throw new Error('error') // 异常执行,返回rejected状态
}).then(() => {
console.log('我不会触发,因为上一步返回的是rejected状态')
}).catch(() =>{
console.log('catch捕获到异常')
})
const p1 = Promise.reject().catch(() => {
console.log('p1: catch,正常执行') // 返回resolved状态
}).then(() => {
console.log('p1: catch正常执行,返回resolved状态,触发then')
})
const p2 = Promise.reject().catch(() => {
console.log('p2: catch,正常执行')
throw new Error('error') // 返回rejected状态
}).catch(() => {
console.log('p2: 返回reject状态我又执行了')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 面试题
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
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
# 手写Promise
promise/A+规范
/**
* @description MyPromise
* @author 小邓
*/
class MyPromise {
state = 'pending' // 状态,'pending' 'fulfilled' 'rejected'
value = undefined // 成功后的值
reason = undefined // 失败的值
onFulfilledCallbacks = [] // pending状态下,成功的回调函数集
onRejectedCallbacks = [] // pending状态下,失败的回调函数集
// 调用的时候是 new Promise ((resolve, reject) => {}), 所以这里要传一个函数
constructor(fn) {
const resolveHandler = (value) => {
// 加 setTimeout ,参考 https://coding.imooc.com/learn/questiondetail/257287.html
setTimeout(() => {
if(this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn(this.value))
}
})
}
const rejectHandler = (reason) => {
setTimeout(() => {
if(this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
})
}
try{
fn(resolveHandler, rejectHandler)
} catch(err) {
rejectHandler(err)
}
}
// const p1 = new Promise((resolve, reject) => {})
// p1.then(() => {}, () => {}) 如果pending状态是不会执行的
// then()里面可以传递两个函数,一个成功的回调,一个失败的回调
then(fn1, fn2) {
fn1 = typeof fn1 === 'function' ? fn1 : (v) => v
fn2 = typeof fn2 === 'function' ? fn2 : (e) => e
if(this.state === 'pending') {
const p1 = new MyPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(() => {
try {
const newValue = fn1(this.value);
resolve(newValue)
} catch (err) {
reject(err)
}
})
this.onRejectedCallbacks.push(() => {
try {
const newValue = fn2(this.reason);
reject(newValue)
} catch (err) {
reject(err)
}
})
})
return p1
}
if(this.state === 'fulfilled') {
const p1 = new MyPromise((resolve, reject) => {
try {
const newValue = fn1(this.value);
resolve(newValue);
} catch (err) {
reject(err)
}
})
return p1
}
if(this.state === 'rejected') {
const p1 = new MyPromise((resolve, reject) => {
try {
const newReason = fn2(this.reason)
reject(newReason)
} catch (err) {
reject(err)
}
})
return p1
}
}
// const p = new Promise((resolve, reject) => { reject() })
// p.catch(() => {}) .catch只能传递一个函数,.then可以传递两个
// then的一个语法糖,简单模式
catch(fn) {
return this.then(null, fn)
}
}
MyPromise.resolve = function(value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
MyPromise.all = function (promiseList = []) {
const p1 = new MyPromise((resolve, reject) => {
const result = []
const length = promiseList.length
let resolvedCount = 0
promiseList.forEach(p => {
p.then(data => {
result.push(data)
resolvedCount++
if(resolvedCount === length) {
resolve(result)
}
}).catch(err => {
reject(err)
})
})
})
return p1
}
MyPromise.race = function (promiseList = []) {
let resolved = false
const p1 = new Promise((resolve, reject) => {
promiseList.forEach(p => {
p.then(data => {
if(!resolved) {
resolve(data)
resolved = true
}
}).catch((err) => {
reject(err)
})
})
})
return p1
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./MyPromise.js"></script>
<script>
const p1 = new MyPromise((resolve, reject) => {
// 同时写两个也只会执行第一个,因为第一个修改后状态就改变了,只有pending状态才能执行
// resolve(100) reject('错误信息')
// setTimeout(() => {
// resolve(100)
// }, 100)
resolve(100)
})
console.log(p1)
const p11 = p1.then(data1 => {
console.log('data1', data1)
return data1 + 1
})
console.log(p11)
const p12 = p11.then(data2 => {
console.log('data2', data2)
return data2 + 2
})
const p13 = p12.catch(err => console.error(err))
console.log(p13)
const p2 = MyPromise.resolve(200)
const p3 = MyPromise.resolve(300)
const p4 = MyPromise.reject('错误信息...')
const p5 = MyPromise.all([p1, p2, p3]) // 传入 promise 数组,等待所有的都 fulfilled 之后,返回新 promise ,包含前面所有的结果
p5.then(result => console.log('all result', result))
const p6 = MyPromise.race([p1, p2, p3]) // 传入 promise 数组,只要有一个 fulfilled 即可返回
p6.then(result => console.log('race result', result))
</script>
</body>
</html>
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
# async/await
- 异步回调callback hell
- Promise then catch 链式调用,但也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
// 用async,await改写上面的代码,使其更加简洁和易读
!(async function() {
let img1 = await loadImg(url)
console.log(img1)
let img2 = await loadImg(url2)
console.log(img2)
})()
2
3
4
5
6
7
# async/await和 Promise的关系
- 执行async函数,返回的是Promise对象
- await相当于Promise 的then
- try...catch 可捕获异常,代替了Promise的catch
- async/await是消灭异步回调的终极武器
- 但和Promise并不互斥
- 反而,两者相辅相成
async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() ) // Promise <pending>
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
2
3
4
5
6
7
8
9
await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
await 后续跟非 Promise 对象:会直接返回
!(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
!(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
!(async function () {
const res = await 100
console.log(res) // 100
})()
!(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try...catch 捕获 rejected 状态
!(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
2
3
4
5
6
7
8
9
- async 封装 Promise
- await 处理 Promise 成功
- try...catch 处理 Promise 失败
# 异步本质
await 是同步写法,但本质还是异步调用。
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
2
3
4
5
6
7
8
9
10
11
12
13
即,只要遇到了 await
,后面的代码都相当于放在 callback 里。
async function async1 () {
console.log('async1 start')
await async2() // 后面的代码相当于放在 callback 中,最后执行
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
// 输出结果:
// script start
// async1 start
// async2
// script end // 先执行同步代码,然后执行 callback 中的代码,所以 async1 end 最后
// async1 end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# for...of
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
// const nums = [1, 2, 3];
// nums.forEach(async x => {
// const res = await multi(x);
// console.log(res);
// })
// }
// test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res)
}
}
test2()
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
forEach
本身不等待异步操作完成就继续执行下一次迭代,所以三个异步操作几乎同时开始。
# 微任务/宏任务
- 宏任务: setTimeout , setInterval , Ajax ,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早(先记住)
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
2
3
4
5
6
7
8
9
- 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
- 然后再进行 event loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 宏任务和微任务的区别
- 宏任务:DOM 渲染后再触发
- 微任务:DOM 渲染前会触发
// 修改 DOM
const p1 = document.createElement('p');
p1.innerHTML = '一段文字'
const p2 = document.createElement('p')
p2.innerHTML = '一段文字'
const p3 = document.createElement('p')
p3.innerHTML = '一段文字'
const container = document.getElementById('container');
container.appendChild(p1)
container.appendChild(p2)
container.appendChild(p3)
// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
// const length = container.children.length
// alert(`micro task ${length}`)
// })
// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
const length = container.children.length
alert(`macro task ${length}`)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?
- 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
- 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。
# 面试题
async function async1 () {
console.log('async1 start')
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 异步,宏任务
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2')
})
console.log('script end')
// 打印结果
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
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
# DOM
# DOM 的本质
讲 DOM 先从 html 讲起,讲 html 先从 XML 讲起。XML 是一种可扩展的标记语言,所谓可扩展就是它可以描述任何结构化的数据,它是一棵树!
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<other>
<a></a>
<b></b>
</other>
</note>
2
3
4
5
6
7
8
9
10
11
HTML 是一个有既定标签标准的 XML 格式,标签的名字、层级关系和属性,都被标准化(否则浏览器无法解析)。同样,它也是一棵树。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div>
<p>this is p</p>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
# DOM 节点操作
获取 DOM 节点
const div1 = document.getElementById('div1') // 元素 const divList = document.getElementsByTagName('div') // 集合 console.log(divList.length) console.log(divList[0]) const containerList = document.getElementsByClassName('.container') // 集合 const pList = document.querySelectorAll('p') // 集合
1
2
3
4
5
6
7prototype
DOM 节点就是一个 JS 对象,它符合之前讲述的对象的特征 ———— 可扩展属性
const pList = document.querySelectorAll('p') const p = pList[0] console.log(p.style.width) // 获取样式 p.style.width = '100px' // 修改样式 console.log(p.className) // 获取 class p.className = 'p1' // 修改 class // 获取 nodeName 和 nodeType console.log(p.nodeName) console.log(p.nodeType)
1
2
3
4
5
6
7
8
9
10Attribute
property 的获取和修改,是直接改变 JS 对象,而 Attibute 是直接改变 html 的属性。两种有很大的区别
const pList = document.querySelectorAll('p') const p = pList[0] p.getAttribute('data-name') p.setAttribute('data-name', 'imooc') p.getAttribute('style') p.setAttribute('style', 'font-size:30px;')
1
2
3
4
5
6
- property 只是一个 JS 属性的修改
- attr 是对 html 标签属性的修改
# DOM 树操作
新增节点
const div1 = document.getElementById('div1')
// 添加新节点
const p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新创建的元素
// 移动已有节点。注意是移动!!!
const p2 = document.getElementById('p2')
div1.appendChild(p2)
2
3
4
5
6
7
8
获取父元素
const div1 = document.getElementById('div1')
const parent = div1.parentNode
2
获取子元素
const div1 = document.getElementById('div1')
const child = div1.childNodes
2
删除节点
const div1 = document.getElementById('div1')
const child = div1.childNodes
div1.removeChild(child[0])
2
3
# DOM 性能
DOM 操作是昂贵的 —— 非常耗费性能。因此针对频繁的 DOM 操作一定要做一些处理。
例如缓存 DOM 查询结果
// 不缓存 DOM 查询结果
for (let = 0; i < document.getElementsByTagName('p').length; i++) {
// 每次循环,都会计算 length ,频繁进行 DOM 查询
}
// 缓存 DOM 查询结果
const pList = document.getElementsByTagName('p')
const length = pList.length
for (let i = 0; i < length; i++) {
// 缓存 length ,只进行一次 DOM 查询
}
2
3
4
5
6
7
8
9
10
11
再例如,插入多个标签时,先插入 Fragment 然后再统一插入DOM
const listNode = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到 DOM 树中
const frag = document.createDocumentFragment()
// 执行插入
for(let x = 0; x < 10; x++) {
const li = document.createElement("li")
li.innerHTML = "List item " + x
frag.appendChild(li)
}
// 都完成之后,再插入到 DOM 树中
listNode.appendChild(frag)
2
3
4
5
6
7
8
9
10
11
12
13
14
# BOM
DOM 是浏览器针对下载的 HTML 代码进行解析得到的 JS 可识别的数据对象。而 BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。
- navigator
- screen
- location
- history
// navigator
var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)
// screen
console.log(screen.width)
console.log(screen.height)
// location
console.log(location.href)
console.log(location.protocol) // 'http:' 'https:'
console.log(location.pathname) // '/learn/199'
console.log(location.search)
console.log(location.hash)
// history
history.back()
history.forward()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 事件
# 事件绑定
const btn = document.getElementById('btn1')
btn.addEventListener('click', event => {
console.log('clicked')
})
2
3
4
通用的事件绑定函数
function bindEvent(elem, type, fn) {
elem.addEventListener(type, fn)
}
const a = document.getElementById('link1')
bindEvent(a, 'click', e => {
e.preventDefault() // 阻止默认行为
alert('clicked')
})
2
3
4
5
6
7
8
# 事件冒泡
<body>
<div id="div1">
<p id="p1">激活</p>
<p id="p2">取消</p>
<p id="p3">取消</p>
<p id="p4">取消</p>
</div>
<div id="div2">
<p id="p5">取消</p>
<p id="p6">取消</p>
</div>
</body>
2
3
4
5
6
7
8
9
10
11
12
对于以上 html 代码结构,点击p1
时候进入激活状态,点击其他任何p
都取消激活状态,如何实现?
const p1 = document.getElementById('p1')
const body = document.body
bindEvent(p1, 'click', e => {
e.stopPropagation() // 注释掉这一行,来体会事件冒泡
alert('激活')
})
bindEvent(body, 'click', e => {
alert('取消')
})
2
3
4
5
6
7
8
9
如果我们在p1
div1
body
中都绑定了事件,它是会根据 DOM 的结构,来冒泡从下到上挨个执行的。但是我们使用e.stopPropagation()
就可以阻止冒泡。
# 代理
我们设定一种场景,如下代码,一个<div>
中包含了若干个<a>
,而且还能继续增加。那如何快捷方便的为所有的<a>
绑定事件呢?
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>
2
3
4
5
6
7
这里就会用到事件代理,我们要监听<a>
的事件,但要把具体的事件绑定到<div>
上,然后看事件的触发点,是不是<a>
const div1 = document.getElementById('div1')
div1.addEventListener('click', e => {
const target = e.target
if (e.nodeName === 'A') {
alert(target.innerHTML)
}
})
2
3
4
5
6
7
通用事件代理封装
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector
selector = null
}
elem.addEventListener(type, function (e) {
let target = e.target
// 需要事件代理
if (selector) {
if (target.matches(selector)) {
fn.call(target, e)
}
} else {
// 直接绑定
fn.call(target, e)
}
})
}
// 使用代理
const div1 = document.getElementById('div1')
bindEvent(div1, 'click', 'a', function(e) {
alert(this.innerHTML)
})
// 不使用代理
const a = document.getElementById('a1')
bindEvent(a, 'click', function(e) {
e.stopPropagation()
alert(this.innerHTML)
})
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
# ajax
# XMLHttpRequest
const xhr = new XMLHttpRequest()
xhr.open('GET', '/面试题/JS面试题/data.json', true)
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status === 200) {
console.log(xhr.responseText)
} else if (xhr.status === 404) {
console.log('404 not found')
}
}
}
xhr.send(null)
2
3
4
5
6
7
8
9
10
11
12
xhr.readyState 的状态吗说明
- 0 - (未初始化)还没有调用send()方法
- 1 -(载入)已调用send()方法,正在发送请求
- 2 -(载入完成)send()方法执行完成,已经接收到全部响应内容
- 3 -(交互)正在解析响应内容
- 4 -(完成)响应内容解析完成,可以在客户端调用了
http 状态吗有 2xx
3xx
4xx
5xx
这几种,比较常用的有以下几种
- 200 正常
- 301 永久重定向;302 临时重定向;304 资源未被修改;
- 404 找不到资源;403 权限不允许;
- 5xx 服务器端出错了
# 手写ajax
function ajax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = function () {
if(xhr.readyState === 4) {
if(xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else if (xhr.status === 404 || xhr.status === 500) {
reject('404 not found')
}
}
}
xhr.send(null)
})
}
ajax('/data.json')
.then(data => {
console.log(data)
})
.catch(err => {
console.log(err)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 跨域
浏览器中有“同源策略”,即一个域下的页面中,无法通过 ajax 获取到其他域的接口。
url 哪些地方不同算作跨域?
- 协议
- 域名
- 端口
但是html中几个标签能逃避过同源策略——<script src="xxx">
、<img src="xxxx"/>
、<link href="xxxx">
,这俩标签的 src
或 href
可以加载其他域的资源,不受同源策略限制。
因此,这是三个标签可以做一些特殊的事情。
<img>
可以做打点统计,因为统计方并不一定是同域的,在讲解JS基础知识异步的时候有过代码示例。除了能跨域之外,<img>
几乎没有浏览器兼容问题,它是一个非常古老的标签。<script>
和<link>
可以使用CDN,CDN基本都是其他域的链接。- 另外
<script>
还可以实现JSONP,能获取其他域接口的信息,接下来马上讲解。
但是请注意,所有的跨域请求方式,最终都需要信息提供方来做出相应的支持和改动,也就是要经过信息提供方的同意才行,否则接收方是无法得到他们的信息的,浏览器是不允许的。
# JSONP
首先,我们在自己的页面这样定义
<script>
window.callback = function (data) {
// 这是我们跨域得到信息
console.log(data)
}
</script>
2
3
4
5
6
请求http://www.baidu.com/api.js
接口,内容如下(之前说过,服务器可动态生成内容)服务端接口返回的数据
callback({x:100, y:200})
最后我们在页面中加入<script src="http://www.baidu.com/api.js"></script>
,那么这个js加载之后,就会执行内容,我们就得到内容了
原理就是:客户端提前定义好函数用来接收数据,服务器返回返回数据调用该函数执行,这样就可以实现跨域
# jQuery 实现 jsonp
提供方提供的数据:
callback({
"x": 100,
"y": 200
})
2
3
4
接收方的写法:
$.ajax({
url: 'http://localhost:8882/x-origin.json',
dataType: 'jsonp',
jsonpCallback: 'callback',
success: function (data) {
console.log(data)
}
})
2
3
4
5
6
7
8
# 服务器端设置 http header
这是需要在服务器端设置的,作为前端工程师我们不用详细掌握,但是要知道有这么个解决方案。而且,现在推崇的跨域解决方案是这一种,比 JSONP 简单许多。
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8011"); // 第二个参数填写允许跨域的域名称,不建议直接写 "*"
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 接收跨域的cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
2
3
4
5
6
# 存储
# cookie
cookie 本身不是用来做服务器端存储的(计算机领域有很多这种“狗拿耗子”的例子,例如 css 中的 float),它设计是用来在服务器和客户端进行信息传递的,因此我们的每个 http 请求都带着 cookie。但是 cookie 也具备浏览器端存储的能力(例如记住用户名和密码),因此就被开发者用上了。
使用起来也非常简单document.cookie = ....
即可。
但是 cookie 有它致命的缺点:
- 存储量太小,只有 4KB
- 所有 http 请求都带着,会影响获取资源的效率
- API 简单,需要封装才能用
# locationStorage 和 sessionStorage
后来,HTML5标准就带来了sessionStorage
和localStorage
,先拿localStorage
来说,它是专门为了浏览器端缓存而设计的。其优点有:
- 存储量增大到 5M
- 不会带到 http 请求中
- API 适用于数据存储
localStorage.setItem(key, value)
localStorage.getItem(key)
sessionStorage
的区别就在于它是根据 session 过去时间而实现,而localStorage
会永久有效,应用场景不懂。例如,一些重要信息需要及时失效的放在sessionStorage
中,一些不重要但是不经常设置的信息,放在localStorage
另外告诉大家一个小技巧,iOS系统的safari浏览器的隐藏模式,使用localStorage.setItem
,因此使用时尽量加入到try-catch
中
# http面试题
# http 状态码
状态码分类
- 1xx 服务器收到请求
- 2xx 成功
- 3xx 重定向
- 4xx 客户端错误
- 5xx 服务器错误
常见状态码
http 协议中的状态码有很多,但只有一些是我们常用的。也是面试常考的。
- 200 成功
- 301 永久重定向(同时返回一个 location ,写明重定向的 url)。例如一个网站的网址永久性的切换了
- 302 临时重定向(同时返回一个 location ,写明重定向的 url)。例如短链跳转
- 304 资源未修改过
- 404 未找到资源
- 403 没有权限,例如需要登录之后才能请求
- 500 服务器内部错误,例如服务器代码异常
- 504 网关超时,例如上游服务器连接失败(服务器不是一台机器,可能会有很多台)
# http methods
之前,常用的方法就是 get 和 post
- get 从服务端获取数据
- post 向服务端提交数据
现在,随着技术更新,以及 Restful API 设计(下文会讲)。有更多的 methods 被应用
- get 获取数据
- post 新建数据
- patch/put 更新数据
- delete 删除数据
# Restful API
Restful API 是前后端接口的一种设计规范,经历了几年的发展,已经被全面应用。前端面试常考。
- 传统 API 设计:把每个 API 当做一个功能
- Restful API 设计:把每个 API 当做一个资源标识
需要用到的手段
- 不使用 url 参数
- 使用 method 表示操作类型
例如要获取一个列表
- (不使用 url 参数)
- 传统 API 设计:
/api/list?pageIndex=2
—— 一个功能 - Restful API 设计:
/api/list/2
—— 一个资源
再例如要操作一个数据
- 传统 API 设计(每个 API 都是功能)
/api/create-blog
,post 请求/api/udpate-blog?id=101
,post 请求/api/get-blog?id=101
, get 请求
- Restful API 设计(每个 API 都是资源)
/api/blog
,post 请求/api/blog/101
,patch 请求/api/blog/101
,get 请求
# http headers
# request headers
浏览器发送请求时,传递给服务端的信息
- Accept 浏览器可接收的数据类型
- Accept-Encoding 浏览器可接收的压缩算法,如 gzip
- Accept-Language 浏览器可接收的语言,如 zh-CN
- Connection: keep-alive 一次 TCP 连接重复使用
- cookie
- Host
- User-Agent 浏览器信息
- Content-type 发送数据的类型,常见的有 application/json,application/x-www-form-urlencoded,multipart/form-data,text/plain 等(用 postman 可演示)
Cache-Control: no-cache 不使用强制缓存
# response headers
- Content-Type 返回的数据类型,对应 Accept
- Content-Length 数据大小
- Content-Encoding 压缩算法,如 gzip ,对应 Accept-Encoding
- Set-Cookie
# http 缓存
缓存,即某些情况下,资源不是每次都去服务端获取,而是第一次获取之后缓存下来。 下次再请求时,直接读取本地缓存,而不再去服务端请求。
# 为什么需要缓存
核心需求,让网页更快的显示出来,即提高性能。
- 计算机执行计算,非常快
- 包括页面渲染,JS 执行等
- 加载资源却非常慢(相比于计算来说),而且受限于网络不可控。
解决好最关键的问题 —— 缓存网络资源
# 哪些资源需要缓存
对于一个网页来说
- html 页面不能缓存
- 业务数据不能缓存(例如一个博客项目,里面的博客信息)
- 静态资源可以缓存,js css 图片等(所有的静态资源累加起来,体积是很大的)
PS:讲 webpack 时讲过 contentHash
,就是给静态资源加上一个唯一的 hash 值,便于缓存。
# 缓存策略 —— 强制缓存,客户端缓存
Cache-Control (response headers 中) 表示该资源,被再次请求时的缓存情况。
max-age:31536000
单位是 s ,该资源被强制缓存 1 年no-cache
不使用强制缓存,但不妨碍使用协商缓存(下文会讲)no-store
禁用一起缓存,每次都从服务器获取最新的资源private
私有缓存(浏览器级缓存)public
共享缓存(代理级缓存)
关于 Expires
- http 1.0 ,设置缓存过期时间的
- 由于本地时间和服务器时间可能不一致,会导致问题
- 已被 Cache-Control 的 max-age 代替
初次请求,强制缓存示意图
缓存过期,强制缓存示意图
# 缓存策略 —— 协商缓存(对比缓存),服务端缓存
当强制缓存失效,请求会被发送到服务端。此时,服务端也不一定每次都要返回资源,如果客户端资源还有效的话。
第一,Last-Modified(Response Headers)和 If-Modified-Since(Request Headers)
- Last-Modified 服务端返回资源的最后修改时间
- If-Modified-Since 再次请求时带着最后修改时间
- 服务器根据时间判断资源是否被修改(如未被修改则返回 304,失败则返回新资源和新的缓存规则)
第二,Etag(Response Headers)和 If-None-Match(Request Headers)
- Etag 服务端返回的资源唯一标识(类似人的指纹,唯一,生成规则由服务器端决定,结果就是一个字符串)
- If-None-Match 再次请求时带着这个标识
- 服务端根据资源和这个标识是否 match (成功则返回 304,失败则返回新资源和新的缓存规则)
如果两者一起使用,则优先使用 Etag 规则。因为 Last-Modified 只能精确到秒级别。
总结
# 刷新操作对应不同的缓存策略
三种操作
- 正常操作:地址栏输入 url ,点击链接,前进后退等
- 手动刷新:F5 或者点击刷新按钮
- 强制刷新:ctrl + F5
对应的缓存策略
- 正常操作:强制缓存有效,协商缓存有效
- 手动刷新:强制缓存失效,协商缓存有效
- 强制刷新,强制缓存失效,协商缓存失效
# https
http 是明文传输,传输的所有内容(如登录的用户名和密码),都会被中间的代理商(无论合法还是非法)获取到。
http + TLS/SSL = https ,即加密传输信息。只有客户端和服务端可以解密为明文,中间的过程无法解密。
# 信息加密
对称加密
一个密钥,既负责加密,又负责解密
- 浏览器访问服务端,服务端生成密钥,并传递给浏览器
- 浏览器和服务端,通过这个密钥来加密、解密信息
但这有一个很严重的问题:密钥也会被劫持
服务端先发送key给客户端,如果这个key被劫持,那么后面请求发送或返回的内容也被劫持,就会被轻松解密传输的内容
非对称加密
生成一对密钥,一个公钥,一个私钥。
公钥加密的信息,只有私钥能解密
私钥加密的信息,只有公钥能解密
浏览器访问服务端,服务端生成公钥、私钥,并把公钥传递给浏览器
浏览器生成一个 key(随机字符串),并用公钥加密,传递给服务端
服务端用私钥解密 key 。这样浏览器和服务端,就都得到了 key ,而且 key 还是加密传输的
然后,浏览器和服务端使用 key 为密钥,做对称加密传输
思考:如果公钥和 key 被劫持,黑客能解密 key 吗?—— 不能,因为解密 key 要使用私钥,而私钥一只在服务端,没有传输。
证书
公钥劫持了不行,那替换行不行呢?
黑客直接劫持请求,替换为自己的公钥(当然他自己有私钥),你的所有请求他劫持到,就都可以解密了。
这叫做“中间人攻击”
这个问题,不好从技术上规避,那就从标准规范上解决 —— CA 证书。
- 由正规的第三方结构,颁发证书(如去阿里云申请,但要花钱)
- 证书包括:公钥,域名,申请人信息,过期时间等 —— 这些都是绑定的
- 浏览器识别到正规的证书,才使用。否则会交给用户确认。
这样,当黑客使用中间人攻击时,浏览器就会识别到它的证书不合规范,就会提示用户。
所以,尽量使用正规渠道申请的证书,花点钱,保证安全和稳定性。
# https 加密原理
# 总流程图
# 开发环境
# Git
文件状态
- untracked 未跟踪的文件(也就是新创建的文件)
- unstaged未暂存的文件 (之前暂存过,但是被修改了的文件)
git 常用命令
- git status 查看文件状态
- git log 查看日志 (日志里面会有Commit,Author,Date)
- git reset --hard commitID 版本回退 (commitID在日志中查看(git log))
- git branch 查看本地分支
- git branch name 创建分支
- git checkout name 切换分支
- git checkout -b name 创建并切换到一个不存在的分支
- git merge dev01 (以dev01合并到master上为例,必须先切换到master分支里)
- git branch -d name 删除分支
- git remote add 远端名称(默认是origin) 仓库路径 添加远程仓库
- git remote 查看远程仓库是否添加
- git push 远端名称 分支名称 推送代码到远程仓库
- git clone 远程仓库地址 把远端仓库克隆到他本地
- git fetch [remote name] [branch name] (抓取指令就是将仓库里的更新都抓取到本地,不会进行合并。如果不指定远端名称和分支名,则抓取所有分支)
- git pull [remote name] [branch name] (取命令就是将远端仓库的修改拉到本地并自动进行合并,等同于fetch+merge 如果不指定远端名称和分支名,则抓取所有并更新当前分支)
参考:http://www.006969.xyz/pages/449d74/
# wepback
参考 https://www.imooc.com/article/287156
# 初始化
npm init -y
npm i webpack webpack-cli -D
- 新建
src/index.js
,随便写点什么 - 新建
webpack.config.js
配置mode
entry
output
- package.json 中增加
"build": "webpack"
- 运行
npm run build
# 启动服务
npm i html-webpack-plugin -D
,并引用、配置插件- 新建
src/index.html
npm i webpack-dev-server -D
,并引用、配置- package.json 中增减
"dev": "webpack-dev-server"
- 运行
npm run dev
# babel
ES6 编译为 ES5
npm i babel-loader @babel/core @babel/preset-env -D
- 增加 babel-loader 配置
- 增加
.babelrc
文件 - 在
src/index.js
中增加一个箭头函数和class
,看打包结果
# 打包到生产环境
- 新建
webpack.prod.js
,注意mode: 'production'
- 加上
contentHash
- 修改 package.json
"build": "webpack --config webpack.prod.js",
- 运行
npm run build
,看打包出来的代码是被压缩过的
webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
mode: "development",
entry: path.join(__dirname, "src", "index.js"), //入口文件
output: {
filename: "bundle.js", //打包后的文件名
path: path.join(__dirname, "dist") // 打包后的目录
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: path.join(__dirname, "src"),
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "index.html"), // 模板文件
filename: "index.html" // 生成的HTML文件名
})
],
devServer: {
port: 3000, // 设置启动时监听的端口
open: true, // 自动打开浏览器
static: {
directory: path.join(__dirname, "dist")
}
// contentBase: path.join(__dirname, "dist") // 设置启动时加载的页面地址
}
}
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
package.json
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack-dev-server --config webpack.config.js"
},
2
3
4
# linux 服务器的基本命令
登录
入职之后,一般会有现有的用户名和密码,拿来之后直接登录就行。运行 ssh name@server
然后输入密码即可登录
目录操作
- 创建目录
mkdir
- 删除目录
rm -rf
- 定位目录
cd
- 查看目录文件
ls
ll
- 修改目录名
mv
- 拷贝目录
cp
文件操作
- 创建文件
touch
vi
- 删除文件
rm
- 修改文件名
mv
- 拷贝文件
cp
scp
文件内容操作
- 查看文件
cat
head
tail
- 编辑文件内容
vi
- 查找文件内容
grep
# 运行环境
# 页面加载
# 浏览器加载资源的过程
加载资源的形式
- 输入 url 加载 html
- http://coding.m.imooc.com
- 加载 html 中的静态资源
<script src="/static/js/jquery.js"></script>
加载一个资源的过程
- 浏览器根据 DNS 服务器得到域名的 IP 地址
- 向这个 IP 的机器发送 http 请求
- 服务器收到、处理并返回 http 请求
- 浏览器得到返回内容
# 浏览器渲染页面的过程
- 根据 HTML 结构生成 DOM Tree
- 根据 CSS 生成 CSS Rule
- 将 DOM 和 CSSOM 整合形成 RenderTree
- 根据 RenderTree 开始渲染和展示
- 遇到
<script>
时,会执行并阻塞渲染
# window.onload 和 DOMContentLoaded 的区别
window.addEventListener('load', function () {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})
2
3
4
5
6
- 页面的全部资源加载完才会执行,包括图片、视频等
- DOM 渲染完即可执行,此时图片、视频还没有加载完
# 性能优化
# 优化原则和方向
原则
- 多使用内存、缓存或者其他方法
- 减少 CPU 计算、较少网络
方向
- 加载页面和静态资源
- 页面渲染
# 加载资源优化
- 静态资源的压缩合并(JS代码压缩合并、CSS代码压缩合并、雪碧图)
- 静态资源缓存(资源名称加 MD5 戳)
- 使用 CND 让资源加载更快
- 使用 SSR 后端渲染,数据直接突出到 HTML 中
# 渲染优化
- CSS 放前面 JS 放后面
- 懒加载(图片懒加载、下拉加载更多)
- 减少DOM 查询,对 DOM 查询做缓存
- 减少DOM 操作,多个操作尽量合并在一起执行(
DocumentFragment
) - 节流和防抖
- 尽早执行操作(
DOMContentLoaded
)
# 补充
静态资源的压缩合并
如果不合并,每个都会走一遍之前介绍的请求过程
<script src="a.js"></script> <script src="b.js"></script> <script src="c.js"></script>
1
2
3如果压缩了,就只走一遍请求过程
<script src="abc.js"></script>
1静态资源缓存
通过连接名称控制缓存
<script src="abc_1.js"></script>
1只有内容改变的时候,链接名称才会改变
<script src="abc_2.js"></script>
1使用 CND 让资源加载更快
<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>
1使用 SSR 后端渲染
如果提到 Vue 和 React 时,可以说一下
CSS 放前面 JS 放后面
将浏览器渲染的时候,已经提高
懒加载
一开始先给为 src 赋值成一个通用的预览图,下拉时候再动态赋值成正式的图片
<img src="preview.png" data-realsrc="abc.png"/>
1DOM 查询做缓存
两端代码做一下对比
// 不缓存 DOM 查询结果 for (let = 0; i < document.getElementsByTagName('p').length; i++) { // 每次循环,都会计算 length ,频繁进行 DOM 查询 } // 缓存 DOM 查询结果 const pList = document.getElementsByTagName('p') const length = pList.length for (let i = 0; i < length; i++) { // 缓存 length ,只进行一次 DOM 查询 }
1
2
3
4
5
6
7
8
9
10
11总结:DOM 操作,无论查询还是修改,都是非常耗费性能的,尽量减少
合并 DOM 插入
DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入DOM
const listNode = document.getElementById('list') // 创建一个文档片段,此时还没有插入到 DOM 树中 const frag = document.createDocumentFragment() // 执行插入 for(let x = 0; x < 10; x++) { const li = document.createElement("li") li.innerHTML = "List item " + x frag.appendChild(li) } // 都完成之后,再插入到 DOM 树中 listNode.appendChild(frag)
1
2
3
4
5
6
7
8
9
10
11
12
13
14尽早执行操作
window.addEventListener('load', function () { // 页面的全部资源加载完才会执行,包括图片、视频等 }) document.addEventListener('DOMContentLoaded', function () { // DOM 渲染完即可执行,此时图片、视频还可能没有加载完 })
1
2
3
4
5
6节流和防抖
例如要在文字改变时触发一个 change 事件,通过 keyup 来监听。使用防抖。
function debounce(func, delay = 200) { // timer在闭包中 let timer = null; // 返回一个函数 return function() { if(timer) { clearTimeout(timer); } timer = setTimeout(() => { func.apply(this, arguments); // 应用函数到正确的上下文中 // 清除定时器 timer = null; }, delay) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15在拖拽时,随时要检测当前的位置信息(如是否覆盖了目标元素的位置),可用节流。
function throttle(func, delay) { let timer = null; return function() { // 当我们发现这个定时器存在时,则表示定时器已经在运行中,还没到该触发的时候,则 return if(timer) return // 定时器不存在了,说明已经触发过了,重新计时 timer = setTimeout(() => { func.apply(this, arguments); timer = null; }, delay); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
# 安全性
【面试】常见的 web 攻击方式有哪些,简述原理?如何预防?
上学的时候就知道有一个“SQL注入”的攻击方式。例如做一个系统的登录界面,输入用户名和密码,提交之后,后端直接拿到数据就拼接 SQL 语句去查询数据库。如果在输入时进行了恶意的 SQL 拼装,那么最后生成的 SQL 就会有问题。但是现在稍微大型的一点系统,都不会这么做,从提交登录信息到最后拿到授权,都经过层层的验证。因此,SQL 注入都只出现在比较低端小型的系统上。
前端端最常见的攻击就是 XSS(Cross Site Scripting,跨站脚本攻击),很多大型网站(例如 FaceBook 都被 XSS 攻击过)。举一个例子,我在一个博客网站正常发表一篇文章,输入汉字、英文和图片,完全没有问题。但是如果我写的是恶意的 js 脚本,例如获取到document.cookie
然后传输到自己的服务器上,那我这篇博客的每一次浏览,都会执行这个脚本,都会把自己的 cookie 中的信息偷偷传递到我的服务器上来。
预防 XSS 攻击就得对输入的内容进行过滤,过滤掉一切可以执行的脚本和脚本链接。大家可以参考xss.js (opens new window)这个开源工具。
简单总结一下,XSS 其实就是攻击者事先在一个页面埋下攻击代码,让登录用户去访问这个页面,然后偷偷执行代码,拿到当前用户的信息。
还有一个比较常见的攻击就是 CSRF/XSRF(Cross-site request forgery,跨站请求伪造)。它是借用了当前操作者的权限来偷偷的完成某个操作,而不是拿到用户的信息。例如,一个购物网站,购物付费的接口是http://buy.com/pay?id=100
,而这个接口在使用时没有任何密码或者 token 的验证,只要打开访问就付费购买了。一个用户已经登录了http://buy.com
在选择商品时,突然收到一封邮件,而这封邮件正文有这么一行代码<img src="http://buy.com/pay?id=100"/>
,他访问了邮件之后,其实就已经完成了购买。
预防 CSRF 就是加入各个层级的权限验证,例如现在的购物网站,只要涉及到现金交易,肯定输入密码或者指纹才行。
# 面试题总结
# var 和 let const 的区别
- var 是 ES5 及之前的语法,let const 是 ES6 语法
- var 和 let 是变量,可修改;const 是常量,不可修改
- var 有变量提升,let const 没有
- var 没有块级作用域,let const 有 (ES6 语法有块级作用域)
// var 变量提升 相当于 var a; console.log(a); a = 10
console.log('a', a) // undefined
var a = 100
// let 没有变量提升
console.log('b', b) // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 200
2
3
4
5
6
7
// var 没有块级作用域
for (var i = 0; i < 10; i++) {
var j = 1 + i
}
console.log(i, j) // 10 10
// let 有块级作用域
for (let x = 0; x < 10; x++) {
let y = 1 + x
}
console.log(x, y) // Uncaught ReferenceError: x is not defined
2
3
4
5
6
7
8
9
10
11
# typeof 有哪些返回类型?
- undefined, string, number, boolean, symbol
- object (typeot null === 'object')
- function
// 判断所有值类型
let a
console.log(a) // 'undefined'
const str = 'abc'
console.log(typeof str) // 'string'
const n = 100
console.log(typeof n) // 'number'
const b = true
console.log(typeof b) // 'boolean'
const s = Symbol('s')
console.log(typeof s) // 'symbol'
// 其他引用类型都为object
console.log(typeof null) // 'object'
obj = {}
console.log(typeof obj) // 'object'
function fun() {}
console.log(typeof fun) // 'function'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 强制类型转换和隐式类型转换
- 强制
parseInt
parseFloat
toString
等 - 隐式
if
,==
,+
拼接字符串
# 手写深度比较,如 lodash isEqual
// 判断是否是 object
function isObject(obj) {
return typeof obj === 'object' && obj !== null;
}
function isEqual(obj1, obj2) {
if(!isObject(obj1) || !isObject(obj2)) {
// 值类型,不是对象或数组(注意,equal 时一般不会有函数,这里忽略)
return obj1 === obj2;
}
// 两个引用类型全相等(同一个地址)
if(obj1 === obj2) {
return true
}
// 两个都是引用类型,不全相等
// 1. 先取出 obje2 obj2 的 keys,比较个数
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if(obj1Keys.length !== obj2Keys.length) {
return false;
}
// 2. 以 obj1 为基准,和 obj2 依次递归比较
for(let key in obj1){
// 递归比较
if(isEqual(obj1[key], obj2[key]) === false) {
return false;
}
}
// 3. 都相等,则返回 true
return true;
}
// 实现如下效果
const obj1 = {a: 10, b: { x: 100, y: 200 }}
const obj2 = {a: 10, b: { x: 100, y: 200 }}
console.log(isEqual(obj1, obj2))
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
# split()
和 join()
的区别
console.log('1-2-3'.split('-')) // [1,2,3]
console.log([1,2,3].join('-')) // 1-2-3
2
# 数组的 pop
push
unshift
shift
分别做什么
注意以下几点
- 函数作用是什么?
- 返回值是什么?
- 对原数组是否造成影响?
- 如何对原数组不造成影响?
concat
slice
map
filter
【扩展】数组 API 的纯函数和非纯函数
纯函数 —— 1. 不改变来源的数组; 2. 返回一个数组
- concat
- map
- filter
- slice
const arr = [100, 200, 300]
const arr1 = arr.concat([400, 500])
const arr2 = arr.map(num => num * 10)
const arr3 = arr.filter(num => num > 100)
const arr4 = arr.slice(-1)
2
3
4
5
非纯函数
情况1,改变了原数组
- push
- reverse
- sort
- splice
情况2,未返回数组
- push
- forEach
- reduce
- some
const arr = [1,2,3,4]
console.log(arr.push(5)) // 返回数组长度5
console.log(arr.pop()) // 5
console.log(arr.shift()) // 1
console.log(arr) // [2,3,4]
console.log(arr.unshift(0)) // 返回数组长度4
console.log(arr) // [0,2,3,4]
2
3
4
5
6
7
# 数组 slice 和 splice 的区别?
slice - 切片;splice - 剪接;
// slice()
const arr1 = [10, 20, 30, 40, 50]
const arr2 = arr1.slice() // arr2 和 arr1 不是一个地址,纯函数,重要!!!
console.log(arr1 === arr2); // false
// arr.slice(start, end) [start, end) 包括左边也不包括右边
const arr3 = [10, 20, 30, 40, 50]
const arr4 = arr1.slice(1, 4) // [20, 30, 40]
// arr.slice(start)
const arr5 = [10, 20, 30, 40, 50]
const arr6 = arr1.slice(2) // [30, 40, 50]
// 负值
const arr7 = [10, 20, 30, 40, 50]
const arr8 = arr1.slice(-2) // [40, 50]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
splice()删除元素/插入元素/替换元素
- 删除元素:第一个参数:从这个索引后面开始删除 第二个参数:传入要删除几个元素(如果没有传,就删除后面所有元素)
- 替换元素:第二个参数:标识我们要替换几个元素,后面就是用于替换前面的元素
- 插入元素:第二个参数:传入0,并且后面跟上要插入的元素
// arr.splice(index, howmany, item1, ....., itemX)
const arr9 = [10, 20, 30, 40, 50]
const arr10 = arr9.splice(1, 2, 'a', 'b', 'c') // [20, 30]返回的是被删除的数
// arr9 会被修改,不是纯函数,即有副作用
// arr9 [10, 'a', 'b', 'c', 40, 50]
2
3
4
5
# [10, 20, 30].map(parseInt)
的结果是什么?
parseInt⽅法接收两个参数,
parseInt(string,radix)
;
string
:要被解析的值。如果参数不是⼀个字符串,则将其转换为字符串(toString)。字符串开头的空⽩符将会被忽略。
radix
:可选。从 2 到 36,表⽰被解析的值的进制。例如说指定 10 就等于指定⼗进位。当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
当忽略参数 radix , JavaScript 默认数字的基数如下:
如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数。
如果 string 以 0 开头,那么 ECMAScript v3 允许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。
如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。
console.log([10,20,30].map(parseInt )) // [10, NaN, NaN]
// 原理
// 拆解开来为
const res = [10,20,30,40,50,60,70,80,90,100].map((num, index) => {
console.log(num, index)
return parseInt(num, index)
// parseInt 第二个参数是进制
// 如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
// 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN
})
console.log(res) // [10, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 81]
// parseInt(20,1) // 20当成1进制来解析,会报错。1进制不能超过1
// parseInt(100,9) 1*9的2次方+0+0 = 81 (100当成9进制来解析)
2
3
4
5
6
7
8
9
10
11
12
13
如果想要实现转换可以使用这种
console.log([10,20,30].map(item => parseInt(item))) // [10, 20, 30]
# ajax 请求中 get 和 post 的区别
- get 一般用于查询操作,post 一般用于提交操作
- get 参数在 url 上,post 在请求体内
- 安全性:post 请求易于防止 CSRF
(post 代码演示:网页,postname)
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 上面的请求也可以这样做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// post请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
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
# call 和 apply 的区别
fn.call(this, p1, p2, p3)
fn.apply(this, arguments)
两者都会立即执行,bind不会
function fun() {
console.log(this);
}
fun(); // Window
var obj = {
name: 'obj',
};
fun.apply(obj, [1, 2, 3])
fun.call(obj, 1, 2, 3)
const res = fun.bind(obj, 1, 2, 3)
res()
2
3
4
5
6
7
8
9
10
11
# 事件委托(代理)是什么
function bindEvent(elem, type, selector, fn) {
if(fn == null) {
fn = selector;
selector = null;
}
elem.addEventListener(type, function(e) {
const target = e.target;
if(selector) {
// 代理绑定
if(target.matches(selector)) {
fn.call(target, e);
}
} else {
// 普通绑定
fn.call(target, e);
}
})
}
const p = document.querySelector('p');
bindEvent(p, 'click', 'a', function(e) {
e.preventDefault();
console.log(this.innerHTML);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.addEventListener(),接收3个参数
第一个参数event:监听的事件
第二个参数是函数:需要执行的事
第三个参数是useCapture(变量):用来判断是捕获还是冒泡
2.第三个参数userCapyure
(1)当useCapture为true的时候是在捕获阶段触发事件 (捕获事件触发顺序是由父到子)
(2)当useCapture为false的时候是在冒泡阶段触发事件(默认为false)(冒泡事件触发顺序是由子到父)
# 闭包是什么,有什么特性,对页面有什么影响
知识点回顾
- 回归作用域和自由变量
- 闭包的应用场景:函数作为参数被传入,函数作为返回值被返回
- 关键点:自由变量的查找,要在函数定义的地方,而不是执行的地方
对页面的影响
- 变量内存得不到释放,可能会造成内存积累(不一定是泄露)
// 自由变量示例 —— 内存会被释放
let a = 0
function fn1() {
let a1 = 100
function fn2() {
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a2 + a3
}
fn3()
}
fn2()
}
fn1()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 闭包 函数作为返回值 —— 内存不会被释放
function create() {
let a = 100
return function() {
console.log(a)
}
}
let fun = create();
let a = 200;
fun(); // 100
// 闭包 函数作为参数 —— 内存不会被释放
function create2(fn) {
let b = 100;
fn()
}
let b = 200
let fun2 = function() {
console.log(b);
}
create2(fun2); // 200
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 如何阻止事件冒泡和默认行为
event.stopPropagation()
阻止事件冒泡event.preventDefault()
阻止默认行为
const body = document.body;
const div = document.querySelector('div');
const p = document.querySelector('p');
body.addEventListener('click', function(e) {
console.log('body 捕获');
}, true)
div.addEventListener('click', function(e) {
console.log('div 捕获');
}, true)
p.addEventListener('click', function(e) {
console.log('p 捕获');
e.stopPropagation(); // 阻止事件冒泡
}, true)
body.addEventListener('click', function(e) {
console.log('body 冒泡');
}, false)
div.addEventListener('click', function(e) {
console.log('div 冒泡');
}, false)
p.addEventListener('click', function(e) {
console.log('p 冒泡');
}, false)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加 删除 替换 插入 移动 DOM 节点的方法
const body = document.body;
// 创建结点
const div = document.createElement('div');
div.innerHTML = '我是被添加的div'
// 添加结点。添加到最后
body.appendChild(div);
const p = document.querySelector('p');
// 添加结点,添加到指定元素的前面 refNode.parentNode.insertBefore(newNode,refNode)
const a = document.createElement('a');
// 直接附带在a标签上的
a.setAttribute('href', 'http://www.baidu.com');
a.style.color = 'red';
a.innerText = '被添加到p标签前面的a';
p.parentNode.insertBefore(a, p);
// 获取子节点
console.log(body.childNodes);
// 替换元素 替换:oldNode.parentNode.replaceChild(newNode,oldNode)
const a2 = document.createElement('a');
a2.innerText = "我是被新替换的a标签"
a.parentNode.replaceChild(a2, a);
// 删除元素 删除:oldNode.parentNode.removeChild(oldNode)
p.parentNode.removeChild(p);
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
# 怎样减少 DOM 操作?
// const body = document.body
// console.time('添加dom')
// for(let i = 0; i < 10* 10000; i++) {
// const div = document.createElement('div')
// div.innerHTML = i
// body.appendChild(div)
// }
// console.timeEnd('添加dom') // 添加dom: 253.501953125 ms
// 利用文档片段
const body = document.body
const fragment = document.createDocumentFragment()
console.time('fragment添加dom')
for(let i = 0; i < 10 * 10000; i++) {
const div = document.createElement('div')
div.innerHTML = i
fragment.appendChild(div)
}
// 都完成之后,再插入到 DOM 树中
body.appendChild(fragment)
console.timeEnd('fragment添加dom') // fragment添加dom: 245.189697265625 ms
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 解释 jsonp 的原理,以及为什么不是真正的 ajax
- 浏览器的同源策略,什么是跨域?
- 哪些 html 标签能绕过跨域?
- jsonp 原理
JSONP 全称“JSON with Padding”,译为“带回调的 JSON”,它是 JSON 的一种使用模式。
参考文章:jsonp 详解 —— 终于搞懂 jsonp 了 (opens new window)
# document load 和 document ready 的区别
window.addEventListener('load', function () {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})
2
3
4
5
6
# ==
和 ===
的不同
- == 会尝试进行类型转换
- === 严格相等
# 函数声明与函数表达式的区别?
const res = sum(10, 20)
console.log(res) // 30
// 函数声明
function sum(x, y) {
return x + y
}
const res = sum(100, 200)
console.log(res) // 报错!!!
// 函数表达式
const sum = function(x, y) {
return x + y
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# new Object()
和 Object.create()
的区别
参考文章:解读new Object()和Object.create()的区别 (opens new window)
{}
等同于new Object() ,原型Object.prototype- Object.create(null)没有原型
- Object.create(f..)可指定原型
const obj1 = {
a: 10,
b: 20,
sum() {
return this.a + this.b
}
}
const obj2 = new Object({
a: 10,
b: 20,
sum() {
return this.a + this.b
}
})
const obj3 = Object.create({
a: 10,
b: 20,
sum() {
return this.a + this.b
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const obj = {a: 1 }
const obj2 = Object.create(obj); // 相当于把obj2的原型指向obj。通过obj2.a修改,obj.a也会发生变化
const obj1 = {
a: 10,
b: 20,
sum() {
return this.a + this.b
}
}
const obj2 = new Object(obj1)
console.log(obj1 === obj2) // true
const obj3 = Object.create(obj1)
console.log(obj1 === obj3) // false
const obj4 = Object.create(obj1)
console.log(obj3 === obj4) // false
// 然后修改 obj1 ,看 obj2 obj3 和 obj4
// obj2,3,4都可以找到printA函数
// obj2直接挂载有, obj3,4需要通过原型链查找
obj1.printA = function () {
console.log(this.a)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 对作用域上下文和 this 的理解,场景题
const User = {
count: 1,
getCount: function() {
return this.count
}
}
console.log(User.getCount()) // 1
const func = User.getCount
console.log( func() ) // undefined
2
3
4
5
6
7
8
9
# 对作用域和自由变量的理解,场景题
let i
for(i = 1; i <= 3; i++) {
setTimeout(function(){
console.log(i) // 输出3个4
}, 0)
}
2
3
4
5
6
# 判断字符串以字母开头,后面可以是数字,下划线,字母,长度为 6-30
const reg = /^[a-zA-Z]\w{5,29}$/
- 查看正则表达式规则 https://www.runoob.com/regexp/regexp-syntax.html
- 查看常见正则表达式
/\d{6}/ // 邮政编码
/^[a-z]+$/ // 小写英文字母
/^[A-Za-z]+$/ // 英文字母
/^\d{4}-\d{1,2}-\d{1,2}$/ // 日期格式
/^[a-zA-Z]\w{5,17}$/ // 用户名(字母开头,字母数字下划线,5-17位)
/\d+\.\d+\.\d+\.\d+/ // 简单的 IP 地址格式
2
3
4
5
6
# 以下代码,分别 alert 出什么?
let a = 100
function test() { // 会被提升到作用域顶部, 里面的内容先不要看,执行的时候再看
alert("test里面第一个a="+ a)
a = 10
alert("test里面第二个a="+ a)
}
test() // 输出100, 10
alert("外面的a="+ a) // 输出 10
2
3
4
5
6
7
8
# 手写 trim 函数,保证浏览器兼容性
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
}
console.log(' hello '.trim()); // 输出 "hello"
2
3
4
# 如何获取多个数值中的最大值?
Math.max(10, 30, 20, 40)
// 以及 Math.min
2
function max() {
const nums = Array.prototype.slice.call(arguments);
let max = nums[0];
nums.forEach(item => {
if(item > max) max = item;
})
return max
}
console.log(max(10,20,30,40))
2
3
4
5
6
7
8
9
# 如何用 JS 实现继承?
- class继承
- prototype继承
# 程序中捕获异常的方法
try {
// todo
} catch (ex) {
console.error(ex) // 手动捕获 catch
} finally {
// todo
}
2
3
4
5
6
7
// 自动捕获 catch(但对跨域的 js 如 CDN 的,不会有详细的报错信息)
window.onerror = function (message, source, lineNom, colNom, error) {
}
2
3
# 什么是 JSON ?
首先,json 是一种数据格式标准,本质是一段字符串,独立于任何语言和平台。注意,json 中的字符串都必须用双引号。
{
"name": "张三",
"info": {
"single": true,
"age": 30,
"city": "北京"
},
"like": ["篮球", "音乐"]
}
2
3
4
5
6
7
8
9
其次,JSON 是 js 中一个内置的全局变量,有 JSON.parse
和 JSON.stringify
两个 API 。
# 获取当前页面 url 参数
// const url = 'https://www.xxx.com/path/index.html?a=100&b=200&c=300#anchor'
function query(name) {
const search = location.search.substring(1); // 去掉?号
// search:a=100&b=200&c=300
// 写new RegExp就不用写// /^abc$/ 和 new RegExp('^abc$', 'i') 效果一样
// (^|&) 开始或者&符号 例如a=100, &b=100都可以匹配到 小括号代表一个组
// ([^&]*) 匹配除了&符号之外的字符串,这个括号代表一个组
// (&|$) 结束或者&符号 例如a=100, &b=100都可以匹配到
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
const res = search.match(reg);
console.log(res) // 0: "a=100&", 1: "", 2: "100", 3: "&"
if (res != null) {
return decodeURIComponent(res[2]); // 解码
}
return null
}
query('a')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
新的 API URLSearchParams
const pList = new URLSearchParams(location.search)
console.log(pList) // URLSearchParams {size: 2}
console.log(pList.get('a')) // 100
2
3
# 将 url 参数解析为 JS 对象?
function parseUrl() {
const search = location.search.substring(1);
const kvList = search.split('&');
const result = {};
for (let kv of kvList) {
const [key, value] = kv.split('=');
result[key] = decodeURIComponent(value);
}
return result;
}
2
3
4
5
6
7
8
9
10
新的 API URLSearchParams
function queryObject() {
const kvList = new URLSearchParams(location.search)
const res = {}
kvList.forEach((value, key) => {
res[key] = value
})
return res
}
console.log(queryObject())
2
3
4
5
6
7
8
9
# 实现数组 flatern ,考虑多层级
function flat(arr) {
// 验证 arr 中,还有没有深层数组,如 [1, [2, 3], 4]
const isDeep = arr.some(item => item instanceof Array);
// 如果没有深层数组了,就直接返回
if(!isDeep) {
return arr;
}
// 多深层的,则 concat 拼接
const res = Array.prototype.concat.apply([], arr);
// console.log(res)
return flat(res); // 递归调用,考虑多层
}
const res = flat([[1,2], 3, [4,5, [6,7, [8, 9, [10, 11]]]]])
console.log(res)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 数组去重
要考虑:
- 顺序是否一致?
- 时间复杂度
ES5 语法手写。
// 解法一: 顺序不能保证
function unique(arr) {
const obj = {}
arr.forEach(item => {
obj[item] = 1
// 用 Object ,去重计算高效,但顺序不能保证。以及,非字符串会被转换为字符串!!!
})
return Object.keys(obj).map(item => +item)
}
console.log(unique([30, 10, 20, 30, 40, 10]))
2
3
4
5
6
7
8
9
10
// 解法二
function unique2(arr) {
const res = []
arr.forEach(item => {
if (res.indexOf(item) === -1) {
// 用数组,每次都得判断是否重复(低效),但能保证顺序
res.push(item)
}
})
return res
}
console.log(unique2([30, 10, 20, 30, 40, 10]))
2
3
4
5
6
7
8
9
10
11
12
用 ES6 Set
// 解法三: 利用 Set
function unique3(arr) {
return Array.from(new Set(arr))
}
console.log(unique3([30, 10, 20, 30, 40, 10]))
2
3
4
5
# 手写深拷贝
【注意】Object.assign
不是深拷贝
Object.assign(obj1, {...})
const obj2 = Object.assign({}, obj1, {...})
function deepClone(obj) {
if(typeof obj !== 'object' || obj == null) {
return obj
}
// 初始化返回结果
let result;
if(obj instanceof Array) {
result = []
}else {
result = {}
}
for(let key in obj) {
// 保证key不是原型的属性
if(obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key])
}
}
return result;
}
const obj = {
a: 1,
b: {
c: 2
}
}
const obj2 = deepClone(obj)
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
# RAF requestAnimationFrame
想用 JS 去实现动画,老旧的方式是用 setTimeout 实时刷新,但这样效率非常低,而且可能会出现卡顿。
- 想要动画流畅,更新频率是 60帧/s ,即每 16.6ms 更新一次试图。太慢了,肉眼会感觉卡顿,太快了肉眼也感觉不到,资源浪费。
- 用 setTimeout 需要自己控制这个频率,而 requestAnimationFrame 不用自己控制,浏览器会自动控制
- 在后台标签或者隐藏的
<iframe>
中,setTimeout 依然会执行,而 requestAnimationFrame 会自动暂停,节省计算资源
传统方法
let currWidth = 100;
let endWidth = 640;
const app = document.getElementById('app');
// 3s 把宽度从 100px 变为 640px ,即增加 540px
function animate() {
currWidth += 3;
app.style.width = currWidth + 'px';
if(currWidth < endWidth) {
setTimeout(animate, 16.7); // 自己控制时间
}
}
animate();
2
3
4
5
6
7
8
9
10
11
12
let currWidth = 100;
let endWidth = 640;
const app = document.getElementById('app');
function animate() {
currWidth += 3;
app.style.width = currWidth + 'px';
if(currWidth < endWidth) {
window.requestAnimationFrame(animate); // 时间不用自己控制
}
}
animate()
2
3
4
5
6
7
8
9
10
11
# 对前端性能优化有什么了解?一般都通过那几个方面去优化的?
原则
- 多使用内存、缓存或者其他方法
- 减少 CPU 计算、较少网络
方向
- 加载页面和静态资源
- 页面渲染
# Map 和 Object 的不同
# API 不同
// 初始化
const m = new Map([
['key1', 'hello'],
['key2', 100],
['key3', { x: 10 }]
])
// 新增
m.set('name', '张三')
// 删除
m.delete('key1')
// 判断
m.has('key2')
// 遍历
m.forEach((value, key) => console.log(key, value))
// 长度(Map 是有序的,下文会讲,所有有长度概念)
m.size
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 以任意类型为 key
const m = new Map()
const o = { p: 'Hello World' }
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
2
3
4
5
6
7
8
9
# Map 是有序结构
Object key 不能按照既定顺序输出
// Object keys 是无序的
const data1 = {'1':'aaa','2':'bbb','3':'ccc','测试':'000'}
Object.keys(data1) // ["1", "2", "3", "测试"]
const data2 = {'测试':'000','1':'aaa','3':'ccc','2':'bbb'};
Object.keys(data2); // ["1", "2", "3", "测试"]
2
3
4
5
6
Map key 可以按照既定顺序输出
const m1 = new Map([
['1', 'aaa'],
['2', 'bbb'],
['3', 'ccc'],
['测试', '000']
])
m1.forEach((val, key) => { console.log(key, val) })
const m2 = new Map([
['测试', '000'],
['1', 'aaa'],
['3', 'ccc'],
['2', 'bbb']
])
m2.forEach((val, key) => { console.log(key, val) })
2
3
4
5
6
7
8
9
10
11
12
13
14
# Map 很快
Map 作为纯净的 key-value 数据结构,它比 Object 承载了更少的功能。 Map 虽然有序,但操作很快,和 Object 效率相当。
// Map
const m = new Map()
for (let i = 0; i < 1000 * 10000; i++) {
m.set(i + '', i)
}
console.time('map find')
m.has('2000000')
console.timeEnd('map find')
console.time('map delete')
m.delete('2000000')
console.timeEnd('map delete')
2
3
4
5
6
7
8
9
10
11
另外,Map 有序,指的是 key 能按照构架顺序输出,并不是说它像数组一样是一个有序结构 —— 否则就不会这么快了
但这就足够满足我们的需求了。
# WeakMap
WeakMap 也是弱引用。但是,WeakMap 弱引用的只是键名 key ,而不是键值 value。
// 函数执行完,obj 会被销毁,因为外面的 WeakMap 是“弱引用”,不算在内
const wMap = new WeakMap()
function fn() {
const obj = {
name: 'zhangsan'
}
// 注意,WeakMap 专门做弱引用的,因此 WeakMap 只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。其他的无意义
wMap.set(obj, 100)
}
fn()
// 代码执行完毕之后,obj 会被销毁,wMap 中也不再存在。但我们无法第一时间看到效果。因为:
// 内存的垃圾回收机制,不是实时的,而且是 JS 代码控制不了的,因此这里不一定能直接看到效果。
2
3
4
5
6
7
8
9
10
11
12
另外,WeakMap 没有 forEach
和 size
,只能 add
delete
has
。因为弱引用,其中的 key 说不定啥时候就被销毁了,不能遍历。
WeakMap 可以做两个对象的关联关系,而不至于循环引用,例如:
const userInfo = { name: '张三' }
const cityInfo = { city: '北京' }
// // 常规关联,可能会造成循环引用
// userInfo.city = cityInfo
// cityInfo.user = userInfo
// 使用 WeakMap 做关联,则无任何副作用
const user_to_city = new WeakMap()
user_to_city.set(userInfo, cityInfo)
2
3
4
5
6
7
8
9
10
# 总结
- key 可以是任意数据类型
- key 会按照构建顺序输出
- 很快
- WeakMap 弱引用
# Set 和数组的区别
# Set 元素不能重复
const arr = [10, 20, 30, 30, 40]
const set = new Set([10, 20, 30, 30, 40]) // 会去重
console.log(set) // Set(4) {10, 20, 30, 40}
2
3
// 数组去重
function unique(arr) {
const set = new Set(arr)
return [...set]
}
unique([10, 20, 30, 30, 40])
2
3
4
5
6
# API 不一样
// 初始化
const set = new Set([10, 20, 30, 30, 40])
// 新增(没有 push unshift ,因为 Set 是无序的,下文会讲)
set.add(50)
// 删除
set.delete(10)
// 判断
set.has(20)
// 长度
set.size
// 遍历
set.forEach(val => console.log(val))
// set 没有 index ,因为是无序的
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Set 是无序的,而数组是有序的
先看几个测试
- 数组:前面插入元素 vs 后面插入元素
- 数组插入元素 vs Set 插入元素
- 数组寻找元素 vs Set 寻找元素
// 构造一个大数组
const arr = []
for (let i = 0; i < 1000000; i++) {
arr.push(i)
}
// 数组 前面插入一个元素
console.time('arr unshift')
arr.unshift('a')
console.timeEnd('arr unshift') // unshift 非常慢
// 数组 后面插入一个元素
console.time('arr push')
arr.push('a')
console.timeEnd('arr push') // push 很快
// 构造一个大 set
const set = new Set()
for (let i = 0; i < 1000000; i++) {
set.add(i)
}
// set 插入一个元素
console.time('set test')
set.add('a')
console.timeEnd('set test') // add 很快
// 最后,同时在 set 和数组中,寻找一个元素
console.time('set find')
set.has(490000)
console.timeEnd('set find') // set 寻找非常快
console.time('arr find')
arr.includes(490000)
console.timeEnd('arr find') // arr 寻找较慢
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
- 无序:插入、查找更快
- 有序:插入、查找更慢
因此,如果没有强有序的需求,请用 Set ,会让你更快更爽!
# WeakSet
WeekSet 和 Set 类似,区别在于 —— 它不会对元素进行引用计数,更不容易造成内存泄漏。
// 函数执行完,obj 就会被 gc 销毁
function fn() {
const obj = {
name: 'zhangsan'
}
}
fn()
2
3
4
5
6
7
// 函数执行完,obj 不会被销毁,因为一直被外面的 arr 引用着
const arr = []
function fn() {
const obj = {
name: 'zhangsan'
}
arr.push(obj)
}
fn()
2
3
4
5
6
7
8
9
// 函数执行完,obj 会被销毁,因为外面的 WeakSet 是“弱引用”,不算在内
const wSet = new WeakSet()
function fn() {
const obj = {
name: 'zhangsan'
}
wSet.add(obj) // 注意,WeakSet 就是为了做弱引用的,因此不能 add 值类型!!!无意义
}
fn()
2
3
4
5
6
7
8
9
【注意】内存的垃圾回收机制,不是实时的,而且是 JS 代码控制不了的,因此这里不一定能直接看到效果。
WeekSet 没有 forEach
和 size
,只能 add
delete
和 has
。因为垃圾回收机制不可控(js 引擎看时机做垃圾回收),那其中的成员也就不可控。
# 总结
- Set 值不能重复
- Set 是无序结构
- WeakSet 对元素若引用
# reduce
数组求和
传统方式
function sum(arr) { let res = 0 arr.forEach(n => res = res + n) return res } const arr = [10, 20, 30] console.log( sum(arr) )
1
2
3
4
5
6
7reduce 方法的使用
const arr = [10, 20, 30, 40, 50] const res = arr.reduce((sum, currVal, index, arr) => { console.log("前一个值:", sum, "。当前值:", currVal, "。索引:", index, "。数组:", arr) return sum + currVal }, 0) console.log(res)
1
2
3
4
5
6
# reduce 的其他用法
// 计数
function count(arr, value) {
// 计算 arr 中有几个和 value 相等的数
return arr.reduce((a, v) => (v === value ? a + 1 : a + 0), 0)
}
const arr2 = [10, 20, 30, 40, 50, 10, 20, 10]
console.log( count(arr2, 20) )
2
3
4
5
6
7
// 数组输出字符串
const arr3 = [
{ name: 'xialuo', number: '100' },
{ name: 'madongmei', number: '101' },
{ name: 'zhangyang', number: '102' }
]
// 普通做法 1(需要声明变量,不好)
let arr3Str = ''
arr3.forEach(item => {
arr3Str += `${item.name} - ${item.number}\n`
})
console.log(arr3Str)
// 普通做法 2(map 生成数组,再进行 join 计算)
console.log(
arr3.map(item => {
return `${item.name} - ${item.number}`
}).join('\n')
)
// reduce 做法(只遍历一次,即可返回结果)
console.log(
arr3.reduce((str, item) => {
return `${str}${item.name} - ${item.number}\n`
}, '')
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Vue面试题
# Vue基本使用
# vue-cli 创建项目
安装最新版本 nodejs ,要求版本 >= 8.11
执行 npm i @vue/cli -g
,然后查看 vue --version
创建项目运行 vue create xxx
# 插值 指令
- 插值
- 表达式
- 指令(即
v-
开头的。后面的事件、循环、判断等也会用到指令,慢慢讲) - 指令缩写
<template>
<div>
<p>文本插值 {{message}}</p>
<p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
<p :id="dynamicId">动态属性 id</p>
<hr/>
<p v-html="rawHtml">
<span>有 xss 风险</span>
<span>【注意】使用 v-html 之后,将会覆盖子元素</span>
</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'hello vue',
flag: true,
rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
dynamicId: `id-${Date.now()}`
}
}
}
</script>
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
# computed 和 watch
- computed 是有缓存的,data 不变就不会重新计算
- watch
- 如何深度监听
- 引用类型无法拿到 oldVal
<template>
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2"/>
</div>
</template>
<script>
export default {
data() {
return {
num: 20
}
},
computed: {
// 计算属性有缓存,只要this.num不变,double1的值就一直是上一次计算的值
double1() {
return this.num * 2
},
double2: {
get() {
return this.num * 2
},
set(val) {
this.num = val/2
}
}
}
}
</script>
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
<template>
<div>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>
<script>
export default {
data() {
return {
name: '张三',
info: {
city: '北京'
}
}
},
watch: {
name(newVal, oldVal) {
// 值类型,可正常拿到 newVal 和 oldVal
console.log('watch name', newVal, oldVal)
},
info: {
handler(newVal, oldVal) {
// 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
// newVal 和 oldVal 都指向了同一个对象
console.log('watch info', newVal, oldVal)
},
deep: true // 深度监听
}
}
}
</script>
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
# class 和 style
- 使用动态属性
- 使用驼峰式写法
<template>
<div>
<p :class="{black: isBlack, yellow: isYellow}">使用 class</p>
<p :class="[black, yellow]">使用 class (数组)</p>
<p :style="styleData">使用 style</p>
</div>
</template>
<script>
export default {
data() {
return {
isBlack: true,
isYellow: true,
black: 'black',
yellow: 'yellow',
styleData: {
fontSize: '40px', // 转换为驼峰式
color: 'red',
backgroundColor: '#ccc' // 转换为驼峰式
}
}
}
}
</script>
<style scoped>
.black {
background-color: #999;
}
.yellow {
color: yellow;
}
</style>
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
# 条件
v-if 和 v-show 的区别,以及使用场景 —— 频繁切换用 v-show ,否则用 v-if
<template>
<div>
<p v-if="type === 'a'">A</p>
<p v-else-if="type === 'b'">B</p>
<p v-else>other</p>
<p v-show="type === 'a'">A by v-show</p>
<p v-show="type === 'b'">B by v-show</p>
</div>
</template>
<script>
export default {
data() {
return {
type: 'a'
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 循环
- 遍历数组,遍历对象
- key
- v-for 和 v-if 不要一起用
- v-for 会优先于 v-if 执行
- 因此 v-if 会在每一个 v-for 中都计算一遍
- v-if 和 v-for 拆开使用
<template>
<div>
<p>遍历数组</p>
<ul>
<li v-for="(item, index) in listArr" :key="item.id">
{{index}} - {{item.id}} - {{item.title}}
</li>
</ul>
<p>遍历对象</p>
<ul >
<li v-for="(val, key, index) in listObj" :key="key">
{{index}} - {{key}} - {{val.title}}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
flag: false,
listArr: [
{ id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
{ id: 'b', title: '标题2' },
{ id: 'c', title: '标题3' }
],
listObj: {
a: { title: '标题1' },
b: { title: '标题2' },
c: { title: '标题3' },
}
}
}
}
</script>
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
# 事件
【注意】vue 事件是被注册到当前 DOM 元素的,和 React 不一样
- 传参
- event 参数
- 事件修饰符
- 按键修饰符
- 【注意】用 vue 绑定的事件,组建销毁时会自动被解绑。自己绑定的事件,需要自己销毁。
事件修饰符
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
按键修饰符
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
2
3
4
5
6
7
8
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
},
methods: {
increment1(event) {
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
console.log(event.target)
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
this.num++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val, event) {
console.log(event.target)
this.num = this.num + val
},
loadHandler() {
// do some thing
console.log('window load')
}
},
mounted() {
window.addEventListener('load', this.loadHandler)
},
beforeDestroy() {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', this.loadHandler)
}
}
</script>
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
# 表单
- textarea 要用 v-model
修饰符 lazy number trim
<template>
<div>
<p>输入框: {{name}}</p>
<input type="text" v-model.trim="name"/>
<input type="text" v-model.lazy="name"/>
<input type="text" v-model.number="age"/>
<p>多行文本: {{desc}}</p>
<textarea v-model="desc"></textarea>
<!-- 注意,<textarea>{{desc}}</textarea> 是不允许的!!! -->
<p>复选框 {{checked}}</p>
<input type="checkbox" v-model="checked"/>
<p>多个复选框 {{checkedNames}}</p>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<p>单选 {{gender}}</p>
<input type="radio" id="male" value="male" v-model="gender"/>
<label for="male">男</label>
<input type="radio" id="female" value="female" v-model="gender"/>
<label for="female">女</label>
<p>下拉列表选择 {{selected}}</p>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>下拉列表选择(多选) {{selectedList}}</p>
<select v-model="selectedList" multiple>
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
name: '张三',
age: 18,
desc: '自我介绍',
checked: true,
checkedNames: [],
gender: 'male',
selected: '',
selectedList: []
}
}
}
</script>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 组件使用
- props 类型和默认值
- $emit 执行父组件的事件
- vue 自带 eventBus 功能
# vue 生命周期
单个组件生命周期
vue 生命周期
- 挂载过程
- 更新过程
- 销毁过程
beforeCreate
在组件实例初始化完成之后立即调用。
会在实例初始化完成、props 解析之后、
data()
和computed
等选项处理之前立即调用。注意,组合式 API 中的
setup()
钩子会在所有选项式 API 钩子之前调用,beforeCreate()
也不例外。
created
在组件实例处理完所有与状态相关的选项后调用。
当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此
$el
属性仍不可用。
beforeMount
在组件被挂载之前调用。
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
这个钩子在服务端渲染时不会被调用。
mounted
在组件被挂载之后调用。
组件在以下情况下被视为已挂载:
- 所有同步子组件都已经被挂载。(不包含异步组件或
<Suspense>
树内的组件) - 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用 (opens new window)中用于确保 DOM 相关代码仅在客户端被调用。
这个钩子在服务端渲染时不会被调用。
- 所有同步子组件都已经被挂载。(不包含异步组件或
beforeUpdate
组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
这个钩子在服务端渲染时不会被调用。
updated
在组件因为一个响应式状态变更而更新其 DOM 树之后调用。
父组件的更新钩子将在其子组件的更新钩子之后调用。
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() (opens new window) 作为替代。
这个钩子在服务端渲染时不会被调用。
beforeUnmount
在一个组件实例被卸载之前调用。
当这个钩子被调用时,组件实例依然还保有全部的功能。
这个钩子在服务端渲染时不会被调用。
unmounted
在一个组件实例被卸载之后调用。
一个组件在以下情况下被视为已卸载:
- 其所有子组件都已经被卸载。
- 所有相关的响应式作用 (渲染作用以及
setup()
时创建的计算属性和侦听器) 都已经停止。
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务端渲染时不会被调用。
index.vue
<template>
<div>
<Input @add="addHandler"/>
<List :list="list" @delete="deleteHandler"/>
</div>
</template>
<script>
import Input from './Input'
import List from './List'
export default {
components: {
Input,
List
},
data() {
return {
list: [
{
id: 'id-1',
title: '标题1'
},
{
id: 'id-2',
title: '标题2'
}
]
}
},
methods: {
addHandler(title) {
this.list.push({
id: `id-${Date.now()}`,
title
})
},
deleteHandler(id) {
this.list = this.list.filter(item => item.id !== id)
}
},
beforeCreate() {
console.log('index beforeCreate')
},
created() {
console.log('index created')
},
beforeMount() {
console.log('index beforeMount')
},
mounted() {
console.log('index mounted')
},
beforeUpdate() {
console.log('index before update')
},
updated() {
console.log('index updated')
}
}
</script>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Input.vue
<template>
<div>
<input type="text" v-model="title"/>
<button @click="addTitle">add</button>
</div>
</template>
<script>
import event from './event'
export default {
data() {
return {
title: ''
}
},
methods: {
addTitle() {
// 调用父组件的事件
this.$emit('add', this.title)
// 调用自定义事件
event.$emit('onAddTitle', this.title)
this.title = ''
}
}
}
</script>
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
List.vue
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
<button @click="deleteItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import event from './event'
export default {
// props: ['list']
props: {
// prop 类型和默认值
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
}
},
methods: {
deleteItem(id) {
this.$emit('delete', id)
},
addTitleHandler(title) {
console.log('on add title', title)
}
},
beforeCreate() {
console.log('list beforeCreate')
},
created() {
console.log('list created')
},
beforeMount() {
console.log('list beforeMount')
},
mounted() {
console.log('list mounted')
// 绑定自定义事件
event.$on('onAddTitle', this.addTitleHandler)
},
beforeUpdate() {
console.log('list before update')
},
updated() {
console.log('list updated')
},
beforeDestroy() {
// 及时销毁,否则可能造成内存泄露
event.$off('onAddTitle', this.addTitleHandler)
}
}
</script>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
event.js
import Vue from 'vue'
export default new Vue()
2
3
考虑父子组件的生命周期,参考代码中 index 和 List 组件。
创建阶段
- index beforeCreated
- index created
- index beforeMounted
- list created
- list beforeMounted
- list mounted
- index mounted
更新阶段
- index before update
- list before update
- list updated
- index updated
销毁阶段
- index before destroy
- list before destroy
- list destroyed
- index destroyed
# Vue高级特性
# 自定义 v-model
<template>
<div>
<p>text: {{ text1 }}</p>
<input type="text1" :value="text1" @input="setModelValue" />
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 change1 和 model.event1 要对应起来
3. text1 属性对应起来
-->
</div>
</template>
<script>
export default {
model: {
prop: "text1", // 对应 props text1
event: "change",
},
props: {
text1: {
type: String,
default: "默认值",
},
},
methods: {
setModelValue(event) {
console.log(event.target.value);
this.$emit("change", event.target.value);
},
},
};
</script>
<style>
</style>
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
使用
<CustomVModel v-model="name"></CustomVModel>
# $nextTick
- Vue是异步渲染
- data改变之后,DOM不会立刻渲染
- $nextTick 会在 DOM渲染之后被触发,以获取最新DOM节点
<template>
<div id="app">
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
console.log(this.$refs.ul1.childNodes.length) // 获取到的是3,添加之前的
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
console.log( ulElem.childNodes.length ) // 获取到的是最新的6
})
}
}
}
</script>
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
# slot
作用域插槽
即子组件管理数据,父组件通过插槽的作用域来获取。
子组件传值给父组件
<template>
<a :href="url">
<!-- slotData这个名字可以替换成任何你想要的名字 -->
<slot :slotData="website">
{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {
website: {
url: 'http://wangEditor.com/',
title: 'wangEditor',
subTitle: '轻量级富文本编辑器'
}
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
父组件获取
<SlotScope url="http://www.baidu.com">
<!-- v-slot这个名字也是任意的 -->
<template v-slot="scoped">
{{ scoped.slotData.title }}
</template>
</SlotScope>
2
3
4
5
6
具名插槽,参考以下代码示例
<!-- NamedSlot 组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
2
3
4
5
6
7
8
9
10
11
12
<NamedSlot>
<template v-slot:header> <!-- 缩写 <template #header> -->
<h1>将插入 header slot 中</h1>
</template>
<p>将插入到 main slot 中,即未命名的 slot</p>
<template v-slot:footer>
<p>将插入到 footer slot 中</p>
</template>
</NamedSlot>
2
3
4
5
6
7
8
9
10
11
# 动态组件
:is= "component-name”
用法- 需要根据数据,动态渲染的场景。即组件类型不确定。
关键代码如下,其中 TplDemoName
要有定义
<component v-bind:is="TplDemoName"></component>
import TplDemo from '../BaseUse/TplDemo'
export default {
components: {
TplDemo
},
data() {
return {
TplDemoName: 'TplDemo'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# 异步组件
什么时候用到,什么时候再加载
<template>
<div id="app">
<SlotScope url="http://www.baidu.com">
<!-- v-slot这个名字也是任意的 -->
<template v-slot="scoped">
{{ scoped.slotData.title }}
</template>
</SlotScope>
<!-- 动态组件 -->
<component :is="currentComponent"></component>
<!-- 异步组件 -->
<FormDemo v-if="show"></FormDemo>
<button @click="show = true">点击展示</button>
</div>
</template>
<script>
// 这是同步写法
// import FormDemo from './views/基本使用/FormDemo.vue'
export default {
name: 'app',
components: {
FormDemo: () => import("./views/基本使用/FormDemo.vue"),
// 相当于
// FormDemo: () => {
// return import("./views/基本使用/FormDemo.vue")
// }
SlotScope
},
data() {
return {
currentComponent: "SlotScope",
show: false
}
}
}
</script>
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
# keep-alive
- 缓存组件
- 频繁切换,不需要重复渲染
- Vue常见性能优化
<keep-alive> <!-- tab 切换 -->
<KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
<KeepAliveStageB v-if="state === 'B'"/>
<KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>
2
3
4
5
不加keep-alive组件切换的时候,会销毁其他的组件
# mixins
MyMixins.js
export default {
data() {
return {
city: '北京'
}
},
methods: {
showName() {
console.log(this.name)
}
},
mounted() {
// 自己定义的变量和函数都可能会存在变量冲突
// 但是周期函数之类的不会
console.log('mixin mounted', this.name)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Mixins.vue
<template>
<div>
Mixinx
</div>
</template>
<script>
import MyMixins from './MyMixins';
export default {
mixins: [MyMixins],
data() {
return {
name: '张三'
}
},
mounted() {
console.log('mounted')
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# vuex 使用
官方文档: https://vuex.vuejs.org/zh/
- state
- getters
- action
- mutation
- 用于 vue
- dispatch
- commit
- mapState
- mapGetters
- mapActions
- mapMutations
import { getItem, setItem } from '@/utils/storage'
const state = {
sidebar: {
opened: true,
withoutAnimation: false
},
language: getItem('lang') || 'zh'
}
const mutations = {
/**
* 设置菜单栏切换
* @param state
* @constructor
*/
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
},
/**
* 设置国际化
* @param state
* @param lang
* @constructor
*/
SET_LANGUAGE: (state, lang) => {
setItem('lang', lang)
state.language = lang
}
}
const actions = {
/**
* 菜单栏切换
* @param commit
*/
toggleSidebar ({ commit }) {
commit('TOGGLE_SIDEBAR')
},
/**
* 语言国家化切换
* @param commit
* @param lang
*/
setLanguage ({ commit }, lang) {
commit('SET_LANGUAGE', lang)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
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
48
49
50
51
52
53
54
55
# vue router 使用
官方文档:https://router.vuejs.org/zh/introduction.html
- vue-router
- 动态路由
- to 和 push
- hash 和 history
- 懒加载(配合动态组件)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
export default new VueRouter({
routes: [
{
path: '/',
component: () => import(
/* webpackChunkName: "navigator" */
'./../components/Navigator'
)
},
{
path: '/feedback',
component: () => import(
/* webpackChunkName: "feedback" */
'./../components/FeedBack'
)
}
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const User = {
// 获取参数如 10 20
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头。能命中 `/user/10` `/user/20` 等格式的路由
{ path: '/user/:id', component: User }
]
})
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
mode: 'history', // 使用 h5 history 模式
routes: [...]
})
2
3
4
# Vue原理
# 组件化
web 开发的历史中,组件化其实很早就有了,在 jsp asp 就有了。
vue 组件
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
2
3
4
5
6
React 组件
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<HelloWorld msg="Welcome to Your React App"/>
</header>
</div>
);
}
2
3
4
5
6
7
8
9
10
# 数据驱动视图
- 传统组件,只是静态渲染,更新还要依赖于操作DOM
- 数据驱动视图-Vue MVVM
- 数据驱动视图-React setState
vue —— MVVM 模型
<template>
<div id="app">
<p @click="changeName">{{name}}</p>
<ul>
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
name: 'vue',
list: ['a', 'b', 'c']
}
},
methods: {
changeName() {
this.name = '张三'
},
addItem() {
this.list.push(`${Date.now()}`)
}
}
}
</script>
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
React —— setState 方式
class List extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'React',
list: ['a', 'b', 'c']
}
}
render() {
return <div>
<p onClick={this.changeName.bind(this)}>{this.state.name}</p>
<ul>{
this.state.list.map((item, index) => {
return <li key={index}>{item}</li>
})
}</ul>
<button onClick={this.addItem.bind(this)}>添加一项</button>
</div>
}
changeName() {
this.setState({
name: '张三'
})
}
addItem() {
this.setState({
list: this.state.list.concat(`${Date.now()}`) // 使用不可变值
})
}
}
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
vue 和 React 对比
都支持组件化,没有啥区别。(在这一点和 ejs 也没啥区别)
数据驱动视图
- vue 声明式
- React 函数式
程序员必须要知道的编程范式,你掌握了吗? (opens new window)
# vue 2.x 响应式
组件data的数据一旦变化,立刻触发视图的更新
核心API - Object.defineProperty
Object.defineProperty 的一些缺点(不能监听数组,新增删除属性无法监听)( Vue3.0启用Proxy )
Proxy兼容性不好,且无法polyfill
// Object.defineProperty 的基本用法
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, "name", {
get: function () {
console.log('get')
return name
},
set: function (newVal) {
console.log('set')
name = newVal
}
});
// 测试
console.log(data.name) // get zhangsan
data.name = 'lisi' // set
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object.defineProperty缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(Vue.set Vue.delete )
- 无法监听数组,需要特殊处理
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(methodName => {
arrProto[methodName] = function() {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if(newValue != value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if(typeof target != 'object' || target == null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for(let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# vdom 和 diff
虚拟DOM(Virtual DOM)和diff
# 背景
- DOM操作非常耗费性能
- 以前用jQuery ,可以自行控制DOM操作的时机,手动调整
- Vue和 React是数据驱动视图,如何有效控制DOM操作?
解决方案vDom
- 有了一定复杂度,想减少计算次数比较难
- 能不能把计算,更多的转移为JS计算,因为JS执行速度很快
- vdom -用JS模拟DOM结构,计算出最小的变更,操作DOM
用 vnode 表示真实 DOM 结构
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>
2
3
4
5
6
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
}
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: { style: 'font-size: 20px' }
children: [
{
tag: 'li',
children: 'a'
}
// ....
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Snabbdom
https://github.com/snabbdom/snabbdom
# diff 算法
# diff 算法概述
diff 算法是一个很广泛的,前端常见的例如文本 diff ,json 对象 diff ,还有这里的“树 diff”。
- 文本 diff ,例如 linux 的 diff 命令
- json diff ,例如 https://github.com/cujojs/jiff
- 树 diff ,如 vdom diff
diff 两棵树的时间复杂度是 O(n^3)
(不可用的复杂度),例如 diff(Tree1, Tree2)
- 遍历 Tree1 ,每个节点都要和 Tree2 对比
- 针对 Tree1 的节点,遍历 Tree2 每个节点和它对比
- 重新排序
但是,vdom diff 算法做了几个改进,让复杂度变为 O(n)
- 只比较同一层级
- tag 或组件不相同的,直接删掉重建,不再继续深入比较
- tag 或组件 & key ,两个都相同的,即认为是相同节点
# diff 算法过程详解
snabbdom https://github.com/snabbdom/snabbdom 是一款比较简洁、高性能的 vdom lib vue2.x 的 diff 算法完全参考它。 即了解 snabbdom 的 diff 算法,也就了解 vue2.x 的 diff 算法。应该面试的 diff 算法问题足够了。
基本流程
- 回顾一下它的基本使用,找出核心的 API
h
patch
- 下载 snabbdom 源码
- 查看源码
注意
- 解读源码,只看主干和要点,不要去扣细节
- 源码是 ts ,但不妨碍我们阅读,不要关注语法细节
# h 函数
【功能】h 函数是一个工厂函数,根据传入的参数,生成 vnode 结构。
源码在 src/h.ts
输入和输出 function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
返回 return vnode(sel, data, children, text, undefined);
# vnode 函数
源码在 src/vnode.ts
返回 return {sel, data, children, text, elm, key};
这里可以结合 demo 中的断点来看数据结构。此时的 elem
应该是 undefined
# text 和 children
一个元素或者有 contentText ,后者有 children ,两者不能共存 demo 中有示例
# patch 函数
【功能】patch 函数将 newVnode 更新到 vnode 或者 elem 上,patch 的过程也就是 diff 的过程。
源码 src/snabbdom.ts
,找到其中的 init
函数,最后返回的就是 patch
函数。
输入输出 function patch(oldVnode: VNode | Element, vnode: VNode): VNode
(画图:elem 和 oldVnode vnode 的关系) (要考虑第一个参数是 VNode 和 Element 两种情况)
# patchVnode 函数
源码在 src/snabbdom.ts
先看 addVnodes
和 removeVnodes
,最后看 updateChildren
# updateChildren 函数
源码在 src/snabbdom.ts
以 todo list 的 items 变化,为例,图解演示即可
# 模板解析
# with 语法
- 改变 {} 内自由变量,当做 obj 属性来查找
- 如果找不到会报错
- with 要慎用,因为它打破了作用域的规则
const obj = {a: 100, b: 200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined
// 使用 with ,能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找
with(obj) {
console.log(a)
console.log(b)
console.log(c) // 会报错 !!!
}
2
3
4
5
6
7
8
9
10
11
12
13
# 编译模版
- 模板编译为render函数,执行render 函数返回vnode
- 基于vnode 再执行patch和diff (后面会讲)
- 使用webpack vue-loader ,会在开发环境下编译模板(重要)
编译 render 函数
- 原生 html 不识别指令
- html 是静态标记语言,不具备运算能力(不是图灵完备的语言,不能顺序、判断、循环运算)
- 将 html 模板转换为 js 函数(render 函数)
- 执行 render 函数,生成 vnode。重要!!!
npm install vue-template-compiler
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 属性和动态属性
// const template = `
// <div id="div1" class="container">
// <img :src="imgUrl"/>
// </div>
// `
// with(this){return _c('div',
// {staticClass:"container",attrs:{"id":"div1"}},
// [
// _c('img',{attrs:{"src":imgUrl}})])}
// // 条件
// const template = `
// <div>
// <p v-if="flag === 'a'">A</p>
// <p v-else>B</p>
// </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
// const template = `
// <ul>
// <li v-for="item in list" :key="item.id">{{item.title}}</li>
// </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
// const template = `
// <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# vue 函数式组件
https://v2.cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6
使用 render 函数代替 template
Vue.component('heading', {
// template: `xxxx`,
render: function (createElement) {
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
}, 'this is a tag')
]
)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 模板到render 函数,再到vnode ,再到渲染和更新
- vue组件可以用render代替template
# 渲染过程
# 初次渲染过程
- 对 data 进行响应式处理,监听 get set
- 解析模板为 render 函数(这一步可能在开发环境打包时就已经完成,重要!!!)
- 执行 render 函数,生成 vnode
- 这一步会触发 data getter ,收集依赖,重要!!!
- 将该数据 “观察”起来
- 注意,不一定所有的 data 都会被观察,得看模板中是否用到了,如下图。
- 将 vnode 渲染到页面上
<p>{{message}}</p>
<script>
export default {
data() {
return {
message: 'hello', // 会触发 get
city: '北京' // 不会触发 get ,因为模板没用到,即和视图没关系
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
# 数据更新过程
- 修改“被观察的”数据,触发 data setter
- 重新执行 render 函数,生成 newVnode
- patch(vnode, newVnode)
# 异步渲染
- 异步渲染
- 汇总 data 变化,一次性渲染
- 尽量减少渲染次数,提高性能
# 前端路由
- hash
- history API
# hash
hash 是什么
// http://127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol // 'http:'
location.hostname // '127.0.0.1'
location.host // '127.0.0.1:8881'
location.port // '8881'
location.pathname // '/01-hash.html'
location.search // '?a=100&b=20'
location.hash // '#/aaa/bbb'
2
3
4
5
6
7
8
hash 的特点,重要!!!
- 会触发页面跳转,即可后退、前进
- 但不会刷新页面,支持 SPA 必须的特性
- hash 不会被提交到 server 端(因此刷新页面也会命中当前页面,让前端根据 hash 处理路由)
url 中的 hash ,是不会发送给 server 端的。前端 onhashchange
拿到自行处理。
onhashchange 监听 hash 变化
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash', location.hash)
})
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash', location.hash)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# history API
常用的两个 API
- history.pushState
- window.onpopstate
页面刷新时,服务端要做处理。即无论什么 url 访问 server ,都要返回该页面。
需要 server 端配合,可参考 https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
正常页面浏览
按照 url 规范,不同的 url 对应不同的资源,例如:
- https://github.com/ 首页 (刷新页面)
- https://github.com/username/ 用户页 (刷新页面)
- https://github.com/username/xxx/ 项目页 (刷新页面)
但是用了 SPA 的前端路由,就改变了这一规则,假如 github 用了的话:
- https://github.com/ 首页
- https://github.com/username/ 首页(前端处理路由,不刷新页面)
- https://github.com/username/xxx/ 首页(前端处理路由,不刷新页面)
所以,从开发者的实现角度来看,前端路由是一个违反规则的形式。 但是从不关心后端,只关心前端页面的用户,或者浏览器来看,更喜欢 pushState 这种方式。
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 总结
- 内部系统或 to B 的管理系统,用 hash 。简单易用,对 url 规范没有要求。
- to C 的页面,用 history API ,对 url 规范和 SEO 要求较高。—— 但需要服务端配合
# 面试题总结
前面出的几个面试题
v-if 和 v-show 的区别
- v-show通过cSS display 控制显示和隐藏
- v-if 组件真正的渲染和销毁,而不是显示和隐藏
- 频繁切换显示状态用v-show ,否则用v-if
为何 v-for 中要用
key
- 必须用key ,且不能是index和random
- diff 算法中通过tag和key来判断,是否是sameNode
- 减少渲染次数,提升渲染性能
描述 vue 组件生命周期(以及有父子组件,两者的生命周期)
组件间如何通讯
- 父子组件props和this.$emit
- 自定义事件event.$no event.$off event.$emit
- vuex
组件渲染和更新的过程
双向数据绑定 v-model 的原理
- input元素的value = this.name
- 绑定input事件this.name = $event.target.value
- data 更新触发re-render
补充面试题 —— 待补充
说一下对 MVVM 的理解
computed 和 method 的区别?
- computed有缓存,data不变不会重新计算
- 提高性能
为何组件 data 必须是一个函数?
为了保证组件的独立性和可复用性,如果
data
是个函数的话,每复用一次组件就会返回新的data
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果
data
是对象的话,对象属于引用类型,会影响到所有的实例。
ajax 请求应该放在哪个生命周期?
- mounted
- JS是单线程的,ajax异步获取数据
- 放在mounted之前没有用,只会让逻辑更加混乱
如何将父组件的所有 props 传递给子组件?—— $props
$props
<User v-bind= "$props”/>
如何自己实现一个 v-model ?
多组件有相同的逻辑,如何抽离?
- mixin
- 以及mixin的一些缺点
何时需要使用异步组件?
- 加载大组件
- 路由异步加载
何时需要使用 keep-alive
- 缓存组件,不需要重复渲染
- 如多个静态tab页的切换
- 优化性能
何时需要使用 beforeDestroy ?
- 解绑自定义事件
event.$off
- 清除定时器
- 解绑自定义的DOM事件,如window scroll 等
- 解绑自定义事件
什么是作用域插槽?为何需要它
vuex 的 action 和 mutation 有何区别?
- action中处理异步,mutation 不可以
- mutation做原子操作
- action可以整合多个mutation
vue-router 路由模式有几种? —— 三种 "hash" | "history" | "abstract"
abstract模式是一种不依赖于浏览器环境的路由模式,它可以在非浏览器环境中使用,如Node.js的服务器端渲染(SSR)。
abstract模式的特点是不会改变URL,而是通过修改内存中的路由状态来实现路由的切换。这种模式适用于一些特殊的场景,如服务器端渲染、桌面应用等。
如何配置 vue-router 异步加载?
场景题:用虚拟 DOM 描述一个 html 结构
vue 如何监听 data 变化?
- Object.defineProperty
vue 如何监听数组变化?
- Object.defineProperty 不能监听数组变化
- 重新定义原型,重写push pop 等方法,实现监听
- Proxy可以原生支持监听数组变化
响应式原理
- 监听data变化
- 组件渲染和更新的流程
简述 diff 算法过程
- patch(elem, vnode)和patch(vnode, newVnode)
- patchVnode和addVnodes和removeVnodes
- updateChildren ( key的重要性)
diff 算法时间复杂度是多少
- o(n)
- 在O(n^3)基础上做了一些调整
vue 为何是异步渲染?
- 异步渲染(以及合并data修改),以提高渲染性能
nextTick 有何作用?
- $nextTick在 DOM更新完之后,触发回调
vue-router 如何实现路由变化?
vue 性能优化 (开发环境编译模板)
- 合理使用v-show和v-if
- 合理使用computed
- v-for 时加key ,以及避免和v-if同时使用
- 自定义事件、DOM事件及时销毁
- 合理使用异步组件
- 合理使用keep-alive
- data层级不要太深
- 使用vue-loader在开发环境做模板编译(预编译)
- webpack层面的优化
- 前端通用的性能优化,如图片懒加载
- 使用SSR
# Vue3面试题
创建vue3项目 npm init vite-app vue3-demo
# Vue3 比 Vue2有什么优势?
- 性能更好
- 体积更小
- 更好的 ts 支持
- 更好的代码组织
- 利于复杂逻辑抽离 - Composition API
- 增加了新功能
Fragment
Teleport
Suspense
# Vue3生命周期
v2.x 的生命周期依然支持,需要注意的是:
- beforeDestroy 改名为 beforeUnmount
- destroyed 改名为 unmounted
新的 Composition API 有了新的生命周期。
# Options API生命周期
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
// beforeDestroy 改名
beforeUnmount() {
console.log('beforeUnmount')
},
// destroyed 改名
unmounted() {
console.log('unmounted')
}
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
# Composition API生命周期
// 等于 beforeCreate 和 created
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Composition API 对比 Options API
Composition API带来了什么
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
如何选择?
- 不建议共用,会引起混乱
- 小型项目、业务逻辑简单,用Options API
- 中大型项目、逻辑复杂,用Composition API
# 如何理解ref toRef 和toRefs
# ref
创建响应式对象一般用 reactive
- 生成值类型的响应式数据
- 可用于模板和reactive
- 通过
.value
修改值
<script>
import { ref, reactive } from "vue";
export default {
setup() {
const ageRef = ref(18); // 值类型 响应式
const nameRef = ref("张三");
const state = reactive({
name: nameRef,
});
setTimeout(() => {
console.log("ageRef", ageRef.value);
ageRef.value = 25; // .value 修改值
nameRef.value = "张三AAAA";
}, 1500);
return { ageRef, state };
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ref还可以用于获取DOM元素
import { onMounted } from 'vue';
<template>
<p ref="elemRef">我是一行文字</p>
</template>
<script>
import { ref, onMounted } from "vue";
export default {
setup() {
const elemRef = ref(null);
onMounted(() => {
console.log(elemRef.value); // 输出:<p>我是一行文字</p>
});
return {
elemRef,
};
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# toRef
可以用来为源响应式对象上的 property 新创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。
- 针对一个响应式对象( reactive封装)的prop
- 创建一个ref ,具有响应式
- 两者保持引用关系
【注意】必须是响应式对象,普通对象不具有响应式。
<template>
<p>toRef demo - {{ ageRef }} - {{ state.name }} {{ state.age }}</p>
</template>
<script>
import { reactive, toRef } from "vue";
export default {
name: "ToRef",
setup() {
const state = reactive({
age: 20,
name: "张三",
});
// toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 20,
// name: '张三'
// }
const ageRef = toRef(state, "age");
setTimeout(() => {
state.age = 25;
}, 1500);
setTimeout(() => {
ageRef.value = 30; // .value 修改值
}, 3000);
return {
ageRef,
state,
};
},
};
</script>
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
# toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref 。
- 将响应式对象( reactive封装)转换为普通对象
- 对象的每个prop都是对应的ref
- 两者保持引用关系
【注意】必须是响应式对象,普通对象不具有响应式。
<template>
<p>toRefs demo {{ age }} {{ name }}</p>
</template>
<script>
import { reactive, toRefs } from "vue";
export default {
name: "ToRefs",
setup() {
const state = reactive({
age: 20,
name: "张三",
});
// const stateAsRefs = toRefs(state); // 将响应式对象,变成普通对象
// const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
// return {
// ageRef,
// nameRef
// }
setTimeout(() => {
state.age = 25;
}, 1500);
return {
...toRefs(state),
// ...state // 这样会丢失响应式
};
},
};
</script>
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
# 合成函数返回响应式对象
# 总结
用reactive做对象的响应式,用ref 做值类型响应式
setup中返回toRefs(state),或者toRef(state,'xxx’)
ref的变量命名都用xxxRef
合成函数返回响应式对象时,使用toRefs
# 为什么需要ref
- 返回值类型,会丢失响应式
- 如在setup、computed、合成函数,都有可能返回值类型
- Vue 如不定义ref ,用户将自造ref ,反而混乱
Proxy对对象才能定义响应式,值类型要用ref
# 为何需要.value ?
因为要保证响应式,需要把值类型封装成一个对象形式,所以用 .value
表示这个值
- ref是一个对象(不丢失响应式) , value存储值
- 通过.value属性的get和set 实现响应式
- 用于模板(如
<p></p>
)、reactive(如state = reactive({age: ageRef})
)时,不需要.value,,其他情况都需要
// 伪代码:不具有响应式,即改变值不会触发渲染
function computed(getter) {
let value
watchEffect(() => {
value = getter() // value 是值类型,值拷贝,value 的变化不会传递到下游
})
return value
}
// 伪代码:具有响应式
function computed(getter) {
const ref = {
value: null,
}
watchEffect(() => {
ref.value = getter() // ref 是引用类型,指针拷贝,ref 的变化会传递到下游
})
return ref
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 为何需要toRef 和toRefs?
- 初衷∶不丢失响应式的情况下,把对象数据分解/扩散
- 必须存在:因为直接 property 或者
...state
,会直接拿到属性的值。如果属性是值类型,则会丢失响应式 !!!(响应式是 Proxy 实现的) - 注意︰不创造响应式,而是延续响应式
toRef toRefs 必须针对一个响应式对象,普通对象是无法变成响应式的。
它俩的设计初衷,本来也是针对响应式对象的 https://vue-composition-api-rfc.netlify.app/zh/api.html#toref
即,想要实现响应式,只有两个方法:ref
reactive
ref
创造响应式toRef
和toRefs
延续响应式
为何必须使用 toRef(state, 'name')
和 toRefs(state)
???
因为直接 property 或者 ...state
,会直接拿到属性的值。如果属性是值类型,则会丢失响应式。(响应式是 Proxy 实现的)
这也是引入 ref
的根源!!!
也是引入 Composition API ,抛弃 this ,带来的结果。
# Vue3新功能
# createApp
// vue2.x
const app = new Vue({ /* 选项 */ })
// vue3
const app = Vue.createApp({ /* 选项 */ })
2
3
4
5
全局 API
// vue2.x
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
// vue3
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
2
3
4
5
6
7
8
9
10
11
# emits 属性
组件中 emits: ['fnName']
,否则有警告 Extraneous non-emits event listeners
# 多事件处理
<!-- 在 methods 里定义 one two 两个函数 -->
<button @click="one($event), two($event)">
Submit
</button>
2
3
4
# Fragment
取消了 v2.x 的“一个组件必须有一个根元素”
<!-- vue2.x 组件模板 -->
<template>
<div class="blog-post">
<h3>{{ title }}</h3>
<div v-html="content"></div>
</div>
</template>
<!-- vue3 组件模板 -->
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
# .sync 改为 v-model 参数
<!-- vue 2.x -->
<MyComponent v-bind:title.sync="title" />
<!-- vue 3.x -->
<MyComponent v-model:title="title" />
2
3
4
5
文档
- vue2.x https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6
- vue3 https://v3.cn.vuejs.org/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0
# 异步组件的引用方式
新增 defineAsyncComponent
方法。
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
2
3
4
5
6
7
8
9
10
vue2.x 写法如下
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component.vue')
}
})
2
3
4
5
6
# 移除 filter
<!-- 以下 filter 在 vue3 中不可用了!!! -->
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
2
3
4
5
6
7
# Teleport
<!-- data 中设置 modalOpen: false -->
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
telePort 弹窗 (父元素是 body)
<button @click="modalOpen = false">Close</button>
</div>
</div>
</teleport>
2
3
4
5
6
7
8
9
10
11
12
13
14
# Suspense
【注意】Suspense 是一个实验性的 API ,后面可能会改变 —— v3.0.2
当内容组件未加载完成时,先显示 loading 的内容。
<Suspense>
<template>
<Test1/> <!-- 是一个异步组件 -->
</template>
<!-- #fallback 就是一个具名插槽。即:
Suspense 组件内部,有两个 slot ,
其中一个具名为 fallback -->
<template #fallback>
Loading...
</template>
</Suspense>
2
3
4
5
6
7
8
9
10
11
12
# Composition API 实现逻辑复用
- 抽离逻辑代码到一个函数
- 函数命名约定为useXxxx格式(React Hooks也是)
- 在setup 中引用useXxx 函数
import {ref, reactive, onMounted, onUnmounted} from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
// 监听鼠标移动
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unmounted')
window.removeEventListener('mousemove', update)
})
return {
x, y
}
}
export function useMousePosition2() {
const state = reactive({
x: 0,
y: 0
})
function update(e) {
state.x = e.pageX
state.y = e.pageY
}
// 监听鼠标移动
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unmounted')
window.removeEventListener('mousemove', update)
})
return state
}
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
48
49
50
<template>
<p>mouse position {{state.x}} {{state.y}}</p>
</template>
<script>
import {useMousePosition, useMousePosition2} from './useMousePosition'
export default {
name: 'mousePosition',
setup() {
// const {x, y} = useMousePosition()
// return {x, y}
// 返回state对象,不能解构
// 为了简便,可以在合成函数中使用toRefs(state)
const state = useMousePosition2()
return {state}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# v-model参数用法
v-model
的参数 (opens new window)
<template>
<p>{{name}} {{age}}</p>
<user-info
v-model:name="name"
v-model:age="age"
></user-info>
</template>
<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './UserInfo.vue'
export default {
name: 'VModel',
components: { UserInfo },
setup() {
const state = reactive({
name: '张三',
age: '20'
})
return toRefs(state)
}
}
</script>
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
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)"/>
<input :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>
<script>
export default {
name: 'UserInfo',
props: {
name: String,
age: String
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# watch和watchEffect的区别
- 两者都可监听data属性变化
- watch需要明确监听哪个属性
- watchEffect会根据其中的属性,自动监听其变化
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} {{age}}</p>
</template>
<script>
import { watch, ref, reactive, toRefs, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
const numberRef = ref(100)
const state = reactive({
name: '张三',
age: 15
})
watchEffect(() => {
// 初始化时,一定会执行一次(收集要监听的数据)
console.log('初始化时候就会调用,因为要收集要监听的数据')
})
watchEffect(() => {
console.log('watchEffect state.name', state.name)
})
watch(numberRef, (newValue, oldValue) => {
console.log('watch numberRef', newValue, oldValue)
}, {
immediate: true // 初始化之前就监听,可选
})
watch([() => state.name, () => state.age], ([newName, newAge], [oldName, oldAge]) => {
console.log('watch state', newName, newAge, oldName, oldAge)
})
watch(
// 第一个参数,确定要监听哪个属性
() => state.age,
// 第二个参数,回调函数
(newValue, oldValue) => {
console.log('watch state.age', newValue, oldValue)
},
// 第三个参数,配置项
{
immediate: true, // 初始化之前就监听,可选
deep: true // 深度监听,可选
}
)
setTimeout(() => {
numberRef.value = 200
state.name = '李四'
state.age = 20
}, 1500)
return {
numberRef,
...toRefs(state)
}
}
}
</script>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# setup中如何获取组件实例
- 在setup 和其他Composition API中没有this
- 可通过getCurrentInstance获取当前实例
- 若使用Options API可照常使用this
这一点和 react 一样。class 组件有 this ,Hooks 函数组件没有 this 。
<template>
<p>get instance</p>
</template>
<script>
import {getCurrentInstance, onMounted} from 'vue'
export default {
data() {
return {
x: 1,
y: 2
}
},
setup() { // 执行于beforeCreate和created
console.log('Composition API this', this) // undefined
const instance = getCurrentInstance()
console.log('Composition API instance', instance)
onMounted(() =>{
console.log('Composition API this.y', instance.data.y) // 2
})
return {}
},
mounted() {
console.log('Options API this', this) // Proxy
console.log('Options API this.y', this.y) // 1
}
}
</script>
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
# Vue3为何比 Vue2快
- Proxy 实现响应式,初始化时更快。之前已经讲过
- 模板编译优化:静态内容直接输出、数据缓存 —— 所有的算法优化,都是拿空间换时间
- diff 算法优化,让组件渲染更快。diff 算法和模板是相关的。PS:其实 diff 算法本身的对比逻辑,跟之前时一样的。可优化的是一些细节和条件分支。
- tree-shaking
# Proxy响应式
Object.defineProperty的缺点:
- 深度监听需要一次性递归
- 无法监听新增属性/删除属性(Vue.set Vue.delete )
- 无法原生监听数组,需要特殊处理
Proxy 作用
- 代理属性和方法
- 监听变化
- 针对对象、数组,全方位
Reflect 作用
- API 和 Proxy 对应
- Reflect 能让编程规范化、标准化、函数式
- Reflect 会替代掉 Object 上的工具函数
// const data = {
// a: 100,
// b: 200
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
// target, 就是原来的对象, receiver被代理后的proxy对象
get(target, key, receiver) {
console.log(receiver)
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result
},
set(target, key, newVal, receiver) {
// 重复的数据,不处理
if(target[key] === newVal) {
return true
}
const result = Reflect.set(target, key, newVal, receiver)
// 如果属性只读,则返回 false。如 object.defineProperty writable false
console.log('set', key, newVal)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
// 测试
// proxyData.a
// proxyData.b
// proxyData.a = 101
// delete proxyData.a
proxyData.push('a')
proxyData[0]
proxyData.length
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
Reflect.has(target, key)
key in target
Reflect.deleteProperty(target, key)
delete target[key]
Reflect.ownKeys({a: 100})
Object.getOwnPropertyNames({a: 100})
2
3
4
5
6
7
8
# PatchFlag
- 编译模板时,动态节点做标记
- 标记,分为不同的类型,如TEXT PROPS
- diff 算法时,可以区分静态节点,以及不同类型的动态节点
https://template-explorer.vuejs.org/
# hoistStatic
- 将静态节点的定义,提升到父作用域,缓存起来
- 多个相邻的静态节点,会被合并起来
- 典型的拿空间换时间的优化策略
# cacheHandler
- 缓存事件
# SSR优化
- 静态节点直接输出,绕过了vdom
- 动态节点,还是需要动态渲染
# tree-shaking
可以看到 import
的内容不一样。
即,针对不通情况,vue 会自动引入所需要的功能,而不会全部引入。 这样就让使用者可以压缩他们项目打包的体积,即 tree-shaking
# Vue3响应式
Object.defineProperty 缺点:
- 深度监听,需要递归到底
- 无法监听新增属性/删除属性(Vue.set Vue.delete)
- 无法监听数组,需要特殊处理
- Proxy 可以原生监听 新增/删除属性
- Proxy 原生支持监听数组变化
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的? 如果是普通对象的话会直接返回
return reactive(result)
},
set(target, key, value, receiver) {
if (value === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, value, receiver)
console.log('set', key, value)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
总结:
- Proxy 能规避Object.defineProperty 的问题
- Proxy无法兼容所有浏览器,无法 polyfill
- 深度监听,都用递归。但前者是一次性递归,后者是访问时再递归。
- Proxy 原生能监听 新增/删除属性
- Proxy 原生支持监听数组变化
但 —— Proxy 兼容性不好 ,我见过,oppo vivo 的某些机型,某些 app 的 webview 还没有支持 ES6 。
# Vite 是什么?
https://v3.cn.vuejs.org/guide/installation.html#vite
- 一个打包工具,Vue 作者发起的
- 借助 Vue 的影响力,和 webpack 竞争
- 优势:开发环境启动快,无需打包
- 开发环境基于 ES6 module https://www.caniuse.com/?search=module
- 生产环境打包时使用 rollup
<script type="module">
import add from './src/add.js'
const res = add(1, 2)
console.log('add res', res)
</script>
2
3
4
5
6
# Composition API和React Hooks 对比
- 组件生命周期中
setup
只调用一次,而 React Hooks 在组件 render 和每次 update 时都会被调用 - 前者无需useMemo useCallback ,因为setup只调用一次
- “不需要顾虑调用顺序,也可以用在条件语句中” (React Hooks 的
useXXX
则有严格规定,这一点是 React Hooks 不太简单的地方)
另外,reactive ref 这俩概念,和 react Hooks 的 useState 相比,还是后者好理解
# vue3 JSX
# vue3 中 JSX 的基本使用
- 使用
.jsx
格式的文件,使用defineComponent
- 可以传入一个配置
- 也可以传入一个
setup
函数
- 引入子组件,传递属性
Demo1.vue
<script lang="jsx">
import {ref} from 'vue';
import Child from './Child'
export default {
setup() {
const countRef = ref(200);
const render = () => {
return <>
<h1>Demo1: {countRef.value}</h1>
<p>demo1: {countRef.value}</p> {/*jsx*/}
<Child a={countRef.value} />
</>
};
return render;
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Demo1.jsx
import {defineComponent, ref, reactive} from 'vue'
import Child from './Child'
// 支持传入setup函数和对象配置两种方式
export default defineComponent(() => {
const flagRef = ref(true)
function changeFlag() {
flagRef.value = !flagRef.value
}
const state = reactive({
list: ['a1', 'b1', 'c1']
})
const render = () => {
return <>
<h1>Demo1.jsx</h1>
<button onClick={changeFlag}>demo1: {flagRef.value.toString()}</button>
{flagRef.value && <Child/>}
<ul>
{state.list.map(item =>{
return <li>{item}</li>
})}
</ul>
</>
}
return render
})
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
Child.jsx
import {defineComponent} from 'vue'
// 支持传入setup函数和对象配置两种方式
export default defineComponent({
props: ['a'],
setup(props) {
const render = () => {
return <p>Child: {props.a}</p>
}
return render
}
})
2
3
4
5
6
7
8
9
10
11
12
# JSX 和 template 的区别
语法的区别:
- JSX 就是 js 代码,它可以使用任何 js 的能力。
- template 是模板语法,目前只能插入一些简单的 js 表达式。逻辑则需要指令,如
v-for
v-if
等。 - JSX 已经成为了 ES 规范语法,babel 支持。而 template 只是 vue 自家的语法规范。
但本质是相同的:他们都会被编译成 render 函数,用于渲染 vnode
- 插值
- template 用
- JSX 用
{xxx}
- template 用
- 自定义组件
- template 可用
<custom-component></custom-component>
或者<CustomComponent></CustomComponent>
- JSX 必须使用
<CustomComponent></CustomComponent>
- template 可用
- 传递属性和事件
- template 中使用
a="xx"
或:a="xx"
,@xx="xx"
- JSX 中全部使用
a={xx}
- template 中使用
- 条件,循环
- template 使用指令
v-for
v-if
等 - JSX 中使用 js 表达式
- template 使用指令
# JSX 和 slot
因为 template 只能内嵌简单的 js 表达式,无法内嵌组件,所以 vue 只能自造一个 <slot>
语法。
vue3 setup 中可以使用 context.slots.default()
获取子组件
Demo.vue
<template>
<tabs default-active-key="1" @change="onTabsChange">
<tab-panel key="1" title="title1">
<div>tab panel content 1</div>
</tab-panel>
<tab-panel key="2" title="title2">
<div>tab panel content 2</div>
</tab-panel>
<tab-panel key="3" title="title3">
<div>tab panel content 3</div>
</tab-panel>
</tabs>
</template>
<script>
import Tabs from './Tabs'
import TabPanel from './TabPanel'
export default {
components: { Tabs, TabPanel },
methods: {
onTabsChange(key) {
console.log('tab changed', key)
}
},
}
</script>
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
TabPanel.vue
<template>
<slot></slot>
</template>
<script>
export default {
name: 'TabPanel',
props: ['key', 'title'],
}
</script>
2
3
4
5
6
7
8
9
10
Tabs.jsx
import { defineComponent, ref } from "vue";
export default defineComponent({
name: 'Tabs',
props: ['defaultActiveKey'],
emits: ['change'],
setup(props, { emit, slots }) {
const children = slots.default();
const tabs = children.map(item => {
const {key, title} = item.props || {};
return {
key,
title
}
})
// 当前 actKey
const actKey = ref(props.defaultActiveKey)
function changeActKey(key) {
actKey.value = key
emit('change', key)
}
// jsx
const render = () => {
return <>
{/* 渲染 buttons */}
<div>
{tabs.map(tab =>{
const { key, title } = tab
return <button
key={key}
style={{ color: actKey.value === key ? 'blue' : '#333' }}
onClick={() => changeActKey(key)}
>{title}</button>
}) }
</div>
<div>
{/*控制显示与隐藏*/}
{children.filter(item => item.props.key === actKey.value)}
</div>
</>
}
return render
}
})
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
# JSX 和作用域 slot
回顾作用域 slot ,就是:父组件想要获取子组件的信息,并渲染到 slot 中
作用域 slot 很难理解,一直是 vue 初学者的噩梦。但用 JSX 将会变的很好理解,因为它就是 js 代码逻辑。
代码的方式,在 react 中叫做“renderProps”
Parent.jsx
import { defineComponent } from "vue";
import Child from "./Child.jsx"
export default defineComponent(() => {
function render(msg) {
return <p>msg: {msg} 123123</p>
}
return () => {
return <>
<p>Demo - JSX</p>
<Child render={render}></Child>
</>
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
Child.jsx
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: ['render'],
setup(props) {
const msgRef = ref('子组件作用域插槽-jsx')
return () => {
return <>{props.render(msgRef.value)}</>
}
}
})
2
3
4
5
6
7
8
9
10
11
12
# vue3 script setup
vue-cli 创建项目之后,升级到 3.2 版本,重新安装即可 yarn add vue@next
Child1.vue
<script setup>
import { defineProps, defineEmits } from 'vue'
// 定义属性
const props = defineProps({
name: String,
age: Number
})
// 定义事件
const emit = defineEmits(['change', 'delete'])
function deleteHandler() {
emit('delete', 'aaa')
}
</script>
<template>
<p>Child1 - name: {{props.name}}, age: {{props.age}}</p>
<button @click="$emit('change', 'bbb')">change</button>
<button @click="deleteHandler">delete</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Child2.vue
<script setup>
import { ref, defineExpose } from 'vue'
const a = ref(101)
const b = 201
defineExpose({
a,
b
})
</script>
<template>
<p>Child2</p>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Demo.vue
<script>
function add (a, b) { return a + b }
</script>
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
const countRef = ref(100)
function addCount() {
countRef.value++
}
const state = reactive({
name: '张三'
})
const { name } = toRefs(state)
console.log( add(10, 20) )
function onChange(info) {
console.log('on change', info)
}
function onDelete(info) {
console.log('on delete', info)
}
const child2Ref = ref(null)
onMounted(() => {
// 拿到 child2 组件的一些数据
console.log(child2Ref.value)
console.log(child2Ref.value.a)
console.log(child2Ref.value.b)
})
</script>
<template>
<p @click="addCount">{{countRef}}</p>
<p>{{name}}</p>
<hr>
<child-1 :name="name" :age="countRef" @change="onChange" @delete="onDelete"></child-1>
<hr>
<child-2 ref="child2Ref"></child-2>
</template>
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
# webpack面试题
# 基本应用
webpack4升级 webpack5 以及周边插件后,代码需要做出的调整:
- package.json 的 dev-server 命令改了
"dev": "webpack serve --config build/webpack.dev.js",
- 升级新版本
const { merge } = require('webpack-merge')
- 升级新版本
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.rules
中loader: ['xxx-loader']
换成use: ['xxx-loader']
filename: 'bundle.[contenthash:8].js'
其中h
小写,不能大写
# 安装和配置 —— 拆分 dev prod 配置,然后 merge
安装 nodejs
初始化
npm init -y
安装插件
npm i webpack webpack-cli webpack-merge --save-dev
新建
src
及其测试 js 代码(包含 ES6 模块化)创建配置文件
增加
scripts
,运行安装
npm i clean-webpack-plugin --save-dev
配置 prod
# 本地服务和代理
- 新建
index.html
- 安装
npm i html-webpack-plugin --save-dev
,并配置 - 安装
npm i webpack-dev-server --save-dev
,并配置 - 修改
scripts
的dev
,运行
报错:[webpack-cli] file:///D:/@%E9%9D%A2%E8%AF%95/%E5%89%8D%E7%AB%AF%E7%BB%BC%E5%90%88/%E9%9D%A2%E8%AF%95%E9%A2%98/WebPack%E9%9D%A2%E8%AF%95%E9%A2%98/web
pack-basic-demo/node_modules/open/index.js:6
import fs, {constants as fsConstants} from 'node:fs/promises';
^^^^^^^^^
SyntaxError: The requested module 'node:fs/promises' does not provide an export named 'constants'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:128:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:194:5)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:385:24)
注释掉 open: true // 自动打开浏览器
webpack.common.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {srcPath, distPath} = require("./paths");
module.exports = {
entry: path.join(srcPath, 'index.js'),
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'development',
devServer: {
historyApiFallback: true, // 解决404问题
// contentBase: distPath, // 根目录,webpack5会报错
// open: true, // 自动打开浏览器 报错:SyntaxError: The requested module 'node:fs/promises' does not provide an export named 'constants'
compress: true, // 压缩
hot: true, // 热更新
port: 8080, // 设置启动时监听的端口
// 设置代理 —— 如果有需要的话!
// proxy: {
// // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
// // '/api': 'http://localhost:3000',
// // 将本地 /api2/xxx 代理到 localhost:3000/xxx
// '/api2': {
// target: 'http://localhost:3000',
// pathRewrite: {
// '/api2': ''
// }
// }
// }
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('development')
})
]
})
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
webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
filename: '[name].[contenthash:8].js',
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 处理 ES6
- 安装
npm i @babel/core @babel/preset-env babel-loader --save-dev
- 配置 webpack module
- 配置
.babelrc
webpack.common.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {srcPath, distPath} = require("./paths");
module.exports = {
entry: path.join(srcPath, 'index.js'),
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: srcPath,
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.babelrc
{
"presets": ["@babel/preset-env"],
"plugins": []
}
2
3
4
@babel/preset-env
根据指定的执行环境提供语法装换,也提供配置 polyfill。
@babel/preset-env
是预设是一系列插件的集合,包含了我们在babel6
中常用es2015
,es2016
,es2017
等最新的语法转化插件,允许我们使用最新的js语法,比如 let,const,箭头函数等等,但不包含低于 Stage 3 的 JavaScript 语法提案。这样以后只要我们安装一个'babel/preset-env'就解决了大部分问题,简单的说,它是 'babel 一个预设'主要用来帮助我们对语法进行转换,会自动的根据当前环境对js语法做转换。
# 处理样式
- 安装
npm i style-loader css-loader less-loader less --save-dev
(注意要安装 less) - 配置 webpack module
- 新建 css less 文件,引入 index.js
- 运行 dev
postcss
- 安装
npm i postcss-loader autoprefixer -D
- 新建
postcss.config.js
- 配置 webpack module ,增加
postcss-loader
- 增加 css
transform: rotate(-45deg);
- 配置需要兼容的浏览器 package.json中指定browserslist
- 运行 dev
1.可以在package.json中指定(推荐)
"browserslist" : [
"last 1 version", // 最后的一个版本
"> 1%" //代表全球超过1%使用的浏览器
]
2.在项目根目录下创建.browserslistrc文件
last 1 version
> 1%
2
3
4
5
6
7
8
css/style1.css
body {
background-color: #f1f1f1;
}
p {
transform: rotate(-45deg);
}
2
3
4
5
6
7
css/style2.less
p {
color: red;
}
2
3
src/index.js中引入
// 引入css
import './style/style1.css'
// 引入less
import './style/style2.less'
2
3
4
webpack.common.js
const path = require("path");
const {srcPath, distPath} = require("./paths");
module.exports = {
entry: path.join(srcPath, 'index.js'),
module: {
rules: [
{
test: /\.css$/,
// loader 的执行顺序是:从后往前, 从下往上
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
},
plugins: [
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
postcss.config.js
module.exports = {
plugins: [require('autoprefixer')]
}
2
3
此时,css是被内嵌在js中
# 处理图片
考虑 base64 格式
- 安装
npm i file-loader url-loader --save-dev
- 分别配置 webpack.dev 和 webpack.prod
- 新建图片文件,并引入到 js 中,插入页面
- 运行 dev
- 运行 build
webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'development',
module: {
rules: [
// 直接引入图片 url
{
test: /\.(png|jpg|jpeg|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('development')
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
npm run dev
后图片全是链接地址
webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
filename: '[name].[contenthash:8].js',
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
module: {
rules: [
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|gif|jpeg)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
})
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
低于5kb转换为base64,大于5kb为连接地址
# 高级应用
# 多入口
- 新建
other.html
和other.js
- 修改 entry
- 修改 output
- 修改 HtmlWebpackPlugin
- 运行 dev
- 运行 build
webpack.common.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {srcPath, distPath} = require("./paths");
module.exports = {
// 单入口配置
// entry: path.join(srcPath, 'index.js'),
// 多入口配置
entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
module: {
rules: [
...
]
},
plugins: [
// 单入口配置
// new HtmlWebpackPlugin({
// template: path.join(srcPath, 'index.html'),
// filename: 'index.html'
// })
// 多入口配置
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
chunks: ['index']
}),
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other']
})
]
}
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
webpack.prod.js
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
// name 即多入口时 entry 的 key
filename: '[name].[contenthash:8].js',
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
module: {
rules: [
...
]
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 抽离 & 压缩 css 文件
抽离
- 安装
npm i mini-css-extract-plugin -D
- 将之前 common 中的 css 处理,移动到 dev 配置中
- 配置 prod (配置 module ,配置 plugin)
- 运行 build
压缩
- 安装
npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D
- 配置 prod
optimize-css-assets-webpack-plugin 不支持webpack5
npm install css-minimizer-webpack-plugin --save-dev
1
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 抽离css
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩css
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const webpackCommonConf = require('./webpack.common')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
// name 即多入口时 entry 的 key
filename: '[name].[contenthash:8].js',
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
module: {
rules: [
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|gif|jpeg)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
// 抽离css
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 注意这里不再使用style-loader
'css-loader',
'postcss-loader'
]
},
// 抽离less
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
'postcss-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
}),
// 抽离css文件
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
})
],
optimization: {
// 压缩css
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
}
})
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 抽离公共代码
- 配置
splitChunks
- 修改 HtmlWebpackPlugin 中的 chunks 。重要!!!
- 安装 lodash
npm i lodash --save
做第三方模块的测试,引用 lodash - 运行 build
未分离前,index.js 打包后84kb,分离后8kb
适用于,多个js文件引入相同的代码块,第三方库
包括css之类的也会抽离
webpack.prod.js
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
// name 即多入口时 entry 的 key
filename: '[name].[contenthash:8].js',
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
...
optimization: {
// 分割代码块
splitChunks: {
chunks: 'all',
/**
* initial 入口chunk,对于异步导入的文件不处理
async 异步chunk,只对异步导入的文件处理
all 全部chunk
*/
// 缓存分组
cacheGroups: {
// 第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!!
test: /node_modules/,
minSize: 0, // 大小限制
minChunks: 1 // 最少复用过几次
},
// 公共的模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
}
})
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
webpack.common.js
chunks里面增加chunk
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {srcPath, distPath} = require("./paths");
module.exports = {
// 单入口配置
// entry: path.join(srcPath, 'index.js'),
// 多入口配置
entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
plugins: [
// 多入口配置
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
chunks: ['index', 'vendor', 'common']
}),
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other', 'vendor', 'common']
})
]
}
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
# 懒加载
- 增加
dynamic-data.js
并动态引入 - 运行 dev 查看效果(看加载 js)
- 运行 build 看打包效果
// 引入动态数据-懒加载 (默认支持)
setTimeout(() => {
import('./dynamic-data.js').then(res => {
console.log(res.default.message)
})
}, 3000)
2
3
4
5
6
- module:就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码。
- chunk: chunk是webpack根据功能拆分出来的,包含三种情况:
- 你的项目入口(entry)
- 通过import()动态引入的代码
- 通过splitChunks拆分出来的代码
- (chunk包含着module,可能是一对多也可能是一对一)
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出。
# 处理 React 和 vue
- vue-loader
- jsx 的编译,babel 已经支持,配置
@babel/preset-react
# 常见 loader 和 plugin
- https://www.webpackjs.com/loaders/
- https://www.webpackjs.com/plugins/
# 性能优化
- 优化打包构建速度-开发体验和效率
- 优化产出代码–产品性能
# 打包效率
# 优化 babel-loader
- babel-loader cache 未修改的不重新编译
- babel-loader include 明确范围
webpack.common.js
{
test: /\.js$/,
use: ['babel-loader?cacheDirectory'], // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// // 排除范围,include 和 exclude 两者选一个即可
// exclude: path.resolve(__dirname, 'node_modules')
},
2
3
4
5
6
7
# IgnorePlugin 避免引入哪些模块
以常用的 moment 为例。安装 npm i moment -d
并且 import moment from 'moment'
之后,monent 默认将所有语言的 js 都加载进来,使得打包文件过大。可以通过 ignorePlugin 插件忽略 locale 下的语言文件,不打包进来。
先去掉分割代码
import moment from 'moment'
moment.locale('zh-cn') // 设置语言为中文
console.log('local', moment.locale())
console.log('date', moment().format('ll'))
2
3
4
webpack.prod.js
plugins: [
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin({
resourceRegExp: '/\.\/locale/',
contextRegExp: '/moment/'
})
]
2
3
4
5
6
7
更换引入方式
import moment from 'moment'
import 'moment/locale/zh-cn' // 手动引入中文语言包
moment.locale('zh-cn')
2
3
# noParse 避免重复打包
module.noParse
配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
module.exports = {
module: {
// 独完整的 `react.min.js` 文件就没有采用模块化
// 忽略对 `react.min.js` 文件的递归解析处理
noParse: [/react\.min\.js$/],
},
}
2
3
4
5
6
7
两者对比一下:
IgnorePlugin
直接不引入,代码中不存在noParse
引入,但不再打包编译
# happyPack 多进程打包
【注意】大型项目,构建速度明显变慢时,作用才能明显。否则,反而会有副作用。
webpack 是基于 nodejs 运行,nodejs 是单线程的,happyPack 可以开启多个进程来进行构建,发挥多核 CPU 的优势。
npm i happyPack --save -dev
报错提示babel-loader,原因版本不匹配
babel原版本:9.1.3 降为8.0.6
const path = require('path')
const HappyPack = require('happypack')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
exclude: path.resolve(__dirname, 'node_modules')
}
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// ... 其它配置项
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ParallelUglifyPlugin 多进程压缩 js
webpack 默认用内置的 uglifyJS 压缩 js 代码。 大型项目压缩 js 代码时,也可能会慢。可以开启多进程压缩,和 happyPack 同理。
npm i webpack-parallel-uglify-plugin --save -dev
webpack.prod.js
const path = require('path')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
// warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};
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
项目较大,打包较慢,开启多进程能提高速度 项目较小,打包很快,开启多进程会降低速度(进程开销)
# 自动刷新
watch 默认关闭。但 webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。
module.export = {
watch: true, // 开启监听,默认为 false
// 注意,开启监听之后,webpack-dev-server 会自动开启刷新浏览器!!!
// 监听配置
watchOptions: {
ignored: /node_modules/, // 忽略哪些
// 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
// 默认为 300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
// 默认每隔1000毫秒询问一次
poll: 1000
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 热更新
上文的自动刷新,会刷新整个网页。
- 速度更慢
- 网页当前的状态会丢失,如 input 输入的文字,图片要重新加载,vuex 和 redux 中的数据
操作步骤
- 把现有的 watch 注释掉
- 增加以下代码
- 修改 css less 实验 —— 热替换生效
- 修改 js 实验 —— 热替换不生效
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
entry:{
// 为每个入口都注入代理客户端
index:[
'webpack-dev-server/client?http://localhost:8080/',
'webpack/hot/dev-server',
path.join(srcPath, 'index.js')
],
// other 先不改了
},
plugins: [
// 该插件的作用就是实现模块热替换,实际上当启动时带上 `--hot` 参数,会注入该插件,生成 .hot-update.json 文件。
new HotModuleReplacementPlugin(),
],
devServer:{
// 告诉 DevServer 要开启模块热替换模式
hot: true,
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js 热替换不生效,是因为我们要自己增加代码逻辑。
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
module.hot.accept(['./math'], () => {
const sumRes = sum(10, 20)
console.log('sumRes in hot', sumRes)
})
}
2
3
4
5
6
7
最后,热替换切勿用于 prod 环境!!!
# DllPlugin
Dll 动态链接库,其中可以包含给其他模块调用的函数和数据。
要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库需要被加载。
为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢?
- 前端依赖于第三方库
vue
react
等 - 其特点是:体积大,构建速度慢,版本升级慢
- 同一个版本,只需要编译一次,之后直接引用即可 —— 不用每次重复构建,提高构建速度
Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
- DllPlugin 插件:打包出 dll 文件
- DllReferencePlugin 插件:使用 dll 文件
打包出 dll 的过程
- 增加 webpack.dll.js
- 修改 package.json scripts
"dll": "webpack --config build/webpack.dll.js"
npm run dll
并查看输出结果
使用 dll
- 引入
DllReferencePlugin
- babel-loader 中排除
node_modules
- 配置
new DllReferencePlugin({...})
- index.html 中引入
react.dll.js
- 运行 dev
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
const { srcPath, distPath } = require('./paths')
module.exports = {
mode: 'development',
// JS 执行入口文件
entry: {
// 把 React 相关模块的放到一个单独的动态链接库
react: ['react', 'react-dom']
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: distPath,
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(distPath, '[name].manifest.json'),
}),
],
}
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
# 总结-提高构建效率的方法
哪些可用于线上,哪些用于线下
- 优化 babel-loader(可用于线上)
- IgnorePlugin 避免引入哪些模块(可用于线上)
- noParse 避免重复打包(可用于线上)
- happyPack 多进程打包(可用于线上)
- ParallelUglifyPlugin 多进程压缩 js(可用于线上)
- 自动刷新(仅开发环境)
- 热更新(仅开发环境)
- DllPlugin(仅开发环境)
# 产出代码优化
# 使用 production
- 开启压缩代码
- 开启 tree shaking(必须是 ES6 Module 语法才行)
ES6 Module 和 commonjs 的区别
- ES6 Module 是静态引入,编译时引入
- commonjs 是动态引入,执行时引入
// commonjs
let apiList = require('../config/api.js')
if (isDev) {
// 可以动态引入,执行时引入
apiList = require('../config/api_dev.js')
}
2
3
4
5
6
import apiList from '../config/api.js'
if (isDev) {
// 编译时报错,只能静态引入
import apiList from '../config/api_dev.js'
}
2
3
4
5
# 小图片 base64 编码
# bundle 加 hash
# 使用 CDN
配置 publicPath
# 提取公共改代码
# 懒加载
# scope hosting 将 module 合并到一个函数中
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
]
}
2
3
4
5
6
7
8
9
10
11
12
同时,考虑到 Scope Hoisting 依赖源码需采用 ES6 模块化语法,还需要配置 mainFields
。因为大部分 Npm 中的第三方库采用了 CommonJS 语法,但部分库会同时提供 ES6 模块化的代码,为了充分发挥 Scope Hoisting 的作用。
# babel
- babel-polyfill —— 7.4 之后弃用,推荐直接使用 corejs 和 regenerator ??
- babel-runtime
# 初始化环境
- 安装 babel 插件
npm i @babel/cli @babel/core @babel/preset-env -D
- 新建
.babelrc
,配置 preset-env - 新建
src/index.js
,写一个箭头函数 - 运行
npx babel src/index.js
,看结果
.babelrc
{
"presets": [
[
"@babel/preset-env"
]
]
}
2
3
4
5
6
7
src/index.js
const sum = (a, b) => a + b
npx babel src/index.js
# 使用 polyfill
什么是 babel-polyfill
- 什么是 polyfill ?—— 即一个补丁,引入以兼容新 API(注意不是新语法,如箭头函数),如搜索“Object.keys polyfill” 和 “Promise polyfill”
- core-js 集合了所有新 API 的 polyfill 。https://github.com/zloirock/core-js
- regenerator 是 generator 的 polyfill 。 https://github.com/facebook/regenerator
- babel-polyfill 即 core-js 和 regenerator 的集合,它只做了一层封装而已。
ES6generator 函数(处理异步),被async/await代替
基本使用
src/index.js
中写一个 Promise,打包看结果npm install --save @babel/polyfill
【注意】要--save
然后引入
import '@babel/polyfill'
再打包,看结果
- 解释:babel 仅仅是处理 ES6 语法,并不关心模模块化的事情。模块化归 webpack 管理
- 全部引入 polyfill ,体积很大
import '@babel/polyfill' const sum = (a, b) => a + b // 新的 API Promise.resolve(100).then(data => data); // 新的 API [10, 20, 30].includes(20)
1
2
3
4
5
6
7
8
按需加载
- 新增
"useBuiltIns": "usage"
(注意要改写 preset 的 json 结构) - 删掉入口的
import '@babel/polyfill'
- 再打包,看结果
- 提示选择 core-js 的版本,增加
"corejs": 3
- 只引入了 promise 的 polyfill
- 提示选择 core-js 的版本,增加
.babelrc
{
"presets": [
[
"@babel/preset-env",
// 配置按需引入
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
src/index.js就不需要再引入@babel/polyfill
# 使用 runtime
babel-polyfill 的问题 —— 会污染全局变量
如果是一个网站或者系统,无碍
如果是做一个第三方工具,给其他系统使用,则会有问题
不能保证其他系统会用 Promise 和 includes 做什么,即便他们用错了,那你也不能偷偷的给更改了
// 源代码 Promise.resolve(100).then(data => data); [10, 20, 30].includes(20); // 结果 —— 可以看到,Promise 和 includes 都未改动,因为以注入全局变量了 "use strict"; require("core-js/modules/es.array.includes.js"); require("core-js/modules/es.object.to-string.js"); require("core-js/modules/es.promise.js"); Promise.resolve(100).then(function (data) { return data; }); [10, 20, 30].includes(20);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
使用 babel-runtime
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
,注意是--save
- 配置
"plugins": ["@babel/plugin-transform-runtime"]
- 其中
"corejs": 3,
v3 支持 API 如数组 includes ,v2.x 不支持
- 其中
- 删掉
"useBuiltIns": "usage"
- 运行代码
.babelrc
{
"presets": [
[
"@babel/preset-env",
// 配置按需引入
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# webpack 面试题
# 前端代码为何要进行构建和打包?
代码层面:
- 体积更小( Tree-Shaking、压缩、合并),加载更快
- 编译高级语言或语法( TS,ES6+,模块化,scss )
- 兼容性和错误检查(Polyfill、postcss、eslint )
流程方面:
- 统一、高效的开发环境
- 统一的构建流程和产出标准
- 集成公司构建规范(提测、上线等)
# module chunk bundle 的区别
- module -各个源码文件,webpack 中一切皆模块
- chunk -多模块合并成的,如entry import() splitChunk
- bundle -最终的输出文件
# loader 和 plugin 的区别
- loader模块转换器,如less >css
- plugin扩展插件,如HtmlWebpackPlugin
# 常用的 loader 和 plugin 有哪些
- https://www.webpackjs.com/loaders/
- https://www.webpackjs.com/plugins/
loader
vue-loader: 加载并编译vue组件
file-loader: 把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件
url-loader: 和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去。 url-loader中内置了 file-loader 所以当使用 url-loader的时候,可以不使用 file-loader
image-loader: 加载并且压缩图⽚⽂件
source-map-loader: 加载额外的 Source Map ⽂件,以⽅便断点调试
babel-loader: 把 ES6 转换成 ES5
post-loader: postcss-loader中的autoprefixer插件,可以帮助我们自动给那些可以添加厂商前缀的样式添加厂商前缀。兼容样式
sass-loader: 加载 Sass/SCSS 文件并将他们编译为 CSS。
less-loader: 将 Less 编译为 CSS
css-loader: 加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性。
css-loader
会对@import
和url()
进行处理,就像 js 解析import/require()
一样。style-loader: 把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。把 CSS 插入到 DOM 中。
eslint-loader: 通过 ESLint 检查 JavaScript 代码
在Webpack中,loader的执行顺序是从右向左执行的。因为webpack选择了compose这样的函数式编程方式,这种方式的表达式执行是从右向左的。
plugin
- define-plugin:定义环境变量
- html-webpack-plugin:简化html⽂件创建 (快速创建 HTML 文件来服务 bundles)
- uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
- webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
- webpack-bundle-analyzer: 可视化webpack输出⽂件的体积
- mini-css-extract-plugin: CSS提取到单独的⽂件中,⽀持按需加载
# webpack 性能优化(如上)
# webpack 构建流程简述
几个核心概念
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
Webpack 的构建流程可以分为以下三大阶段:
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
# 如何产出多页,如何产出 lib
- 参考webpack.dll.js
- output.library
# babel 和 webpack 的区别
- babel-JS新语法编译工具,不关心模块化
- webpack-打包构建工具,是多个loader plugin的集合
# babel-runtime 和 babel-polyfill 区别
- babel-polyfill会污染全局
- babel-runtime不会污染全局
- 产出第三方lib 要用babel-runtime
# 为何 Proxy 无法 polyfill
- 如Class可以用function模拟
- 如Promise可以用callback来模拟
- 但Proxy的功能用Object.defineProperty无法模拟
# webpack 如何实现懒加载
- import()
- 结合Vue React异步组件
- 结合Vue-router React-router异步加载路由
# 开发 loader
以 less-loader
为例,回顾一下使用规则
{
test: /\.less$/,
// 注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
2
3
4
5
所以,loader 的作用:
- 一个代码转换器,将 less 代码转换为 css 代码
- 再例如 vue-template-compiler
- 再例如 babel 编译 jsx
所以一个 loader 的基本开发模式:
const less = require('node-less')
module.exports = function(source) {
// source 即 less 代码,需要返回 css 代码
return less(source)
}
2
3
4
5
以上是 loader 的基本开发方式,实际开发中可能还会有更多要求
- 支持 options ,如
url-loader
的使用 - 支持异步 loader
- 缓存策略
// options
const loaderUtils = require('loader-utils')
module.exports = function(source) {
// 获取 loader 的 options
const options = loaderUtils.getOptions(this)
return source // 示例,直接返回 source 了
}
2
3
4
5
6
7
// 异步 loader
module.exports = function(source) {
// 使用异步 loader
const callback = this.async()
// 执行异步函数,如读取文件
someAsyncFn(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast)
})
}
2
3
4
5
6
7
8
9
10
// 缓存
module.exports = function(source) {
// 关闭该 Loader 的缓存功能(webpack 默认开启缓存)
this.cacheable(false)
return source
}
2
3
4
5
6
# 补充
# innerText和textContent
https://blog.csdn.net/qq_41807489/article/details/102550566
# 基础类型存放在栈上,引用类型存放在堆上,请问是为什么? 字符串是存放在栈上么?
https://blog.csdn.net/weixin_44730897/article/details/127888425
# 箭头函数不能作为构造函数
# forin和foreach
对应async那里
https://blog.csdn.net/baidu_33438652/article/details/107266260
jd 京东
pyg 品优购
snyg 苏宁易购
xiec 携程网
wph 唯品会
xiaomi 小米登陆页面
xuec 学成网
hmlv 旅游网
randb 随机方块
snake 贪吃蛇
css3 css3案例