CSS
flex弹性布局
flex
属性是flex-grow
, flex-shrink
和 flex-basis
的简写,默认值为0 1 auto
。后两个属性可选。
flex-grow
属性定义项目的放大比例,默认为0
,即如果存在剩余空间,也不放大。
flex-shrink
属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
flex-basis
属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。
flex-flow
属性是flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrap
。
align-self
属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items
属性
详见:www.ruanyifeng.com/blog/2015/0…
transition
和 animation
transition
:是过渡。用于简单动画效果,显示和隐藏,鼠标悬停缩放效果等等。
animation
:是动画。用于复杂的动画效果,可以定义关键帧,控制动画每一步。
JS
原型
-
prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型
prototype
属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。 -
proto: 当使用构造函数创建一个实例对象后,可以通过
__proto__
访问到prototype
属性。 -
constructor:实例对象通过这个属性可以访问到构造函数。
原型链
每个实例对象都有一个__proto__
属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null
,这样就形成了一个原型链。
闭包
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
垃圾回收
内存泄露:不再使用但是无法回收的内存空间。
闭包为什么会造成内存泄露:函数没有销毁,里面的变量还可触达,不敢回收内存。
闭包有一个隐蔽泄漏点:无法触达,但是还没被回收。
闭包会不会造成内存泄露
两种情况下闭包会造成泄露
-
错误的持有本该被销毁的函数引用,会导致函数关联的词法环境无法销毁,造成内存泄露。
-
当多个函数共享词法环境时,可能造成词法环境膨胀,就可能出现无法触达且无法销毁的内存,造成内存泄露。
手写instanceof
实例对象的隐式原型 = 构造函数的显式原型
function myInstanceof(left, right) {
// 获取left的原型
let proto = Object.getPrototypeOf(left);
// 获取right构造函数中的原型对象
let prototype = right.prototype;
// 判断right构造函数中的prototype是否在left的原型链中
while(true) {
if(!proto) return false;
if(proto === prototype) return true;
// 如果没找到,继续在原型对象里的原型找
proto = Object.getPrototypeOf(proto);
}
}
new
当我们使用 new
关键字创建一个函数的实例时,实例对象的 [[Prototype]]
属性会指向该函数的 prototype
属性的值。这样,实例对象就可以访问函数原型上定义的属性和方法。
function myNew(fn,...args) {
// 创建一个对象,让这个对象的原型_proto_指向构造函数fn的原型prototype上
let obj = Object.create(fn.prototype);
// 执行构造函数,并将obj绑定到this上
let res = fn.apply(obj,args);
// 判断构造函数执行后是否为对象,是则返回,不是则返回创建的对象
return res instanceOf Object ? res : obj;
}
简而言之,new会创建一个实例对象,这个实例对象可以访问函数原型上的属性和方法。
reduce
原理
用于迭代数组中每个元素
它接收两个参数,一个callback回调函数,一个累积器初始值
callback中接收4个参数:
- 累积器:用于累计回调函数中的返回值。
- 当前数组元素值
- 当前数组元素索引
- 原数组
如果设定了累积器初始值,则初始索引从0开始,否则从1开始。 如果设定了累积器初始值,则累积器取初始值,否则取数组第一个元素arr[0]。
function myReduce( arr, callback, initVal) {
// 判断是否提累积器供初始值
let total = initVal ? initVal : arr[0];
// 设定累积器初始值,索引从0开始,否则从1
const starIndex = initVal ? 0 : 1;
// 循环执行callback
for(let i = starIndex; i < arr.length; i++) {
total = callback(total,arr[i],i,arr)
}
return total;
}
箭头函数和普通函数区别
普通函数:
- 在普通函数中,
this
的值取决于函数是如何被调用的。 - 如果是作为对象的方法调用,
this
将指向调用该方法的对象。 - 如果是作为普通函数调用,
this
将指向全局对象(在浏览器环境中通常是window
)。 - 普通函数有一个
arguments
对象,它包含了函数调用时传递的参数。 - 普通函数可以用作构造函数,通过
new
关键字创建实例。
箭头函数:
- 箭头函数没有自己的
this
绑定,它继承自外部作用域。这通常被称为“词法作用域”。 - 箭头函数的
this
是在函数定义时确定的,而不是在函数调用时。 - 箭头函数没有自己的
arguments
对象,但可以使用剩余参数(rest parameters)来达到类似的效果。 - 箭头函数不能被用作构造函数,不能通过
new
创建实例。因为箭头函数没有prototype
属性,无法像普通构造函数那样创建具有原型链的对象。箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this)。
Set
和 Map
的区别
Set用于数据重组,Map用于数据储存。两种方法具有极快的查找速度。
Set:
- 成员不能重复(可以用来数组去重)
- 只存键值,没有键名,类似数组。
- 可以遍历,方法有add,delete,has,clear
Map:
- Map不允许键重复。
- 本质上是键值对的集合,类似对象。
- 可以遍历,和各种数据格式转换。
Map
和Object
的区别
map
和Object
都是用键值对来存储数据,区别如下:
- 键的类型:
Map
的键可以是任意数据类型(包括对象、函数、NaN
等),而Object
的键只能是字符串或者Symbol
类型。 - 键值对的顺序:
Map
中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。 - 键值对的遍例:
Map
的键值对可以使用for...of
进行遍历,而Object
的键值对需要手动遍历键值对。 - 继承关系:
Map
没有继承关系,而Object
是所有对象的基类。
Map
和 weakMap
的区别
Map:
- 键可以是任何类型。
- 键跟内存地址绑定,只要内存地址不同,就视为两个键
- 可以遍历。
weakMap
- 键只能是对象(null除外)
- 键是弱引用,如果键不再有其他引用,垃圾回收机制可以自动回收键值对。
- 不可以遍历。
map
和 foreach
- map 是创建一个新数组,有返回值,可以return出去。常用于不希望改变原数组情况。
- foreach 没有返回值。常用于希望改变原数组情况。
- 二者是否更改原来的数组,取决于数据类型,基本数据类型都不会改变,引用数据类型都会改变
数组方法和返回值
push()
和pop()
,都是对数组尾部
进行操作,对应 添加 和 删除shift()
和unshift()
,都是对数组头部
进行操作,对应 删除 和 添加- 删除的,
pop()
和shift()
都是返回删除的元素
- 添加的,
push()
和unshift()
都是返回数组长度
splice
(开始删除的索引,从索引算要删除的个数,要替换的元素,要替换...),返回删除的元素组成的数组
- --------------------以下为不改变原数组的分割线-------------
slice
(开始截取的索引,结束的索引但是不包含该索引元素),创建新数组不改变原数组
,返回包含截取的元素新数组
,是浅拷贝- splice 和 slice 若索引为负数,如-1,则索引按
-1+arr.length
计算 every()
所有元素是否都通过,返回布尔值
some()
有一个通过,就返回truefilter()
,创建新数组不改变原数组
返回符合包含过滤条件的元素新数组
toString()
,数组转字符串[1,2,3]->'1,2,3'join()
,数组转字符串[1,2,3]->'1,2,3'。join('')
数组转字符串[1,2,3]->'123'
事件监听机制
- 捕获阶段:事件从文档根节点向目标元素传播,即从外向内。
- 目标阶段:事件到达目标元素时,触发目标阶段。
- 冒泡阶段:事件从目标元素向文档根节点传播,即从内向外。
通常使用冒泡阶段处理事件。addEventListener('click',myFun,false),第三个参数默认false冒泡阶段执行,true是捕获阶段执行。
Babel 的原理是什么?
- 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
- 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作;
- ⽣成 Generate: 将变换后的 AST 再转换为JS 代码, 使用到的模块是 babel-generator。
ES6 代码转成 ES5 代码的实现思路是什么
ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:
- 解析:解析代码字符串,生成 AST;
- 转换:按一定的规则转换、修改 AST;
- 生成:将修改后的 AST 转换成普通代码。
如果不用工具,纯人工的话,就是使用或自己写各种polyfill
打印出 1 - 10000 中的对称数 如11,121,565
// [...Array(10000).keys()]生成 从0-10000的数组 [0,1,2,...,10000]
[...Array(10000).keys()].filter((x) => {
return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join(''))
})
如何取消重复请求
原生ajax取消请求
通过调用XMLHttpRequest
对象实例的abort
方法把请求给取消掉
axios取消请求
通过axios
的CancelToken
对象实例cancel
方法把请求给取消掉。通过axios.CancelToken.source
产生cancelToken
和cancel
方法。调用cancel方法取消。
通过axios请求拦截器取消重复请求
- 通过
axios
请求拦截器,在每次请求前把请求信息和请求的取消方法放到一个map对象当中,并且判断map对象当中是否已经存在该请求信息的请求,如果存在取消上传请求 - 通过
axios
的响应拦截器,在请求成功后在map对象当中,删除该请求信息的数据 - 通过
axios
的响应拦截器,在请求失败后在map对象当中,删除该请求信息的数据
XHR 和 Fetch
XHR | Fetch |
---|---|
监控请求进度✅ | 监控请求进度❌ |
监控响应进度✅ | 监控响应进度✅ |
service worker中不可用❌ | service worker中可用✅ |
控制cookie携带❌ | 控制cookie携带✅ |
控制重定向❌ | 控制重定向✅ |
流❌ | 流✅ |
event | promise |
请求可取消✅ | 请求可取消✅ |
文件上传,如果需要监控上传进度,需要使用XHR
问:axios和fetch的区别
axios: 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
fetch:是一个浏览器内置的 API,它是真实存在的,它是基于 promise 的。
umi-request:也是一个请求库,基于fetch
优点:
- 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
- 更好更方便的写法
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式
缺点:
- fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
- fetch默认不会带cookie,需要添加配置项
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
总结
- 发送数据时,fetch()使用body属性,而Axios使用data属性
- fetch()中的数据是字符串化的,JSON.stringify()
- URL作为参数传递给fetch()。但是在Axios中,URL是在options对象中设置的
浏览器
浏览器跨域
浏览器发送不同源的跨域请求,服务端会收到请求并返回响应数据,浏览器会进行校验,如果不通过则会跨域报错。
CORS:是一套规则,帮助浏览器判断校验是否通过。浏览器校验响应数据的规则就是CORS规则
- 只要服务器明确表示允许,则校验通过
- 服务器明确表示拒绝或没有表示,则校验不通过
- 使用CORS解决跨域,必须保证服务器是“自己人”
对简单请求的验证:简单请求时,请求头中会带有
Origin: xxx
,通知服务器当前源是什么,询问是否允许通过,服务器响应头会带有Access-Control-Allow-Origin: xxx
对预检请求的验证:浏览器会先发送一个请求方法是
OPTIONS
的给服务器,没有请求体。请求头中会带有
Origin: xxx
当前页面源,Access-Control-Request-Method: POST
当前请求的方法是什么,Access-Control-Request-Headers: venderId,Content-Type
改动了哪些请求头,把这三个告诉服务器,服务器自行判断是否允许。
服务器响应头中会带
Access-Control-Allow-Origin: xxx
允许通过的源Access-Control-Request-Method: POST
允许通过的请求的方法,Access-Control-Request-Headers: venderId,Content-Type
允许改动的请求头,Access-Control-Max-Age: 86400
告诉浏览器可缓存时间,86400毫秒内资源都不会变,不需要再预检请求了。这条不算校验规则。预检请求通过后,才会发送真实请求(和简单请求一样)。
简单请求条件(同时满足):
请求方法是
GET
/POST
/HEAD
之一。请求头是浏览器默认自带的请求头,没有自己增加或删除。
如果有
Content-Type
,必须是text/plain
,mutipart/form-data
,application/x-www-from-urlencoded
之一。不满足以上任何一个条件的都是预检请求。
Tips:XHR和Fetch都默认请求头不携带cookie
,需要自行设置。如果浏览器简单或预检请求头携带cookie,而服务器响应头未设置Access-Control-Allow-Credentials:true
携带身份凭证是否允许,浏览器仍视为跨域。对于携带身份凭证的请求,服务器不可将Access-Control-Allow-Origin
设置为*
。
Tips:在跨域访问时,js只能拿到一些最基本的响应头,如:Cache-Control
,Content-Language
,Content-Type
,Expries
,Last-Modified
,Pragma
。如需要其他头,则服务端需要手动设置允许客户端拿到的响应头白名单。如Access-Control-Expose-Headers: authorization, a, b
JSONP
很久很久以前,没有CORS,通过<script src="xxx.jsonp">
实现跨域,也要求服务器是‘自己人’。
但是JSONP只能发送GET
请求。容易产生安全隐患,恶意攻击者可能篡改callback=恶意函数
造成XSS
攻击。除非特殊原因,否则永远不要用JSONP。
运行模式:JS会准备一个callback回调函数function callback(res){}
,服务器响应结果是一个函数调用callback({data:xx})
,响应后会运行该函数,并传递数据。
nginx代理
当需要ajax请求不是自己的服务器时,可以使用代理服务器,就是在请求目标前加一个中间层,用自己的服务器作为代理,代理服务器去请求目标服务器,返回的数据再根据情况是使用CORS还是JSONP。
浏览器缓存机制
缓存位置
Service Worker
:运行在浏览器背后的独立线程,一般可以用来实现缓存功能。传输协议必须为HTTPS
。先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件。用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。Memory Cache
:内存中的缓存。包含的是当前中页面中已经抓取到的资源,如页面上已经下载的样式、脚本、图片等。关闭 浏览器标签 页面,内存中的缓存也就被释放了。内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">
)下载的资源。众所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。Disk Cache
:存储在硬盘中的缓存,读取速度慢点。绝大部分的缓存都来自 Disk Cache。Push Cache
:推送缓存是 HTTP/2 中的内容。当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
缓存策略
浏览器先去查浏览器缓存,没有再去请求服务器。都是通过设置http-header来实现的
-
强缓存:通过设置
Expires
过期时间,和Cache-Control
控制缓存的方式,指定缓存最大存活时间等,返回200
状态码。 -
协商缓存:设置
Last-Modified
最近一次修改时间,和Etag
资源生成的唯一标识符。浏览器再次请求资源将时间传给服务器,若资源未改变,返回304
状态码。
从输入URL到渲染发生了什么
- URL地址解析:判断输入的是一个合法的URL还是一个待搜索的关键词,并且根据你输入的内容进行自动完成、字符编码等操作
- DNS解析域名:DNS是域名和系统相互映射的一个分布式数据库,解析器和域名服务器组成,通过域名可以找到相对应的ip地址。因为ip地址很难记住,所以用户输入域名,而ip地址才是真正的表示网站的位置,所以要把域名转化为ip地址。域名解析就是要获取到域名对应的ip地址的过程。
- 先找浏览器有没有DNS缓存(之前有访问记录),如果有则返回ip
- 如果没有,则寻找本地的host文件,看看有没有域名记录,如果有则返回ip
- 都没有,会向本地DNS服务器请求,如果还没有,继续向上级DNS服务器请求,直到根服务器
- 建立TCP连接:三次握手
- 第一次握手:客户端发送
SYN
数据包给服务端 - 第二次握手:服务端收到客户端的数据包,返回
SYN/ACK
数据包给客户端 - 第三次握手:客户端收到服务端的返回后,发送
ACK
数据包给服务端
- 第一次握手:客户端发送
- 发起http请求:拿到返回数据后会开始对资源进行解析。
- 接下面的浏览器渲染原理
浏览器渲染原理
-
解析html:生成DOM树和CSSOM树。浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
-
计算样式:让每一个DOM节点都得到最终样式。这一过程很多预设值会变成绝对值,比如
red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
得到带样式的DOM树。 -
布局:计算出每一个DOM节点的
几何信息
,布局树和DOM树不一样,不是一一对应。比如display:none
的节点没有几何信息,因此不会生成到布局树; -
分层:为了提高后续渲染效率,根据策略把页面分成图层,产生绘制指令。分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
-
绘制:通过GPU把每个层单独进行绘制,单独绘制后产生合成指令交给分块。完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
-
分块:得到很多小块。
-
光栅化:对每个小块进行光栅化,优先光栅化靠近屏幕的小块。
-
画:把光栅化后的小块画出来。
绘制后开始合成图层,最终渲染到页面上。
更改元素的几何属性会触发重排
更改元素的绘制属性变化会触发重绘
为什么 transform 的效率高?
因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段
由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。
断开TCP链接为什么要四次挥手?
- 完成数据传输后客户端会发起断开链接的请求,向服务器发送FIN,进入等待阶段。
- 服务器收到后得知要断开链接,先返回ACK给客户端,告诉客户端我知道了。
- 服务器确认可以断开链接时,再发送一个FIN给客户端。
- 客户端收到后,发送ACK给服务器。
浏览器事件循环
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
HTTPS协议中间人攻击是什么?
指攻击者通过与客户端和服务器同时建立连接,作为客户端和服务器的桥梁,处理双方的数据,整个会话期间的内容几乎是完全被攻击者控制的。攻击者可以拦截双方的会话并且插入新的数据内容。
中间人攻击的过程:
- 服务器向客户端发送公钥。
- 攻击者截获公钥,保留在自己手上,然后攻击者自己生成一个伪造的公钥,发给客户端。
- 客户端收到伪造的公钥后,假公钥加密随机码key,发给服务器。
- 攻击者获得加密的随机码key,用对应的假私钥解密获得随机码key。
- 同时生成假的加密随机码key,发给服务器。
- 服务器用私钥解密获得假随机码key。
- 服务器用假随机码key加密传输信息。
为了防止中间人攻击,可以采取以下措施:
- 使用可信任的证书:确保服务器使用的数字证书是由可信任的证书颁发机构签发的,以防止攻击者伪造证书。
- 强制使用HTTPS:通过强制使用HTTPS来保护通信内容,避免使用不安全的HTTP协议。
- 安全验证机制:使用双向认证,要求服务器验证客户端的身份,以确保通信双方的身份合法。
- 公钥固定:在客户端中预先存储服务器的公钥,以防止攻击者替换公钥。
- 定期更换证书:定期更换服务器的数字证书,以减少攻击者伪造证书的机会。
如何实现浏览器内多个标签页之间的通信?
-
localStorage
-
postMessage
:如果能够获得对应标签页的引用,就可以使用 postMessage 方法,进行通信。 -
websocket
:因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。 -
ShareWorker
:shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。
点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
- 点击刷新按钮或者按 F5:浏览器直接对本地的缓存文件过期,但是会带上 If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是
304
,也有可能是200
。 - 用户按 Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是
200
。 - 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
XSS
和 CSRF
XSS
:跨站脚本注入。CSRF
:跨站请求伪造。攻击者冒充用户发起恶意请求。利用用户登录态,获取cookie,在不登出的情况下访问危险网站。
CommonJS、AMD、CMD、ESModules
CommonJS:node.js提出的标准
- 每个文件都是一个模块
- 每个模块都有单独的作用域
- 约定使用module.exports导出,require导入
- 以同步的模式加载模块,不适用浏览器端
AMD:异步模块规范,适合浏览器端 require.js
- 约定使用
define
(模块名字,数组:用来声明模块依赖项,函数:参数与前面依赖项一一对应) 函数来定义
,函数有三个参数
define('module1', ['jquery', './module2'], function($, module2){
return {}
})
require
函数来加载
函数- AMD需要使用大量 define require函数代码,使用起来相对比较复杂
- 模块划分比较细的话,在同一个JS文件下请求次数会比较多,页面效率低下
- 只能是模块化路上的一个过渡
CMD:和AMD同期淘宝推出的标准 Sea.js
- 类似CommonJS
- 使用上和require.js差不多
ES Modules
特性
- 自动采用严格模式,等同于‘use strict’
- 每个模块都有独立的作用域
- ESM是通过CORS(跨域)来请求外部JS模块的,请求的地址需要支持CORS
- ESM模块会延迟执行脚本,等同于加了defer,所以会出现console.log执行顺序和引入顺序是不同的情况
导出和导入
import时需要填写完整路径,例如.js扩展名不可省略
动态导入模块
// 传入导入模块的路径,返回一个promise,会自动执行then当中的回调函数
import('./modules.js').then(function(module) {
console.log(module)
})
import 和 require
- import是ESM的语法。
异步
加载,不会阻塞后续代码执行。import
语句会在代码解析和编译阶段执行
- require是CommonJs语法。
同步
加载的,会阻塞后续代码执行。模块的加载和执行都是在程序运行时
发生。
Grunt、 Gulp 自动化构建
都是微内核。
- Grunt:插件生态非常完善,可以自动化的完成任何想要做的事情。但工作过程是基于临时文件实现的,所以构建速度较慢。例如对Sass文件进行构建,先对文件进行编译操作,之后添加自有属性的前缀,再压缩代码。每一步都会有磁盘读写操作。每一步执行后写入临时文件,下一步再读取。处理环节越多,文件读写次数越多。大型项目文件多,构建自然很慢。
- Gulp:遵循CommonJS模块规则。高效、易用,插件也很完善。工作过程基于node的流式操作和内存缓存实现。对文件的处理环节都是在内存中完成的,相对于磁盘读写自然快很多。默认支持同时执行多个任务,效率自然大大提高。使用方式相比于Grunt更加直观易懂。
核心思想是“流”,三个核心概念:读取流,转换流,写入流*。希望实现一个构建管道的概念,这样的话在后续扩展插件有一个统一的方式。它通过将数据流传递到各种插件中来进行转换。 通过Gulp提供的src方法创建读取流,借助插件提供的转换流实现加工,再通过Gulp提供的dest方法创建写入流。
Rollup
遵循ESM规则。支持多种输出格式,包括CommonJS、AMD、UMD、ES6等。仅仅是一个ESM的打包器,充分利用ESM各种特性高效打包,比webpack小巧的多。内置tree-shaking优化,自动过滤掉未引用的代码,减小打包后文件的体积。Rollup 的目标是产生更小、更快、更高效的代码,因此在构建 js 库时非常有用。
Rollup并没有webpack中的HMR这种热更新等高级的功能,但是可以使用插件去扩展功能,插件是唯一的扩展方式 。
Vite
- 更快的开发服务器冷启动:基于浏览器原生 ES 模块化,启动前使用Esbuild预构建。从而实现“按需编译、按需加载”的特性。可以快速响应页面请求,无需等待整个项目打包完成
- 热更新:使用了浏览器原生的模块热更新技术,只请求和更新变化的模块,而不是整个页面。
- 真正的按需编译:基于浏览器原生 ES 模块化,只编译当前文件及其依赖,而不是整个项目。这在大型项目中可以显著提高构建速度。
- 内置Rollup,还支持各种模块规范,如 CommonJS、ESM、AMD
- 支持 Rollup 的插件系统。可以扩展和定制 Vite 的功能。
vite和webpack的区别
-
构建原理:
webpack是通过入口文件递归依赖构建。
vite是用浏览器原生支持的 ES Module(ESM)特性,以模块为单位进行开发。基于esbulid预构建依赖 -
打包速度vite大于webpack:
Webpack 的打包速度相对较慢,尤其在大型项目中,因为它需要对整个项目进行扫描和分析,而且还需要通过各种插件和加载器来实现各种功能,因此构建时间往往会比较长。Vite 的打包速度非常快,因为它不需要对整个项目进行扫描和分析,在浏览器中使用原生 ES 模块的方式加载文件,开发模式下,使用可以
<script type='module'>
加载模块,直接在浏览器中运行未经构建的源码,而无需提前进行打包。因此构建时间往往比 Webpack 快数倍。 -
热更新:
webpack:模块以及模块依赖的模块需重新编译
vite:浏览器重新请求该模块即可,它只会重新加载变化的模块,而不是整个页面。 -
webpack的生态优于vite: 插件多 适合大项目
webpack
- 模块打包器,支持 CommonJS、AMD、ES6 等多种模块化规范。将零散的模块代码,打包到一个或多个js文件当中,bundle.js是webpack打包产物是一个立即执行函数。默认打包后默认放在网站根目录下,可以设
publicPath
网站根目录路径,如publicPath: 'dist/'
。 - 提供加载器
loader
和插件plugin
来处理各种资源文件(js,css,图片等)。 - 代码拆分
Code Splitting
,将代码文件拆分成较小的文件,其中每个文件可能包含多个模块。这样做可以在初始加载时减少数据量,但仍然需要一次性加载所需的文件。 - 开箱即用的方案:
dev server
,HMR
,sourceMap
等。
webpack工作原理
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数
初始化 Compiler 对象
,加载所有配置的插件,执行对象的 run 方法开始执行编译 - 确定入口:webpack根据配置
找到所有的入口文件
。 - 依赖分析:根据资源入口里出现的import或require语句来解析推断出来资源依赖的模块,分别
递归解析每个模块对应的依赖
,最后形成一个所有依赖关系的依赖树 - 完成模块编译:
webpack递归依赖树
,找到每个节点对应的依赖文件,通过配置文件中的rules属性找到对应的加载器loader,将文件进行处理转换转成js。 - 生成 Chunk::将模块组装成一个或多个 Chunk,把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 生成bundle:
将生成的 Chunk 输出为静态资源文件也就是生成bundle
。可以根据配置生成单个文件或多个文件。 - 生成bundle过程插件优化:生成 Bundle 的过程中会通过pulugin优化,包括代码压缩、去重、提取公共模块等。这些优化措施旨在减小文件体积、提高加载速度。
- 输出:将最终生成的 Bundle 输出到指定的目录中。输出的文件名和路径等信息也可以在配置文件中进行设置。从而实现整个项目的打包。
loader机制是webpack的核心
loader:模块加载器
对于打包过程中有环境兼容问题的,可以通过loader进行编译转换。
编译转化类:
- css-loader:样式加载器,把css文件打包成js模块
- style-loader:以 style 标签的形式把打包好的css追加到页面里
文件操作类:
- file-loader:图片、字体等资源文件加载器,先把文件拷贝到输出目录,bundle.js将访问路径向外导出(输出目录的路径作为当前模块返回值返回)。
- url-loader:可以将文件直接转化成
Data URL
形式。 不需要拷贝文件到输出目录,适合体积小的文件,否则打包结果会大。可设置limit,限制文件大小内的使用url-loader,超过则使用file-loader。
优化点:
- 小文件可以使用url-loader,转化成Data URLs,减少请求次数
- 大文件可以使用file-loader,单独提取存放,减少打包体积,提高加载速度
代码检查类:
- eslint-loader:代码校验加载
- babel-loader:用于处理es新特性的兼容,需要配置插件preset-env
Data URLs:用URL直接代表文件
data:text/html;charset=UTF-8,<h1>html content</h1>
loader工作原理:管道特性 处理资源加载
- 资源文件输入到输出的一个转换
- 管道特性:同一个资源依次使用多个loader
loader都需要去导出一个函数,函数就是对我们加载资源的一个处理过程。
输入:资源文件内容
过程:可以直接入其他loader,依次处理
输出:加工后的结果,必须是js代码。
// markdown-loader.js
const marked = require('marked');
module.exports = source => {
const html = marked(source);
// return `module.exports = ${JSON.stringify(html)}`
return `export default ${JSON.stringify(html)}`
}
plugin:解决除了资源加载以外的自动化工作
- clean-webpack-plugin:自动清除打包前的dist目录,即清除上一次打包结果。
- copy-webpack-plugin:拷贝不需要打包的静态资源文件到dist目录。
- terser-webpack-plugin:压缩打包输出的代码,减小体积。
- html-webpack-plugin:自动生成打包结果bundle.js的html,放到dist目录下。确保路径引用正常。 html中bundler引用是注入进去的,不需要手动硬编码。
- definePlugin(自有):为代码注入全局成员的,比如定义环境变量
plugin工作原理:钩子机制,往webpack生命周期中的钩子函数上挂载任务函数
plugin机制其实是钩子机制
,为了便于webpackkuozhan,给每一个事件都埋下了一个钩子,开发插件时可以根据不同的节点,挂载不同任务
,来扩展webpack的能力
- 插件必须是一个函数,或者包含apply()方法的一个对象。一般
把插件定义一个类,类里定义apply方法
- 使用:通过定义的类,构建一个实例去使用。
// 清除bundle.js里没用的注释,会在webpack启动时自动调用
// compiler:webpack核心对象,包含此次构建的配置信息,通过这个对象去注册钩子函数
// emit钩子:即将往输出目录输出文件时执行。
class MyPlugin {
apply(compiler) {
console.log('MyPlugin 启动');
// 通过compiler里的hooks属性,来访问emit钩子,通过tap()方法注册钩子函数
// tap接收两个参数,tap(插件名称,挂载到钩子上的函数)
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation:可以理解为此次打包的上下文,打包的结果都会放到这个对象中
// assets:获取即将写入目录的资源文件信息
for(const name in compilation.assets) {
if(name.endsWith('.js')) {
const contents = compilation.assets[name].source();
const withoutComments = contents.repalce(/\/\*\*+\*\//g, '');
compilation.assets[name] = {
source: ()=>withoutComments,
size: ()=>withoutComments.length
}
}
}
})
}
}
webpack dev server: 集成了自动编译,和自动刷新浏览器,将打包结果暂时存放在内存中,没有写入dist,减少磁盘读写次数,提升构建效率
webpack自动编译打包:watch工作模式
页面自动刷新:Browsersync插件自动监听编译,自动刷新浏览器
- 自动编译打包
- 自动刷新浏览器:问题,导致页面 状态丢失
- 支持配置代理服务
- 页面不刷新,模块也能更新,使用HMR 模块热更新
source-map:便于调试代码,有12种模式
- 开发模式建议选择:cheap-module-eval-source-map
- 生产模式建议选择:不选择source-map,会暴露源代码,其次可以选择nosource-source-map
HMR(Hot Module Replacement)模块热更新,已集成在web dev server中
运行webpack-dev-serve --hot
需要手动处理模块热更新的逻辑,不能开箱即用。
HMR可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 web dev server
与客户端之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向客户端推送更新,并带上构建时的 hash
,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
webpack生产环境下优化
- definePlugin(自有):为代码注入全局成员的,比如定义环境变量,定义base url
- tree-shaking:去掉未引用的代码,生产模式下自动开启
tree-shaking
在 Webpack 中启用 Tree shaking 需要满足以下条件:
- 使用 ESM 模块(即使用
import
和export
)。 - 配置 Webpack 时,确保在
mode
选项中设置为'production'
,以开启相关优化。 - 在
package.json
文件中,确保"sideEffects"
字段设置好有副作用的文件,不会被shaking(没有副作用:一个函数会、或者可能会对函数外部变量产生影响的行为。)
一些代码或模块可能会有副作用,例如修改全局变量、产生网络请求等。在这种情况下,确保正确配置 "sideEffects"
字段,以避免误删有副作用的代码。
// 所有文件都有副作用,全都不可 tree-shaking
{
"sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking,即告知 webpack,它可以安全地删除未用到的 export。
{
"sideEffects": false
}
// 除了数组中包含的文件外有副作用,所有其他文件都可以 tree-shaking,但会保留符合数组中条件的文件
{
"sideEffects": [
"*.css",
"*.less"
]
不是配置项,是一组功能搭配使用后的效果
- usedExports:只导出外部使用的成员,负责标记「枯树叶」。
- concatenateModules:合并输出模块函数(也叫Scope Hoisting作用域提升),尽可能将所有模块合并到一个函数中,既提升了输出效率,又减小了代码体积。
- minimize:开启代码压缩功能,负责把枯树叶「摇」下来。
// 集中配置webpack优化功能
optimization: {
usedExports: true, // 只导出外部使用的成员, 负责标记「枯树叶」
concatenateModules: true, // 合并输出模块函数 Scope Hoisting,尽可能将
minimize: true, // 开启代码压缩功能, 负责把枯树叶「摇」下来
}
tree-shaking&babel-loader不生效问题:
最新版本的babel-loader并不会导致tree-shaking失效, 失效原因是以下:
tree-shaking的前提是使用ES Modules
,然而webpack在打包前会根据配置把不同的资源模块交给loader处理,最后再将所有loader处理后的结果,打包到一起。那么为了转化ES新特性,很多时候会选择引入babel-loader,老版本babel-loader会将ES Modules转化成Common JS,可以通过配置属性module:false不开启ESM转化的插件来解决
code splitting:代码拆分
可以按需打包, 不用担心所有文件打包到一起,包体积会大的问题;
先打包应用初次运行时必须加载的模块,然后其他模块会单独存放,等到应用实际需要这个模块时,再异步加载;
从而实现增量加载或叫渐进式加载;
不用担心文件太碎,或文件太大,这两个极端的问题。
- 多入口打包:适用多页应用程序,一个页面对应一个打包入口。
- 动态导入:单页应用,模块会被自动分包
webpack提升构建速度?
构建速度和打包体积是有关系的,体积小了,打包速度也快了。
- 升级webpack版本
- 配置持久化缓存,
cache-loader
,减少重复构建 tree-shaking
,去掉未引用的代码- 合理的使用loader、plugin
- 使用
include
和exclude
选项来排除不必要的目录,只对需要处理的目录使用对应的Loader - 开启多线程打包,
thread-loader
,happyPack
,加速编译 - 代码分割,
code spliting
,初次运行只打包必须加载的模块,其他模块按需加载 - 将一些不经常变动的库使用
DllPlugin
,资源打到一起,并通过DLLReferencePlugin
进行预编译 mode
分开发环境 和 生产环境两套不同的配置,生产环境可以不开启source-map
webpack减小打包体积?
tree-shaking
,去掉未引用的代码- 开启代码压缩
minimize
,使用terser-webpack-plugin
- 减小模块体积,图片提前压缩
- 分割代码,只加载用户需要的部分,
splitChunks
- 使用 CDN 引入三方库
- 减小 polyfill 的体积,
core-js
的按需引入特性。
解释一下Webpack的文件指纹(file fingerprint)和缓存(caching)机制
- 文件指纹:是在构建生成静态资源过程中为文件生成唯一的标识符。在 Webpack 中,文件指纹通常用于生成输出文件的名称,在输出文件名中引入文件指纹,可以有效解决浏览器缓存问题,确保用户在访问网站时能够获取到最新的文件版本。
- 缓存:缓存机制是指浏览器在加载页面时,会将静态资源(如 JS、CSS、图片等)保存在本地,以便下次加载相同资源时可以直接使用缓存副本,从而提高网页加载速度。缓存机制分为强缓存和协商缓存两种方式。
Webpack的Resolve模块解析是什么?请解释resolve.modules、resolve.alias和resolve.extensions的作用。
Webpack的Resolve模块解析是用于解析模块路径的配置选项。它可以帮助Webpack正确地确定模块的位置。
resolve.modules
用于指定模块的搜索路径。当Webpack在解析导入语句时,它会按照指定的顺序依次查找这些路径来确定模块的位置。默认情况下,Webpack会在当前工作目录和node_modules文件夹中查找。resolve.alias
用于创建模块的路径别名。通过配置别名,可以让Webpack在导入模块时使用更简短的路径。这对于减少代码中的冗余路径非常有用。resolve.extensions
用于指定可以省略的文件扩展名。当导入模块时没有指定文件扩展名时,Webpack会按照指定的顺序依次尝试添加扩展名来解析模块。这样可以让我们在导入模块时省略掉繁琐的扩展名,提高开发效率。
防抖&节流
防抖
只执行最后一次。操作时不执行,最后一次操作结束后等待n秒后,再执行。
- 输入框输入实时校验:连续的输入事件的最后⼀次执行。
- 表单提交:只执行最后一次。
- 窗口大小变化:为了减少重排重绘
function debounce(fn, delay) {
let timer;
return function() {
// 但凡有操作进来,都清楚之前的,重新定时
if(timer) clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this, arguments);
},delay)
}
}
节流
有规律的执行。到时间必须执行一次。如:
- 滚动加载
- 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
- 缩放场景:监控浏览器 resize
- 动画场景:避免短时间内多次触发动画引起性能问题
function throttle(fn, wait) {
let time;
return function {
if(time) return;
time = setTimeout(()=>{
fn.apply(this, arguments);
time = null;
}, wait)
}
}
或
function throttle(fn, wait) {
let old = 0;
return function() {
let new = new Date().valueOf();
if(new - old >= wait) {
fn.apply(this, arguments);
old = new;
}
}
}
defer
和 async
都是立即下载脚本,执行时机和顺序有差异,同一个script上同时有defer 和 async,会按async执行
- defer:立即下载脚本,等HTML解析完成后,再按引入顺序执行。依赖文档结构可使用
- async:立即下载脚本,下载完成后立即执行(可能中断HTML解析),先后顺序不一定。不依赖文档结构可使用
for ... in
和 for ... of
- for in:遍历key,查找原型链,用于普通对象
- for of:遍历value,不查找原型链,用于数组,类数组,字符串,Set,Map
apply()
、call()
和 bind()
都是用来改变函数执行上下文,改变 this 指向 apply 和 call 都会立即执行,bind返回一个函数,需要手动调用
哪些情况会导致内存泄露
- 引用却未使用的对象
- 闭包
- 全局对象
- 循环引用
- 定时器和事件监听器
数字和字符串相互转换
TS
type
和 interface
interface
- 主要用来定义对象结构,包含属性、方法、继承等。
- 可以多次声明同名接口,会自动合并同名接口
- 可以继承,使用
extends
- 可以间接使用
in
关键字遍历对象的键(KeyOf 操作)
type
- 主要用来定义其他类型,基本类型、联合类型
|
、交叉类型&
、交集类型,差集类型。 - 不可多次声明,同名type会报错
- 不可继承,但可以通过交叉类型
&
实现类似继承效果 - 可以直接使用
in
关键字遍历对象的键(KeyOf 操作)
any
unknow
never
区别
any
类型表示任何类型,即 TypeScript 编译器对该值不进行类型检查,慎用,类型安全问题。
unknown
类型表示未知类型,需要显式的类型检查或类型断言来使用。
let value: unknown = 42;
// 类型检查或类型断言
if (typeof value === 'number') {
let result: number = value + 10;
}
// 类型断言
let result: number = (value as number) + 10;
never
类型表示永远不存在的类型。用于标识不会正常返回值的函数和不可到达的代码块。如函数抛出异常、死循环等情况,这些函数不会正常返回值。
function throwError(message: string): never {
throw new Error(message);
}
设计模式
单例模式:
保证一个类只有一个实例,并提供一个访问它的全局访问点。 如:模态框组件,Vuex全局的 Store
工厂模式:
用来创建对象,根据不同的参数返回不同的对象实例。 根据抽象程度的不同可以分为:简单工厂、工厂方法、抽象工厂。 如:工厂里有很多种颜色的鞋子,只需要告知要那种颜色鞋子就可以输出,不需要关注输出过程。
装饰器模式:
在不改变对象原型的基础上,对其进行包装扩展。 如:vue中的计算属性computed,还有高阶组件。
策略模式:
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。
观察者/发布订阅模式:
定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它的对象都会得到通知。 如:Vue2响应式源码,EventBus。
React
Redux
redux 的三个原则:单一数据源、状态是只读的、使用纯函数来执行状态更改。
- store:Redux 应用只有一个单一的 store,管理的state的容器,维持应用的
state
; - action:是一个普通的js对象,强制使用action更新,清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的。用来描述状态更改。
- reducer:是一个纯函数,将传入的
state
和action
结合起来生成一个新的state
,用来更新状态。 - middleware:允许在
action
和reducer
之间添加额外的逻辑,用于处理异步操作和副作用。
React 中 setState 什么时候是同步的,什么时候是异步的?
- 由 React 控制的事件处理程序,以及生命周期函数调用setState 是异步更新 state。
- React 控制之外的事件中调用 setState 是同步更新的。比如原生js 绑定的事件setTimeout/setInterval 等。
即时通讯的方法
- websocket
- setInterval:定时任务询问
- http2持续连接
跨标签页通信
- indexDB
- localstorage
性能优化(输入URL到渲染的每一步都可以进行优化)
业务通用优化手段
- vue3中可以使用
defineAsyncComponent
工具来异步加载组件。 - 优化老旧代码,将callback()改成
async/await
。 - 通过控制台去看接口响应时间,尽量控制在300ms以内,若无法缩短,考虑是否可以使用异步请求。
- 手动替换体积大的三方库,如moment.js 替换成 day.js。
- 对图片进行压缩,选择适合的格式,使用懒加载。
- 静态文件采用
CDN
。使用DNS预解析CDN域名<link rel='dns-prefetc' href='cdn.com'
- 合理使用防抖和节流,避免重排重绘。
- 页面销毁前清除定时器,防止内存泄露。
- 使用
performance
工具来查看性能和内存。 - 使用
HTTP
缓存,如强缓存、协商缓存
小程序优化
- 合理的分包subpackages,可以根据用户行为预加载分包,提前加载首屏资源,提高页面加载速度。
- 不要频繁调用微信内置api,比如getSystemInfo,首页瀑布流中每个商品组件都调用了一次getSystemInfo,影响性能。
- 启用初始渲染缓存,让逻辑层和视图层并行运行,直接把视图层的data初始值展示出来,可以预先展示一个骨架屏,可以展示固定不变的模块。
构建工具方面
- 抽取公共包:使用
DLLPlugin
,将三方库(day.js等)和不会改变的资源打到一起,并通过DLLReferencePlugin
进行预编译; - code-spliting:代码分割,配置
splitChunks
将代码分块,只加载必要部分 - webpack5 模块联邦,可抽取公共代码,进行共享,使业务代码打包体积减小,加载更快。