大家好,小编来为大家解答以下问题,今日头条浏览器前进和后退在哪设置,今日头条向右滑翻页怎么关闭,今天让我们一起来看看吧!
三栏布局
<style>
* {
padding: 0;
margin: 0;
}
.left {
width: 100px;
height: 100px;
background-color: orange;
float: left;
}
.right {
width: 200px;
height: 100px;
background-color: green;
float: right;
}
.center {
height: 100px;
background-color: black;
overflow: hidden;
}
</style>
<div class="left"></div>
<div class="right"></div>
<div class="center"></div>
var、let、const的区别
- 变量提升:var会有变量提升;let、const没有,变量的使用必须在声明之后
- 全局属性:var变量如果未声明就直接赋值,则会被当做全局变量;而使用let、const不会
- 指针指向:var、let在赋值后可以改变变量值,const不可以改变,const指向内存地址的指针不会改变,但如果使用const定义的变量是一个引用类型,那么就无法保证const变量的值是否改变。因为const实际上指向的是引用类型在堆内存中的地址,而不直接指向变量的值Python解释器的安装步骤。
- 暂时性死区:let、const存在暂时性死区,在变量声明之前,该变量都是不可用的;而var可以使用
- 块级作用域:let、const具有块级作用域,var没有。块级作用域解决的两个问题:1. 避免了内部变量可能会覆盖外部变量的可能性;2. 防止循环计数变量进行变量提升,泄露为全局变量
- 初始值设置:const在声明时必须指定初始值,var、let不需要
- 重复声明:var变量可以重复声明;const、let不可以重复声明
如何判断一个空对象
- 使用for … in
let obj1 = {}
function isObj(obj) {
for (const key in obj1) {
return true
}
return false
}
console.log(isObj(obj1));
- 使用JSON.stringify()
let obj1 = {}
let res = JSON.stringify(obj1) === "{}"
console.log(res);
- 使用Object.keys()
let obj1 = {}
let res = Object.keys(obj1).length === 0
console.log(res);
每隔一秒打印一行
// 需要实现的函数
function repeat(func, times, wait) {
return async function (...args) {
for (let i = 0; i < times; i++) {
await new Promise((resolve, reject) => {
setTimeout(() => {
func.apply(this, args)
resolve()
}, wait);
})
}
}
}
// 使下面调用代码能正常工作
const repeatFunc = repeat(console.log, 4, 1000);
repeatFunc("hellworld");//会输出4次 helloworld, 每次间隔1秒
== 和 ===的区别
- == 在进行比较时如果两边的类型相同,直接比较值的大小,如果两边类型不同,则先转换为相同类型,在比较大小。
- ===在进行比较时,如果两边类型不同,则直接返回false,类型相同时才比较大小。
箭头函数和普通函数的区别
- 箭头函数都是匿名函数;普通函数可以是具名函数也可以是匿名函数
- 箭头函数不能被当作构造函数;普通函数可以并创建实例
- 箭头函数没有this,它的this需要捕获上下文中的this,一旦箭头函数确定了this,那么就无法改变,即使使用call/apply/bind;而普通函数的this指向调用它的对象
- 箭头函数的写法简洁
- 箭头函数没有prototype属性
- 箭头函数没有arguments
Symbol
symbol是为了解决全局变量命名冲突提出的,代表创建以后独一无二且不可变的数据类型
setState原理
setState的作用就是改变组件自身的状态值。在调用setState时,不会马上就进行状态的更新,而是调用内部的enqueueState方法,将需要改变的状态放到状态队列中。然后调用enqueueUpdate方法来实现更新,所以state的真正更新其实是在enqueueUpdate方法中,enqueueUpdate方法中有一个batchingStrategy锁管理器,在这个锁管理器中有一个isBatchingUpdates变量,该变量起到一个锁的作用,默认值为false,代表当前并没有状态更新操作。当有状态需要更新时,首先要查询这个属性是否为false,如果为false,则立即更新,并将值改为true,代表当前正在进行更新状态操作,这样如果在更新时还要修改其它state的状态,发现isBatchingUpdates的值为true,则加入到队列中等待更新。
总的来说,setState更像是一个更新请求,它不会立即更新state,而是会查询isBatchingUpdates属性是否为false,如果为false则立即更新;如果为true则等待更新。这样setState就实现了批量更新,可以将多次setState合并为一次知行,避免了多次调用render渲染页面,提升了效率。若同时更新一个状态的多次,那么setState不会累加知行,而是只执行最后一次。并且,setState的更新可能是异步的也可能是同步的,在原生方法中,比如在某些React控制不到的地方:addEventListener、setTimeout、setInerval就是同步的。而在React生命周期或合成事件中,就是异步的。因为setState有可能是异步的,所以最好不要在执行setState之后立即调用更新的state,因为此时可能还没更新好。可以在setState的回调函数,或componentDidUpdate函数中使用更新后的state。
为什么会有白屏
- 等待HTML文件返回
- 浏览器兼容问题
- js未完成加载
如何减少首屏和白屏加载时间
- 加速或减少HTTP请求损耗:使用CDN加载公用库,使用强换存与协商缓存,小图片使用base64代替,使用get请求代替post请求。
- 延迟加载:非首屏资源延迟加载,使用懒加载,webpack按需加载等。
- 减少请求内容的体积:通过JS、CSS文件压缩,减少cookie大小。
- 优化用户等待体验:白屏使用进度条、loading图等。
- 使用HTTP2。
- 使用Defer加载JS。尽量把css文件放在头部,js文件放在底部,避免堵塞渲染。
计算首屏时间
通过document.addEventListener对DOMContentLoad进行监听或者performance计算。
如何优化首屏加载
- webpack代码分割,将公共代码抽取,避免重复
- 按需加载第三方依赖
- 路由懒加载、图片懒加载、组件动态加载
- 避免使用相似的依赖包,例如less和scss,Element UI和antd
什么是浏览器缓存
浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。
浏览器缓存的优点
- 减少了冗余的数据传输
- 减少了服务器压力,提升了网站性能
- 加快了页面加载的速度
协商缓存与强缓存
强缓存:如果缓存资源有效,就不会向服务器发送请求,直接从缓存中读取资源,返回200状态码。
强缓存可以通过两种方式来设置:分别是htpp头信息中的Expires属性和Cache-Control属性。
- 服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器时间。使用服务器时间有这个坏处:客户端与服务器的时间可能会不一样并且用户可以对客户端时间进行修改,影响缓存命中的结果。
- Cache-Control是HTTP1.1中提出的,提供了对资源更精确的控制。一般使用max-age设置缓存最大的有效期;no-store设置不进行缓存,每次请求都向服务器发起请求;no-cache设置请求前先向服务器确认返回的资源是否发生变化,如果未发生变化,则直接使用本地缓存。
Cache-Control的优先级比Expires高。
协商缓存:如果命中了强制缓存,那我们就不需要再向服务器发起请求,直接使用缓存资源。若没有命中则使用协商缓存。
命中协商缓存的条件有两个:
- max-age过期
- 使用no-cache
协商缓存策略:先向服务器发送一个请求,如果资源没有发生修改,则返回一个304状态,让浏览器使用本地缓存,如果资源发生了修改,则返回修改后的资源。
协商缓存通过两种方式设置,分别是http头信息中的Etag和Last-Modified:
- 服务器通过响应头来添加Last-Modified表示资源最后一次修改的时间。当浏览器下一次发起请求时,会在请求头中加一个if-Modified-Since属性,表示上次返回资源时的Last-Modified属性。当服务器收到请求后,通过if-Modified-Since属性与资源最后一次修改时间进行对比,以此来判断是否做了修改。
- 因为使用Last-Modified可能会不准确,http提供了Etag属性。当服务器返回资源时,会在头信息中添加Etag属性,这个属性是资源生成的唯一标识符,当资源发生改变时,Etag也会发生变化。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变。
总结:
强缓存的优先级高于协商缓存。强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求,判断缓存是否命中。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则服务器返回最新的资源给浏览器。
对浏览器的缓存机制的理解
浏览器缓存全过程:
- 浏览器第一次加载资源时,服务器返回200,浏览器从服务器下载该资源文件,并缓存资源文件与response header,以供下次加载时对比使用
- 下次加载资源时,由于强制缓存优先级较高,先比较当前时间与上次返回200时的时间差,如果没有超过cache-control(控制HTTP缓存)设置的max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用Expires头判断是否过期
- 如果资源已过期,则表明强缓存没有被命中,则开始协商缓存,向服务器发送带有if-None-Match(Etag)和if-Modified-Since(Last-Modified)的请求
- 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200
- 如果服务器收到的请求没有Etag值,则将if-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200
很多网站的资源后面都加了版本号,这么做是因为:每次升级了JS或CSS文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的JS或CSS文件,以保证用户能够及时获得网站的最新更新。
点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
- 点击刷新按钮或者按F5:浏览器直接对本地缓存的文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务,返回结果可能是 304,也有可能是 200。
- 按Ctrl+F5 (强制刷新):浏览器不仅对本地缓存文件过期,还不会带上If-Modifed-Since,If-None-Match,相当于之前没有请求过,返回200状态。
- 地址栏回车:浏览器发起请求,按正常的强缓存 -> 协商缓存 -> 请求资源的过程执行。
不同标签页如何进行通信
- Cookie:可以使用Cookie+setInterval的方法,将想要传递的数据存储到Cookie中,然后使用setInterval定期读取Cookie信息,可随时获取需要得到的信息。
- LocalStorage:LocalStorage是浏览器多个标签共用的存储空间,LocalStorage有一个onstorage事件,会针对非当前页面对LocalStorage进行修改时才会触发。利用这个特性,我们可以在发送消息时将消息写入到某个LocalStorage中,这其它标签页就可以通过onstorage事件收到通知了。
- WebSocket:WebSocket允许服务器向客户端推送消息,那么服务器就可以当这个中间人,在标签页向服务器发送数据后,服务器可以将这些数据推送给其它标签页。
跨域
跨域是指在浏览器对其他网站发起请求时,由于受到同源策略的限制,服务器返回的结果被浏览器限制了,导致无法得到请求结果。
跨域解决方案
- CORS:CORS就是在发起请求时,它使用额外的一个HTTP请求头来告诉浏览器,让运行在一个Origin上web应用允许访问另一个源中的指定资源。当一个请求从与该请求本身所在的协议、域名或端口号不同的域中请求资源时,就会发起一个跨域请求。
- CORS分为简单请求和非简单请求
- 简单请求就是:CORS直接发送一个跨域请求,该请求携带一个Origin字段,代表当前请求来自哪个源,服务器会判断请求的域是否在许可范围内,如果在,则返回一个Access-Control-Allow-Origin等信息头和请求的资源,表示请求成功;如果不在许可范围内,则返回一个正常的HTTP响应。浏览器通过返回结果判断是否有Access-Control-Allow-Origin等头部信息,就知道请求结果了。
- 非简单请求:非简单请求就是在正式发起请求之前,浏览器会发送一个预检请求,预检请求的作用是询问服务器当前所在的网页是否允许访问,以及可以使用哪些HTTP请求。
- 预检请求使用的请求方式是OPTIONS,表示这个请求时来询问的,他的头部信息中有Origin字段,表示当前请求来自哪个源;Access-Control-Request-Method,表示需要用到哪些请求方法;Acccess-Control-Request-Headers,表示额外发送的头信息字段,服务器在收到浏览器发送的预检请求后,会判断浏览器的请求是否在许可范围内,如果在则在头信息中返回Access-Control-Allow-Origin字段,浏览器判断头信息中是否存在该字段来判断是否可以进行跨域请求。如果可以进行跨域请求,那么浏览器在每次请求时都会发送一个Origin字段,服务器每次也会返回Access-Control-Allow-Origin字段。
- 因为OPTIONS请求对性能消耗非常大,所以我们应该尽量减少OPTIONS请求,可以使用缓存来减少请求次数。服务器返回时设置一个max-age,代表返回结果可以被缓存多久,在这个缓存的有效期内,再次发送请求就不需要进行预检请求了。
- JSONP:JSONP就是利用标签对跨域没有任何限制,可以对任何资源进行请求来实现跨域的。如果访问的资源中有JS代码,它会在下载后自动执行,
- JSONP的缺点:容易遭受XSS攻击,仅支持GET方法
- Nginx代理:使用反向代理
- nodejs代理:nodejs通过target代理跨域的目标接口,cookieDomainRewrite来修改cookie中的域名,实现当前域的cookie写入。
正向代理与反向代理
- 正向代理:客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
- 反向代理:服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
Cookie
- name:cookie的名称,cookie的名称一旦确认就无法修改了
- value:cookie的值
- domain:设置可以访问此cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
- path:可以访问此cookie的页面路径。比如domain是abc.com,path是“/test”,那么只有“/test”路径下的页面可以读取此cookie,path为“/”,那么这一域名下的cookie都可访问。
- expires/max-age:cookie的超时时间
- size:cookie大小
- http:设置为true表示只有在http请求中才会带上cookie,可以防止XSS攻击
登录设计
Cookie+Session
HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态。
Cookie 是服务器端发送给客户端的一段特殊信息,
这些信息以文本的方式存放在客户端,
客户端每次向服务器端发送请求时都会带上这些特殊信息。
有了 Cookie 之后,服务器端就能够获取到客户端传递过来的信息了,如果需要对信息进行验证,还需要通过 Session。
客户端请求服务端,服务端会为这次请求开辟一块内存空间,
这个便是 Session 对象。
Cookie + Session 实现流程 :
用户首次登录时:
- 用户访问 a.com/pageA,并输入密码登录。
- 服务器验证密码无误后,会创建 SessionId,并将它保存起来。
- 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。
第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:
- 用户访问 a.com/pageB 页面时,会自动带上第一次登录时写入的 Cookie。
- 服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。
- 如果一致,则身份验证成功。
Cookie+Session存在的问题
- 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。
- 如果服务器端是一个集群,为了同步登录态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端维护成本。
- 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击。
- Cookie无法跨域,难以实现单点登录
Token登录
Token 是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。
token的实现流程:
用户首次登录时:
- 用户输入账号密码,并点击登录。
- 服务器端验证账号密码无误,创建 Token。
- 服务器将 Token 返回给客户端,由客户端自由保存。
后续页面访问时:
- 用户访问 a.com/pageB 时,带上第一次登录时获取的 Token。
- 服务器端验证 Token ,有效则身份验证成功。
Token 机制的特点:
- 服务器端不需要存放 Token,所以不会对服务器端造成压力,即使是服务器集群,也不需要增加维护成本。
- Token 可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。
- Token 下发之后,只要在生效时间之内,就一直有效,如果服务器端想收回此 Token 的权限,并不容易。
- Token在跨域后不会存在信息丢失的问题。
- 不会遭受CSRF攻击
Token的生成方式
最常见的 Token 生成方式是使用 JWT(Json Web Token),JWT的本质就是一个字符串,它将用户的信息保存到一个JSON字符串中,然后编码得到一个Token,并且这个Token带有签名信息,在接收后可以校验是否被篡改。
JWT的结构
JWT由Header(JWT头)、Payload(有效载荷)和Signature(签名)组成。
- Header中存储着一些描述信息
- Payload中存放着需要传递的数据,一般会把用户信息数据放在payload中
- Signature:签名部分,确保数据不被篡改
JWT的认证流程如下:
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
- 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
- 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
- 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
- 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
- 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
如何增加JWT的安全性
- 将JWT Token放在请求头中传输,避免网络劫持
- 使用HTTPS传输
- JWT可以使用暴力穷举破解,所以应该定期更换服务器的哈希签名密钥
单点登录
单点登录就是公司在内部搭建一个认证中心,公司下的所有产品都在认证中心进行登录,当一个产品在认证中心登录后,那么该公司的其他产品也会保留这个登录状态,使用其他产品时就不需要在登录了。
第三方登录
第三方账号进行登录,比如抖音可以使用今日头条的帐号登录。
关于子类在父类中重写方法的问题
class Animal {
sayName = () => { throw new Error('你应该自己实现这个方法');
} }
class Monkey extends Animal {
sayName() { console.log('I love coding');
} }
const monkey = new Monkey();
monkey.sayName();
- 父类使用箭头函数,子类使用传统函数:如果使用传统函数声明,那么声明的方法会被放在类的构造函数的prototype属性上,如果在类中使用的箭头函数,那么会给该类的所有的子类都加上该方法。所以这里父类使用箭头函数声明方法时会给子类的每个实例加上了sayName方法,而子类使用普通函数声明的sayName方法被放到了子类的构造函数的prototype属性上,根据JS变量的访问规则,子类的实例对象在调用方法时,会先在子类的实例中查找,如果没有查找到再去构造函数的prototype中查找。这里子类的实例中已经有了父类的sayName方法,所以会直接调用。
Router
在SPA单页面应用中,只存在一个页面,在进行页面跳转时,仅仅只有URL改变,页面不会重新加载,Router就是实现通过URL来渲染对应的页面的。Router就是URL与对应组件的映射关系,而这种映射是单向的,只能由通过URL的改变来改变页面,不能通过页面的改变而改变URL。
React-Router是基于history实现的,总共有两种模式,browserHistory、hashHistory。
browserHistory内部封装了history的pushState和replaceState方法,pushState会跳转到新的url,点击后退就会回到之前的url,而replaceState会覆盖当前url,后退不会回到之前的url,这两个方法只会改变URL,而不会进行页面的更新。还有一个popState方法,会监听当前的url是否发生变化,如果发生变化,就会根据当前url的pathname与匹配route中的path,然后渲染对应的页面。
hashHistory根据hashchange事件来监听url是否发生变化,如果url发生变化后就通过window.location.hash获得hash字段,匹配对应的组件,渲染页面。
<Route>、<Switch>、<Link>、<NavLink>、<Redirect>、excat、<Router>
Link和a标签的区别
两者在DOM中都被渲染为a标签,区别在于在SPA单页面应用中,每次进行路由切换都是在一个HTML文件中进行的,这个HTML文件在页面首次加载时就已经被下载下来了,不需要每次跳转都去额外请求。使用a标签都导致每次进行链接跳转时都重新请求html文档,在某些网速慢的情况下,会造成空白页的情况。而是用link标签会阻止a标签的默认事件,在进行链接跳转时不会重新请求,而是渲染对应的页面。
路由传参的几种方式
- params:刷新页面参数不会消失;参数会在地址栏显示;需要在Route标签中配置参数名;不适用于参数传太多的情况,因为不同浏览器对URL的长度限制不同。首先在Route标签中的oath属性配置参数名,配置方式是
:变量名
,然后再link标签中传入参数
,然后在对应的组件中就可以使用props.match.params.变量名
的形式取出传递的参数了。 - query:刷新页面参数会消失,参数不会在地址栏显示,link的to属性接受一个参数,里面包括
pathname和要传递的参数对象
,可以使用props.query.参数名
的形式取出参数。 - state:刷新页面参数不会消失,参数不会在地址栏显示,link的to属性接受一个参数,里面包括
pathname和要传递的参数对象
,可以使用props.state.参数名
的形式取出参数。 - search:取得的参数是字符串,需要进一步做类型转换,可以使用
props.search.参数名
的形式取出参数。
Router V6的新特性
- 将<Switch>改为<Routes>
- <Route>中的的component和render属性被统一替换为element属性
- 使用useParams和useSearchParmas传参
- 统一使用useNavigate进行编程式路由
DOM
DOM就是以树的结构来表示一个文档,树的每一个分支都是一个节点,可以通过编写HTML、CSS、JS的方式来修改树的结构,以改变文档结构、样式或内容。
虚拟DOM
虚拟DOM就是一个对象,使用对象的方式来表示一个文档。React通过事务处理机制,将多次对DOM的修改合并为一次进行,提高了页面渲染的效率,减少了真实DOM重绘重排的时间,减少了重新页面渲染的次数。
使用虚拟DOM的好处:
- 渲染效率更高:对比真实DOM渲染,当真实DOM发生改变时,会直接更新全部DOM元素;而虚拟DOM会对比有差异的地方,进行局部更新DOM元素。
- 跨平台:虚拟DOM的本质是一个js对象,可以轻松实现跨平台。
diff算法
当虚拟DOM树发生变化时,会通过对比新旧虚拟DOM树的差异,这些差异包括节点的删除、修改、新增等,diff算法根据对比得到的差异生成一个patch补丁,等待对比完成一次性将patch更新到真实DOM中。diff算法共有三个层级比较策略,
- 基于树比较:基于树的比较策略会忽略节点跨层级的通信,新旧虚拟DOM树只对同一层级的节点进行比较,如果发现节点已经不存在了,就不再继续向下对比,而是将节点与其子节点全部删除,提高对比效率。使用该策略只有创建和删除操作。
- 基于组件比较:如果是同一个类的组件,那么会继续向下进行diff算法,如果是不同类的,即使他们结构相似,那么也直接删除,创建新的节点
- 基于节点比较:基于节点的比较通常在同一层级中进行,由于对节点的修改涉及到了节点的重新排序,需要对节点进行增加,删除,移动都操作,这些操作的性能消耗都很高,所以给同一层级的每个节点都附加一个key,该key值可以唯一标识一个节点,diff算法借助key来判断节点是新创建的还是被移动来的元素,以此增加对比效率,减少不必要的渲染。该key值不可以采用随机数,不可使用循环的索引。
key就是一个辅助标识,可以判断哪些元素是新增的,哪些元素是被移动而来的,可以提升对比效率。
React和vue的diff算法比较
相同点:
- 都不进行跨层级的比较
- 对数组或对象等深层次的数据变化无法检测到
不同点:
- Vue的diff算法会一边比较一边通过patch对真实DOM进行更新;而React会比较完再通过patch一次性更新真实DOM
- Vue进行节点对比时,如果节点元素类型相同但是className不同,则会认为是不同类型的节点,直接删除;而React则认为是相同类型的节点,只进行修改
- Vue在对列表进行对比时采用双指针的方法;React从左到右依次进行对比
- Vue将最后一个节点插入到第一个节点之前采用直接插入的方式;而React将前面的节点依次向后移动
React fiber
在虚拟DOM向真实DOM进行更新时,React会占用浏览器资源,使用户触发的事件无法得到响应,给用户造成一种卡顿的感觉。而React Fiber会将渲染页面的进行暂停,让浏览器先去执行更高级的任务,等待主线程空闲,再进行页面渲染。这样做可以延时对DOM的操作,避免一次性渲染大量的DOM节点。还可以提高浏览器响应速度。
对React的理解
React就是一个用于构建用户界面的JS库,只考虑视图层。
React有很多特性:
- JSX语法
- 虚拟DOM
- 单向数据流
- 声明式编程
- 组件
JSX语法
JSX扩展了JS语法,使代码更加安全,使写法更加简洁,代码看起来更加清晰,在编译时会通过babel将jsx转化为js,一边浏览器可以识别。
虚拟DOM
React采用虚拟DOM和diff算法对真实DOM进行更新,而不是直接更新真实DOM,因为直接更新真实DOM每次都要构建完整的DOM树,而虚拟DOM只需要更新那些变化的DOM节点,以此来提升更新效率。
单向数据流
React中的数据来源主要有两种,分别是state和props。state是组件内部自身定义的,仅可在组件内部进行修改;props只能外部传递到组件内部,在组件内部不可以修改props,props的修改只能通过外部组件来实现。
React是单向传递的,只可以由上向下通过props传递,数据的改变只能影响下面的节点,不会影响上一个层级的节点,如果是双向数据流,那么子组件更改了props,会导致父组件和其关联组件随着数据改变进行页面渲染,会导致数据不可控,保证了数据的可控性。
Vue中的双向绑定和React中的单向数据流
双向绑定就是绑定Model层和View层的映射关系。
单向数据绑定:Model的更新会触发View的更新,而View的更新不会触发Model的更新,它们的作用是单向的。
双向数据绑定:Model的更新会触发View的更新,View的更新也会触发Model的更新,它们的作用是相互的。
声明式编程
React采用声明式编程,更关心我们想做什么,而不是如何做。声明式编程使得组件更容易使用,易于维护。
组件
React采用组件设计模式,分为函数式组件和类式组件,将页面分为了一个个小块,每一个小块就是一个组件,这些组件之间组合嵌套,就形成了整个页面。
0.1 + 0.2 为什么不等于0.3
因为计算机是通过二进制存储数字的,而0.1和0.2使用二进制表示是无限循环小数,JS中的Number类型遵循IEEE754规则,使用64位固定长度来表示,也就是标准的double双精度浮点数,遵循0舍1入的原则
八股
八股:HTTP、HTTPS、HTTP和HTTPS的对比、HTTP1.0和HTTP1.1和HTTP2.0和HTTP3.0、SSL/TSL握手、三次握手、四次挥手(为什么是三次、四次;为什么设置2MSL、TIME_WAIT过多会造成什么影响、TIME_WAIT一定出现在客户端吗?)、DNS(DNS使用的协议、递归查询、迭代查询、DNS查询过程)、TCP/UDP区别和适用场景、TCP粘包、TCP保证可靠传输的方法(校验和、序号和返回确认、滑动窗口、拥塞控制、流量控制、重传机制)、OSI、TCP五层、PUT和POST的区别、GET和POST的区别、常见的HTTP请求、用户输入网址到页面呈现中间经历了哪些步骤?HTTP常见的请求状态、301和302重定向的区别。
HTML
HTML:href和src的区别、HTML5新特性、DOCTYPE的作用、标签的defer和async属性、web worker、label的作用
CSS
CSS: CSS选择器优先级、display的属性、隐藏元素的方法、link和@import的区别、伪类与伪元素、盒模型、使用translate然不使用定位来改变元素位置、CSS3新特性、CSS精灵图、CSS性能提高的方式、CSS预处理器、媒体查询、对CSS工程化的理解、布局单位、flex为1代表什么、BFC、absolute和fixed的区别、实现水平垂直居中、实现品字布局、实现九宫格布局、实现三角形、椭圆、半圆、梯形、自适应的正方形、0.5px的线
JS
JS基础:JS的数据类型、JS如何判断数据类型、如何判断数组、null和undefined的区别、==和===的区别、包装类、JS脚本延迟加载的方法、AJAX,Fetch和axios的区别、内存泄露的几种情况、垃圾回收机制、如何实现单点登录
函数声明和函数表达式的区别
函数的创建有2种方式:函数声明和函数表达式。
函数声明的语法:
function functionName(arg0, arg1, arg2) {
//函数体
}
函数声明的最重要的特征就是函数声明提升,意思是在执行代码之前会先读取函数声明,所以函数生命可以放在调用函数语句之后
sayHello();
function sayHello(){
console.log("Hello");
}
函数表达式语法:
var functionName = function(arg0, arg1, arg2) {
//函数体
}
这种形式看起来像常规的变量赋值,先创建一个匿名函数,然后赋值给变量functionName。我们知道在JS中如果要想使用一个变量,必须先给这个变量赋值,所以函数表达式也不例外,在调用之前,必须先给它赋值。否则会报错。
sayHi(); //在这里调用会报错
var sayHi = function(){
console.log("Hi");
}
sayHi(); //Hi
二者的区别在于:
- 函数声明必须包含函数名;函数表达式可以省略函数名
- 函数声明存在函数提升,可以在函数调用语句之后进行声明;函数表达式必须在函数调用语句之前进行声明
- 函数声明不能出现在条件语句、循环语句或其他语句中,而函数表达式没有位置限制,可以出现在语句中实现动态编程。
String转Number的方法
- Number()。可以将字符串转为数字类型。如果字符串中含有非数字,那么会返回
NaN
。字符串中可以是负数,也可以是小数,均可以转换。 - parseInt()。对于数字开头的字符串,转换后得到的是数字,例如
123a
仍可转换为123
;对于非数字开头的字符串,返回NaN
- parseFloat()。类似于parseInt(),但可以把小数转为小数
异步编程
- Ajax
Ajax可以在不重新加载整个页面的情况下,通过XmlHttpRequest
向服务器请求资源,然后更新局部页面。
Ajax的缺点:
- 返回的结构式XML,如果需要JSON格式,需要进行额外的转换
- 针对MVC框架进行设计的,不符合MVVM的浪潮
- 配置和使用较复杂
- 存在回调地狱问题
- Fetch
Fetch是http请求数据的方式,它使用Promise,采用模块化设计,通过数据流进行处理,对于请求大文件或网速慢的情况非常适用。
Fetch的优点:
- 采用模块化设计,将输入、输出、状态跟踪分离
- 基于Promise,返回一个Promise对象
- 不适用回调函数,没有回调地狱问题
Fetch的缺点:
- 过于底层,很多状态码没有封装
- 无法阻断请求
- 兼容性差
- 无法检测请求进度
- axios
axios使用promise封装了ajax,它内部有两个拦截器,分别是请求拦截器
和相应拦截器
。
- 请求拦截器:用于在发送请求之前进行一些操作,比如设置请求体,携带token、cookie等。
- 响应拦截器:接收到响应后做的一些操作,例如登录失效后跳转到登录页重新登陆。
axios的特点:
- 由浏览器发起请求
- 支持Promise的API
- 可以对请求和响应进行监听
- 更好的格式化,将格式转化为JSON格式
- 安全性更高,可以抵御CSRF攻击
axios常用的方法有:
- get:返回Promise对象
- post:返回Promise对象
- put:用于更新数据,必须提供完整的数据结构进行更新
- patch:用于更新数据,可以局部更新,可以只提供需要修改的数据进行更新
- delete
axios相关配置:
- url:用于请求资源的url
- method:请求方法,默认为get
- baseURL:会自动加到URL前面
- proxy:用于配置代理
- transformRequest:允许在服务器发送请求之前修改请求数据
总结
Ajax
是一种web数据交互的方式,它可以使页面在不重新加载的情况下请求数据并进行局部更新,它内部使用了XHR
来进行异步请求。Ajax
在使用XHR
发起异步请求时得到的是XML
格式的数据,如果想要JSON格式,需要进行额外的转换;Ajax
本身针对的是MVC框架
,不符合现在的MVVM架构
;Ajax
有回调地狱问题;Ajax
的配置复杂
而Fetch
是XHR的代替品,它基于Promise
实现的,并且不使用回调函数,它采用模块化结构设计,并使用数据流进行传输,对于大文件和网速慢的情况非常友好。但是Fetch
不会对请求和响应进行监听;不能阻断请求;过于底层,对一些状态码没有封装;兼容性差。
axios
是基于Promise
对XHR
进行封装,它内部封装了两个拦截器,分别是请求拦截器和响应拦截器。请求拦截器用于在请求发出之前进行一些操作,比如:设置请求体,携带Cookie、token等;响应拦截器用于在得到响应后进行一些操作,比如:登录失效后跳转到登录页面重新登录。axios
有get、post、put、patch、delete等方法。axios可以对请求和响应进行监听;返回Promise
对象,可以使用Promise
的API;返回JSON
格式的数据;由浏览器发起请求;安全性更高,可以抵御CSRF攻击。
JS异步编程的解决方案
- 回调函数:使用回调函数有很多缺点,例如回调地狱;上下两层的代码耦合度高等
- Promise
- async
ES6
ES6:ES6新特性、Let、const、var的区别;箭头函数和普通函数的区别
JS进阶
原型与原型链、JS何如实现多继承、执行上下文/闭包/作用域链、this/call/apply/bind、异步编程(Promise、async)、事件循环、面向对象、垃圾回收机制
介绍一下Tailwind
Tailwind是一款css框架,可以快速构建UI。
- Tailwind将所有的css属性全部封装成语义化的类,只需要在标签中的class引入即可。比如:在写行内样式时,我们如果想设置字体的大小,我们需要考虑使用多大的字体,使用什么单位,而且写法比较冗余;而使用Tailwind只需要在class中写上text-lg就可以指定当前标签包含的文字为大字体。
- Tailwind使用更加语义化的名称
- 更高效的响应式写法
既然 TailwindCSS 这么好用,那岂不是可以摆脱手写 CSS 了
- 当我们遇到给复杂选择器添加样式时,还需要手写CSS,比如当父元素鼠标悬浮时,子级元素的样式控制。
- 使用css函数calc()
对比其他CSS框架
- 原生css写法,写法简单,但是复用性差,大多数时候还是需要什么就写什么CSS
- CSS组件化,比如:BootStrap、elementUI、antd。他们将表单,输入框,按钮等封装为一个个组件,可以直接使用,不需要手动写css
- CSS原子化:Tailwind就是CSS原子化框架,将单一的CSS属性进行了封装,比如想写一个宽度12像素,那么只需要写成w-3即可,他没有对任何表单、按钮等进行封装
用什么不统一使用redux,而是用redux+父子组件
- Redux写法固定,样板代码过多,使开发者写了大量重复的代码
- 每一次执行dispatch都会从根reducer到子reducer嵌套递归执行,效率低
Redux
对Redux的理解
Redux是一个用来管理数据状态的工具。因为React传递数据是单向的,父组件可以向子组件通过props传递数据,而子组件无法直接向父组件传递数据,这样的单向数据流成就了React的数据可控性。随着项目越来越大,state也越来越难以管理,而使用Redux可以轻松管理这些state。
Redux专门用于管理数据状态(容器组件),而React用于处理视图层逻辑,实现页面渲染(UI组件),两者通过connect连接起来。
Redux主要解决的问题
Redux主要解决的问题是将Redux的状态与React的UI绑定到一起,当使用dispatch(action)改变state时可以自动更新页面。
Redux的工作原理
当组件想要更新状态时,Redux会创建一个action对象,action对象包含两个数据,一个是必备的type,表示action类型,第二个state,在action不会修改state的值,而是等待store调用dispatch()方法将action对象传递给reducer,reducer才是真正更新state值的对象。reducer接收两个参数,一个是preState,一个是action,通过匹配不同的action.type来执行不同的逻辑,然后返回一个新的state。store在组件挂载到页面后(componentDidmount)通过subscribe()方法一直监听reducer,一旦reducer改变完状态,就可以通过getState()方法得到新的经过reducer处理后的state。
Redux深入理解
Redux源码主要分为以下几个模块文件:
- compose.js:提供从右到左进行函数式编程
- createStore.js:提供作为生成唯一store的函数
- combineReducers.js:提供合并多个reducer的函数,保证store的唯一性
- bindActionCreators.js:可以让开发者在不直接接触dispacth的前提下进行更改state的操作
- applyMiddleware.js:通过中间件来增强dispatch的功能
Redux工作流程
- 首先用户通过dispatch()函数发出Action对象
- Store自动调用Reducer并传入两个参数,一个是当前的State,另一个是Action对象
- Reducer更新后返回新的State
- State一旦发生变化,Store就会调用监听函数,来更新View
Redux怎么实现属性传递的,原理
view -> action -> reducer -> store -> view
- 用户在view通过触发某些事件调用mapDispatchToProps()将action对象传给store
- store自动调用reducer修改state
- store通过subscribe()监听state是否发生变化,当state发生改变时,store就可以调用getState()方法获取到新的state
- store通过mapStateToProps()方法将新的state映射到view中
Redux的中间件是什么
view -> action -> middleware -> reducer -> store -> view
Redux中间件是对dispatch的扩展,位于action -> reducer之间,使用中间件可以进行异步操作、action过滤、异常报告等功能。
Redux异步中间件
- redux-thunk
优点:
- 体积小,只有不到20行代码
- 使用简单
缺点:
- 通常一个请求需要大量的代码,而且很多都是重复性质的
- 耦合严重:异步代码和action耦合在一起,不便于管理
- 功能少,在开发中有些功能需要自己封装
- redux-saga
redux-saga是一个管理redux应用异步操作的中间件,它通过创建Sagas将所有异步操作逻辑存放在一个文件进行集中处理,一次将同步与异步操作分离,以便于管理与维护。
优点:
- 异步解耦:异步操作放在单独的文件中,降低耦合性
- 异常可以直接使用try/catch捕获
- 功能强大:提供了大量的saga辅助函数供开发者使用
- 灵活:可以将多个saga并行/串行起来,形成异步流
- 易于测试
缺点:
- 体积庞大
- 功能过剩:其实有很多功能都很难能用到
- ts支持不友好
- 学习难度大
Redux怎么处理并行操作
使用redux-saga。
- takeEvery:可以让多个saga任务并行被fork执行
import {
fork,
take
} from "redux-saga/effects"
const takeEvery = (pattern, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(pattern)
yield fork(saga, ...args.concat(action))
}
})
- takeLastest
takeLastest不允许多个saga并行执行,一旦收到新的发起的action,就会取消前面的所有fork任务。在处理AJAX请求的时候,如果只希望获取最后一个请求的响应,taskLastest变得非常有用。
import {
cancel,
fork,
take
} from "redux-saga/effects"
const takeLatest = (pattern, saga, ...args) => fork(function*() {
let lastTask
while (true) {
const action = yield take(pattern)
if (lastTask) {
yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作
}
lastTask = yield fork(saga, ...args.concat(action))
}
})
Redux状态管理器和变量挂载到window中有什么区别
二者都是存储数据以供后期使用。
- Redux状态更改可以回溯,数据多了的时候可以清晰地知道改动在哪里发生,完整的提供了一套状态管理模式;而window不可以。
Redux和VueX的区别和共同思想
- 区别:
- VueX改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需要在对应的mutations中改变state的值即可
- 由于Vue自动重新渲染的特性,无需subscribe重新渲染函数,只要生成新的state即可
- VueX数据流的顺序是:View调用store.commit提交对应的请求到Store中对应的mutations函数 -> store改变
通俗的理解就是:VueX弱化了dispatch,通过commit进行store状态的更改,取消了action的概念,不必传入特定的action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架变得更加简单。
- 共同思想:Redux和VueX都是以MVVM思想进行设计,将数据从视图层抽离出来,实现变化可预测、单一数据源。
Redux中的connect有什么作用
connect负责连接Redux和React
- 获取State:connect通过context获取Provider中store,通过store.getState()获取整个store tree上所有state。
- 包装原组件
- 监听Store tree变化:connect缓存了store tree中state的状态,通过当前state状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发Connect及其子组件的重新渲染。
HOOKS
React组件分为类式组件和函数式组件,不同于类式组件,函数式组件是无状态组件,它没有自己的状态,数据只能通过外部组件传入,没有自己的生命周期函数。Hooks赋予了函数式组件这些能力。
useState
状态钩子,为组件提供自己的状态。useState的参数是状态的初始值,返回一个数组,数组的第一个参数是一个state值,第二个参数改变状态的方法。
- 函数式组件中直接通过useState设置状态的初始值;类式组件在constructor中设置状态的初始值
- 函数式组件中直接调用状态名使用;类式组件通过this.state.状态名调用
- 函数式组件中使用setState改变状态;类式组件通过this.setState改变
useEffect
useEffect通常用于消息订阅、发送请求、事件处理等。
- 当useEffect不传入第二个参数时,组件每次渲染时都会执行useEffect中的副作用函数
- 当useEffect传入一个空数组时,只会在组件挂载时进行调用,相当于componentDidMount()
- 当useEffect传入一个不为空数组时,当数组中的任意一个数据发生变化时,都会执行副作用函数
- 当useEffect的副作用函数中使用return时,相当于componentWillUnmount()函数,可以在这里进行一些清除定时器、取消订阅、取消发送请求等操作。
useCallback
useCallback用于防止当组件重新渲染时,导致组件的方法被重新创建,起到缓存的作用。传入参数的情况与useEffect类似。
useMemo
useMemo与useCallback类似,区别在于useCallback会将函数作为返回结果返回;而useMemo会将函数执行,将执行结果返回。
useRef的作用
useRef有两种用法:
- 变量持久化:当组件重新渲染时不会重新初始化变量,变量依旧保存之前的值
- 获得DOM元素,比如获取输入框中的值。
useContext
用于组件之间共享数据。
memo、useMemo和useCallback的区别
- memo:针对一个组件是否重新渲染
- useMemo:针对的是一段代码逻辑是否执行
- useCallback:针对的是组件在重新渲染时,函数是否重新创建
useEffect和useLayoutEffect
useEffect的执行时机是在浏览器完成渲染后,而useLayoutEffect是浏览器把内容真正的渲染到界面之前。如果在回调函数中要对DOM进行操作,那么最好使用useLayoutEffect,可以防止页面闪烁。useLayoutEffect总是比useEffect先执行。
hooks的坑
- 不要在循环,条件或嵌套函数中调用Hooks,因为React需要利用正确的调用顺序来更新对应的状态,如果在循环、条件或嵌套函数中执行Hooks,很容易造成调用顺序出错,产生难以预计的后果。
- state只能通过setState改变,不可以使用push、pop等方法
- 要善用useMemo、useCallback,谨慎使用useContext
React事件机制
React基于浏览器机制实现了一套事件机制,包括:事件注册
、事件合成
、事件冒泡
、事件派发
等。
合成事件
合成事件是react模拟DOM原生事件的一个事件对象,其优点如下:
- 兼容所有浏览器,兼容性好
- 方便react统一管理和进行事件处理。对于原生事件来说,浏览器会监听事件是否被触发,当事件触发时会创建一个事件对象,当多个事件被触发时就会创建多个事件对象,这样存在内部分配的问题。对于合成事件来说,有一个专门事件池来管理事件的创建和销毁,当需要使用事件时,就会在事件池中复用对象,事件回调结束后,再销毁事件对象上的属性,以便于下次再复用对象。
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>按钮命名</button>
虽然看似合成事件被绑定到DOM上,React并不会把合成事件直接绑定到真实节点上,而是把所有的事件挂载到document上,使用一个统一的事件监听器去监听。
事件代理
React未将事件处理函数与对应的DOM节点直接关联,而是在顶层使用了一个全局事件监听器监听所有的事件。
React会在内部维护一个映射表记录事件与组件事件处理函数的对应关系。当某个事件触发时,React根据映射表将事件分派给指定的事件处理函数。当一个组件挂载与卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。
这个事件监听器维持了一个映射来保存所以组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也提升很大。
合成事件
React中的onClick、onChange等事件是合成事件,并不是浏览器的原生事件。这些事件并没有绑定到对应的真实DOM上,而是通过事件代理的方式,将所有事件绑定到了document上。当事件发生并冒泡到document时,React将事件内容封装并交由真正的处理函数运行,这样做不仅可以减少内存消耗,还可以在组件挂载销毁时统一订阅和移除事件。
可以使用event.preventDefault阻止事件冒泡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZKU0S8oZ-1652020270018)(https://user-images.githubusercontent.com/70066311/157618463-b5fc6510-f7f9-498f-9722-cf7458d6972c.jpg)]
实现合成事件的目的
- 合成事件是一个跨浏览器的原生事件包装器,赋予了跨浏览器开发的能力,解决了浏览器之间的兼容问题。
- 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象,如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题,但对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
React的事件和普通的HTML事件有什么不同?
- 事件的命名方式不同,原生事件为全小写,react事件为小驼峰
- 事件函数处理语法不同,原生事件为字符串,react事件为函数
- react事件不能采用return false的方式来阻止浏览器的默认行为,而必须明确调用preventDefault()来阻止默认行为
react事件执行顺序
事件的执行顺序为原生事件先执行,合成事件再执行。合成事件会冒泡到document上,所以尽量避免原生事件和合成事件混用。如果原生事件阻止冒泡,那么就会导致合成事件不执行。
SPA
SPA为单页面应用,它将所有活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JS、CSS,一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转,取而代之的是利用JS动态的变换HTML的内容,从而实现UI与用户交互。
优点:
- 避免了页面的重新加载
- 对服务器的压力小
- 前后端职责分离,前端负责页面交互,后端负责数据处理
缺点:
- 初次加载耗时多。SPA应用需要在页面初次加载时就将JS、CSS同一加载,可以通过路由懒加载实现部分页面按需加载
- 兼容性差。因为有的浏览器不支持前进后退
- 不利于SEO。因为SPA是由客户端渲染的,通过执行JS来创建DOM元素构建页面,而SEO爬虫只请求静态资源,不会执行JS文件,所以抓取不到DOM结构,也分析不出来有用的信息。(例如百度的搜索引擎,谷歌的搜索引擎会执行JS脚本,但对异步获取的数据不支持,爬虫不会等待我们获取到异步数据再去执行)
SPA、SEO和SSR
SPA如上。
SEO
SEO(搜索引擎优化)是一种了解搜索殷勤的运作规则(如何抓取网站页面、如何索引以及如何根据特定的关键字展现搜索结果排序等)来调整网站,以提高该网站在搜索引擎中某些关键词的搜索结果排名。
SSR(服务器端渲染)
在普通的SPA中,一般是将框架及网页代码发送到浏览器,然后在浏览器中生成和操作DOM(这也是第一次访问SPA页面在同等带宽及网络延迟下比传统的在后端生成HTML发送到浏览器要更慢的主要原因),但其实也可以将SPA应用打包到服务器上,在服务器上渲染出HTML,发送到浏览器,这样的HTML页面还不具备交互能力,所以还需要与SPA框架配合,在浏览器上“混合”成可交互的应用程序。
SSR能够在服务端先进行请求渲染,由于服务端进行请求数据的时延较小,能够快速拿到数据并且返回HTML代码。在客户端可以直接渲染数据而不需要花费一些请求数据的时间,这是服务端渲染的好处。返回内容SSR会比普通的SPA在HTML代码中多出首次渲染的结果,这样在初始化的时候直接将页面进行渲染,无需花费时间去请求数据再次渲染。SSR并不是说只在服务端进行渲染,而是说SSR会比普通的客户端渲染多一次在服务端渲染。到浏览器这边,SSR还是需要进行再次初始化React,并且经过生命周期
SSR的优势
- 对SEO友好
- 所有模板、图片等资源都存在服务器端
- 一个html返回所有数据
- 减少HTTP请求
- 响应快、用户体验好、首屏渲染快:首屏发送来的是
html字符串
,不再依赖于js文件了,这就会使用户更快地看到页面的内容,尤其针对大型单页面应用。
SSR的局限性
- 服务器压力大:在服务器端渲染会占用CPU资源
- 开发条件受限:在服务器端渲染中,只会执行到componentDidMount之前的生命周期函数。
- 一些常用的浏览器API可能不可以使用,例如:window、document和alert等
CSR和SSR的比较
CSR:用户输入url访问页面 -> 先得到一个html模板页 -> 然后通过异步请求服务器端数据 -> 得到服务器端的数据 -> 渲染成局部页面 -> 用户
SSR:用户输入url访问页面 -> 服务器收到请求 -> 将对应请求的数据渲染为一个完整的网页 -> 返回给用户
SPA和SSR的时间消耗比较
由于SSR是在服务器端渲染,而不是在客户端请求首屏数据,所以比SPA受屏渲染更快。服务器在内网进行请求时,数据响应更快。而SPA在客户端进行请求,由于不同的网络环境,导致时间差不同。
- 客户端请求数据
- 服务器端请求数据
SPA和SSR的html渲染比较
SSR是先向后端服务器请求数据,然后生成完整的首屏html返回给浏览器;而客户端渲染是等js代码下载、加载、解析完成后再去请求数据渲染,等待的过程页面是什么都没有的,就是用户看到的白屏。
- CSR html渲染
- SSR html渲染
浏览器渲染原理
- 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
- 然后对 CSS 进行解析,生成 CSSOM 规则树。
- 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
- 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
浏览器渲染优化
- 针对JS:尽量将JavaScript文件放在文档底部;使用defer;动态生成标签
- 针对CSS:尽量使用Link而不是@import;如果样式少就写成内联样式
- 针对HTML代码:代码层级不要太深;使用语义化标签
- 减少对DOM的操作
- 减少回流和重绘:使用display:none;使用absolute或fixed
js放在文档底部有什么问题
由于网页基本结构与样式均已经加载完成,那么此时负责交互的JS并没有下载下来,必然也会对用户的体验造成影响。
文档预解析
当执行JS脚本时,由辅助线程解析剩下的文档,并家在后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整个的加载速度更快。预解析并不改变DOM树,只解析需要用到的外部资源。
CSS如何阻塞文档解析
理论上,样式不会改变DOM结构,在解析时也就没必要停下文档解析等待它们。然后,如果在JS执行时可能会在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值。所以如果没有完成CSSOM树的构建而想在此时执行脚本,那么浏览器将延迟JS脚本执行和文档的解析,直至其完成CSSOM的下载和构建。这就是说,浏览器会先下载和构建CSSOM,然后再执行JS,最后再继续文档的解析。
介绍项目
项目一:News Hub
- 登录。首先是登录功能,使用了token认证+localstorage存储用户信息,(这里可以扩展为什么使用token;token认证的具体过程;对比cookie+session的认证方式,token有哪些好处;使用token认证的缺点;如何使用后端如何使用JWT生成token),如果进行客户端身份认证。在token认证成功后,用户登录成功。然后使用localstorage存储当前登录用户的信息,比如用户名、用户等级、权限等信息。
- 管理系统菜单栏显示。登录之后跳转到首页,首先向服务器请求登录用户可以使用的权限,因为在登录时,已经对用户等级、权限等信息保存到了localstorage,所以这里将直接返回304状态码,并返回用户权限数组。然后对菜单栏数组进行遍历,判断如果当前用户的权限包含了遍历的菜单栏,那么就对该菜单栏进行渲染。因为这里还涉及到二级菜单,所以遍历的可能是一个菜单对象,要进行判断,如果是对象的话,还要对二级菜单的权限进行递归遍历。这样讲整个菜单栏数组遍历完成后,就生成了当前用户可以有权限访问的菜单栏了。
- URL权限控制。这里主要做了登录页的路由拦截和登录后跨权限的路由拦截。登录页的拦截主要是针对用户此时还没有登录但在浏览器地址输入对应的url来进行访问。主要做法是用户输入url并按下回车后查看localstorage中是否有该用户的登录状态,如果没有则重定向到登录页面,如果有再进行跳转。在登录后跨权限的路由拦截主要做法是根据用户输入的url中的pathname和用户权限数组进行对比,对用户权限数组进行遍历,如果包含pathname,那么就进行跳转;否则,不进行任何操作。
- 自己封装Hooks。封装了一个usePub的hooks,用于管理博客发布的状态。usePub接受一个参数type,代表当前文章的状态,内部实现了三个方法分别是发布、下线和重新上线的函数。这三个接收一个参数id,代表当前是哪篇文章。首先使用useState和useEffect根据type参数渲染最初的文章列表。然后我们对文章进行发布、下线或重新上线的操作时,对应的函数会向服务器发送一个axios.patch请求,用于局部更新,它会把当前修改的文章的状态。最后useEffect根据type的变化重新请求文章列表,进行渲染。
项目中使用到的性能优化
CDN
CDN的概念
CDN(内容分发系统)就是一个分布式的网络系统,它可以将图片、视频、应用程序等文件发送给距离它较近的用户,以此来提高性能,减少页面加载延迟。
CDN共分为三个部分:
- 分发服务系统:负责响应最终的用户请求,将缓存在本地的资源快速的提供给用户;负责与主站点同步资源,将更新的内容和没有的内容从主站点同步过来
- 负载均衡系统:负责对所有发起请求的用户进行调度,提供给用户最终的CDN服务器的访问地址。分为全局负载均衡设备和本地负载均衡设备。全局负载均衡设备负责选取最优的CDN服务器以提供给用户进行访问;本地负载均衡设备负责本地CDN服务器内部的负载均衡
- 运营管理系统:负责处理业务层面与外界系统交互所必须处理的工作,比如:产品管理、计费管理、统计分析等
CDN的作用
性能方面:
- 可以是用户的请求得到更快的响应,页面加载的更快,延迟更小
- 请求被分发到其它CDN服务器中,减小了服务器压力
安全性方面:
- 可以防止DDoS攻击:可以对异常流量进行检测,限制其请求次数
- 全链路使用HTTPS
CDN工作原理
- 对于用户发起的请求,本地DNS发现改请求对应的是一个CDN专用的DNS服务器,会将该请求转发给CDN DNS服务器
- CDN DNS服务器会将全局负载均衡设备的IP地址返回给用户
- 用户向全局负载均衡设备发起请求
- 全局负载均衡设备根据用户的IP地址选取一个用户所属区域的负载均衡设备,将请求转发给区域负载均衡设备
- 区域负载均衡设备选择一个最优的设备,将该设备的IP地址返回给全局负载均衡设备
- 全局负载均衡设备再将该设备的IP地址返回给用户
- 用户最终向该设备发起请求,获取资源
懒加载
图片懒加载
图片懒加载就是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在长网页中,如果图片很多,所有图片都在页面首次加载时加载出来,而用户看到的只是可视窗口那一部分,这样会浪费性能。
而使用图片懒加载可以只加载可视窗口之内的图片,可视区域之外的图片不会进行加载。可以使页面的加载速度更快,减少了服务器的负载。图片懒加载适用于图片较多,页面列表较长的场景中。
特点
- 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担
- 提升用户体验:懒加载可以是页面加载更快,用户就可以更快的浏览页面
- 防止加载过多的图片影响其他文件的加载
实现原理
- 首先需要知道当前视口的高度:
window.innerHeight
- 然后要知道图片资源距离浏览器顶部的距离:
img.offsetTop
- 最后要知道当前滚动的距离:
document.body.scrollTop || document.documentElement.scrollTop
当img.offsetTop < window.innerHeight + document.body.scrollTop
时,再去加载对应的图片
主要过程为:将页面上的图片的src
属性设置为空字符串,将图片的真实路径保存在一个自定义的属性中,当页面 滚动的时候,进行判断,如果图片进行页面的可视区域,则从自定义属性中取出真实路径赋值给图片的src
属性,以此来实现图片的懒加载
路由懒加载
路由懒加载就是使用变量来导入路由对应的组件,当时用到这个变量时才去加载对应的路由。
刚打开整个网页默认加载所有页面,路由懒加载就是只加载你当前点击的那个模块。按需加载路由对应的资源,提高首屏加载速度。
实现原理就是不直接import对应的路由,而是写成异步的形式,只有当变量被调用的,才去导入对应的路由。
懒加载和预加载的区别
两种方式都是提高网页性能的方式
- 懒加载也要延迟加载,指的是在网页中加载资源的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户体验,并且可以减少服务器压力。
- 预加载是指将所需的资源提前加载到本地,这样后面在需要用到时就直接从缓存取资源。通过预加载能够减少用户的等待时间,提高用户体验。
预加载的实现方式
使用纯css进行图片预加载
body:after {
content: "";
display: block;
position: absolute;
background: url("../image/manage/help/help_item2_01.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_02.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_03.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_04.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_05.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_06.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_07.png?v=201707241359") no-repeat -10000px -1000px,url("../image/manage/help/help_item2_01.png?v=201707241359") no-repeat -10000px -1000px;
width: 0;
height: 0
}
原理就是加载了该图片,但是不在可视范围内,方式比较简单。但也有缺点:图片跟随文档一同加载,如果为了提高文档的加载速度,那么这种方式就不合适了。
利用JS进行预加载
// 存放图片路径的数组
const imgSrcArr = [
'imgsrc1',
'imgsrc2',
'imgsrc3',
'imgsrc4'
];
const imgWrap = [];
function preloadImg(arr) {
for(let i =0; i< arr.length ;i++) {
imgWrap[i] = new Image();
imgWrap[i].src = arr[i];
}
}
preloadImg(imgSrcArr);
// 或者延迟的文档加载完毕再加载图片
$(function () {
preloadImg(imgSrcArr);
})
还可以使用css+js或者ajax实现。
重排和重绘
重排
重排就是:当渲染树中的部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程。
下列这些操作都会引起重排:
- 元素的大小、位置、字体大小发生变化
- 页面首次渲染
- 浏览器窗口大小发生变化
- 激活CSS伪类
- 添加或删除DOM元素
- 查询某些属性或者调用某些方法
重绘
当页面中某些元素的样式发生改变时,不但会影响其在文档流中的位置,浏览器还会对该元素进行重新绘制,这个过程就是重绘。
下列这些操作会引起重绘:
- 元素的color、background 相关属性:background-color、background-image 等发生变化
- outline 相关属性:outline-color、outline-width 、text-decoration
- border-radius、visibility、box-shadow
重绘不一定重排,重排一定引起重绘。
如何避免重绘和重排
- 避免频繁操作DOM,操作DOM时,尽量对低节点的DOM进行操作
- 不要使用table布局,一个小的改动可能会使整个table进行重新布局
- 不要频繁操作元素的样式
- 使用absolute或者fixed,是元素脱离文档流,这样他们的改变就不会影响其他元素了
- 将DOM的多个读或写操作放在一起进行,而不是(读写穿插)
浏览器针对重绘和重排的优化
浏览器实现了渲染队列,可以将所有的重绘和重排放在一个队列中,当队列中的操作到了一定数量或者到了一定的时间间隔,浏览器就会对队列进行批处理,这样可以让多次重绘重排操作合并为一次。
节流和防抖
节流
节流就是规定一个时间间隔,在该时间间隔内,事件只能被触发一次。
防抖
防抖就是定义一个定时器,如果在这个定时器未结束之前事件被调用了,那么该事件不会触发,并且重新计时。
防抖和节流的应用场景
防抖:
- 按钮多次点击,只执行最后一次
- 服务器验证场景:表单验证需要服务器配合,只执行连续输入的最后一次
节流:
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:浏览器resize
- 动画场景:避免短时间触发多次动画引起性能问题