前言:疫情当下,面试不易,大厂难进,小厂要求又高,提升自己能力才是生存之道,整理一些面试题帮助大家,包括前端各方面的内容(设计模式、leetcode 刷题等),后续陆续更新。
往期文章:
一、CSS
1、HTML5 新增的内容有哪些
新增语义化标签 :
Header 页面头部
main 页面主要内容
footer 页面底部
Nav 导航栏
aside 侧边栏
article 加载页面一块独立内容
Section 相当于 div
多媒体标签 : audio video
语义化标签优点:1. 提升可访问性 2. 有利于 seo 3. 结构清晰,利于维护
2、Css3 新增的特性
边框:border
背景:Background-size Background-origin 规定 background-position 属性相对于什么位置定
渐变:Linear-gradient()线性渐变 Radial-gradient()径向渐变
文本效果: Word-break:定义如何换行 Word-wrap:允许长的内容可以自动换行 Text-overflow:指定当文本溢出包含它的元素,应该干啥 Text-shadow:文字阴影(水平位移,垂直位移,模糊半径,阴影颜色)
转换 : Transform 应用于 2D3D 转换,可以将元素旋转,缩放,移动,倾斜 Transform-origin 可以更改元素转换的位置,(改变 xyz 轴) Transform-style 指定嵌套元素怎么样在三位空间中呈现
过渡 : Transition-proprety 过渡属性名 Transition-duration 完成过渡效果需要花费的时间 Transition-timing-function 指定切换效果的速度 Transition-delay 指定什么时候开始切换效果
动画:animation
Animation-name 为@keyframes 动画名称 animation-duration 动画需要花费的时间 animation-timing-function 动画如何完成一个周期 animation-delay 动画启动前的延迟间隔 animation-iteration-count 动画播放次数 animation-direction 是否轮流反向播放动画
3、两列定宽,中间自适应
圣杯布局与双飞翼布局,双飞翼布局是圣杯布局的优化版
双飞翼布局:
- 三个部分都设定为左浮动,然后设置 center 的宽度为100%,此时,left 和right 部分会跳到下一行;
- 通过设置 margin-left 为负值,让 left 和 right 部分回到上一行;
- center 部分增加一个内层 div,并设 左 margin 和 右 margin,将 center 挤到中间显示。
圣杯布局:
1、给三列的父元素,加上左 margin 和右 margin(也有人使用 padding),将三列挤到中间来,
这样左边和右边就会预留出位置。
2、给 left 和 right 设置相对定位,将它们移动到相应的位置。
4、flex 常用属性有哪些?
justify-content 决定在主轴上的对齐方式
flex-start | flex-end | center | space-between | space-around
align-items 决定项目在侧轴上对齐方式
flex-start | flex-end | center | baseline | stretch;
align-self 单个项目对齐方式
flex-start | flex-end | center | baseline | stretch | auto
flex-direction 决定主轴的方向
row | row-reverse | column | column-reverse
5、flex:1
flex 属性 是 flex-grow、flex-shrink、flex-basis三个属性的缩写。
flex-grow:定义项目的的放大比例;
flex-shrink:定义项目的缩小比例;
flex-basis: 定义在分配多余空间之前,项目占据的主轴空间(main size),浏览器根据此属性计算主轴是否有多余空间,
所以flex属性的默认值为:0 1 auto (不放大会缩小)
flex 为 none:0 0 auto (不放大也不缩小)
flex 为 auto:1 1 auto (放大且缩小)
flex:1即为 flex-grow:1,经常用作自适应布局,将父容器的 display:flex,侧边栏大小固定后,将内容区 flex:1,内容区则会自动放大占满剩余空间。
6、水平垂直局中
水平局中:
内联元素,text-align: center
块级元素, margin: 0 auto;
垂直局中:
内联元素,align-items: center;
块级元素
子元素宽高确定的情况下,使用position absolute + 负margin
子元素宽高不确定的,使用position absolute + transform
grid布局: justify-self: center;align-self: center
7、BFC 是什么?
BFC 全称是 Block Formatting Context,块级格式化上下文。
BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。
怎么形成 BFC:
- 根元素
- float属性不为none
- position为absolute或fixed
- display为inline-block, table-cell, table-caption, flex, inline-flex
- overflow不为visible
BFC 主要的作用是: 1). 清除浮动 2). 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
8、清除浮动说一下
第一种用伪元素
.clearfix:after{
content: "";
display: block;
clear: both;
}
.clearfix{
zoom: 1; /* IE 兼容*/
}
第二种给父容器添加 overflow:hidden 或者 auto 样式
9、1px问题来源?怎么解决?
设计稿是显示的物理像素, css 中的像素是逻辑像素,
如果设备像素比为2的话,那么设计稿上的 1px 宽度实际代表的 css 参数应该是 0.5px,但并不是所有手机都能识别border: 0.5px,有的系统里,0.5px会被当成为0px处理
解决方法:
使用 border-image 实现 缺点是改边框颜色时要改图片,不是很方便。
.border{
border-width: 1px;
border-image: url(border.gif) 2 repeat;
}
使用伪元素 + transform 实现
把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并将 transform 的 scale 缩小一半,原先的元素相对定位,新的 border 绝对定位。
使用 box-shadow 模拟边框实现 缺点是颜色不好处理,有阴影出现。
.hairlines li {
border: none;
box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5);
}
10、介绍下重绘和回流
重绘是节点的集合属性或样式改变。回流是布局或几何属性发生改变。
回流必定会发生重绘,重绘不一定会引发回流
减少重绘和回流:
使用 transform 代替 top;
使用 visibility:hidden 替换 display: none
避免使用 table 布局;
尽可能在 DOM 树的最末端改变 class;
避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多;
将动画效果应用到 position 属性为 absolute 或 fixed 的元素上,避免影响其他元素的布局;
避免使用 CSS 表达式,可能会引发回流;
CSS 硬件加速:
- transform
- opacity
- filter
- will-change
11、简述 CSS 盒模型
标准盒模型:box-sizing: content-box width 不包括 padding 和 border
IE 盒模型: box-sizing: border-box width 包括 padding 和 border
12、简述伪类和伪元素
伪类:只有处于 dom 树无法描述的状态才能为元素添加样式。比如 :hover。
伪元素:用于创建一些原本不在文档树中的元素,并为其添加样式。比如 ::before。
区别
伪类的操作对象是文档树中已存在的元素,而伪元素是创建一个文档树外的元素。
二、JS
13、原型及原型链
- 在 js 中,我们通常会使用构造函数来创建一个对象,每一个构造函数的内部都有一个 prototype 属性,这个属性对应的值是一个对象,这个对象包含了可以由该构造函数的所有实例都共享的属性和方法,我们把它称为原型。
- 原型分为显示原型和隐式原型,一般称 prototype 为显示原型,__proto__称为隐式原型。
- 当我们查找一个对象的属性时,首先会在这个对象身上查找,如果没有的话,再通过这个对象的__proto__找到该对象的原型,然后在这个原型对象中找,如果这个原型对象也没有的话,一直找下去,这就构成了原型链。原型链的尽头是 Object.prototype。
14、作用域及作用域链
作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
15、闭包及闭包用途
闭包:一个内部函数有权访问包含它的外部函数的变量
用途: 1.在函数外部能够访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
应用场景:
1.柯里化函数:避免频繁调用具有相同参数的函数,轻松实现复用,其实就是封装一个高阶函数。
2.使用闭包实现私有方法/变量
3.匿名自执行函数
4.缓存一些结果: 外部函数中创建一个数组,闭包函数可以获取或者修改这个数组的值,延长了变量的生命周期。
16、继承
构造函数继承: 只能继承父类的实例属性和方法,不能继承原型属性/方法
原型链继承: 原型属性被所有实例所共享,多个实例对引用类型操作会被篡改
组合继承: 就是将上两种方法结合起来,融合了优点,最常用
原型式继承:不能做到函数复用,共享引用类型属性的值,无法传递参数
寄生式继承:在原型式继承的基础上,增强对象,返回构造函数
17、ES5/ES6 的继承除了写法以外还有什么区别?
-
ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this)).
-
ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this。
-
ES5 的继承是通过原型或构造函数机制来实现。
-
ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。
-
子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。 如果不调用 super 方法,子类得不到 this 对象。
-
注意 super 关键字指代父类的实例,即父类的 this 对象。
-
注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。
18、new 原理
new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函数后返回这个对象
function _new() {
let obj = {};
let [constructor, ...args] = [...arguments];
obj.__proto__ = constructor.prototype;
let result = constructor.apply(obj, args);
if (result && typeof result === 'function' || typeof result === 'object') {
return result;
}
return obj;
}
19、ES6 新特性
const 和 let、模板字符串、箭头函数、Symbol、展开运算符、for...of 和 for...in、Reflect.
reflect用途:
- 从Reflect对象上可以拿到语言内部的方法。
- 操作对象出现报错时返回false
- 让操作对象都变为函数式编程
- 保持和proxy对象的方法的对应:只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
展开运算符用途:
解构赋值、展开收集、把类数组转换为数组、增加元素或属性
for of 和 for in 区别:
for..of 适用遍历数组,不能遍历对象,与 forEach() 不同的是,它可以正确响应 break、continue和return语句。
for in 可以遍历对象,并且会遍历原型以及可枚举属性
forEach() 会改变原始的数组的值,而 map() 会返回一个新数组。
浅拷贝是拷贝对象的引用,当原对象发生变化的时候,拷贝对象也跟着变化;
深拷贝是另外申请了一块内存,内容和原对象一样,更改原对象,拷贝对象不会发生变化。
20、箭头函数与普通函数的区别是什么?构造函数可以使用 new生成实例,那么箭头函数可以吗?
区别:
1).函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
2).不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替;
3).不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
不可以使用箭头函数生成实例,因为:
-
没有自己的 this,无法调用 call、apply;
-
没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新对象的 proto
21、Let 与 var 与 const 的区别
Var 声明的变量存在变量提升,let 和 const 不存在变量提升
Let 和 const 声明会形成块级作用域
Let 存在暂存死区
Const 一旦声明必须赋值,不能用 null 占位,声明后不能再修改,如果声明的是复合类型数据,可以修改属性
22、Js 中常见的内存泄漏
1.意外的全局变量
2.未被清除的定时器或回调函数
3.脱离 DOM 的引用
4.闭包
如何避免内存泄漏?
减少不必要的全局变量
使用完数据后,及时解除引用
23、bind,apply,call 的区别
call 和 apply: 第一个参数都是指定函数体内 this 的指向;第二个参数开始不同,apply 是传入数组或者类数组, call 传入的参数是不固定的。call 比 apply 的性能要好,call 传入参数的格式正是内部所需要的格式。绑定 this 后直接调用该函数。
bind 改变 this 作用域会返回一个新的函数,这个函数不会马上执行。
bind 之后不能修改 this 指向。 当执行绑定函数时,this 指向与形参在 bind 方法执行时已经确定了,无法改变。bind 多次后执行,函数 this 还是指向第一次 bind 的对象。
24、什么是this ?
1、面向对象语言中 this 表示当前对象的一个引用。但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。
- 在方法中,this 表示该方法所属的对象。如果单独使用,this 表示全局对象。
- 在函数中,this 表示全局对象。在严格模式下,this 是未定义的(undefined)。
- 在事件中,this 表示接收事件的元素。类似 call() 和 apply() 方法可以将 this 引用到任何对象。
2、this指向
1、对于直接调⽤的函数来说,不管函数被放在了什么地⽅,this都是window
2、对于被别⼈调⽤的函数来说,被谁点出来的,this就是谁
3、在构造函数中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的⼀个实例
4、call、apply时,this是第⼀个参数。bind要优与call/apply哦,call参数多,apply参数少
5、箭头函数没有⾃⼰的this,需要看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数 的this,如果没有,则this是window
3、什么场景下,不能使用箭头函数 ?
1、构造函数的原型方法上
2、arguments、new、prototype
3、generator
25、Symbol
1、用来解决属性名冲突的问题,构造唯一的属性名或者变量
2、私有属性
function getObj() {
const symbol = Symbol('test');
const obj = {};
obj[symbol] = 'test';
return obj;
}
3、JSON.stringify 会忽略 symbol ? 除了这个,还会忽略什么呢?
undefined function
4、如果对象有循环引用,可以用 JSON.stringify 来处理吗?
会报错
5、确定是 stringify 会报错,而不是 parse 会报错吗?
stringify 会报错,循环调用,堆栈无休止调用,内存上限
6、平时都如何判断对象类型的呀,分别适合哪些场景呢?哪种方法最准确呢?
- typeof
- instanceof
- Object.prototype.toString.call(obj)
- Array.isArray
26、Proxy与Object.defineProperty的优劣对⽐?
Proxy 可以直接监听对象⽽⾮属性
Proxy 可以直接监听数组的变化
Proxy 有多达13种拦截⽅法, 是 Object.defineProperty 不具备的
Proxy 返回的是⼀个新对象,可以只操作新的对象达到⽬的, Object.defineProperty 只能遍历对象属性直接修改
为什么 Vue2.x 采用 Object.defineProperty?
时代背景决定:浏览器兼容性,Proxy 是ES6语法
27、平时有关注过前端的内存处理吗?
1、内存的生命周期
内存分配:声明变量、函数、对象的时候,js 会自动分配内存
内存使用:调用的时候、使用的时候
内存回收:释放内存
2、js中的垃圾回收机制
(1)引用计数垃圾回收
a对象对b对象有访问权限,那么称为a引用b对象
缺陷:循环引用
(2)标记清除算法
无法达到的对象
1.在运行的时候给存储在内存的所有变量加上标记
2.从根部出发,能触及的对象,打标记清除
3.哪些有标记的就被视为即将要删除的变量
4.js执行内存回收
3、js中有哪些常见的内存泄漏?
(1)全局变量
window.a='xxxx'; window.a = null;
(2)未被清除的定时器和回调
(3)闭包
一个内部函数有权访问包含它的外部函数的变量
(4)dom的引用
const elements = {
image: document.getElementById('image')
}
document.body.removeChild(document.getElementById('image'));
elements.image = null;
4、如何避免内存泄漏?
减少不必要的全局变量
使用完数据后,及时解除引用
28、JS 异步解决方案的发展历程以及优缺点
回调函数 优点:解决了同步的问题(整体任务执行时长); 缺点:回调地狱,不能用 try catch 捕获错误,不能 return;
Promise 优点:解决了回调地狱的问题; 缺点:无法取消 Promise,错误需要通过回调函数来捕获;
Generator 特点:可以控制函数的执行。
Async/Await 优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题;
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低
29、你了解Promise吗?平时用的多吗?
1、讲一下 Promise
Javascript 是⼀⻔单线程语⾔,以前解决异步时,⼤部分情况都是通过回调函数来进⾏。 为了能使回调函数以更优雅的⽅式进⾏调⽤,在 ES6 中 js 产⽣了⼀个名为 promise 的新规范,他让异步操作变得近乎「同步化」。在⽀持 ES6 的⾼级浏览器环境中,我们通过 new Promise() 即可构造⼀个 promise 实例。 这个构造函数接受⼀个函数,分别接受两个参数,resolve 和 reject,代表我们需要改变当前实例的状态到 已完成 或是 已拒绝 。
-
promise 会有三种状态,「进⾏中」「已完成」和「已拒绝」,进⾏中状态可以更改为已完成或 已拒绝,已经更改过状态后⽆法继续更改(例如从已完成改为已拒绝)。
-
ES6 中的 Promise 构造函数,我们构造之后需要传⼊⼀个函数,他接受两个函数参数,执⾏第⼀个参数之后就会改变当前 promise 为「已完成」状态,执⾏第⼆个参数之后就会变为「已拒绝」 状态。
-
通过 .then ⽅法,即可在上⼀个 promise 达到已完成时继续执⾏下⼀个函数或 promise。同时通过 resolve 或 reject 时传⼊参数,即可给下⼀个函数或 promise 传⼊初始值。
-
已拒绝的 promise,后续可以通过 .catch ⽅法或是 .then ⽅法的第⼆个参数或是 try catch 进⾏捕 获。
2、Promise.all 你知道有什么特性吗?
接收一个 Promise 数组,数组里所有元素可以不全是 Promise,可以是常量。所有 Promise 数组执行完,才会返回结果。 其中一个报错,整个 Promise 就会进入 Catch,其他也会继续执行(Promise在实例化的时候就已经执行了,await/then只是为了拿到它的结果)
30、防抖和节流
防抖函数原理:在事件被触发n秒后再执⾏回调,如果n秒内⼜被触发,则重新计时。
适⽤场景:按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似
节流函数原理: ⼀个单位时间内,只能触发⼀次函数,如果触发多次,则只有第⼀次⽣效。
适⽤场景:拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动 ,如scroll
缩放场景:监控浏览器resize
动画场景:避免短时间内多次触发动画引起性能问题
31、ts 相比 js 有什么优势?
- TypeScript 的本质
是一个添加了类型注解的 JavaScript
- TypeScript 更加可靠
得益于 TypeScript 的静态类型检测,让一些低级JavaScript 错误能在开发阶段就被发现并解决。
- 面向接口编程
编写 TypeScript 类型注解,本质就是接口设计。能清楚了解组件接收数据的结构和类型,并知道如何在组件内部编写安全稳定的 JSX 代码。
32、泛型?有什么作用?
泛型是指类型参数化,即将原来某种具体的类型进行参数化。和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或接口成员和方法之间的关系。
1、泛型最常用的场景是用来约束函数参数的类型,我们可以给函数定义若干个被调用时才会传入明确类型的参数。通过泛型,可以约束函数参数和返回值的类型关系。
2、在类的定义中,可以使用泛型来约束构造函数、属性和方法的类型。
3、泛型类型:将类型入参的定义移动到类型别名或接口名称后,此时定义的一个接收具体类型、入参后返回一个新类型的类型就是泛型类型。
4、泛型约束:我们可以通过“泛型入参名 extends 类型”把泛型入参限定在一个相对更明确的集合内(几种原始类型的集合中),以便对入参进行约束。
三、Webpack
33、webpack与grunt、gulp的不同?
Grunt、Gulp是基于任务运⾏的⼯具:
它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,能⽅便的打造各种⼯作流。
Webpack是基于模块化打包的⼯具:
⾃动化处理模块,把⼀切当成模块,构建⼀个依赖关系图,然后将所有模块打包成⼀个或多个 bundle。
因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script代替Grunt、Gulp,npm script同样可以打造任务流.
34、有哪些常⻅的Loader?
file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件
url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去
source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
image-loader:加载并且压缩图⽚⽂件
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
35、有哪些常⻅的 Plugin?
define-plugin:定义环境变量
html-webpack-plugin:简化html⽂件创建
uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
mini-css-extract-plugin: CSS提取到单独的⽂件中,⽀持按需加载
webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
webpack-bundle-analyzer: 可视化webpack输出⽂件的体积
36、Loader和 Plugin的不同?
1、Loader
模块转换器,将非 js 模块转化为 webpack 能识别的 js 模块。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图可以直接引用的模块。
2、Plugin
扩展插件,webpack 运行的各个阶段,都会广播出对应的事件,插件去监听对应的事件。
37、能简单描述一下 webpack 的打包过程吗?
1、初始化参数:shell webpack.config.js
2、开始编译:初始化一个 Compiler 对象,加载所有的配置,开始执行编译
3、确定入口:根据 entry 中的配置,找出所有的入口文件
4、编译模块:从入口文件开始,调用所有的 loader,再去递归的找依赖
5、完成模块编译:得到每个模块被编译后的最终内容以及它们之间的依赖关系
6、输出资源:根据得到的依赖关系,组装成一个个包含多个 module 的 chunk
7、输出完成:根据配置,确定要输出的文件名以及文件路径
38、介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
1).当修改了一个或多个文件;
2).文件系统接收更改并通知 webpack;
3).webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
4).HMR 服务器使用 Websocket 通知 HMR runtime 更新,HMR runtime 通过 HTTP 请求更新 jsonp;
5).HMR runtime 替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新;
39、Webpack 中的 Module 是指什么?
1、webpack 支持 ESModule,CommonJS,AMD,Assests(image,font,video,audio,json)
(1)ESModule
关键字 export,允许将 ESModule 中内容暴露给其他模块
关键字 import
// package. json
type:module -> ESModule
type:commonjs -> CommonJS
(2)CommonJS
module. exports,允许将 CommonJS 中的内容暴露给其他模块
require
所以 webpack modules,如何表达自己的各种依赖关系?
ESModule import 语句
CommonJS require
AMD define require
css/sass/less @import
2、我们常说的 chunk 和 bundle 的区别是什么?(important !!!)
(1)Chunk
Chunk 是 webpack 打包过程中 Modules 的集合,是(打包过程中)的概念。
Webpack 的打包是从一个入口模块开始,入口模块引用其他模块,其他模块引用其他模块......
Webpack 通过引用关系逐个打包模块,这些 module 就形成了一个 chunk。
如果有多个入口模块,可能会产出多条打包路径,每条路径都会形成一个 chunk。
(2)Bundle
是我们最终输出的一个或者多个打包好的文件
(3)chunk 和 bundle 的关系是什么?
大多数情况下,一个 chunk 会产生一个 bundle,但是也有例外,
如果加了 sourcemap,一个 entry、一个 chunk 对应 两个bundle。
Chunk 是过程中代码块,Bundle 是打包结果输出的代码块。Chunk 在构建完成就呈现为 Bundle。
40、是否写过Loader和Plugin?描述⼀下编写loader或plugin的思路?
编写 Loader 时要遵循单⼀原则,每个 Loader 只做⼀种"转义"⼯作。 每个 Loader 的拿到的是源⽂件内容,可以通过返回值的⽅式将处理后的内容输出,也可以调⽤ this.callback() ⽅法,将内容返回给 webpack。 还可以通 this.async() ⽣成⼀个 callback 函数,再⽤这个 callback 将处理后的内容输出出去。 此外 webpack 还为开发者准备了⼯具函数集—— loader-utils 。
相对于 Loader ⽽⾔,Plugin 的编写就灵活了许多。 webpack 在运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
41、webpack插件配置顺序有影响吗
plugin 顺序没有限制,因为每个 plugin 内部实现都是用钩子处理,即回调函数。初始化的 hooks 会在构建流程的不同阶段调用,根据不同的 hooks 触发不同的回调机制,从而保证plugins的执行顺序。
Loader 执行顺序?
大多数情况下loader的执行顺序确实是从右到左。
所有一个接一个地进入的 loader,都有两个阶段:
- Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader。
- Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。
所有普通 loader 可以通过在请求中加上 ! 前缀来忽略(覆盖)。
所有普通和前置 loader 可以通过在请求中加上 -! 前缀来忽略(覆盖)。
所有普通,后置和前置 loader 可以通过在请求中加上 !! 前缀来忽略(覆盖)。
// 禁用普通 loaders
import { a } from '!./file1.js';
// 禁用前置和普通 loaders
import { b } from '-!./file2.js';
// 禁用所有的 laoders
import { c } from '!!./file3.js';
42、如何⽤webpack来优化前端性能?
压缩代码:删除多余的代码、注释、简化代码。 可以利⽤webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader ? minimize)来压缩css 。
利⽤CDN加速: 将引⽤的静态资源路径改为CDN上对应的路径。 可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径 。
Tree Shaking: 将代码中永远不会⾛到的⽚段删除。 可以通过在启动webpack时追加参数 --optimize-minimize 来 实现 。
Code Splitting: 将代码按路由或组件分块(chunk),实现按需加载。
提取公共第三⽅库: 利用 SplitChunksPlugin 插件抽取公共模块,利⽤浏览器缓存这些⽆需频繁变动的公共代码。
43、如何提⾼webpack的打包速度?
wepack 的打包优化没有固定的模式,需要我们针对项目去进行分块、拆包、压缩等,常见的优化思路主要分为四部分
- 优化搜索时间,即开始打包时获取所有依赖模块的时间
- 优化解析时间,即根据配置的 loader 解析相应文件所花费的时间
- 优化压缩时间,即 wepack 对代码进行优化压缩所花费的时间
- 优化二次打包时间,即重新打包时所花费的时间
1、优化搜索时间 - 缩小文件搜索范围
-
优化 loader 配置
使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件
-
优化 resolve.module 配置
resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules 的默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推。
-
优化 resolve.alias 配置
resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。
-
优化 resolve.extensions 配置
在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:
- resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
- 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
- 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。
-
优化 resolve.mainFields 配置
有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 package.json 文件里,如下:
{
"jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
"main": "lib/index.js" // 采用 ES5 语法的代码入口文件
}
webpack 会根据 mainFields 的配置去决定优先采用那份代码,mainFields 默认如下:
mainFields: ['browser', 'main']
webpack 会按照数组里的顺序去 package.json 文件里寻找,只会使用找到的第一个。
假如你想优先采用 ES6 的那份代码,可以这样配置:
mainFields: ['jsnext:main', 'browser', 'main']
-
优化 module.noParse 配置
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
2、优化解析时间 - 开启多进程打包
-
thread-loader(webpack4 官方推荐)
-
HappyPack
3、优化压缩时间
webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码
4、优化二次打包时间 - 合理利用缓存
-
cache-loader
-
HardSourceWebpackPlugin
44、Babel的原理是什么?
解析 Parse: 将代码解析⽣成抽象语法树( 即AST ),即词法分析与语法分析的过程
转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在 此过程中进⾏添加、更新及移除等操作
⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator
45、ES6模块与CommonJS的区别
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
运行时加载: 在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法
编译时加载: 在import
时可以指定加载某个输出值,而不是加载整个模块
四、浏览器相关
46、了解浏览器的事件循环吗?
事件循环会维护一个或多个任务队列,事件作为任务源往队列中加入任务,每执行完一个就从队列中移除它, 这就是一次事件循环。
1、为什么JS在浏览器中有事件循环机制?
JS是单线程的
实现非阻塞事件,比如用户点击、页面渲染、脚本执行、网络请求等,
所以使用 event loop
2、两种任务?
宏任务:整体代码、setTimeout、setInterval、I/O操作
微任务:new Promise().then、MutaionObserver(前端的回溯)
3、为什么要引入微任务的概念,只有宏任务可以吗?
宏任务 先进先出的原则执行。
4、Node中的事件循环和浏览器中的事件循环有什么区别?
宏任务的执行顺序:
1.timers定时器:执行已经安排的setTimeout和setInterval的回调函数
2.pending callback 待定回调:执行延迟到下一个循环迭代的I/O回调
3.idle、prepare:仅系统内部使用
4.poll: 检索新的I/O事件,执行与I/O相关的回调
5.check: 执行setImmediate()回调函数
6.close callbacks: socket.on('close', ()=>{} )
微任务和宏任务在node的执行顺序:
Node v10及以前
1.执行完一个阶段中的所有任务
2.执行nextTick队列里的内容
3.执行完微任务队列的内容
Node v10及以后
和浏览器的行为统一了。
47、浏览器缓存
- 首先检查
Cache-Control
, Expires,看强缓存是否可用 - 如果可用的话,直接使用
- 否则进入协商缓存,发送HTTP请求,服务器通过请求头中的
If-Modified-Since
或者If-None-Match
字段检查资源是否更新 - 资源更新,返回资源和200。
- 否则,返回304,告诉浏览器直接从缓存中取资源。
强缓存包括**「Expires」,「Cache-Control」**
HTTP1.0版本,使用的是Expires,HTTP1.1使用的是Cache-Control
Expires
即过期时间,时间是相对于服务器的时间而言的,存在于服务端返回的响应头中,在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。
Cache-Control 是过期时长,对应的是max-age。
- 当Expires和Cache-Control同时存在时,优先考虑Cache-Control。
- 当缓存资源失效,也就是没有命中强缓存时,就进入协商缓存
协商缓存分为两种,「Last-Modified」 和 「ETag」
Last-Modified表示「最后修改时间」。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,「如果再次请求」,会在请求头中携带If-Modified-Since
字段,这个字段的值也就是服务器传来的最后修改时间。
服务器拿到请求头中的If-Modified-Since
的字段后,其实会和这个服务器中该资源的最后修改时间
对比:
- 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
- 否则返回304,告诉浏览器直接使用缓存。
ETag是服务器根据当前文件的内容,对文件生成唯一的标识,只要里面的内容有改动,这个值就会修改,服务器通过把响应头把该字段给浏览器。
浏览器接受到ETag值,会在下次请求的时候,将这个值作为「If-None-Match」这个字段的内容,发给服务器。
服务器接收到「If-None-Match」后 , 会跟服务器上该资源的「ETag」进行比对👇
- 如果两者一样的话,直接返回304,告诉浏览器直接使用缓存
- 如果不一样的话,说明内容更新了,返回新的资源,跟常规的HTTP请求响应的流程一样
性能上, Last-Modified
优于ETag
, Last-Modified
记录的是时间点,而Etag
需要根据文件的MD5算法生成对应的hash值。
精度上,ETag 优于 Last-Modified
-
- 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
- Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,这时候 Last-Modified 并没有体现出修改。
最后,「如果两种方式都支持的话,服务器会优先考虑ETag」
浏览器缓存位置,可以分为四种,优先级从高到低排列分别是
- Service Worker :应用场景主要是PWA,它的功能有:离线缓存、消息推送和网络代理
- Memory Cache:效率上最快,存活时间最短
- Disk Cache:存取效率慢,优势在于存储容量和存储时长,存放比较大的JS,CSS文件
- Push Cache:推送缓存,主要用于HTTP/2
48、Cookie 、LocalStorage 、SessionStorage 及其应用场景
分类 | 生命周期 | 存储容量 | 存储位置 |
---|---|---|---|
cookie | 默认保存在内存中,随浏览器关闭失效(如果设置过期时间,在到过期时间后失效) | 4KB | 保存在客户端,每次请求时都会带上 |
localStorage | 理论上永久有效的,除非主动清除。 | 4.98MB(不同浏览器情况不同,safari 2.49M) | 保存在客户端,不与服务端交互。节省网络流量 |
sessionStorage | 仅在当前网页会话下有效,关闭页面或浏览器后会被清除。 | 4.98MB(部分浏览器没有限制) | 同上 |
49、cookie 与 session 的区别
Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中
Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。
浏览器如何做到 session 的功能的?
http是无状态的协议,客户每次读取web页面时,服务器都打开新的会话,而且服务器也不会自动维护客户的上下文信息,那么要怎么才能实现网上商店中的购物车呢,session就是一种保存上下文信息的机制,它是针对每一个用户的,变量的值保存在服务器端,通过SessionID来区分不同的客户,session是以cookie或URL重写为基础的,默认使用cookie来实现。
50、Cookie有哪些字段?
name/value: 用 JavaScript 操作 Cookie 的时候注意对 Value 进行编码处理。
expires/max-age: Expires 用于设置 Cookie 的过期时间,当 Expires 属性缺省时,表示是会话性 Cookie。Max-Age 用于设置在 Cookie 失效之前需要经过的秒数。Expires 和 Max-Age 都存在,Max-Age 优先级更高。
domain: 指定 Cookie 可以送达的主机名。没有指定,默认值为当前文档访问地址中的主机部分(但是不包含子域名)。在这里注意的是,不能跨域设置 Cookie
path: 指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。比如设置 Path=/docs,/docs/Web/ 下的资源会带 Cookie 首部,/test 则不会携带 Cookie 首部。
s ecure: 标记为 Secure 的 Cookie 只应通过被HTTPS协议加密过的请求发送给服务端
HttpOnly: 设置 HTTPOnly 属性可以防止客户端脚本通过 document.cookie 等方式访问 Cookie,避免 XSS 攻击
SameSite: 可以让 Cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)
Cookie 主要用于以下三个方面:
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
Cookie 的缺点
- 容量缺陷。Cookie 的体积上限只有
4KB
,只能用来存储少量的信息。 - 降低性能,Cookie紧跟着域名,不管域名下的某个地址是否需要这个Cookie,请求都会带上完整的Cookie,请求数量增加,会造成巨大的浪费。
- 安全缺陷,Cookie是以纯文本的形式在浏览器和服务器中传递,很容易被非法用户获取,当HTTPOnly为false时,Cookie信息还可以直接通过JS脚本读取。
51、什么是 XSS?如何预防?
XSS (Cross Site Scripting),即“跨站脚本”。指黑客往 HTML 文件或 DOM 中注入恶意脚本,在用户浏览页面时实施攻击的一种手段。
利用恶意脚本可以:
- 窃取Cookie
- 监听用户行为,比如输入账号密码后之间发给黑客服务器
- 在网页中生成浮窗广告
- 修改DOM伪造登入表单
XSS攻击主要有三种方式
- 存储型 XSS 攻击 :将恶意脚本提交到网站的数据库中,当用户请求包含恶意脚本的页面时,恶意脚本就会执行。
- 反射型 XSS 攻击:恶意脚本作为「网络请求的一部分」,网站又把恶意脚本返回给用户,恶意脚本被执行。
- 基于 DOM 的 XSS 攻击:不涉及 Web 服务器,将恶意脚本注入到用户的页面中,在数据传输的时候劫持网络数据包。
防范措施
- 对输入的内容进行过滤或转码,尤其是类似于
<script>
、<img>
、<a>
标签 - 利用CSP:限制加载其他域下的资源文件,禁止向第三方域提交数据,提供上报机制,禁止执行内联脚本和未授权的脚本。
- 利用Cookie的HttpOnly属性。
52、什么是 CSRF?如何预防?
CSRF 全称是 Cross-site request forgery,即“跨站请求伪造”。指黑客引诱用户打开某个链接,利用用户的登录态发起的跨站请求。
自动发起 Get 请求、自动发起 POST 请求、引诱用户点击链接
防范措施:
验证码机制:验证来源站点
利用Cookie的SameSite属性:Strict、Lax和None。在Strict模式下,浏览器完全禁止第三方请求携带Cookie。在Lax模式下,只能在 get 方法提交表单或a 标签发送 get 请求的情况下可以携带 Cookie。在None模式下,Cookie将在所有上下文中发送,即允许跨域发送。
CSRF Token:使用Token,在不涉及XSS的前提下,一般黑客很难拿到Token。
53、一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
- 构建请求:构建请求行信息
- 查找缓存:在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。如果缓存查找失败,就会进入网络请求过程了。
- 准备 IP 和端口:浏览器会请求 DNS 返回域名对应的 IP地址。HTTP 默认 80 端口,HTTPS默认443端口
- 等待 TCP 队列:Chrome 同一个域名同时最多只能建立 6 个 TCP 连接,超出的请求会进入排队等待状态
- 建立 TCP 连接:三次握手建立连接
- 发起 HTTP 请求:请求行(请求方法,请求URL,HTTP版本)、请求头(浏览器的基础信息)、请求体
- 服务器处理请求
- 服务器返回请求和断开连接:响应行(HTTP版本,状态码)、响应头(服务器信息)、响应体(HTML 内容)。四次挥手断开连接,如果头信息中有Connection:Keep-Alive ,TCP连接会继续保持,继续发送请求。
渲染阶段
- 构建DOM树:解析HTML为浏览器DOM树结构,字节→字符→令牌→节点→对象模型(DOM)
- 样式计算:生成CSSOM树
- 布局:根据DOM树和CSSOM树,合成渲染树
- 分层:生成图层树
- 绘制:把一个图层拆分为许多绘制指令,按照指令顺序生成绘制列表
- 分块:将图层划分为图块,加速首屏渲染(先展示低分辨率图块,正常的图块内容绘制完毕后替换)
- 光栅化:将图块转换为位图,按照视口附近的图块来优先生成位图,使用 GPU 进行加速,最后发送给合成线程
- 合成:合成线程生成绘制命令,发送给浏览器进程,生成页面,发送给显卡。显卡接收到图像保存到后缓冲区,系统自动将前缓冲区和后缓冲区对换位置,显卡显示前缓冲区内容。
DNS解析
运行在UDP协议之上,端口号53
一般向本地DNS服务器发送请求是递归查询,本地 DNS 服务器向其他域名服务器发送请求是迭代查询。
- 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。用户只需要发出一次查询请求。
- 迭代查询指的是查询请求发出后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。用户需要发出多次的查询请求。
DNS缓存
当某个DNS服务器收到一个DNS回答后,将回答中的信息缓存在本地存储器中
DNS实现负载平衡
- 当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合
- 在每个回答中,会循环这些 IP 地址的顺序,一般会选择排在前面的地址发送请求。
- 以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。
DNS 为什么使用 UDP 协议作为传输层协议?
为了避免使用 TCP 协议造成的连接时延。
- 为了得到一个域名的 IP 地址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢。
- 大多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。
为什么需要三次握手?
这是由TCP的可靠传输决定的。可靠传输需要确认双方的发送能力和接收能力。
第一次握手可以确认客户端的发送能力。第二次握手,服务端SYN确认了发送能力,ACK确认了接收能力。第三次握手ACK才可以确认客户端的接收能力。不然容易出现丢包的现象。
为什么需要四次挥手?
当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文,并不会立即关闭SOCKET,只能先回复一个ACK报文,告诉客户端收到了FIN报文。只有等服务端所有的报文都发送完了,服务端才能发送FIN报文,因此不能一起发送。故需要四次挥手。
创建新图层:一种是显式合成,一种是隐式合成
显式合成: 1.拥有层叠上下文的节点
- HTML根元素本身就具有层叠上下文。
- 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
- 元素的 opacity 值不是 1
- 元素的 transform 值不是 none
- 元素的 filter 值不是 none
- 元素的 isolation 值是isolate
- will-change指定的属性值为上面任意一个。
2.需要剪裁(clip)的地方:超出的文字部分或出现了滚动条
隐式合成: z-index比较低的节点会提升为一个单独图层,那么层叠等级比它高的节点都会成为一个独立的图层。
层数过多会导致层爆炸。
实践意义
- 使用createDocumentFragment进行批量的 DOM 操作
- 对于 resize、scroll 等进行防抖/节流处理。
- 动画使用transform或者opacity实现,开启GPU加速
- 将元素的will-change 设置为 opacity、transform、top、left、bottom、right 。渲染引擎会为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。
- 对于不支持will-change 属性的浏览器,使用3D transform属性来强制提升为合成 transform: translateZ(0);
- rAF优化等等
54、script 标签中 defer 和 async 的区别
defer: 表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并 未停止解析,这两个过程是并行的。 当整个 document 解析完毕后再执行脚本文件,在 DOMContentLoaded 事件触发之前完成。多个脚本按顺序执行。
async: 表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,也就是说它的执行仍然会阻塞文档的解析,只是它的加载过程不会阻塞。多个脚本的执行顺序无法保证。
async 和 defer 异步加载,async 下载完立即执行,defer 待界面文档解析完成之后执行
不带属性:加载到 script 立即下载并执行,阻塞后续渲染的执行。
最佳方案:外部引用文件放在 /body 之前执行。
55、为什么通常将 css 的 link 放置 在head 之间,而将 js 的 script 放置在 body 之前?有哪些例外吗?
浏览器在处理 HTML 页面渲染和 JavaScript 脚本执行的时候是单一进程的,所以当浏览器在渲染 HTML 遇到了script 标签后,会先去执行标签内的代码(如果是使用 src 属性加载的外链文件,则先下载再执行),在这个过程中,页面渲染和交互都会被阻塞。所以将 script 放在 body 之前,当页面渲染完成再去执行 script。
一般希望 DOM 还没加载必须需要先加载的 js 会放置在 head 中,有些加了 defer、async 的 script 也会放在head中。
56、说下跨域
跨域,指浏览器不能执行其他网站的脚本。是由浏览器的同源策略造成,是浏览器对Js实施的安全限制。
同源策略是一个安全策略。同源指的是协议、域名、端口号相同。
限制的行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获取
- Ajax请求发送不出去
解决方案
jsonp: 利用 script 标签没有跨域限制的漏洞,网页可以拿到其他来源产生的动态 JSON 数据。兼容性比较好,缺点就是仅支持 get 请求,可能会受到XSS攻击。
跨域资源共享 CORS: 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS
简单请求:GET、HEAD、POST 或 Content-Type 的值为 text/plain、multipart/form-data、application/x-www-form-urlencoded
复杂请求:不符合以上条件的请求就是复杂请求。 复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求。该请求是 option 方法的,可以用来知道服务端是否允许跨域请求。
node正向代理 : /api ->同域的node服务 -> /api ->前端
nginx反向代理:proxy_pass
57、HTTP协议各版本及优缺点
HTTP 0.9
只有一个命令GET
只支持纯文本
HTTP 1.0
- 任何格式的内容都可以发送,不仅可以传输文字,还能传输图像、视频、二进制等文件。
- 除了GET命令,还引入了POST命令和HEAD命令。
- http请求和回应的格式改变,除了数据部分,每次通信都必须包括头信息
- 只使用 header 中的 Expires 和 Last-Modified 作为缓存失效的标准。
- 不支持断点续传,每次都会传送全部的页面和数据。
- 通常每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名
HTTP 1.1
http1.1是目前最为主流的http协议版本
- 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。长连接的连接时长可以通过请求头中的 keep-alive 来设置
- 引入了管道机制,即在同一个TCP连接里,客户端可以同时发送多个请求,进一步改进了HTTP协议的效率。
- 新增加了Catch-Control、 E-tag,If-None-Match 等缓存控制标头来控制缓存失效。
- 支持断点续传
- 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。
- 新增方法:PUT、 TRACE、 OPTIONS、 DELETE。
http1.x版本问题
- 在传输数据过程中,所有内容都是明文,无法保证数据的安全性。
- 在 HTTP/1 中,每次请求都会建立一次 HTTP 连接,也就是3 次握手和 4 次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive,解决了多次连接的问题,但是依然有两个效率上的问题,一是串行的文件传输,二是连接数过多导致的性能问题。
HTTP 2.0
- 二进制分帧: 二进制格式,便于机器解码。帧是最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
- 头部压缩: 避免了重复请求头的传输,又减少了传输的大小
- 多路复用: 一个 TCP 连接中可以存在多条流。也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。避免了旧版本中的队头阻塞问题,极大的提高传输性能
- 服务器推送: 浏览器发送一个请求后,服务器主动向浏览器推送与这个请求相关的资源,之后浏览器就不用再次发送后续的请求了
- 请求优先级: 可以设置请求优先级,按照优先级来解决阻塞的问题,先处理重要资源,优化用户体验。
HTTP 3
QUIC“快速UDP互联网连接”
HTTP3 的主要改进在传输层上,一切都会走 UDP。
58、HTTP/1.0/1.1/2.0 在并发请求上主要的区别是什么?
1、HTTP/1.0
每个 TCP 连接只能发送一个请求,当服务器响应后就会关闭这个连接,下一次请求需要再次建立 TCP 连接。
追问:你说的每个连接只能发一个请求,这个连接是指 HTTP 连接还是 TCP 连接?
TCP 连接
2、HTTP/1.1
默认采用持久连接,TCP,Connection:keep-alive
Connection:close
管道机制,在同一个 TCP 连接里,允许多个请求同时发送,一问一答的形式。
所有的数据通信是有顺序的,A B C,A 先到达服务器开始响应,10s,队头阻塞。
3、HTTP/2.0
加了双工模式,服务器能同时处理多个请求,解决了队头阻塞的问题。
多路复用,没有次序概念。
加了服务器推送功能。
59、HTTP /1.1的长连接和2.0的多路复用有什么区别?
1.1:同一时间一个 TCP 连接只能处理一个请求,采用一问一答的形式,上一个请求响应后才能处理下一个请求。
追问:听说 chrome 浏览器支持最大6个同域请求的并发
因为 chrome 支持最大6个 TCP 连接
2.0:同域名上的所有通信都在单个连接上完成,单个连接上可以并行交错的进行请求和响应。
为什么1.1不呢实现多路复用呢?
HTTP /2.0是基于二进制帧的协议,HTTP/1.1是基于文本分割解析的协议。
1.1的报文结构里,服务器需要不断的读入字节,直到遇到换行符,处理的顺序是串行的。
GET / HTTP/1.1
Accept:
host:
referer:
POST
2.0以帧为最小数据单位,每个帧都会有标识自己属于哪个流,多个帧组成一个流。
多路复用,其实就是一个 TCP 里存在多条流
60、HTTP 常见状态码
1xx 信息类
信息提示,请求正在处理
2xx 成功
200 OK 表示从客户端发来的请求在服务器端被正确请求
204 No content,表示请求成功,无内容返回。
3xx 重定向
301 永久移动(回应 GET 响应时会自动将请求者转到新
304 未修改(协商缓存)
4XX 客户端错误
400 Bad Request
401 未授权
403 服务器拒绝访问
404 未找到
409 请求发生冲突
5XX 服务器错误
500 服务器内部错误
502 错误网关
503 服务不可用
61、域名分片
在一个域名下分出多个二级域名出来,它们最终指向的还是同一个服务器,这样就可以并发处理更多的任务队列,也更好的解决了队头阻塞的问题
62、TCP、UDP
TCP---传输控制协议
提供的是面向连接、可靠的字节流服务。交换数据前,需要建立TCP连接,才能传输数据。TCP提供超时重发,流量控制,拥塞控制等功能,保证数据能从一端传到另一端。
应用场景: HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议
UDP---用户数据报协议
是一个简单的面向数据报的传输层协议。UDP不保证可靠性,只是把数据报发送出去,不保证能到达目的地,可能丢包。由于不需要建立连接,且没有流量控制,拥塞控制、 超时重发等机制,故而传输速度很快。
应用场景:如直播、实时游戏、物联网等。
quic 基于 udp 怎么保证可靠性 ?
TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 和 Ack 来确认消息的有序到达。
QUIC 同样是一个可靠的协议,它使用 Packet Number 代替 TCP 的 sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 也已经不是 N,而是一个比 N 大的值,比如 Packet N+M。
但是单纯依靠严格递增的 Packet Number 肯定无法保证数据的顺序性和可靠性,QUIC 又引入了一个 Stream Offset 的概念。即一个 Stream 可以经过多个 Packet 传输,Packet Number 严格递增,没有依赖。但是 Packet 里的 Payload 如果是 Stream 的话,就需要依靠 Stream 的 Offset 来保证应用数据的顺序。
63、GET 和 POST 的区别
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求大小一般是(1024字节),POST理论上来说没有大小限制
- GET请求参数只接受ASCII字符,而POST没有限制。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
64、介绍一下HTTPS
HTTP + TLS/SSL 协议组合而成
「SSL」安全套接层(Secure Sockets Layer)
「TLS」传输层安全(Transport Layer Security)
TLS/SSL 的功能实现主要依赖于三种算法:散列函数 、对称加密和非对称加密。利用非对称加密实现身份认证和密钥协商,利用对称加密算法对数据加密,利用散列函数验证信息的完整性。
对称加密: 加密和解密用同一个秘钥。密钥容易被截取
非对称加密:
- 有一对秘钥,「 公钥 」和「 私钥 」。
- 公钥加密的内容,只有私钥可以解开;私钥加密的内容,所有的公钥都可以解开
- 公钥可以发送给所有的客户端,私钥只保存在服务器端。
常⻅算法 RSA(⼤质数)、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)
结合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可以使⽤对称加密来进⾏沟通。
此时⼜带来⼀个问题,中间⼈问题:
如果此时在客户端和服务器之间存在⼀个中间⼈,这个中间⼈只需要把原本双⽅通信互发的公钥,换成⾃⼰的公钥,中间⼈就可以轻松解密通信双⽅所发送的所有数据。
这个时候需要⼀个安全的第三⽅颁发证书(CA),证明身份的身份,防⽌被中间⼈攻击。
证书中包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的HASH算法、证书到期时间等
数字签名就是⽤CA⾃带的HASH算法对证书的内容进⾏HASH得到⼀个摘要,再⽤CA的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,再⽤同样的Hash算法,再次⽣成消息摘要,然后⽤CA的公钥对数字签名解密,得到CA创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。
握手过程
-
客户端发起请求
-
服务器收到请求后,会将网站的证书以及公钥发给客户端;
-
客户端收到网站证书后会检查证书的颁发机构以及过期时间,如果没有问题就随机产生一个秘钥;
-
客户端利用公钥将会话秘钥加密,并传送给服务器,服务器利用自己的私钥解密出会话秘钥;
-
之后服务器与客户端使用秘钥加密传输
65、介绍下 HTTPS 中间人攻击
中间人攻击过程如下:
-
服务器向客户端发送公钥;
-
攻击者截获公钥,保留在自己手上;
-
然后攻击者自己生成一个【伪造的】公钥,发给客户端;
-
客户端收到伪造的公钥后,生成加密 hash(秘钥) 值发给服务器;
-
攻击者获得加密 hash 值,用自己的私钥解密获得真秘钥;
-
同时生成假的加密 hash 值,发给服务器;
-
服务器用私钥解密获得假秘钥;
-
服务器用假秘钥加密传输信息;
防范方法:
服务器在发送浏览器的公钥中加入 CA 证书,浏览器可以验证 CA 证书的有效性;(现有 HTTPS 很难被劫持,除非信任了劫持者的 CA 证书)
66、HTTPS 握手过程中,客户端如何验证证书的合法性
-
首先浏览器对证书的证书所有者、有效期等信息进行校验,校验证书的网站域名是否与证书颁发的域名一致,校验证书是否在有效期内;
-
然后浏览器开始查找操作系统中已内置的证书发布机构 CA,与服务器发来的证书中的 CA 对比,用于校验证书是否为合法机构颁发;
-
如果找不到,浏览器就会报错,说明证书是不可信任的;
-
如果找到,那么浏览器就会从操作系统中取出颁发者 CA 的公钥(多数浏览器开发商发布版本时,会实现在内部植入常用认证机关的公开密钥),然后对服务器发来的证书里面的签名进行解密;
-
浏览器使用相同的 hash 算法计算出服务器发来的证书的 hash 值,将这个计算的 hash 值与证书中签名做对比;
-
对比结果一致,则证书是合法的
五、Vue
67、v-if和v-for哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能?
-
显然v-for优先于v-if被解析(把你是怎么知道的告诉面试官)
-
如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能
-
要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环
-
如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项
68、Vue组件 data为什么必须是个函数而 Vue 的根实例则没有此限制?
Vue 组件可能存在多个实例,如果使用对象形式定义 data,则会导致它们共用一个 data 对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在 initData 时会将其作为工厂函数返回全新 data 对象,有效规避多实例之间状态污染问题。而在 Vue 根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况
69、 你知道 vue 中 key 的作用和工作原理吗?
-
key 的作用主要是为了高效的更新虚拟 DOM,通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效,减少 DOM 操作量,提高性能。
-
另外,若不设置 key 还可能在列表更新时引发一些隐蔽的 bug
-
vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
70、 你怎么理解vue中的diff算法?
1.diff 算法是虚拟 DOM 技术的必然产物:通过新旧虚拟 DOM 作对比(即diff),将变化的地方更新在真实 DOM上;另外,也需要 diff 高效的执行对比过程,从而降低时间复杂度为 O(n)。
2.vue 2.x中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,只有引入 diff 才能精确找到 发生变化的地方。
3.vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果 oldVnode 和新的渲染结果newVnode,此过程称为 patch。
4.整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助 key 通常可以非常精确找到相同节点,因此整个 patch过程非常高效。
71、如何实现MVVM?优缺点?
MVVM 模式,即 Model-View-ViewModel 模式
Model 层: 对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。
View 层: 作为视图模板存在,它是⼀个动态模板。不负责处理状态,除了定义结构、布局外,它展示的是ViewModel 层的数据和状态。
ViewModel 层: 处理 View 层的具体业务逻辑,把 View 层需要的数据暴露。ViewModel 底层会做好绑定属性的监听。当 ViewModel 中数据变化,View 层会得到更新;⽽当 View 中声明了数据的双向绑定(通常是表单元素),框架也会监听 View 层(表单)值的变化。⼀旦值变化,View 层绑定的 ViewModel 中的数据也会得到⾃动更新。
优点:
-
分离视图和模型,降低代码耦合,提⾼视图或者逻辑的重⽤性
-
提⾼可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码
-
⾃动更新dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动dom中解放
缺点:
-
Bug很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是View层的代码有Bug,也可能是Model 层的代码有问题。
-
⼀个⼤的模块中model也会很⼤,如果⻓期持有,不释放内存就造成了花费更多的内存
-
对于⼤型的图形应⽤程序,视图状态较多,ViewModel的构建和维护的成本都会⽐较⾼
72、Vue性能优化方法?
路由懒加载: 将路由分块打包到一个单独的js中,只有加载该路由的时候,才会加载这个chunk文件。
keep-alive缓存页面: 在组件切换过程中将状态保留在内存中,防止重复渲染DOM
使用v-show复用DOM
v-for 遍历避免同时使用 v-if
长列表性能优化:如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化,Object 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容vue-virtual-scroller、vue-virtual-scroll-list
事件的销毁: Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
图片懒加载: 对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域 内的图片先不做加载, 等到滚动到可视区域后再去加载。
首先将页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置 在 data-original 属性中,当页面滚动的时候需要去监听 scroll 事件, 在 scroll 事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内则将图片的 src 属性设置为 data-original 的值,这样就可以实现 延迟加载。
73、vue中组件之间的通信方式?
-
props (父传子) $emit/v-on (子传父) 父子、兄弟
-
Eventbus 父子组件、兄弟组件、跨级组件
-
vuex 优点:一次存储数据,所有页面都可访问
-
children (父 = 子 项目中不建议使用)缺点:不可跨层级
-
provide/inject 祖先组件中通过provider来提供变量,子孙组件中通过inject来注入变量。缺点:不是响应式
74、vuex
Vuex 是一个状态管理模式,保证状态以一种可预测的方式发生变化。
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。
所有异步操作或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,需要通过 Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。
state : vuex 的唯一数据源,如果获取多个 state ,可以使用 ...mapState
getter : 计算属性, getter 的返回值根据他的依赖缓存起来,依赖发生变化才会被重新计算。
mutation :更改 state 中唯一的方法是提交 mutation ,接收 state 作为第一个参数 ,自定义函数payload 为第二个参数,mutation 必须是同步函数。
action : 可以包含异步操作。action 中的第一个参数是 context ,第二个参数是自定义函数payload
action 通过 store.dispatch 触发, mutation 通过 store.commit 提交
vuex 存储的数据是响应式的,刷新之后会消失,所以数据要存储到 localStorage 里面。刷新之后,如果 localStorage 里有保存的数据,取出来再替换 store 里的 state 。
vuex 里保存的状态,都是数组,而localStorage 只支持字符串,要使用JSON.stringify转换。
vuex应用场景(登陆验证,购物车,播放器等)
75、vue-router 中的导航钩子由那些?
路由中的导航钩子有三种
全局 : beforeEach (to,from,next) 路由改变前调用,afterEach (to,from) 路由改变后的钩子
to :即将要进入的目标路由对象
from:当前正要离开的路由对象
next:路由控制参数
next():如果一切正常,则调用这个方法进入下一个钩子
next(false):取消导航(即路由不发生改变)
next('/login'):当前导航被中断,然后进行一个新的导航
组件内 : beforeRouteEnter (to,from,next)、beforeRouteUpdate (to,from,next)、beforeRouteLeave (to,from,next)
路由配置: beforeEnter (to,from,next)
76、说一说vue响应式理解?
data通过Observer转换成了getter/setter的形式来追踪变化
当外界通过Watcher读取数据时,会触发getter从而将watcher添加到依赖中
当数据变化时,会触发setter从而向(watcher)发送通知, watcher接收通知后,会向外界发送通知,触发视图更新或回调函数
77、你了解 Vue 的双向绑定原理吗?
所谓的双向绑定是建立在 MVVM 的模型基础上的:
- 数据层 Model:应用的数据以及业务逻辑
- 视图层 View:应用的展示效果,各类的 UI 组件等
- 业务逻辑层 ViewModel:负责将数据和视图关联起来
1、数据变化后更新视图
2、视图变化后更新数据
包含两个主要的组成部分
- 监听器 Observer:对所有的数据属性进行监听
- 解析器 Compiler:对每个元素节点的指令进行扫描和解析,根据指令替换数据,绑定对应的更新函数。
具体的实现原理:
1、new Vue( ) 初始化时,对 data 通过 Object.defineProperty 进行响应化处理,这个过程发生在 Observer 中,每个 key 都会有一个 dep 实例来存储 watcher 实例数组。
2、对模板进行编译时,v - 开头的关键词作为指令解析,找到动态绑定的数据,从 data 中获取数据并初始化视图,这个过程发生在 Compiler 里。如果遇到了 v - model,就监听 input 事件,更新 data 对应的数值。
3、在解析指令的过程中,会定义一个更新函数和 Watcher,之后对应的数据变化时,Watcher 会调用更新函数,在new Watcher 的过程中会去读取 data 中的 key,触发 getter 的依赖收集,将对应的 Watcher 添加到 dep 中。
4、将来 data 中数据一旦发生变化,会首先找到对应的 dep,然后通知所有的 watcher 执行更新函数。
78、你了解虚拟 DOM 吗?能说一下它的优缺点吗?
对于真实 DOM 的抽象,用嵌套对象表示,用属性来描述节点,最终通过一系列的操作映射到真实 dom 上。
优点:
1、保证性能的下限:
在不进行手动优化的前提下,也能提供过得去的性能。
2、无需手动操作 dom
3、跨平台
虚拟 dom 本质上就是一个 js 对象,它可以很方便的跨平台,比如服务端渲染、uniapp.
缺点:
1、首次渲染大量 dom 的时候,由于多了一层虚拟 DOM 的计算,会比 innerHTML 的插入速度慢。
2、做一些针对性的优化的时候,真实 dom 的操作还是更快一点
79、计算属性的实现原理
computed watcher,计算属性的监听器,持有一个 dep 实例,通过 dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖值改变后,就会通知订阅的 watcher 进行更新, computed watcher 会将 dirty 属性设置为 true,并且进行计算属性方法的调用
1、computed 所谓的缓存是指什么?
计算属性是基于它的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值
2、那 computed 缓存存在的意义是什么?或者说你经常在什么时候使用?
比如计算属性方法内部操作非常的耗时,遍历一个极大的数组,计算一次可能要耗时1s
3、以下情况,computed 可以监听到数据的变化吗?
template
{{ storageMsg }}
computed: {
storageMsg: function() {
return sessionStorage.getItem('xxx');
},
time: function() {
return Date.now();
}
}
created() {
sessionStorage.setItem('xxx', 1111);
}
onClick() {
sessionStorage.setItem('xxx', Math.random());
}
不可以,没有经过初始化 data { },也没有经过响应式对象 Observer
80、vue如果想要扩展某个组件现有组件时怎么做?
-
使用mixin全局混入:从执行的先后顺序来说,混入对象的钩子将在组件自身钩子之前调用,如果遇到全局混入 (Vue.mixin),全局混入的执行顺序要前于混入对象和组件。
-
使用slot扩展: 默认插槽和具名插槽
81、vue为什么要求组件模版只能有一个根元素?
单文件组件中,template下的元素div,其实就是"树"状数据结构中的"根"。
如果同时设置了多个入口,那么vue就不知道哪一个才是真正的入口。
82、watch和computed的区别
computed 一个属性受多个属性影响,可以缓存数据。例如 购物车商品结算时
watch 一条数据影响多条数据,可以观察数据变化,例如 搜索数据
83、nextTick的原理
nextTick 确保操作的是更新后的DOM
-
vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
-
microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
-
因为兼容性问题,vue不得不做了microtask向macrotask的降级方案
微任务:Promise.then、MutationObserver、nodejs中的 process.nextTick.
宏任务:setImmediate、MessageChannel、setTimeout
MutationObserver HTML5新增的特性,在iOS上尚有bug。
Promise 是ES6新增,也存在兼容问题
setImmediate 只有IE和nodejs支持。
MessageChanne 也是个新API,面临兼容性问题
一般什么时候用到nextTick呢?
在数据变化后要执行某个操作,而这个操作依赖因你数据改变而改变的 dom,这个操作就应该被放到 vue.nextTick 回调中。
<tempalte>
<div v-if="loaded" ref="test"></div>
</tempalte>
async showDiv() {
this.loaded = true;
this.$refs.test // 同步获取不到 test
await Vue.nextTick();
this.$refs.test.xxx();
}
84、vue生命周期
1.beforeCreate:组件实例被创建之初,组件的属性⽣效之前
2.created:组件实例已经完全创建,属性也绑定,但真实dom还没有⽣成, $el 还不可⽤
3.beforemount: 在挂载开始之前被调⽤:相关的 render 函数⾸次被调⽤
4.mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调⽤该钩⼦
5.beforeupdata:组件数据更新之前调⽤,发⽣在虚拟 DOM 打补丁之前
6.updated:组件数据更新之后
7.beforedestory:组件销毁前调⽤
8.destroyed:组件销毁后调⽤
85、说说 vue 的模板编译
- 解析html,生成ast: 用于描述节点信息
- 对ast做一些优化: 给ast对象添加一个标记,如果这个节点永远不会更新,标记为静态节点,diff过程直接跳过。
- 生成render函数字符串: 将ast标记优化好后,遍历ast树,将节点变成一个等待调用的函数,用于创建该节点
86、vue如何监听数组变化
Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的Observer。如果有新的值,就调用observeArray 对新的值进行监听,然后调用 notify,通知 render watcher,执行更新
87、vue组件化的理解
-
组件是独立和可复用的代码组织单元。组件系统是 Vue 核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用;
-
组件化开发能大幅提高应用开发效率、测试性、复用性等;
-
组件使用按分类有:页面组件、业务组件、通用组件;
-
vue的组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函 数,它们基于VueComponent,扩展于Vue;
-
vue中常见组件化技术有:属性prop,自定义事件,插槽等,它们主要用于组件通信、扩展等;
-
合理的划分组件,有助于提升应用性能;
-
组件应该是高内聚、低耦合的;
-
遵循单向数据流的原则。
88、为什么不建议用index作为key
当以数组的下标 index 作为 key 值时,其中一个元素发生了变化 ,就有可能导致所有元素的 key 值发生改变。 index 作为 key 值和没加 index 是一样的,并不能提升性能
89、如何使用V-model进行组件间通信
v-model的双向数据绑定是依靠v-bind绑定prop和v-on监听传值所完成的
1.子组件内通过监听某事件,向上传递改变组件自身的value值,通过v-on传递给父组件数据
2.父组件通过v-bind传给子组件value值。
但注意,此时数据变动仅仅只被value接收,最后一步还需要在子组件内用watch监听value将值传给组件内的数据
90、mutation怎么判断是否需要处理数据
//实现mutation
let mutations = obj.mutations;
this.mutations = {}
Object.keys(mutations).forEach(key=>{
this.mutations[key] = (options)=>{
mutations[key].call(this,this.state,options)
}
1.这里的循环遍历的操作的目的就是,根据参数在this.mutations中添加响应的方法,实现的
2.原理是为this.mutations添加对应的属性,属性值为函数,执行这个函数,就是执行mutations中的方法,
3.但是我们要保证mutations中的每一个方法中的this,都是这类的实例
4.mutations中的第一个参数也要是这个类的实例,
5.第三个参数就是传递的值;
6.所以我们里面的函数调用就是mutations[key].call(this,this,options);
});
//实现actions
this.actions = {};
let actions = obj.actions;
Object.keys(actions).forEach(key=>{
this.actions[key] = (options)=>{
actions[key].call(this,this,options);
}
});
//这里的原理和mutations是差不多的
六、React
91、React 生命周期
⽬前React 16.8 +的⽣命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段
挂载阶段:
constructor: 构造函数,最先被执⾏,我们通常在构造函数⾥初始化 state 对象或者给⾃定义⽅法绑定 this getDerivedStateFromProps: static getDerivedStateFromProps(nextProps, prevState) 这是个静态⽅法,当我们接收到新的属性想去修改我们 state,可以使⽤ getDerivedStateFromProps
render: render函数是纯函数,只返回需要渲染的东⻄,不应该包含其它的业务逻辑,可以返回原⽣的 DOM、React 组件、Fragment、Portals、字符串和数字、Boolean 和 null 等内容
componentDidMount: 组件装载之后调⽤,此时我们可以获取到DOM节点并操作,⽐如对canvas,svg的操作,服 务器请求,订阅都可以写在这个⾥⾯,但是记得在 componentWillUnmount 中取消订阅
更新阶段:
getDerivedStateFromProps: 此⽅法在更新各个挂载阶段都可能会调⽤
shouldComponentUpdate: shouldComponentUpdate(nextProps, nextState) ,有两个参数nextProps和nextState,表示 新的属性和变化之后的 state,返回⼀个布尔值,true 表示会触发重新渲染,false 表示不会触发重新渲染,默认返回 true ,我们通常利⽤此⽣命周期来优化 React 程序性能
render: 更新阶段也会触发此⽣命周期
getSnapshotBeforeUpdate: getSnapshotBeforeUpdate(prevProps, prevState) ,这个⽅法在 render 之后, componentDidUpdate之前调⽤,有两个参数 prevProps和 prevState,表示之前的属性和之前的 state,这个函数有⼀个返回值,会作为第三个参数传给 componentDidUpdate,如果你不想要返回值,可以返回 null,此⽣命周期必须与 componentDidUpdate 搭配使⽤
componentDidUpdate: componentDidUpdate(prevProps, prevState, snapshot) ,该⽅法在getSnapshotBeforeUpdate ⽅法之后被调⽤,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的 state,和 snapshot。 第三个参数是 getSnapshotBeforeUpdate 返回的,如果触发某些回调函数时需要⽤到 DOM 元素的状态,则将对⽐或计算的过程迁移⾄ getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统⼀触发回调或更新状态。
卸载阶段:
componentWillUnmount: 当我们的组件被卸载或者销毁了就会调⽤,我们可以在这个函数⾥去清除⼀些定时器,取消⽹络请求,清理⽆效的 DOM 元素等垃圾清理⼯作
React 16之后有三个⽣命周期被废弃
componentWillMount
componentWillReceiveProps
componentWillUpdate
废除原因
1.在 Fiber 机制下,render 阶段是允许暂停、终止和重启的,重启就是“重复执行一遍整个任务”,这就导致 render 阶段的生命周期是有可能被重复执行的。这三个生命周期都处于 render 阶段,都可能重复被执行,导致bug
2.避免开发者不合理的编程习惯,可能在生命周期中滥用 setState 导致重复渲染死循环,阻止用户在其内部使用 this
92、React的 diff 算法
调和就是将虚拟 DOM 映射到真实 DOM 的过程
调和不等于 Diff ,Diff 只是调和的一部分
1.分层处理:同层级才会进行比较,跨层级的直接跳过,销毁旧的,重建新的
2.类型相同的节点才会比较,类型不同的直接替换旧的
3.key作为唯一标识,可以减少同一层级节点的不必要比较
93、React 数据流
1. 基于 props 的单向数据流:实现父-子通信、子-父通信和兄弟组件通信。
父子: this.props
子父:父传给子一个绑定了自身上下文的函数,那么子组件在调用该函数时,将数据以函数入参的形式给出去
2. 利用“发布-订阅”模式驱动数据流
3. 使用 Context API 维护全局状态: Provider 提供数据,Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新
4.Redux: Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
在 Redux 的整个工作过程中,数据流是严格单向的
Redux 主要由三部分组成:
store:单一的数据源,而且是只读的;
action:对变化的描述。
reducer: 它负责对变化进行分发和处理, 最终将新的数据返回给 store。reducer 一定是一个纯函数
-
使用 createStore 来完成 store 对象的创建
-
reducer 的作用是将新的 state 返回给 store: 接受action和旧的state,返回新的state。
-
action 的作用是通知 reducer “让改变发生” :action 对象中只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”
-
派发 action,靠的是 dispatch
94、setState到底是异步还是同步?
-
setState 只在合成事件和钩⼦函数中是“异步”的,在原⽣事件和 setTimeout 中都是同步的。
-
setState 的“异步”不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的,只是合成事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形成了所谓的“异步”,可以通过第⼆个参数 callback 拿到更新后的结果。
-
setState 的批量更新优化也是建⽴在“异步”(合成事件、钩⼦函数)之上的,在原⽣事件和setTimeout 中不会 批量更新,在“异步”中如果对同⼀个值进⾏多次 setState ,批量更新策略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时 setState 多个不同的值,在更新时会对其进⾏合并批量更新。
95、React Fiber
JavaScript 是单线程的,浏览器是多线程的
JavaScript 线程和渲染线程是互斥的:当其中一个线程执行时,另一个线程只能挂起等待。
若 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,带给用户的体验就是所谓的“卡顿”
Stack Reconciler 是一个同步的递归过程,不可以被打断
当处理结构相对复杂、体量相对庞大的虚拟 DOM 树时,Stack Reconciler 需要的调和时间会很长,这就意味着 JavaScript 线程将长时间地霸占主线程,进而导致渲染卡顿/卡死、交互长时间无响应等问题。
Fiber 架构实现了“增量渲染”。所谓“增量渲染”,就是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面。不过严格来说,增量渲染其实也只是一种手段,实现增量渲染的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用户体验。
Fiber 架构核心:“可中断”“可恢复”与“优先级”
React 16 中,为了实现“可中断”和“优先级”,多出来一层架构,Scheduler(调度器),作用是调度更新的优先级。
新老两种架构对 React 生命周期的影响主要在 render 这个阶段,这个影响是通过增加 Scheduler 层和改写 Reconciler 层来实现的。
在 render 阶段,一个庞大的更新任务被分解为了一个个的工作单元,这些工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是 0 感知。
总结:为了使 react 渲染的过程中可以被中断,可以将控制权交还给浏览器,可以让位给高优先级的任务,浏览器空闲后再恢复渲染,对于计算量比较大的 js 计算或者 dom 计算,就不会显得特别卡顿,而是一帧一帧有规律的执行任务。
1、generator 有类似的功能,为什么不直接使用?
- 要使用 generator ,需要将涉及到的所有代码都封装成 generator * 的形式,非常麻烦,工作量很大。
- generator 内部是有状态的,react 认为这种状态是麻烦的,想要自己完整地控制整个过程,所以摒弃了这种写法。
function *doWork(a, b, c) {
const x = doExpensiveWorkA(a);
yield;
const y = doExpensiveWorkB(b);
yield;
const z = doExpensiveWorkC(x, y, c);
return z;
}
我们已经执行完了 doExpensiveWorkA 和 doExpensiveWorkB,还未执行 doExpensiveWorkC,如果此时 b 被更新了,那么在新的时间分片里,我们只能沿用之前获取到的 x,y 结果。
2、如何判断当前是否有高优任务呢?
当前 js 的环境其实并没有办法去判断是否有高优任务。
只能约定一个合理的执行时间(每秒60帧,1000ms/60f = 16ms。) ,当超过了这个执行时间,如果任务仍然没有执行完成,则中断当前任务,将控制权交还给浏览器。
requestIdleCallback 使浏览器在有空的时候执行我们的回调,这个回调会传入一个参数,表示浏览器有多少时间供我们执行任务。
浏览器在一帧内要做什么事情:
处理用户输入事件
Js 的执行
requestAnimation 调用
布局 layout
绘制 paint
浏览器很忙怎么办?
requestIdleCallback timeout参数,100ms,如果超过这个 timeout 后,回调还没有被执行,那么会在下一帧强制执行回调。
兼容性?
requestIdleCallback 兼容性很差,通过 messageChannel 模拟实现了 requestIdleCallback 的功能。
timeout 超时后就一定要被执行吗?
不是的,react 预定了5个优先级:
Immediate 最高优先级,这个优先级的任务应该被马上执行不能中断
UserBlocking 这些任务一般是用户交互的结果,需要及时得到反馈
Normal 不需要用户立即就感受到的变化,比如网络请求
Low 这些任务可以被延后,但是最终也需要执行
Idle 可以被无限期延后
96、平时用过高阶组件吗?什么是高阶组件?高阶组件能用来做什么?
简称 HOC ,High Order Component
1、是一个函数
2、入参:原来的 react 组件
3、返回值:新的 react 组件
4、是一个纯函数,不应该有任何的副作用
怎么写一个高阶组件?
1、普通方式
2、装饰器
3、多个高阶组件的组合
高阶组件能用来做什么?技术层面上
1、属性代理
操作 props
操作组件实例
2、继承/劫持
97、什么是 react hooks ? 它有什么优势?
可以不写 class 组件的情况下,使用 state 和其他 react 特性。 如 useState useEffect useMemo
react hooks 有什么优势?
class 的缺点:
1、组件间的状态逻辑很难复用
组件间如果有 state 的逻辑是相似的,class 模式下基本上是用高阶组件来解决的。虽然能够解决问题,但是需要在组件外部再包一层元素,会导致层级非常冗余。
2、复杂业务的有状态组件会越来越复杂
3、监听和定时器的操作,被分散在多个区域
4、this 指向问题
Hooks 的优点:
1、有利于业务逻辑的封装和拆分,可以自由的组合各种自定义hooks
2、可以在无需修改组件结构的情况下,复用状态逻辑
3、定时器、监听等都被聚合到同一块代码下
react hooks 的使用注意事项
1、只能在函数内部的最外层调用 hook,不要在循环、条件判断或者子函数中调用
2、只能在 React 的函数组件中调用 hook,不要在其他的 js 函数里调用
Hooks 暂时还不能完全地为函数组件补齐类组件的能力:比如 getSnapshotBeforeUpdate、componentDidCatch 这些生命周期,目前都还是强依赖类组件的。
98、useState 怎么做缓存的 ?useState 为什么不能放到条件语句里面?
- 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局数组中。
- 每次声明 state,都要将 cursor 增加 1。
- 更新 state,触发再次渲染的时候,cursor 被重置为 0。
- 按照 useState 的声明顺序,依次拿出最新的 state 的值,更新视图。
因为是按照数组索引存储的, 如果在判断中使用, 某个 state 没有按照对应索引存储,那么后面的 state 索引也会有问题。
怎么解决 useState 闭包的问题 ?
使用箭头函数
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count + 1); // 问题所在:此时的 count 为5s前的count!!!
setCount(count => count + 1); // 此时用的是最新的 state
}, 5000);
}
关于 Hook 中的闭包:
useEffect 、 useMemo 、 useCallback 都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state、props)。所以每一次这三种 Hook 的执行,反映的也都是当时的状态,无法获取最新的状态。对于这种情况,应该使用 ref 来访问。
useReducer 比 redux 好在哪里 ?
使用 redux 的步骤
- 定义 reducer
- 引入 connect
- 定义 mapStateToProps 方法,mapDispatchToProps 方法(分别用来接收 state 和 dispatch)
使用 useReducer 的步骤
- 定义 reducer
- useReducer 引入 reducer 和初始值
useReducer 简洁了很多,更易于编写和阅读。同时也减少了方法的定义、减少了命名的次数。
99、Redux 中的 reducer 为什么每次必须返回一个新对象
Redux 是通过比较新旧两个对象是否一致,来更新页面的。如果直接返回旧的 state 对象,页面就不会更新。
比较两个 js 对象中所有的属性是否完全相同,唯一的办法就是深比较,但是,深比较在真实的应用中代码是非常大的,非常耗性能的,所以一个有效的解决方案就是做一个规定,无论发生任何变化,都要返回一个新的对象,没有变化时,开发者返回旧的对象,这也就是 redux 为什么要把 reducer 设计成纯函数的原因。
100、useMemo和useCallback的区别及使用场景
useCallback 可以在函数组件多次执行时仍然返回同一个回调函数,主要为了避免子组件因为每次新创建回调函数而重新渲染。
useMemo和useCallback都是用于缓存数据,优化性能;两者接收的参数是一样的,第一个参数表示回调函数,第二个表示依赖数据。在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
useMemo 缓存的是回调函数中 return 回来的值,主要用于缓存计算结果。 应用场景如需要计算的状态
useCallback 缓存的是函数,主要用于缓存函数。 应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费。useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。
101、React 函数组件为什么 props 只会接受第一次的值,父组件的参数更新后,子组件的 props 为什么不会变化
原因:第一次加载成功后,后面的异步(http 请求返回的数据、setTimeout、点击事件等)改变值,react 只会重新渲染 render,其他初始值的渲染在加载页面就已经渲染完毕,值的更改不会触发重新渲染。
子组件显示父组件穿过来的 props 有两种方式:
1、直接使用
这种方式,父组件改变props后,子组件重新渲染,由于直接使用的props,所以我们不需要做什么就可以正常显示最新的props
class Child extends Component {
render() {
return <div>{this.props.someThings}</div>
}
}
2、转换成自己的 state
这种方式,由于我们使用的是 state,所以每当父组件每次重新传递 props 时,我们需要重新处理下,将 props 转换成自己的 state,这里就用到了 componentWillReceiveProps。
关于不会二次渲染是这样的:每次子组件接收到新的 props,都会重新渲染一次,除非你做了处理来阻止(比如使用:shouldComponentUpdate),但是你可以在这次渲染前,根据新的 props 更新 state,更新 state 也会触发一次重新渲染,但 react 不会这么傻,所以只会渲染一次,这对应用的性能是有利的。
class Child extends Component {
constructor(props) {
super(props);
this.state = {
someThings: props.someThings
};
}
componentWillReceiveProps(nextProps) {
this.setState({someThings: nextProps.someThings});
}
render() {
return <div>{this.state.someThings}</div>
}
}
102、useEffect 和 useLayoutEffect 区别?
useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景。
useLayoutEffect 是在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。因为是同步处理,所以需要避免在 useLayoutEffect 中做计算量较大的耗时任务从而造成阻塞。
103、ReactDOM.render原理
- 首先 JSX 语法被 Babel 转义,然后使用 React.creatElement 进行创建,转换为 React 元素
- 使用 ReactDom.render 创建 root 对象,执行 root.render。
- 一系列函数调用之后,workLoop 在一次渲染周期内,遍历虚拟 DOM,将这些虚拟 DOM 传递给performUnitOfWork 函数,performUnitOfWork 函数开启一次 workTime,将虚拟 DOM 传递给 beginWork。
- beginWork 根据虚拟 DOM 的类型,进行不同的处理,将子元素处理为 Fiber 类型,为 Fiber 类型的虚拟DOM 添加父节点、兄弟节点等(就是转换为 Fiber 树)。
- beginWork 处理完一次操作之后,返回需要处理的子元素再继续处理,直到沒有子元素(即返回 null),
- 此时 performUnitOfWork 调用 completeUnitOfWork 进行初始化生命周期的挂载,以及调用 completeWork 进行 DOM 的渲染。
- completeWork 对节点类型进行操作,发现是 html 相关的节点类型,添加渲染为真实的 DOM。
- 最后将所有的虚拟 DOM,渲染为真实的 DOM。
104、时间切片
根据浏览器的帧率,计算出时间切片的大小,并结合当前时间计算出每一个切片的到期时间。在每次while循环前,判断当前时间切片是否到期,若已到期,则结束循环,让出主线程的控制权。
105、合成事件
React 的事件系统沿袭了事件委托的思想, 实现了对所有事件的中心化管控
当事件在具体的 DOM 节点上被触发后,最终会冒泡到 document 上,document 上所绑定的统一事件处理程序会将事件分发到具体的组件实例。
在分发事件之前,React 首先会对事件进行包装,把原生 DOM 事件包装成合成事件。
在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。
在 React 17 中,事件的中心化管控被转移到了每个组件的容器 DOM 节点中
document 是整个文档树的根节点,操作 document 带来的影响范围太大,这会使事情变得更加不可控
106、React 性能优化
1.使用 shouldComponentUpdate 规避冗余的更新逻辑: React 组件会根据 shouldComponentUpdate 的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行 re-render(重渲染)。在 React 中,只要父组件发生了更新,那么所有的子组件都会被无条件更新。
使用 shouldComponentUpdate 来避免不必要的更新,避免无意义的 re-render 发生, 这是 React 组件中最基本的性能优化手段,也是最重要的手段。
2.PureComponent + Immutable.js: PureComponent 内置了对 shouldComponentUpdate 的实现:PureComponent 将会在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。
PureComponent 浅比较带来的问题是对“变化”的不能精准判断。Immutable.js将数据内容的变化和数据的引用严格地关联了起来,使得“变化”无处遁形。
3.React.memo 与 useMemo: React.memo:“函数版”shouldComponentUpdate/PureComponent。和 shouldComponentUpdate 不同的是,React.memo 只负责对比 props,而不会去感知组件内部状态(state)的变化。React.memo 控制是否重渲染一个组件,而 useMemo 控制是否重复执行组件内某一段逻辑。
React.memo :组件级别,无法感知函数内部状态
useMemo :组件内
107、前端路由
在前端技术早期,一个 URL 对应一个页面,如果要切换页面,那么必然伴随着页面的刷新。
Ajax 出现后,产生了SPA(单页面应用),不刷新页面即可更新页面内容。在内容切换前后,页面的 URL 都是一样的,无法确定当前的页面“进展到了哪一步”。
前端路由可以在仅有一个页面的情况下,“记住”当前走到了哪一步,前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便刷新页面,当前的 URL 也可以标识出他所处的位置,内容也会丢失。
解决思路:拦截用户的刷新操作、感知 URL 的变化
hash 模式:通过改变 URL 后面以“#”分隔的字符串( URL 上的哈希值),让页面感知到路由变化
history 模式: 浏览器的 history API 赋予了页面间的跳转能力,前进、后退、修改、新增。pushState 和 replaceState 两个 API,允许对浏览历史进行修改和新增
108、Redux 中间件
中间件的执行时机:action 被分发之后、reducer 触发之前;
中间件的执行前提:即 applyMiddleware 将会对 dispatch 函数进行改写,使得 dispatch 在触发 reducer 之前,会首先执行对 Redux 中间件的链式调用。
和普通 Redux 调用最大的不同就是 dispatch 的入参从 action 对象变成了一个函数
redux-thunk 在拦截到 action 以后,会去检查它是否是一个函数。若 action 是一个函数,就会执行它并且返回执行结果;若 action 不是一个函数,直接调用 next。
109、redux与mobx的区别?
redux 将数据保存在单⼀的store中,mobx 将数据保存在分散的多个store中
redux 需要⼿动处理变化后的操作;mobx 数据变化后⾃动处理响应的操作
redux 使⽤不可变状态,状态是只读的,不能直接去修改它,⽽是应该返回⼀个新的状态,同时使⽤纯函数;mobx中的状态是可变的,可以直接对其进⾏修改
mobx ⽐较简单,更多的使⽤⾯向对象的编程思维;redux会⽐较复杂,函数式编程思想不是那么容易,同时需要借助中间件来处理异步和副作⽤
mobx 中有更多的抽象和封装,调试会⽐较困难,同时结果也难以预测;⽽ redux 提供能够进⾏时间回溯的开发⼯ 具,同时其纯函数以及更少的抽象,让调试更加容易
110、react 如何做动态加载
React.lazy ,另外通过 webpack 的动态加载:import() 和 ensure.require
动态加载原理
webpack 动态加载两种方式:import()和 require.ensure,实现原理相同。
根据 installedChunks 检查是否加载过该 chunk
假如没加载过,则发起一个 JSONP 请求去加载 chunk
设置一些请求的错误处理,然后返回一个 Promise。
当 Promise 返回之后,就会继续执行我们之前的异步请求回调
111、React ssr 原理
SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在
SSR 的工程中,React 代码会在客户端和服务器端各执行一次。你可能会想,这没什么问题,都是 JavaScript 代码,既可以在浏览器上运行,又可以在 Node 环境下运行。但事实并非如此,如果你的 React 代码里,存在直接操作 DOM 的代码,那么就无法实现 SSR 这种技术了,因为在 Node 环境下,是没有 DOM 这个概念存在的,所以这些代码在 Node 环境下是会报错的。
好在 React 框架中引入了一个概念叫做虚拟 DOM,虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务端,判断是服务器环境,就把虚拟 DOM 映射成字符串输出;在客户端,判断是客户端环境,就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载。
其他的一些框架,比如 Vue,它能够实现 SSR 也是因为引入了和 React 中一样的虚拟 DOM 技术。
六、React Native
112、移动端布局
1、px 为主,搭配 vw/vh、媒体查询与 flex 进行布局
先查看一下当前各大网站移动端适配的结果:
移动端适配的宗旨是:让拥有不同屏幕大小的终端设备拥有一致的 UI 界面,让拥有更大屏幕的终端展示更多的内容, 那么高度固定、宽度自适应针对文本较多的网页是更好的,针对大屏幕,限制最大宽度可以让大屏有更好的体验。
从页面编写的角度出发,页面中更多的是文本和布局,关于文本,我们应该使用 px 作为单位,来实现在大屏设备显示更多的内容,而不是更大的文本;关于布局,可以使用 flex 实现弹性布局,当实现特定宽高元素时,可以适当的使用 vw/vh,当特定的值使用 vw/vh 计算复杂或存在误差时,也可以使用 rem
适配流程
- 编写 标签设置 viewport 的 width = device-width,让网页宽度等于视窗宽度
- 在 css 中使用 px
- 在适当的场景使用 flex 布局,或者配合 vw 进行自适应
- 在跨设备类型的时候(pc <-> 手机 <-> 平板)使用媒体查询
- 如果跨设备类型交互差异过大,可以考虑分项目开发
2、rem 布局
rem 是CSS3新增的一个相对单位,它以 HTML 元素的 font-size 为比例:
/* 设置html元素的字体大小为 16px,即 1rem = 16px */
html {
font-size: 16px;
}
/* 设置box元素宽 160px,10rem = 160px */
.box {
width: 10rem; /* 160px */
}
修改HTML元素的字体大小可以成比例的调整以 rem 为单位的属性,通过这个特性,我们可以实现将视窗按一定比例划分为一份一份的,当页面内元素刚好分完所有的份数,页面内容即充满整个视窗且无横向滚动条。
页面伸缩,网页布局也会进行伸缩****。目前,除了IE8及更早版本外,所有浏览器均已支持rem。
rem应用:
使用媒体查询根据不同设备按比例设置html的字体大小,然后页面元素使用rem做尺寸单位,当html字体大小变化,元素尺寸也会发生变化,从而达到等比缩放的适配。
@media screen and (min-width: 320px) {
html {
font-size: 20px;
}
}
@media screen and (min-width: 640px) {
html {
font-size: 40px;
}
}
/*在屏幕宽度为320px-639px的设备上,div的宽高就都是20px;
在屏幕宽度大与等于640px的设备上,div的宽高就都是40px;*/
div {
width:1rem;
height:1rem;
}
总结
优点:rem 布局能很好的实现在不同尺寸的屏幕横向填满屏幕,且在不同屏幕元素大小比例一致
缺点:在大屏设备(Pad)上,元素尺寸会很大,页面显示更少的内容。
针对大屏改进方案:
(在移动端浏览器 rem 方案是解决移动端适配的主流方案,这套方案的另一个名字叫做 flexible 方案,通过动态设置 rem 实现在不同尺寸的设备上界面展示效果一致。
在 lib-flexible 库里有这样一段话:
由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方。
可知,rem 方案的目的是解决 viewport 不兼容的问题,但是我个人认为,rem 的目的提供一种比例单位,不仅可以解决移动端适配问题,也还有其他的用处。)
3、vw/vh 布局
vw/vh 方案与 rem 方案类似,都是将页面分成一份一份的,只不过 vw/vh 是将页面分为 100 份,
1vw = device-width/100
总结
优点:vw、vh布局能良好的实现在不同尺寸的屏幕横向填满屏幕
(使用 postcss-px-to-viewport 能很好的帮我们进行单位转换)
缺点:
- 无法修改 vw/vh 的值,在大屏设备(Pad)中元素会放大,且无法通过 js 干预
- 兼容性- 大多数浏览器都支持、ie11不支持 少数低版本手机系统 ios8、android4.4以下不支持
4、百分比布局
在 css 中,我们可以使用百分比来实现布局,但是需要特定宽度时,这个百分比的计算对开发者来说并不友好,且元素百分比参考的对象为父元素,元素嵌套较深时会有问题。
.box {
/* w = 200 / (750/100) = 26.66667 */
/* 可知,计算复杂,且会存在误差 */
width: 26.6%;
}
扩展: 子元素的 width 和 height 百分比参考对象是父元素的 width 和 height,margin、padding 的参考对象为父元素的 width,border-radius、background-size、transform: translate()、transform-origin 的参考对象为自身宽高
5、响应式布局
通过媒体查询,可以针对不同的屏幕进行单独设置,但是针对所有的屏幕尺寸做适配显然是不合理的,但是可以用来处理极端情况(例如 IPad 大屏设备)或做简单的适配(隐藏元素或改变元素位置)
媒体查询案例:
body {
background-color: yellow;
}
/* 针对大屏产品 ipad pro */
@media screen and (min-width: 1024px) {
body {
background-color: blue;
}
}
113、js bridge 原理
JavaScript 调用 Native 的方式,主要有两种:注入 API (主流)和 拦截 URL SCHEME。
注入 API :通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
拦截 URL SCHEME :Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
在时间过程中,这种方式有一定的 缺陷:
使用 iframe.src 发送 URL SCHEME , url 太长会被截断。
创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。
Native 调用 JavaScript
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。 (闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)
通信原理小总结
通信原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。这里,笔者推荐的实现方式如下:
JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。
比如 React Native 的 iOS 端:JavaScript 运行在 JSCore 中,实际上可以利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C, 而是 为了与 Native 开发中的事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。
当然不仅仅 iOS 和 Android,其他手机操作系统也用相应的 API,例如 WMP(Win 10)下可以用 window.external.notify 和 WebView.InvokeScript/InvokeScriptAsync 进行双向通信。其他系统也类似。
114、H5 离线包更新原理
开发阶段H5代码可以通过手机设置HTTP代理方式直接访问开发机。
完成开发之后,将H5代码推送到管理平台进行构建、打包,然后管理平台再通过事先设计好的长连接通道将H5新版本信息推送给客户端,客户端收到更新指令后开始下载新包、对包进行完整性校验、merge回本地对应的包,更新结束。
其中,管理平台推送给客户端的信息主要包括项目名(包名)、版本号、更新策略(增量or全量)、包CDN地址、MD5等。
115、RN 热更新原理
React Native中的热更新类似于App的版本更新,根据查询 server 端的版本和手机端 App 的版本进行对比,然后决定是否更新。
React Native的加载启动机制:
RN会将一系列资源打包成 js bundle文件,系统加载 js bundle 文件,解析并渲染。所以,RN热更新的根本原理就是替换js bundle文件,并重新加载,新的内容就完美的展示出来了。
目前 RN 热更新主要有3种,差量热更新、热更新Pushy、 微软的 codepush。