大四学生的前端知识点面试学习记录
前端面试学习
开始于2021年3月17日。最后编辑于2022年2月10日
JavaScript基础
数据类型
基本类型:null
,undefined
,boolean
,number
,string
,symbol
,bigInt
、引用类型:object
-
当对字符串进行parseInt转换等其他转换时,返回的
NaN
(NaN属于number
)跟另一个字符串转换获得的NaN
进行比较时会返回false
。因'A'不是一个数字 ,'B'也不是一个数字,无法证明两者一样的!所以要用isNaN()方法来判断。NaN互不相等 -
栈去存放变量和其值。基本类型是它真实的值,引用类型保存的是对象在堆中的地址值。所以对基本类型比较时是比较实际的值。对引用类型(Object)比较的时候则是比较地址值,赋值也是赋值在堆区中的地址值。js是值传递
let a={}, b=a, c={}; console.log(`${a===b},${a==c}`); // true,false
-
js跟java一样也有包装类型,如boolean->Boolean,number->Numbe类比java里的int->Integer,boolean->Boolean。当对基本类型调用方法或者获取属性时,会隐式的创建一个包装类,用完就销毁。自动包装和拆箱
var str = 1; str.pro = 2; console.log(str.pro + 10); // NaN
-
明白对象赋值是传递引用。对象深浅拷贝的问题就解决了,浅拷贝只拷贝对象里的第一层。
-
使用
typeof
可以查看其属于的类型是什么,数组[]也是object哦,函数会返回function。typeof null
返回object←历史遗留问题,因为js引擎对二进位前三位都为0就判为object,恰好null全0。(可以改,但没必要) -
对于Boolean的转换,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,[]
,其他所有值都转为true
,包括所有对象。-
symbol最主要是用来定义一个独一无二的属性名,来避免重名
ES6引入新的基础类型,类似唯一标识ID,通过
Symbol()
来创建实例Symbol可以作为对象的键,且在序列化,Object.keys()或者for...in来枚举都不会被包含进去
所以还可以使用Symbol来定义类的私有属性/方法。总的来说,见的很少。
-
-
判断方法:typeof/instanceof/construtor/isPrototypeOf/Object.getPrototypeOf()/Object.prototype.toString.call()
原型和原型链
??有名的js原型图
精简版
- js通过原型和原型链来实现类似继承的机制。就像OOP中的继承、Java中的父类,类方法等。
在Java中可以使用Cat cat=new Cat();cat.eat()调用属于类的方法也可以使用Cat.eat()直接调用。由于js的动态语言特性,可以在'类'中增加方法和属性。让其'实例对象'都可以调用到这个方法。同时继承也让'实例对象'知道它爹是谁。
-
使用关键关键字function声明函数有
prototype
属性,使用const fun1=()=>{}
创建没有prototype属性const fun4=(){ console.log('is func4') } fun4(); // is func4 console.log(fun4.prototype); // undefined
-
使用function声明函数实际上是内部调用
new Function(...)
,并被添加__proto__
属性来链接到其构造函数原型上。 -
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它 -
Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它 -
Function.prototype
和Object.prototype
是两个特殊的对象,他们由引擎来创建 -
除了以上两个特殊对象,其他对象都是通过构造器
new
出来的 -
函数的
prototype
是一个对象,也就是原型 -
对象的
__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链 -
所有引用类型的
_proto_
属性指向它构造函数的prototype -
Function.[[Prototype]] === Function.prototype; // true
-
Object.prototype是浏览器底层根据
ECMAScript
规范创造的一个对象。 -
以上来自 https://github.com/KieSun/Dream/issues/2
关键字new
-
使用
new FunctionName
来创建对象发生四件事:新生成了一个对象、链接到原型、绑定this、返回新对象 -
创建对象最好还是用字面量的方式
let a = { b: 1 }
方法来创建,可读性高,性能好。 -
new内部默认返回this,如果手动返回一个引用类型则不会返回this,返回基本类型不生效
function _new(constructor,...args) { let obj = new Object()// 创建空对象 obj.__proto__ = constructor.prototype // 链接到原型 let result = constructor.call(obj, ...args) // 绑定this执行构造函数 return typeof result === 'object' ? result : obj // 确保 new 出来的是个对象 }
关键字instanceof
-
判断对象的类型,通过判断其原型链上是否能找到类型的prototype,大概就是一直通过
obj.__proto__
跟目标的``prototype比较
let obj={}; console.log(obj instanceof Object) // true`function instanceof(left, right) { // 获得类型的原型 let prototype = right.prototype // 获得对象的原型 left = left.__proto__ // 判断对象的类型是否等于类型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ } }
关键字this
官方中文文档指路
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
- this是一个执行上下文(context)的属性,它会在执行的过程中用到。它的指向取决于在哪里被调用,并就近查找。
- 可以用call,apply,bind可以改变this为第一个参数
- 箭头函数()=>{}没有this,所以this是向外一层层查找的。并且this绑定了上下文就不会被更改
- 在严格模式下,函数被直接调用(不作为对象的属性/方法)时会出现
undefined
。 - 使用new关键字new一个对象,这个对象并被绑定为this,并隐式的返回这个this
- addEventListener的事件函数this指向当前DOM元素,可以使用箭头函数或者bind改变this指向
var obj = {
bar() {
return (() => this);
}
};
----es5↓----
var obj = {
bar: function bar() {
var _this = this;
return function () {
return _this;
};
}
};
关键字var、let、const
- let跟const差不多,都是块级作用域,不能再被声明前使用,不能重复定义,存在暂时性死区。const不能更改值且必须定义时就赋初值
- 在变量提升的过程中,相同名的函数会覆盖上一个,且函数优先于变量提升
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world'; // 会导致内层变量会覆盖外层变量(变量提升的原因)
}
}
?
f(); // undefined
// ==循环陷阱问题==
for (var i = 0; i < 3; i++){}
关键字class
- 函数声明和类声明之间的一个重要区别在于, 函数声明会提升,类声明不会。
题目:下面这个 class 的四个属性分别属于这个 class 的什么,fn 和 f 有什么区别 (babel在线编一下就知道了)
class A {
static a = 1;
b = 2;
fn() {console.log(this)}
f = () => {console.log(this)};
}
相等(==)
在两个操作数是不同类型会尝试转换成相同类型
- 数字和字符串比,尝试字符串转成数字 |
0 == '' // true
- 有一个是Bool,则将Bool换成数字 |
false == 0 // true
- 有一个是对象,则尝试使用对象的
valueOf()
和toString()
Number和Number的比较:+0和-0视为相同值,NaN和NaN返回false
[] == false ?
- 应用规则2=>
[] == 0
; - 对象类型会转换为其原始值=>
[].valueOf().toString() = ""
"" == 0
,""被转换为数字Number("") === 0
作用域
当创建函数时,该函数的作用域便已经声明,为所处的上下文,
函数中查找变量的过程中形成的链条就叫作用域链(当前作用域没查到就会向上一级作用域查找)
箭头函数
不能用new 、不绑定arguments
、new.target
、 不绑定this、没有prototype
属性
函数对象是一个支持[[Call]]
、[[Construct]]
内部方法的对象,不过箭头函数没有[[Construct]]
,所有不能new
闭包
有句话叫做:闭包是懒人的对象,对象是天然的闭包。闭包让无状态的函数有了状态,可以记录函数用到的变量。
-
闭包的产生条件:函数嵌套、嵌套的内部函数必须引用外部函数定义的变量、内部函数被执行
-
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
-
不过闭包对于变量的引用,让引用类型一直被引用,无法被GC,若使用不当会导致内存泄漏的问题
-
变量的存储:除了局部变量,其他的全都(全局变量和被捕获变量)存在堆中 跟下面对比着看。。
实现词法绑定的一种技术。
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用)。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
-
基本类型存放: https://www.zhihu.com/question/482433315/answer/2083349992
var a = 1; function foo() { // foo函数在创建时也会创建一个内部的VO(变量对象):{b : 2; innerFoo : <函数地址引用>} // 同时建立作用域链,对外层的VO链接起来,就能够读取到变量 a var b = 2; return function innerFoo() { console.log(a,b); } } const foo1=foo(); foo1(); //1,2 因为闭包,函数innerFoo记录了他的上下文。b被引用了,就不被释放。(当前作用域产生对父作用域的引用)
-
使用场景:能访问函数定义时所在的词法作用域,让变量私有化,模拟块级作用域、函数懒执行(比如实现柯里化和反柯里化)
参考:https://juejin.cn/post/6844903997615128583
模块化
ES Modules
就ES6才有的。好用、静态导入、动态引用、导入导出的值都指向同一个内存地址CommonJs
是 Node 独有的规范,浏览器不能直接用,支持动态导入- [javascript模块化演进及原理浅析] https://juejin.cn/post/6940163713345257486
CommonJs提供了exports(导出) 和 require(导入) 两个对象。并在在运行时才确定引入然后再执行这个模块(相当于函数调用)。输出是值的浅拷贝。this指向当前模块
ES6 Module是静态的。是在代码正式运行前执行(编译阶段,只能写在头部)。导出的是值的内存的地址的引用(不能更改,相当于用const定义),this指向undefined,利于打包工具进行tree shaking
!
Es Module
还可以 import()
懒加载方式实现代码分割。
包装函数的本质
function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {'
+ script +
'\n})'
}
Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
-
Promise让异步函数原来的回调函数处理变成链式处理
.then().then() ...
,上一个处理完交给下一个then() -
一个promise可以接收多个then,每个then都返回一个新的Promise对象
-
Promise.catch(()=>{}) 等于 Promise.then(undefined,()=>{})
-
Promise.rejected有传递机制,会不断向下传递直到遇见
onRejected
处理函数当一个没有绑定处理函数的 Promsie 被调用 reject 则会创建一个 微任务来再次检测这个 Promise 是否存在处理函数,如果此时还不存在则输出报错
-
内部有3个状态,未完成(收集依赖),成功和失败(将then/catch中的回调函数加入任务队列)
-
then的
onFulfilled
可以返回一个thenable对象(包含then方法的任意对象)let p2 = Promise.resolve().then(() => { console.log(0); let p3 = Promise.resolve(4) return p3; })
p2要达到Fulfilled,会先函数一个微任务并推入队列,然后当该任务被执行再调用p3.then()
microtask(() => { p3.then((value) => { ReslovePromise(p2, value) }) // 再次加入微任务队列 }) // https://juejin.cn/post/7055202073511460895#heading-30
-
async
和await
是es7有的。await
用来等待一个Promise
对象的结果。代替.then()
的写法。async
用来定义异步函数,返回一个Promse
对象。 -
async
和await
是Generator 函数的语法糖 ,await
跟yield
命令作用一样。await必须在async函数的上下文中 -
手写 Promise类之内部重要组成部分
-
当前Promise实例的状态 'Pending' 'Fulfilled' 'Rejected' 三种之一 (转变后不能再转变)
-
当前Promise接受构造器执行完的结果 result,(作为依赖的参数值)
-
then方法传入的回调函数的队列 resolveQueue [] & rejectQueue []
-
改变Promise实例的状态的函数 _resolve & _reject,(调用收集的依赖,放入微任务队列)
-
-
Promise静态方法四兄弟
- Promise.all接收多个Promise实例,都成功时,返回结果数组(被then接收),当有一个失败时,发生短路返回第一个失败的实例(被catch接收)
- Promise.race接收多个实例,最先做完的被返回,是成功就被then接收,是失败就被catch接收
- Promise.allSettled都做完再执行成功回调
- Promise.any跟all相反,都失败了reject,一个成功就fulfiled
- all和race都具有短路特性,all有一个失败就reject,race参数中某个
promise
解决或拒绝,返回的 promise就会解决或拒绝。
Generator
-
Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程
// 使用 * 表示这是一个 Generator 函数 // 内部可以通过 yield 暂停代码,并 声明内部状态的值 // 通过调用 next 恢复执行 function* test() { let a = 1 + 2; yield 2; yield 3; } let b = test(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true }
-
从以上代码可以发现,加上
*
的函数执行后拥有了next
函数,也就是说函数执行后返回了一个对象。每次调用next
函数可以继续执行被暂停的代码。 -
Generator函数内部遇到yield命令,那么就不往下执行了,就把执行权交出来给别的函数。别的函数做完,返还执行权后才继续执行。
Proxy
用来代理一个对象,通过Proxy可以轻松监视到对象的读写过程
Reflect
Reflect 可以用于获取目标对象的行为,它与 Object 类似,它的方法与 Proxy 是对应的。
Map&Set
map和Object的区别 事件订阅用Map不错
来源:https://juejin.cn/post/6940945178899251230#heading-37
Map | Object | |
---|---|---|
意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。 |
键的类型 | 任意 any! | Object 的键必须是 String 或是Symbol。 |
键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 |
Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某种方式获取它的键然后才能迭代。 |
性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
- 如果需要遍历键值对,并且考虑顺序,优先考虑 Map
- Map是纯Hash结构,对频繁增删键值对的场景表现更好
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open("post", "http://localhost:8080/api/test");
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify({a: 1, b: 2 }));
xhr.onreadystatechange = function(){
if(xhr.status==200 && xhr.readyState==4){
let result=xhr.responseText;//获取到结果
alert(result);
}
}
ajax!=XHR ajax是一种概念,XHR是基于浏览器是一种实现
fetch和XHR的区别
- fetch更加语义化和简洁、基于Promise实现、更加底层
- fetch不能检测请求进度,不能中断请求(abort)
for in & for of
for...of是ES6新增的,用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。获取的是对象的值。
// 普通对象使用for...of遍历 来自: https://juejin.cn/post/6940945178899251230#heading-77
var obj = {
a:1,
b:2,
c:3
};
//方法一:
obj[Symbol.iterator] = function(){
var keys = Object.keys(this);
var count = 0;
return {
next(){
if(count
for...in主要是为了遍历为对象,获取的是对象的键。
let obj={a:1,b:2}
for (const key in obj) {
console.log(key);
}
JSON.stringify
列举一些注意事项
-
对于不能被序列化的转换值,可以添加
toJSON()
方法(Date对象自带),如RegExp,Error和function,且toJSON方法会覆盖对象默认的序列化行为 -
对象的属性值为undefined和Symbol的属性则会被忽略(在数组中变成
null
) -
NaN 和 Infinity 格式的数值及 null 都会被当做
null
-
无法解决包含循环引用的对象,会抛出错误
-
只遍历可枚举属性,对于Set、Map家族,默认返回
{}
事件循环
https://juejin.cn/post/7049385716765163534
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop,异步线程与主线程通讯靠的就是Event Loop(比如网络请求,定时器)
设计 Loop 机制和 Task 队列是为了支持异步,解决逻辑执行阻塞主线程的问题,设计 MicroTask 队列的插队机制是为了解决高优任务尽早执行的问题。
微任务:Promise.then/catch/finally、MutationObserver、process.nextTick(Node)
宏任务:script、setTimeout()、setInterval()、requestAnimationFrame()、I/O、Ajax
Timers等是先放到放到Event Table中,等满足触发条件才添加到宏任务队列中的(定时器线程处理)
简易:每次执行一个宏任务,然后执行所有微任务!
首先事件循环从宏任务队列开始,有就取一个任务放入主进程(一个调用栈)中执行,然后检查微任务队列,并执行清空所有微任务队列。然后就检查是否需要视图更新(也有个更新队列),这样就算一个tick。
btw,在视图更新前会执行‘请求动画帧’的回调函数。浏览器只保证请求动画帧的回调在重绘前执行,没有确定时间。何时重绘有浏览器决定,且频率不高于主显示器的刷新率
Nodejs的事件循环与浏览器事件循环的区别
Node中的宏任务之间也有优先级,比如定时器 Timer 的逻辑就比 IO 的逻辑优先级高。
Timers Callback: 涉及到时间,肯定越早执行越准确,所以这个优先级最高很容易理解。
Pending Callback:处理网络、IO 等异常时的回调,有的 lniux 系统会等待发生错误的上报,所以得处理下。
Poll Callback:处理 IO 的 data、网络的 connection,服务器主要处理的就是这个。
Check Callback:执行 setImmediate 的回调,特点是刚执行完 IO 之后就能回调这个。
Close Callback:关闭资源的回调,晚点执行影响也不到,优先级最低。
链接:https://juejin.cn/post/7049385716765163534#heading-1
process.nextTick方法总是发生在所有异步任务之前。(总是在当前"执行栈"的尾部触发,高优先级的微任务)
事件模型
“事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。”
浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。
事件的操作和触发都定义在EventTarget
接口,分别具有addEventListener
、removeEventListener
和dispatchEvent
方法
element1.addEventListener('click', hello); // 注册事件,调用 hello方法,默认冒泡阶段触发
element1.removeEventListener('click',hello);// 移除事件,第二 、第三个参数都要一样才能移除
element1.dispatchEvent(new Event('click')); // 触发事件,传入一个事件对象(必须指定事件类型)
事件流
事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
捕获阶段是从window
对象传导到目标节点
冒泡阶段是从目标节点导回到window
对象
父元素 子元素
父级先捕获→子级捕获→子级冒泡→父级冒泡
事件委托/代理
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
利用事件冒泡(里面往外面冒泡li>ul>body),只指定一个事件处理(ul)程序,就可以管理某一类型的所有事件。(公司前台MM帮忙取快递例子)
比如ul>li*6,不必为每个li绑定事件,ul绑定事件即可, 利用e.target即可知道触发的是哪个li了。
这样节省了绑定事件的数量,进而节省内存,提高性能。同时后面新来的元素也不用麻烦的添加事件函数,也能处理事件了
鼠标事件坐标
属性 | 说明 | 兼容性 |
---|---|---|
offsetX/Y | 以当前的目标元素左上角为原点,定位x/y轴坐标 | |
clientX/Y | 以浏览器可视窗口左上角为原点,定位x/y轴坐标 | all |
pageX/Y | 鼠标指针相对于整个文档(document)的X/y坐标; | |
screenX/Y | 鼠标指针相对于全局(屏幕)的X/y坐标 | all |
元素视图尺寸
属性 | 说明 |
---|---|
offsetLeft/Top | 获取当前元素到定位父节点的left/top方向的距离 |
offsetWidth/Height | 返回一个元素的布局宽度/高度(包含到border和padding) |
clientWidth/Height | 表示元素的内部宽度/高度(包含padding,不包含border) |
scrollWidth/Height | 包含clientWidth/Height以及溢出的内容(如果有) |
scrollLeft/Top | 可以读取或设置元素滚动条到元素左/上边的距离 |
Window.innerWidth/Height | 浏览器窗口可视区宽度/高度(不包含菜单栏、工具栏等) |
script标签中的async和defer
async:开启另外一个线程下载js文件,下载完成,立马执行。(此时才发生阻塞)
defer:开启另一个线程下载js文件,直到页面加载完成时才执行。(根本不阻塞)
在开发中,defer常用于需要整个DOM的脚步(依赖于执行顺序),async用于独立脚本如计数器或广告
有 defer
属性的脚本会阻止 DOMContentLoaded
事件,直到脚本被加载并且解析完成。
ES6(ES2015)
从以下方面数据类型、关键字、机制、对象、便利性
symbol、const/let、Class与extend、ES Module 、解构、字符串模板、箭头函数、新的自带对象orApi( Set、Map、Promise、Promise、Genrator、Proxy、Refect) 、迭代器和for...of、函数默认值、对象属性名简写、以及自带对象再增强
ES2016-ES2020
- ES7:
includes()
方法(更好的语义化和NaN比较) 、求幂运算符**
- ES8:
async/await
关键字 、Object.values
,Object.entries
等 - ES9: for await of(异步迭代)、改进正则表达式(命名捕获组、dotAll等) 、剩余运算符和对象扩展运算符
- ES10: flat/flatMap、新类型BigInt、catch变量可选、function.toString()等
- ES11: 可选的链接操作
?.
、??
运算符、Promise.allSettled
、global this
、动态引入import()
、BigInt、String.prototype.matchAll()
CSS基础
文章推荐
- 一个比较全的总结:1.5 万字 CSS 基础拾遗(核心知识、常见需求): https://juejin.cn/post/6941206439624966152
- css篇--100道近两万字帮你巩固css知识点: https://juejin.cn/post/6844904185847087111
- 你未必知道的49个CSS知识点: https://juejin.cn/post/6844903902123393032
psition 定位
- static:默认定位
- relative:相对其正常位置定位,不脱离文档流
- absolute:相对于最近定位为非static的父级元素进行定位,脱离文档流
- fixed:生成固定定位的元素,相对于浏览器窗口进行定位!
- sticky:集合了fixed和relative,但受控于父元素们。需要设置top属性,当具体元素距离上边距{{top}}时,由relative变成类似的fixed效果,但父元素滚动出去了它也要跟着出去,用于实现跟随窗口的效果
- inherit:继承父级元素的position属性值
overflow 溢出
来自:https://juejin.cn/post/6844904199772176392#heading-3
属性值 | 描述 |
---|---|
visible | 不剪切内容也不添加滚动条 |
hidden | 不显示超过对象尺寸的内容,超出的部分隐藏掉 |
scroll | 不管超出内容否,总是显示滚动条 |
auto | 超出自动显示滚动条,不超出不显示滚动条 |
文本溢出处理:
/*1. 先强制一行内显示文本*/
white-space: nowrap;
/*2. 超出的部分隐藏*/
overflow: hidden;
/*3. 文字用省略号替代超出的部分*/
text-overflow: ellipsis;
BFC 块级格式化上下文
BFC 就是页面上的一个隔离的独立容器(断绝空间内外元素间相互的作用),容器里面的子元素不会影响到外面的元素。反之也如此。计算BFC的高度会包含所有子元素进行计算。浮动盒区域不叠加到BFC上。
-
哪些条件可以触发
-
浮动元素:float 除 none 以外的值
-
绝对定位元素:position (absolute、fixed)
-
display 为 inline-block、table-cells、flex、grid
-
overflow 除了 visible 以外的值 (hidden、auto、scroll)【最常用】
-
-
BFC的应用
- 解决margin重叠:当父元素和子元素发生 margin 重叠时,解决办法:给子元素或父元素创建BFC
- BFC区域不与float区域重叠(清除浮动原理:计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算;)
- 分栏布局:一边float,另一边BCF占满剩余空间 (浮动盒区域不叠加到BFC上;)
来自:https://juejin.cn/post/6844904071497777165#heading-5
box-sizing 盒模型
- box-sizing:content-box; 默认值,只计算内容的宽度,border和padding不计算入width之内
- box-sizing:border-box; border和padding计算入width之内
- box-sizingpadding-box; padding计算入width之内
CSS优先级
-
!important 会覆盖页面内任何位置的元素样式
-
内联样式,如 style="color: green",权值为 1000
-
ID 选择器,如#app,权值为 0100
-
类、伪类、属性选择器,权值为 0010
-
标签、伪元素选择器,如 div::first-line,权值为 0001
-
通配符、子类选择器、兄弟选择器,如*, >, +,权值为 0000
-
继承的样式没有权值
CSS变量
// 定义在这个伪类确保所有选择器可以访问
:root{
--red: #ff0e0e; // 变量名必须 --开头 区分大小写
--tiny-shadow: 4px 4px 2px 0 rgba(0, 0, 0, 0.8);
}
// 使用
li {
color:var(--red,red); // 第二个可选参数是当第一个参数不生效时使用(备用选项)
box-shadow: var(--tiny-shadow);
}
CSS变量在管理颜色,主题切换,减少重复代码,增加易读性方面很有作用
在其他CSS变量中、calc()函数中也可以使用
z-index
z-index只应用在定位的元素,默认z-index:auto;
在层叠上下文中 ,子级层叠上下文的z-index
值只在父级容器中才有效(即在同一层叠上下文中)。其值只决定在同一父级容器中,同级子元素的堆叠顺序。
常用单位
- px:像素单位
- %:百分比
- em: 相对于元素的字体大小(font-size)
- rem:作用于非根元素时,是相对于根元素字体大小 (root em)
- vh/vw:v是view,视窗的意思 100vh就是100%的视窗高度
margin padding的百分比计算
padding-top,padding-bottom,margin-top,margin-bottom取值为百分比的时候,参照的是父元素的宽度。
top
, left
, right
, bottom
取值百分比时相对于父级定位元素宽高
CSS元素分类
块级元素(block):能设置宽高,独占一行。
内联元素(inline):不能设置宽和高,不影响换行。
替换元素和非替换元素:非替换元素的表现由内容决定。替换元素相反可随意设置宽高,其展现效果不是由CSS来控制的。简单来说,它们的内容不受当前文档的样式的影响。 CSS 可以影响可替换元素的位置,但不会影响到可替换元素自身的内容。
inline-level 元素分类 | 具体元素 | 默认特征 |
---|---|---|
可替换元素 | img、input、iframe、video、canvas、 | 宽高可任意设定 |
非替换元素 | a、strong、code、label、span | 宽高由内容决定 |
伪类和伪元素
https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Building_blocks/Selectors/Pseudo-classes_and_pseudo-elements
伪元素 (它表现像加入全新的HTML元素一样)
选择器 | 描述 |
---|---|
::after |
匹配出现在原有元素的实际内容之后的一个可样式化元素。 |
::before |
匹配出现在原有元素的实际内容之前的一个可样式化元素。 |
::first-letter |
匹配元素的第一个字母。 |
::first-line |
匹配包含此伪元素的元素的第一行 |
::selection |
匹配文档中被选择的那部分。 |
伪类(它用于选择处于特定状态的元素)
选择器 | 描述 |
---|---|
:active |
在用户激活(例如点击)元素的时候匹配 |
:checked |
匹配处于选中状态的单选或者复选框。 |
:focus |
当一个元素有焦点的时候匹配。 |
:hover |
当用户悬浮到一个元素之上的时候匹配。 |
:disabled |
匹配处于关闭/禁用状态的用户界面元素 |
:nth-... | :nth-child 、:nth-of-type 等等 |
:visited |
匹配已访问链接。 |
ele:nth-of-type(n) 指父元素下第n个ele元素 (计算n时只纳入ele元素)
ele:nth-child(n) 指父元素下第n个元素且这个元素是ele元素才匹配
选择器优先级
按照权重排列 !important 拥有最高优先级
- 内联样式(style=“ ”) 1000
- id选择器(#myid) 100
- 类选择器(.myclass) 10
- 属性选择器(a[rel="external"]) 10
- 伪类选择器( :active , :hover) 10
- 元素选择器(div, h1,p,after) 1
- 关系选择器、通配符 0
- 继承样式
- 默认样式
inline inline-block block的区别
- block:
- block元素会独占一行,多个block元素会各自新起一行。默认情况下,block元素宽度自动填满其父元素宽度。
- block元素可以设置width,height属性。块级元素即使设置了宽度,仍然是独占一行。
- block元素可以设置margin和padding属性。
-
inline-block:
简单来说就是将对象呈现为inline对象,但是对象的内容作为block对象呈现。之后的内联对象会被排列在同一行内。比如我们可以给一个link(a元素)inline-block属性值,使其既具有block的宽度高度特性又具有inline的同行特性。
-
inline:
- inline元素不会独占一行,多个相邻的行内元素会排列在同一行里,直到一行排列不下,才会新换一行,其宽度随元素的内容而变化。
- inline元素设置width,height属性无效。
- inline元素的margin和padding属性,水平方向的padding-left, padding-right, margin-left, margin-right都产生边距效果;但竖直方向的padding-top, padding-bottom, margin-top, margin-bottom不会产生边距效果。
注: 一些inline元素同时又是可替换元素,比如img\input
这些,本身带有width height
属性,所以可以设置宽高
作者:homyeeking
链接:https://juejin.cn/post/6844904197435949064
transition和animation
transition:
? 语法:transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)
? 作用:为元素的变化添加过度效果
animation:
? 语法:animation:动画名称,一个周期花费时间,运动曲线(默认ease),动画延迟(默认0),播放次数(默认1),是否反向播放动画(默认normal),是否暂停动画(默认running)
需要 @keyframes 动画名称{ }
display:none和visibility:hidden
前者不会保留元素(不被渲染),位置会被之后正常的文档流覆盖。更改值后会触发reflow(回流)
后者仍会保留元素,只是看不见。更改值后会触发repaint(重绘)
dom树:display:none和visibility:hidden
渲染树:visibility:hidden
此外opacity:0和visibility:hidden一样也只是隐身,但opacity:0可以触发点击等事件
水平居中
- 已知宽度的元素设置文本内容水平居中:
text-align:center
- 设置有宽度的块级元素水平居中:
margin:0 auto
display:flex
+justify-content:center
设置子元素水平居中
垂直居中
- 已知高度的元素设置文本内容垂直居中:
line-height:高度
或者给父元素设置display:table-cell;vertical-align:middle
display:flex
+aligin-items:center
设置子元素垂直居中
水平+垂直居中
- 绝对定位居中除了
top/left:50%
之外需要使用margin-top/left:高度/宽度的一半
或者transform: translate(-50%, -50%)
来让中心点而非左上角居中。 .box1 {display: grid;place-items: center;}
最便捷实现水平+垂直居中.box1 {display: flex; justify-content: center; align-items: center;
}` 第二便捷实现水平+垂直居中- 以上两种若有多个元素要居中,使用grid布局会在一列上放,flex布局会在一行上放(默认主轴方向为行)
等高居中
使用Flexbox或者Grid布局,子元素默认等高(flex:align-items的默认值为
stretch)。
三列布局
float(两边各自浮动到两边)、flex(flex属性)、grid(grid-template-columns)、table(display:table-cell)、position(都用绝对定位,中间使用left,right扩展开)
清除浮动
不清除子元素的浮动,可能会导致没东西撑起父元素,从而造成高度塌陷
最好的方案就使用伪元素
.clearfix {
zoom: 1; /* 为了兼容IE低版本 */
&::after {
display: block;
content: ' ';
clear:both
}
}
其他不常用方法(面试官可能会问还有其他方式吗)
- 父级元素添加overflow属性 触发BFC
- 添加额外标签(再最后一个浮动标签后再添加一个新标签 给其设置clear:both)
扩大可点击区域
利用伪元素代替主元素响应鼠标交互
button {
position:relative;
/* ... */
}
button:before {
content:'';
position:absolute;
top:-10px;
right:-10px;
bottom:-10px;
left:-10px;
}
BEM命名规范
BEM的命名规矩很容易记:block-name__element-name--modifier-name,也就是模块名 + 元素名 + 修饰器名/状态。
一般来说,根据组件目录名来作为组件名字:
比如分页组件:/app/components/page-btn/
那么该组件模块就名为page-btn,组件内部的元素命名都必须加上模块名,比如:
.header__logo{ border-radius: 50%; } .header__title{ font-size:16px; }
.page-btn__prev--loading{ background:gray; } .page-btn__prev--disabled{ cursor: not-allowed; }
在Sass中使用
.header {
&__title {
wdith: 100px;
padding: 20px;
}
&__button {
&--primary {
background: blue;
}
&--default {
background: white;
}
}
} // from https://juejin.cn/post/6969524904400011301#heading-6
CSS面试题
-
css的匹配顺序: 从右向左(更高效)
-
两栏布局方式 三列布局 BFC 要么FLEX 顶宽可以用margin
-
移动端适配
- 响应式布局(媒体查询)
- 使用postcss px-rem加上flexible
- 使用postcss px - vw
Webpack基础
webpack编译项目从解析webpack.config.js
开始,每完成特定的一步会调用响应的钩子。
- 首先是要解析入口文件,并将文件转换成AST(
@babel/parser
)。 - 找出入口文件所有的依赖模块(
@babel/traverse
) - 将文件转换成可执行的代码,并按照第二步这样递归下去重复执行1、2、3
- 重写
require
函数,并按照步骤4生成递归关系图,输出到bundle中
涉及到多个区块的代码打包,通过import()
实现code spliting,webpack会利用jsonp
加载 chunk 的运行时代码。
先从配置文件和shell语句读取参数并初始化Compiler对象,加载所有配置插件,执行run()开始编译。webpack使用nodejs的fs模块,读取定义的入口entry文件,将文件转换成AST树,并递归地获取所依赖的模块文件,调用对用的loader处理匹配后缀的文件,将遇到的模块文件都放入_webpack_modules_这个对象中,并用文件的相对src路径作为对象key,key对应的是一个函数,该函数的参数是模块、导出、导入三个对象/方法,方法内部利用eval()
执行原本文件的js内容(被webpack用babel转成AST,进行遍历,再生成浏览器可执行的代码,里面的用到的导入导出功能,就是由参数传进来的[源代码被包裹函数进行包裹])。_webpack_modules_处于的代码中,还有对模块的缓存、重写后的__webpack_require__方法等。
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = (__webpack_module_cache__[moduleId] = { exports: {}, });
// 原文件的require、module被重写了并作为参数传入
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_modules__ = {
"./src/add.js": ( __unused_webpack_module, __webpack_exports__,__webpack_require__) => {
eval(`__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => __WEBPACK_DEFAULT_EXPORT__
});
var add = function add(a, b) { return a + b; };
const __WEBPACK_DEFAULT_EXPORT__ = (add);`);
},
"./src/index.js": (__unused_webpack_module,__webpack_exports__,__webpack_require__) => {
eval(
'__webpack_require__.r(__webpack_exports__);\n var _cute_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/cute.js");\n var _add_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/add.js");\n\n\n var num1 = (0,_add_js__WEBPACK_IMPORTED_MODULE_1__.default)(1, 2);\nvar num2 = (0,_cute_js__WEBPACK_IMPORTED_MODULE_0__.cute)(100, 22);\nconsole.log(num1, num2);'
);
},
}
//.d是定义属性 .o是hasOwnProperty调用 .r 不懂`Object.defineProperty(exports, "__esModule", { value: true });`
loader
loader是文件加载器(本质是一个函数,接受源代码输出处理后的结果),能对文件进行编译压缩等处理(webpack默认支持js),并将它们转换为有效模块,最后打包进去,loader的执行顺序和配置中相反。
在处理文件前,首先会经历patch
阶段,pitich loader属性返回非undefined会产生熔断效果。
详解:https://juejin.cn/post/7036379350710616078
plugin
plugin是对webpack在运行的生命周期中广播出的事件进行处理,可通过Webpack的API改变结果。(基于Tapable )
Q: plugin1可以派发事件让plugin2监听吗?
A: webpack的事件机制是基于观察者模式的。plugin不仅能够监听事件,也能够广播事件和其他的插件进行通信。
// 广播事件
compiler.apply('event-name', params)
// 监听同名的事件,当这个事件触发的时候,回调函数就会被执行
compiler.plugin('event-name', params => {
。。。
})
// https://zhuanlan.zhihu.com/p/40930680
Babel
核心就是利用一系列plugin来管理编译的案例,对不同es版本的js、甚至jsx。来把它们编译成所需要的js,来让更多浏览器支持该js的运行
解析、遍历、生成
babel.config.js 和.babelrc 有什么区别
全局配置 (babel.config.js) :即针对第三方代码也针对自己的代码
局部配置 (.babelrc):按目录加载 ,只影响版项目
Q: module
、chunk
、bundle
、asset
的区别?
asset:就是图片字体等资源文件
chunk:webpack处理时根据文件引用关系组成的chunk文件
moudule:每个文件都可以视为一个模块
bundle:处理chunk文件后,生成可在浏览器中运行的代码
https://blog.51cto.com/u_15283585/2957111
打包体积优化
主要思路是分离&提取、按需加载、提取通用模块、压缩&混淆代码、Tree Shaking、图片压缩、
热更新原理
当启动一个服务之后(用的webpack-dev-server),浏览器和服务端是通过websocket进行长连接,webpack内部实现的watch(基于chokidar库)就会去监听文件修改。只要文件有修改,webpack就会重新打包编译到内存中,然后webpack-dev-server依赖中间件webpack-dev-middleware和webpack之间进行交互,每次热更新都会请求一个携带hash值的json文件和一个js,websocke传递的也是hash值,内部机制通过hash值检查进行热更新。
- 用
Hash
值代表每一次编译的标识 - 编译完成后通过
websocket
向客户端推送当前编译的hash戳
- 客户端的
websocket
监听到有文件改动推送过来的hash戳
,会和上一次对比- 一致则走缓存,不去请求
- 不一致则通过
ajax
和jsonp
向服务端获取最新资源,并替换删除缓存
- 使用内存文件系统(memfs)去替换有修改的内容实现局部刷新
- https://blog.csdn.net/chern1992/article/details/106893227/
- https://segmentfault.com/a/1190000020310371
- https://mp.weixin.qq.com/s/gG_FwVGHiJGjQOvt5rZheA (未看)
与Rollup的对比
Webpack强调的是前端模块化方案,侧重模块打包。Rollup简洁且打包出体积更小的文件(tree shaking)。
开发库的时候用Rollup多,比如Vue和React等。
Vite基础
vite主要特性:bundless,基于浏览器原生支持的ES module,相当于按需引入模块。生产环境下使用rollup打包编译。开发和生产环境下共享同一套 Rollup 插件机制。
模块解析
预构建是用来提升页面重载速度,它将 CommonJS、UMD 等转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包程序都要快得多。
热更新HMR
跟webpack-dev-server类似,服务端监听文件改动(通过给.vue
文件注册Watcher)和编译资源,通过websocket想客户端发送消息,客户端进行逻辑判断(文件指纹)是否要更新。
基于ESM的devServer插件在启动时会先初始化服务器和加载对应插件。插件包括拦截请求,将其转换成浏览器可识别的ESM语法、对.ts
、.vue
的即使编译以及 sass
或 less
的预编译、与浏览器建立socket
连接,用于实现HMR。
启动一开始通过插件向向index.html注入代码,劫持/vite/hmr
的请求,然后返回client.js
文件,该文件主要用于跟服务器(koa)建立websocket连接。
参考:https://juejin.cn/post/6854573209329598477
Nodejs基础
NodeJS 是基于Chrome V8引擎的 JavaScript 运行环境。NodeJS使用事件驱动,非阻塞型I/O的模型,使其轻量又高效。且有一堆优化的API类库调用(如c++的libuv)。
Nodejs异步编程最直接的体现就是回调。使得代码执行时没有阻塞或者等待文件I/O的操作,在读文件的同时执行接下来的代码,提高了程序性能。
事件循环
Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。
Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发。
Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
Node.js 单线程类似进入一个
while(true)
的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.作者:前端开发小马哥
链接:https://juejin.cn/post/6901093313756332040
包管理器
依赖管理
yarn最先时候loack文件锁定版本,后npm也才增加上
都采用扁平化管理依赖,避免嵌套深、大量包重复安装的问题
安装速速
yarn采用并行安装依赖方式,比串行的npm快些
浏览器原理
最推荐:https://zhuanlan.zhihu.com/p/102149546 (太长就看了一部分)
浏览器原理
-
页面加载过程(仅为HTTP)
-
构建请求行(
GET / HTTP/1.1
),然后检查强缓存,命中则直接使用不发起请求 -
进行DNS域名解析
-
通过三次握手建立TCP连接(第三次是判断客户端的接收能力)
-
浏览器构造HTTP请求报文并发起请求(请求资源前会判断是否有缓存)
-
服务器处理请求并返回(响应行
HTTP/1.1 200 OK
)处理结果给浏览器 -
当不需要连接时,任意一方可以发起关闭请求,通过四次挥手来关闭(四次是为了确保数据传输完毕)
-
解析HTML并生成
DOM
树,CSS下载完后解析生成CSSOM
树 -
当DOM和CSSOM树构建完毕后,确定元素位置,生成渲染树
? JavaScript/CSS > 样式计算 > 布局 > 绘制 > 渲染层合并 > 光栅化
-
构建完渲染树之后,还会对特定节点进行分层,构建
图层树
,用于图层绘制并展现。-
某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层共用一个绘图层(节点的图层默认属于父节点图层) (Composite)
-
对于拥有层叠上下文的节点属于显示合成,会提升为单独的一个合成层
-
与显示合成对比的是隐示合成:在一个单独图层上还有层叠等级更高的节点,都会被提升为一个单独的图层,可能就会造成层爆炸,但浏览器也会尽力优化。
-
-- 分割线 ++
-
图层树构建后,就是图层绘制,渲染引擎会将图层的绘制拆成绘制指令,并组成绘制列表
-
准备完绘制列表,主线程会把绘制列表commit给合成线程(compositor thread)
-
合成线程会将图层分块(以便按需绘制),然后便是将视图附近的图块转换成位图(点阵图像)
- 由栅格化线程池完成转换工作,做的事就叫栅格化(过程中会使用GPU来加速生成)
-
合成线程发送绘制图块命令
DrawQuad
给浏览器进程 -
浏览器进程根据
DrawQuad
消息生成页面,并显示到显示器上
-
-
-- ++ == -- ++ == --++== --++==
-
浏览器渲染
零散的知识点记录。。。
https://fed.taobao.org/blog/taofed/do71ct/performance-composite/
https://csstriggers.com/
合成,就是把图层(GraphicsLayer)合并!
绘制阶段,并不是真绘制,而是生成绘制指令列表!有了绘制列表在进行光栅化生成图片(位图)。
每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。合成操作是在合成线程上完成的。这也就意味着在执行合成操作时,是不会影响到主线程执行的。
能直接在合成线程中实现的是整个图层的几何变换,透明度变换,阴影等,这些变换都不会影响到图层的内容。比如滚动页面的时候,整个页面内容没有变化,这时候做的其实是对图层做上下移动,这种操作直接在合成线程里面就可以完成了。
合成层拥有独立的绘图层(GraphicsLayer),而其他不是合成层的渲染层(RenderLayer),则和其第一个拥有绘图层的父层共用一个绘图层。
-
合成层的位图,会交由 GPU 合成,比 CPU 处理要快得多;
-
渲染层决定渲染的层级顺序
-
当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层;
-
元素提升为合成层后,transform 和 opacity 才不会触发 repaint,如果不是合成层,则其依然会触发 repaint。
整个渲染流程
- 帧开始:浏览器发送垂直同步信号(Vsync), 表明新一帧的开始。
- 处理输入事件:输入事件(例如
touchmove
、scroll
、click
)在每一帧只会被触发一次。
优化:使用createDocumentFragment
进行批量的 DOM 操作、对resize、scroll防抖 、为动画元素创建合成层、使用请求动画帧(rAF)
层爆炸:一个非显示合成层被渲染层的元素覆盖产生交叠(overlap),会导致覆盖元素也被提升到合成层(隐式合成),就可能产生此问题。但浏览器也会有对应处理,将隐式合成的多个渲染层压缩到同一个绘图层中进行渲染,但也是有极限的。
详细博客(★):
重排与重绘
-
只要元素位置大小发生改变,就是重排(css3-transform除外)
-
元素节点内部渲染如颜色阴影字体家族等变化才是重绘
-
触发创建单独图层的,浏览器将其放在合成层,不影响默认复合图层,所以不影响周末DOM结构,属性的改变也交给GPU处理。(各个复合图层都是单独绘制,所以互不影响)
故transform和opacity改变的仅是图层的结合不会触发回流和重绘:
opactity是GPU在绘画时简单的降低了之前已经画好的纹理的alpha值来达到效果,故不会触发回流和重绘
-
降低重排的方式:要么减少DOM节点属性的读取,要么减少修改次数,要么降低影响范围,创建新的复合图层
-
重绘&回流与事件循环的关系 (渲染流程)
- 当一次事件循环结束后,会判断文档是否需要更新,这个间隔为16ms(60Hz为例)
- 浏览器发送垂直同步信号(Vsync), 表明新一帧的开始(Frame Start)
- 先会判断是否有
reize
、scroll
、touchmove
事件(每帧只触发一次) - 然后会判断是否触发媒体查询、并更新动画和发送事件、判断是否全屏操作
- 执行
requestAnimationFrame
回调,该函数告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 - 执行
IntersectionObserver
回调,该接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法(元素是否可见) - 更新界面,如果还有空闲时间会执行
requestIdleCallback
回调- 样式计算、布局、绘制、合成、光栅化、将合成帧发送给GPU(Frame End)
浏览器多进程
好处:避免单个线程崩溃导致进程卡死,更好利用多核优势,方便使用沙箱模型,提高稳定性
-
每个浏览器有一个浏览器
Browser
进程负责协调和主控,以及插件进程,GPU进程 -
然后每个页面(Tab)都有一个进程,负责页面渲染和脚本执行、事件处理(★)
- 主线程★(main thread)负责HTML/CSS解析、对象树的构建和JS解析执行
- 合成线程★(compositor thread)负责页面的各个部分分层、单独光栅化,最后变成一个位图
- 事件触发线程:将setTimeout,AJAX,鼠标点击等对应任务加入事件线程中。当事件符合触发条件时,该线程会将事件添加到任务队列
- 定时器触发线程:
setInterval
与setTimeout
所在线程,用来计时并触发,然后加入任务队列 - 异步HTTP请求线程,一个XHR开一个线程请求
- Worker threads 、Raster thread
-
Browser
进程收到用户请求,获取页面,然后交给Tab的渲染进程。渲染的过程中可能会有Browser
进程获取资源和需要GPU进程帮助渲染。渲染Render
进程会讲结果传递给Browser
进程。再由Browser
进程接收并绘制。 -
GPU 进程(GPU Process)不是在 GPU 中执行的,而是负责将渲染进程中绘制好的 tile 位图作为纹理上传至 GPU,最终绘制至屏幕上。
垃圾回收
常见标记清除算法(JS引擎常用)和引用计数算法。标记清除算法就是打标记(0 | 1),实现简单,当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。但容易让内存碎片化,以及分配内存时要遍历一次(O(n)的操作)。
V8引擎对GC的优化:
V8 的垃圾回收策略主要基于分代式垃圾回收机制(JVM也是),分成新老两代用不同策略来管理。新生代还有2个区域,一个使用区,一个空闲区,标记阶段会将活动对象复制到空闲区,在清理阶段将非活动对象(也就是原先的使用区)清掉。交换两个区的角色。多次不被清掉的、大的对象会被移入老生代。
其次就是并行回收,减少暂停时间。
内存泄漏
全局(window)属性、闭包、遗忘的定时器 总之就是可能存在引用,导致未被释放
vue中可能发生内存泄漏场景
例如使用v-if或者vue router跳转时从VNode中移除元素时,该元素由第三方库创建的,可能就会导致泄漏,要做好即使的清理工作
这些内存泄漏往往会发生在使用 Vue 之外的其它进行 DOM 操作的三方库时,没有正确调用销毁函数。
Service Worker
Service Worker实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
Service Worker的目的在于离线缓存,转发请求和网络代理。它有自己的生命周期。
网络基础
TCP 和 UDP 的差别
tcp 可靠,有序,面向连接要握手挥手,需要消耗更多的资源,速度较慢,适合少量数据,只能一对一,能全双工
upd 不可靠,无序,面向非链接,资源消耗更少,速度更快(实时性好),结构简单,可能丢包,适合大量数据,提供单播,多播,广播的功能。
TCP协议为什么需要三次握手?
- 表因:三次挥手是在信道不可靠的基础上,避免已失效的连接请求报文段让server端建立无用的连接(会白白浪费资源,一直等client端消息)。 三次通信是理论上的最小值
- 本质:因为通讯的双方维护一个序列号
seq
,用于标识发送出去的数据包中, 哪些是已经被对方收到的。三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。 - 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认(没被 确认就建立连接岂不是不行!)
TCP协议为什么需要四次挥手?2MSL又是什么?
A:主动方 B:被动方
第二次挥手后(B→A),A不会再想B发送数据,但B还可以继续发(可用于发送没发完的数据)
第三次挥手后(B→A):③,B便进入LAST-ACK状态
第四次挥手(A→B):④,是在A接到第三次挥手后,立即发送确认应答,A会进入TIME-WAIT状态。该状态会持续2MSL时间。在这期间没B没请求,才会进入CLOSED状态。B在收到确认应答也便进入CLOSED状态。
第四次挥手原因也是要确保B能收到A的确认应答!如果A的确认应答(第四次挥手)丢失,会导致B无法正常关闭。因为B没有收到确实应答④的话,就不能确认A受否收到③,B就自动会重传③。不论是重传③或者收到④,A都需要等待,2MSL就是去MSL+来MSL的时间。B不重传就证明B收到了嘛。并会让B重传的FIN在网络中消逝。
摘要
- 来MSL+去MSL=2MSL,MSL:报文生存时间
- 不论是否要收到B的重传,A都得等。收到就重新应答,没收到就默认B收到,B不进行重传
- 确认B能收到A的确认应答,进而让B正常关闭
- 让B重传的FIN在网络中消逝
OSI和TCPIP模型
层级 | OSI七层模型 | TCP/IP四层 | 常用协议 |
---|---|---|---|
7 | 应用层 | 应用层 | HTTPS、HTTP、SSH、DNS、FTP、SMTP(Email) |
6 | 表示层 | 应用层 | |
5 | 会话层 | 应用层 | |
4 | 传输层 | 传输层 | TCP、UDP |
3 | 网络层 | 网络层 | IP、ICMP、ARP |
2 | 数据链路层 | 网络接口层 | 物理线路、光纤等处理连接网络的硬件部分 |
1 | 物理层 | 网络接口层 |
WebSocket
不受同源限制,是一个新的协议。一般需要HTTP协议来帮忙握手建立连接。后面就一直保持着全双工通信方式。
HTTP缓存
浏览器缓存通常分为两种:强缓存和协商缓存。
强缓存 在缓存期间不发起新请求,返回200
- Expires,http1.0,是绝对时间的GMT格式字符串,在此时间前都有效
- Cache-Control,http1.1,max-age=xxx秒,代表缓存xxx秒,优先级高
- 二者都是响应中携带的字段,用来表示资源缓存时间,Expires使用绝对时间,当服务器与客户端时间偏差大可能就会导致缓存混乱。
- 强制刷新时,请求中会携带Cache-Control:no-cache和Pragma:no-cache
Cache-Control的常用指令:
no-cache
:不使用本地缓存,需要使用协商缓存(校验新鲜度)no-store
:禁用缓存,没次都从原始服务器获取public
:响应可以被任何对象(浏览器、代理)缓存private
: 只能被浏览器缓存,代理服务器不得缓存
协商缓存 如果缓存过期了,就要用协商缓存,需要一次请求,如果缓存有效,返回304
- Last-Modify(in 响应头)和If-Modify-Since(in 请求头)是一对的,值是资源最后修改时间(GMT格式字符串)
- Etag(每次都携带)和If-None-Match也是一对的,是文件的校验码,用来判断是否命中缓存
- 服务器会优先验证ETag,
If-None-Match
字段和Etag一致则返回304 - ETag对比Last-Modified优势:
- 一些文件可能内容不变,但修改时间便了,但这并不算是修改
- Last-Modified精度是秒,要是文件修改频繁也可能认不出
- 可能服务器不能精确获取文件的最后修改时间
HTTP Header
请求字段(Request)
字段 | 描述 | 实例 |
---|---|---|
Accept | 可接受的内容类型(MIME) | Accept: text/plain |
Accept-Charset | 可接受的字符集 | Accept-Charset: utf-8 |
Accept-Encoding | 可接受的编码 | Accept-Encoding:gzip, deflate |
Accept-Language | 可接受的语言 | Accept-Language: en,zh |
Accept-Ranges | 定义范围请求的单位 | Accept-Ranges: bytes |
Range | 以字节为单位,传输0到499字节范围的内容 | Range: bytes=0-499 |
Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: token字符串 |
Cache-Control | 强缓存相关 | Cache-Control: no-cache |
Cookie | 由服务端返回的Cookie | Cookie: $Version=1; Skin=new; |
Content-Length | 请求内容长度 | Content-Length: 348 |
Content-Type | 请求体的MIME类型 | Content-Type: application/x-www-form-urlencoded |
Referer | 请求来路 | Referer: http://www.baidu.com |
Upgrade | 协议转换(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 (Websocket也是) |
User-Agent | 浏览器的浏览器身份标识字符串 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36 |
响应字段
字段 | 描述 | 实例 |
---|---|---|
Set-Cookie | 设置Http Cookie | Set-Cookie: UserID=JohnDoe; Max-Age=3600; |
Content-Encoding | 返回内容的压缩编码类型 | Content-Encoding: gzip |
Content-Length | 返回内容的长度 | Content-Length: 9951 |
Date | 原始服务器消息发出的时间 | Date: Thu, 10 Feb 2022 14:06:04 GMT |
Location | 重定向到新位置 | Location: http://www.baidu.com |
Keep-Alive | 便于连接复用 | Keep-Alive: 300 |
Allow | 对某网络资源的有效的请求行为,不允许则返回405 | Allow: GET, HEAD |
内容对应系列
Accpet-* 代表可接受的类型
Content-* 代表要返回的类型
缓存系列
请求头的host,origin,refer的区别是什么
TODO
HTTP状态码
- 1xx:表示还在协议处理的中间状态,例如要建立websocket链接
- 2xx:表示成功,204与200相同,但响应头后没有body数据,206表示部分内容,用于HTTP分块下载和断点续传,搭配响应头字段
Content-Range
- 301:永久重定向, 302:临时重定向,304:协商缓存命中
- 400:错误请求, 401:未授权, 403:禁止访问,405:请求方法不允许,408:超时
- 500:服务器错误, 503 服务器忙不可用
HTTP流式传输
判断数据流结束的方法
-
Content-Length 对于已知大小的数据,可以在请求头中添加。
-
Transfer-Encoding:chunk 分块发送,由一个标明长度为0的chunk标示结束。
每个Chunk由头+正文组成(
CRFL
分割),头部内容指定正文的字符总数(16进制),正文就是实际内容
HTTP断点续传
HTTP1.1开始支持,通过Header的两个参数实现。客户端发送请求时对应Range
,服务端响应Content-Range
,同时响应头变成HTTP/1.1 206 Partial Content
Range: bytes=0-499 //以字节为单位,传输0到499字节范围的内容
??请求----响应??
Content-Range: bytes 0-499/22400 //0-499 是指当前发送的数据的范围,22400则是文件的总大小
Accept-Ranges: bytes // 服务器支持按字节下载
搭配If-Range
可判断实体是否发生改变(判断Etag或者Last-Modified返回值)
补充:
前端使用localStorage记录已上传切片的Hash值。
字节跳动面试官:请你实现一个大文件上传和断点续传
HTTPS
HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。(TLS是更为安全的的升级版SSL)
Http+加密+认证+完整性保护=Https
HTTPS页面中发送HTTP请求
HTTP请求的默认会被浏览器阻止(Mixed Content错误),而不是跨域错误。
混合内容又分为主动混合内容和被动混合内容。
- 被动混合内容是指不与页面其余部分进行交互的内容,包括图像、视频和音频内容 ,以及无法与页面其余部分进行交互的其他资源。
- 主动混合内容指的是能与页面交互的内容,包括浏览器可下载和执行的脚本、样式表、iframe、flash、Ajax请求等
借助被动混合内容可以突破限制
const img = new Image();
img.src = 'http的请求地址'
HTTP2.0
增强核心便是二进制分帧层。
帧是最小通信单位。不同数据流(Stream)的帧可以交错发送(并发!),然后接收端根据帧头重新组装。之前HTTP/1x中,一个连接每次只能交互一个响应(串行)。
2.0的分帧突破了这个限制,这让级联文件,雪碧图,域名分片不是成为必要的优化。就这就是多路复用
二进制帧结构:https://juejin.cn/post/6844904100035821575#heading-97
其次是头部压缩:建立索引查表,让请求头字段得到极大程度的精简和服用,还利用霍夫曼编码对整数和字符串处理,压缩头部,减少大小。
最后就是服务端推送,比如在返回HTML的基础上,还把HTML引用的其他资源一起返回给客户端,减少客户端等待时间。
Cookie
每个cookie:name、value、Domain、Path、Expires/Max-Age、size、HttpOnly、Secure、SameSite、SameParty、Priority
Set-Cookie: session=abc123; SameSite=None
字段详解:
字段 | 解释 |
---|---|
Domain | 指定域(对子域生效),在指定域下的请求会携带此Cookie,需要. 开始 |
Path | 指定路径(对子路径生效),且同Domain,需要/ 结尾 |
Expires/Max-Age | 有效期,一个是几点到期(GMT格式),另一个是能活多久(单位秒) |
HttpOnly | true 或 false。true时不允许Javascript操作此cookie |
Secure | true 或 false。true时在HTTPS下才传输此cookie |
SameSite | Strict、Lax或None。用来限制第三方 Cookie(可以参考阮一峰的文章) |
Cookie 隔离
请求资源的时候不要让它带cookie怎么做,以此降低请求头大小
使用非主要域名
框架对比
共同点
两者都是数据驱动视图(声明式编程),组件化,虚拟DOM+同层diff,
不同点
Vue核心是数据跟视图绑定,数据收集组件的render函数,在变化时以最小代价生成vnode并diff,且内置功能多,自带编译优化
react是函数式思想,局部重新刷新,推崇纯组件,数据不可变,通过js操作一切
两者DIFF的不同点:
1.React 首位是除删除外是固定不动的,然后依次遍历对比;
2.Vue 的compile 阶段的optimize标记了static 点,可以减少 differ 次数,而且是采用双向遍历方法;
React的更新粒度
在不优化的情况,所有层次都会重新render,生成VDOM并通过diff算法决定要更新的视图部分。不过利用Fiber提供异步渲染,进行弥补,利用memo和shouldComponentUpdate进行优化。
Vue的更新粒度
通过依赖收集对应组件进行精确更新。
当父组件更新时,会重新计算子组件的props,保证只有更变数据所对应的Watcher被调用。
框架特性、生态、开发体验、社区评价、性能、源码等多个角度
其他
低代码
TODO
微前端
前端体系
esbuild
TODO
测试驱动开发TDD
Canvas 和 SVG 区别
Canvas依赖分辨率,文本渲染能力弱,颜色丰富,适合图像密集型,方便保存图像
SVG(Scalable Vector Graphics)使用XML定义,基于矢量,易于编辑,有事件机制
Element 和 Node 区别
Node是基类,node是相对tree这种数据结构而言的。tree就是由node组成!
Element就是Node的子类,Text节点,document 也是Node的子类。Element扩展了更多的方法
HTMLCollection 和 NodeList
都是实时变动的(live)的伪数组,document上的更改会反映到相关对象上(例外:document.querySelectorAll
返回的NodeList
不是实时的)
函数式编程
- 可抽象出细粒度的函数,可以组合为更强大的函数
- 函数式编程讲究就是一个纯,不能有副作用,无状态和数据不可变
- 函数式编程是运算过程的抽象
- 复用性好,方便测试和优化,方便理解
圈复杂度CC
圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度,是一种衡量代码复杂度的标准,其符号为V(G)。
节点判定法:V (G) = P + 1,常见P(判定点):
- if 语句
- while 语句
- for 语句
- case 语句
- catch 语句
- and 和 or 布尔操作
- ? : 三元运算符
降低圈复杂度的方法
- 简化、合并条件表达式
- 将条件判定提炼出独立函数
- 将大函数拆成小函数
- 以明确函数取代参数
- 替换算法
TS中Type 跟 Interface 的区别?
都是描述类型,差距不大。interface更注重于描述数据结构 ,type侧重于描述类型
interface Person{
name:string;
age: number;
}
type Sex = 'MALE'|'FEMALE'
type还有专属的联合类型
interface Dog {
name:string
}
interface Cat {
name:string
}
type Pet = Dog | Cat
let a:Dog={name:'1'} , b:Cat={name:'2'}
let c:Pet = a;
协变和逆变
协变: 允许子类型转换为父类型
let dog:Dog=new Dog(); let animal:Animal=dog; dog=animal; // Error,Aniaml不满足子类Dog(比如没有狗叫方法)
逆变: 允许父类型转换为子类型
interface Animal{} interface Dog extends Animal{ bark:()=>void } let db:(d:Dog)=>void=function(d:Dog){ d.bark() } let ab:(a:Animal)=>void=function(a:Animal){} db = ab ab = db // TS Error animal没有bark方法 db({bark(){}}); // 调用原ab的函数,Dog发散为Aniamal,安全 ab({}) // 调用原db的函数,但{}并没有bark方法
协变表示类型收敛,即类型范围缩小或不变。逆变反之(发散)
除了函数参数类型是逆变,都是协变
iframe安全
iframe内容获取: iframe.contentWindow
、iframe.contentDocument
、window.frames['ifr1']
iframe获取父级内容: window.top(最顶级)
、window.parent
嵌套检测:window.self === window.top
| top.location.host === self.location.host
(限定域名)
禁止被作为iframe
: CSP、X-Frame-Options、framekiller
- 设置HTTP响应头:
Content-Security-Policy:
(frame-ancestors 'none | self | xx.com'
) - 设置HTTP响应头:
X-Frame-Options:
(deny、sameorigin、allow-from xxx.com
) - 写脚本进行嵌套检测
同源和跨域
同源:同源就是"协议+域名+端口"三者都相同
CORS:主要利用HTTP头的Access-Control-Allow-Origin 来指示请求的资源能共享给哪些域。
JSONP:只支持get,通过创建script并添加全局回调
proxy:反向代理,例如NGINX
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
postMessage:H5 API 类似消息订阅机制,可在不同域的页面发送跨域消息
document.domain: 在同个基础域名的前提下,设置该属性为基础域名,实现不同页面间跨域操作
? 在chrome101版本中将要变成可读属性,添加Origin-Agent-Cluster
window.name: 可以直接操作该属性,因此可以向不同域的页面发送消息
软链接和硬链接
- 硬链接: 与普通文件没什么不同,
inode
(指针)都指向同一个文件在硬盘中的区块。由文件系统维护一个引用计数。 - 软链接: 保存了其代表的文件的绝对路径,是另外一种文件,在硬盘上有独立的区块,访问时替换自身路径。 软链接是另外一种类型的文件
SourceMap理解
构建处理前以及处理后的代码之间的一座桥梁,方便定位bug出现的位置
开启source-map后文件末尾会保存map文件的url,map文件中,mappings属性保存这源码对应信息
第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
第三层是位置转换,以VLQ编码表示,代表该位置对应的转换前的源码位置。
以"mappings": "AAAA;AACA,c"
为例子,分号;
分隔的内容是行对应,逗号,
分隔前的转换后的位置对应,后面是转换前的位置,所有可以得出转换后的源码分成两行,第一行有一个位置,第二行有两个位置。字母由VLQ编码而成。
Webpack 中的 Source Map主要分为内联和外部两种。开发dev环境推荐eval-source-map
内联速度也快,信息也完整些。
代码压缩混淆
TODO
路由库原理Hash&History
Hash
- 原理是用
hashchange
监听hash变化 - URL中的hash部分不会被浏览器发出去
History
- 原理是用
popState
监听URL的变化 - 利用H5提供的pushState 和 repalceState 两个 API 来操作实现 URL 的变化
- 对于原生a标签,可以拦截a标签的点击事件以支持URL变化监听
- 服务端要进行相应配置
try file
,应对资源不存在时,返回默认index页面
调用history.pushState()
或history.replaceState()
不会触发popstate
事件
vue-router是监听当前url的变化(this.$router.data.current= to),让router-view
动态渲染,对应组件
XSS和CSRF
xxs:攻击者在网站上注入的恶意代码,通常是存储型:例如评论中的script标签,可以检查输入输出和httpOnly进行防范。
- 对HTML标签转义
- 对于链接属性
href|src
和事件方法,禁用恶意代码
csrf:利用(冒)用户的cookie恶意发起请求,进行非用户预期请求的攻击,比如修改删除等。可用验证码,token严重,referer检查进行预防
User->黑网站->黑网站要求访问正常网站->正常网站不知道请求是用户自己主动发出的->接受了恶意请求
预渲染
https://zhuanlan.zhihu.com/p/395828896
https://juejin.cn/post/7046898330000949285
指在服务端完成页面的html拼接处理,然后发送浏览器。为了更好的SEO支持,以及更快的首屏渲染。
源码在经过Webpack build时,会分成两份,Server Bundle
&Client Bundle
客户端阶段
同步服务端的一些状态数据,避免造成两端组件状态不一致,在挂载vue时,判断mount的dom是否含有data-server-rendered
属性,如果有就跳过渲染阶段,执行组件生命周期的钩子。
前端错误监控
页面性能提升
- 选择合适的缓存策略
- 对于代码文件,文件名添加hash,文件名变化立即更新
- 对于频繁变动的资源,可以使用
Cache-Control: no-cache
并配合ETag
使用 - 对于无需缓存的资源 ,可以使用
Cache-control: no-store
表示资源无需缓存
- 升级HTTP协议到HTTP/2
- 懒执行 & 懒加载 &预执行 & 预取
- 图片配合CDN,根据屏幕宽度选择适合的图片资源(大小,格式等)
- 其他静态资源也使用CDN加载,避免占用单域名的并发请求
- webpack方面使用ES6开启
tree shaking
、优化图片、代码/路由分割、文件名加hash、代码压缩等 - 使用
requestAnimationFrame
优化动画效果 - 使用
Intersection Observer API
代替Element.getBoundingClientReact
检测元素是否出现 - 将长时间运行的 JavaScript 从主线程移到 Web Worker。
- 使用
transform
和opacity
,will-change
建立独立图层,减少paint涉及的范围 或者 减短渲染流水线 - 减少不合理的访问元素的布局属性或计算属性,避免触发Force Layout
参考:https://blog.towavephone.com/front-end-performance-optimization-2021/
图片懒加载
性能指标
性能分析工具 Performance 和 Lighthouse
FP,FCP,白屏时间,首屏加载时间
性能监控-埋点
白屏检测
白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。白屏时间是首屏时间的一个子集。
白屏时间:performance.timing.responseStart - performance.timing.navigationStart
首屏时间
DOMContentLoad
扫码登陆
简易原理
- 用户请求登陆的二维码图片,并生成一个uuid,作为该页面唯一标识,存入redis中
- 浏览器拿到二维码和uuid后就不断轮询查看该uuid是否已经登陆成功,是就跳转
- 手机端扫码后会将uuid和token提交给服务端,并把uuid 和userid 存储redis
- 轮询的具体就是看uuid和userid这个键值对是否存在,在就返回用户信息和token等
安全
二维码设置过期时间、限制二维码使用次数、二维码长度足够长,防止穷举、使用HTTPS
单点登陆
1、用户访问A系统,系统A发现用户未登录,跳转至sso认证中心,并把自己的地址作为参数。
2、sso认证中心发现用户未登录,则引导用户到登录页面。
3、用户输入用户名和密码提交登录。
4、sso认证中心验证用户信息,创建用户->sso之间的会话(全局会话),同时创建授权令牌。
5、sso认证中心带着令牌跳转到A系统
6、系统A拿到令牌,去sso认证中心校验令牌是否有效。
7、sso认证中心校验令牌,返回有效,注册系统A。
8、系统A使用该令牌创建与用户的会话(局部会话),返回请求资源。
9、用户访问系统B。
10、系统B发现用户未登录,跳转至sso认证中心,也将自己的地址作为参数。
11、sso认证中心发现用户已登录,跳转到系统B,并附上令牌。
12、系统B拿到令牌,去sso认证中心校验令牌是否有效。
13、sso认证中心校验令牌,返回有效,注册系统B。
14、系统B使用该令牌创建与用户的局部会话,返回请求资源。
// 参考 https://juejin.cn/post/6911565511226556430#heading-28
OAuth2
OAuth 2.0 是一个授权协议,它允许软件应用代表(而不是充当)资源拥有者去访问资源拥有者的资源。应用向资源拥有者请求授权,然后取得令牌(token),并用它来访问资源,并且资源拥有者不用向应用提供用户名和密码等敏感数据。
参考
JS - 前端面试之道 - http://caibaojian.com/interview-map/frontend/
js常见面试题总结 - 大厂面试题每日一题
金九银十,你准备好面试了吗?(太多了,暂时没看) - https://juejin.cn/post/6996841019094335519
浏览器灵魂之问,请问你能接得住几个? - https://juejin.cn/post/6844904021308735502
浏览器渲染流程 - https://zhuanlan.zhihu.com/p/162722524
迟到的大厂前端面试记录(面试题+部分答案)- https://juejin.cn/post/7017655711291146253