面试第七篇
在浏览器渲染页面的时候,当DOM节点的数量越多,每一次重绘的时候,对性能的影响也就越大。
假如我们需要展示一个信息量很大,大约有数十万条数据。遇到这样子的情况,其实现在有许多的方案,我们最常见的方案就类似PC上的下一页、上一页,但是这个方案在体验上其实并不友好。大部分的用户会比较喜欢不停的向下滚动就可以看到新的内容,但是这个就会遇到一个问题,不停的加载数据,导致页面堆积的节点越来越多,所消耗的内存不断增大,最后连滚动都会卡顿。
其实有很多数据我们大多数情况下是不需要看见的,如果只考虑我们能看到数据的话,其实需要渲染的数据量就会非常的少了,很好的提高了渲染的效率,减少因为大量的重绘照成不必要的影响。
虚拟列表
虚拟列表是一种展示列表的思路,在页面上创建一个容器作为可视区,在这个可视区内展示长列表中的一部分,也就是在可视区渲染列表。
根据容器元素的高度 clientHeight
以及列表项元素的高度 offsetHeight
来显示长列表数据中的某一个部分,而不是去完整地渲染整个长列表。
懒渲染/懒加载
懒渲染就是大家平常说的无限滚动,指的就是在滚动到页面底部的时候,再去加载剩余的数据。这是一种前后端共同优化的方式,后端一次加载比较少的数据可以节省流量,前端首次渲染更少的数据速度会更快。
思路:监听父元素的 scroll 事件(一般是 window),通过父元素的 scrollTop 判断是否到了页面是否到了页面底部,如果到了页面底部,就加载更多的数据。
懒加载与虚拟列表其实都是延时加载的一种实现,原理相同但场景略有不同。
使用h5 IntersectionObserver api
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个 DOM 节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
滚动事件
把滚动事件替换为一个带有计时器的滚动处理程序,每100毫秒进行简单检查,看这段时间内用户是否滚动过。如果没有,则什么都不做;如果有,就进行处理。
也就是类似于防抖事件
背景图片
不使用img src,使用background
websocket缓冲池
列表锁定/滚动锚定
几万个DOM同时插入的场景
使用appendChild方法插入
纯appendChild插入就是你直接操作DOM树,通过找到父亲节点然后根据要插入的DIV数量循环调用appendChild
插入,
从JS性能上而言,直接操作DOM是一件性能很低的事情;其次,我们每一次直接插入DIV都会导致重排(reflow)发生页面重渲染;另外JS是单线程的,它跑在浏览的主线程中,这条主线程与浏览器的渲染线程是互斥的,即当我们同步执行按钮回调时,不但页面被锁定,无法进行别的JS交互动作(比如有个别的按钮你想点,此时按钮回调就无法响应),页面渲染也会被阻塞。一旦这个处理环节比较长,用户就会明显感到卡顿,并且期间无法做别的事情。
<ul id="container">
<li></li>
</ul>
<script>
var container = document.getElementById('container')
for(let i = 0; i < 10000; i++){
let li = document.createElement('li')
li.innerHTML = 'hello world'
container.appendChild(li);
}
</script>
innerHTML插入
使用innerHTML
来处理,就是先循环构造出DOM的字符串,再设置父容器的innerHTML
,使页面重新渲染。这种方案从原理上来看,性能肯定是要比纯appendChild
插入要高的,首先它只操作了一次DOM,其次它不会多次重排。
使用Fragement插入
通过document.createDocumentFragment
这种方式我们可以创建一个Fragment
节点,在这个Fragment
内进行DOM操作并不会直接应用到实际DOM树中,我们往往将一些比较重的活如本文的大量DOM插入放到这里面处理,最后再将这个Fragment
插入到父亲节点,其子元素会被应用到实际DOM内,而Fragment
则不会。因此,该方案只存在Fragment
应用时的一次重排,且也只有最后应用Fragment
时操作了DOM,与方案二相比,我觉得主要提升体现在无需海量的字符串拼接操作
<script>
var container = document.getElementById('container')
var fragment = document.createDocumentFragment()
for(let i = 0; i < 10000; i++){
let li = document.createElement('li')
li.innerHTML = 'hello world'
fragment.appendChild(li)
}
container.appendChild(fragment);
</script>
分批处理
可以对DOM分批处理,并且使用户可以介入到这个过程中,换言之就是间断地进行渲染,中途可以让出线程让主线程操作,这也是requestIdleCallback
的思想。
通过setTimeout
,将20W的量分组拆成一个个1K的量(这个分批的量由我们实际执行一批任务的时长决定,这个时长须在16.7ms
,即一帧内),然后放入宏任务队列中维护,每一个LOOP尾端由浏览器自身决定是否进行直接渲染或者与之后的内容合并渲染(这个过程我们无法感知)
function chunkPaint() {
let root = document.querySelector('.container');
let LIMIT = 200000;
let CHUNK = 1000;
let sum = 0;
while (sum < LIMIT) {
setTimeout(function () {
for (let i = 0; i < CHUNK; i++) {
root.appendChild(document.createElement('div'));
}
}, 0);
sum += CHUNK;
}
}
总结:
innerHTML
来替代直接DOM操作,如果实在需要,可以放入Fragment
中进行;网页端:
首先用户打开网站的登录页面的时候,向浏览器的服务器发送获取登录二维码的请求。服务器收到请求后,随机生成一个uuid,将这个id作为key值存入redis服务器,同时设置一个过期时间,再过期后,用户登录二维码需要进行刷新重新获取。
同时,将这个key值和本公司的验证字符串合在一起,通过二维码生成接口,生成一个二维码的图片(二维码生成,网上有很多现成的接口和源码,这里不再介绍)。然后,将二维码图片和uuid一起返回给用户浏览器。
浏览器拿到二维码和uuid后,会每隔一秒向浏览器发送一次,登录是否成功的请求。请求中携带有uuid作为当前页面的标识符。这里有的同学就会奇怪了,服务器只存了个uuid在redis中作为key值,怎么会有用户的id信息呢?
浏览器拿到二维码后,将二维码展示到网页上,并给用户一个提示:请掏出您的手机,打开扫一扫进行登录。
手机端
用户拿出手机扫描二维码,就可以得到一个验证信息和一个uuid(扫描二维码获取字符串的功能在网上同样有很多demo,这里就不详细介绍了)。
由于手机端已经进行过了登录,在访问手机端的服务器的时候,参数中都回携带一个用户的token,手机端服务器可以从中解析到用户的userId(这里从token中取值而不是手机端直接传userid是为了安全,直接传userid可能会被截获和修改,token是加密的,被修改的风险会小很多)。手机端将解析到的数据和用户token一起作为参数,向服务器发送验证登录请求(这里的服务器是手机服务器,手机端的服务器跟网页端服务器不是同一台服务器)。
服务器收到请求后,首先对比参数中的验证信息,确定是否为用户登录请求接口。如果是,返回一个确认信息给手机端。
手机端收到返回后,将登录确认框显示给用户(防止用户误操作,同时使登录更加人性化)。用户确认是进行的登录操作后,手机再次发送请求。服务器拿到uuId和userId后,将用户的userid作为value值存入redis中以uuid作为key的键值对中。
通过getBoundingClientReact().top函数获取元素相对视窗的位置集合,集合中有left、top、bottom等属性
function isInViewPort (el) {
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
console.log('top', top)
return top <= viewPortHeight + 100
}
其中el.getBoundingClientRect().top = el.offsetTop - document.documentElement.scrollTop , 可以替换
也可以利用IntersectionObserver api
// 定义一个交叉观察器
const io = new IntersectionObserver(ioes => {
ioes.forEach(ioe => {
const el = ioe.target
const intersectionRatio = ioe.intersectionRatio
if (intersectionRatio > 0 && intersectionRatio <= 1) {
loadImg(el)
io.unobserve(el)
}
el.onload = el.onerror = () => io.unobserve(el)
})
})
// 执行交叉观察器
function isInViewPortOfThree (el) {
io.observe(el)
}
当intersectionRatio > 0 && intersectionRatio <= 1,在窗口范围内
在前端领域里,有一些其他的组件,例如antd的message,是一种命令式的调用方式-调用即渲染,即用即调。可以省去控制的变量visivble,可以将model部分的代码独立成模块。
实现方式有很多,我这里采用的是高阶函数和高阶组件,这两者原理都是一样,且不仅仅可以用来做Model 的命令化,可以使用与任何其他组件。
原理:通过高阶函数对原组件添加show,hide方法,实现内部控制modal的 渲染和卸载
import React from 'react'
import ReactDOM from 'react-dom'
import { Modal } from 'antd'
import { CloseOutlined } from '@ant-design/icons'
import { ModalProps } from 'antd/lib/modal'
import { CSS_MODAL_PREFIX } from './contant'
import ModalFooter from './ModalBottom'
import cls from 'classcombine'
import Styles from './index.less'
interface MyProps extends ModalProps {
footer?: any
okText?: string
cancelText?: string
onOk?: () => any
onCancel?: () => any
className?: string
confirmLoading?: boolean
children?: JSX.Element
hide(): void
}
...
return (
<Modal
className={cls({
[Styles[`${CSS_MODAL_PREFIX}-content`]]: true,
[className]: !!className,
})}
closeIcon={renderCloseIcon()}
footer={footer || renderFooter()}
destroyOnClose
{...props}
/>
)
}
function Wrapper(Component: React.FC<MyProps>) {
const container = document.createElement('div')
const hide = function () {
ReactDOM.unmountComponentAtNode(container)
document.body.removeChild(container)
}
const show = function (props: MyProps) {
document.body.appendChild(container)
ReactDOM.render(<Component {...props} hide={hide} />, container)
}
return {
show,
}
}
export default Wrapper(ExhibitorModal)
面向对象(OOP)的三大特性:封装、继承、多态
封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat
和 Dog
都继承自 Animal
,但是分别实现了自己的 eat
方法。此时针对某一个实例,我们无需了解它是 Cat
还是 Dog
,就可以直接调用 eat
方法,程序会自动判断出来应该如何执行 eat
抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
Myisam、InnoDB
MyISAM没有事务,插入不频繁,查询频繁使用
InnoDB:可靠性比较高
数据库查询比较慢时设置索引
主键索引、唯一索引、普通索引、复合索引
索引设置规则:
执行器首先让InnoDB去查找这一行,看这一行的数据有没有在内存中,如果有返回,没有去磁盘中找,读入内存后再返回数据
执行器把数据拿到后,修改数据属性,就得到了新数据
引擎把新数据更新到内存,同时把更新记录操作记录到redo log,redo log处于准备状态
执行器在生成这个操作的bin log,把bin log写入磁盘中
最后执行器调用引擎的提交事务接口,把redo log的准备状态改为提交状态,更新完成
CDN为内容分发网络,将网站内容发布到离用户最近的节点,使用户就近获取所需内容,提高响应速度
XSS(Cross-Site Scripting)是跨站脚本攻击。恶意攻击者向web页面中插入恶意script代码,当用户浏览页面时执行到script代码就会执行,从而达到恶意攻击用户的目的。
简单来说,XSS就是”教唆“浏览器去执行网页中原本不存在的前端代码
XSS攻击分为反射型XSS和储存型XSS,又称非持久型XSS和持久型XSS。
反射型XSS就是攻击相对于访问者而言是一次性的,想要触发漏洞需要访问特定的链接才可以。
储存型XSS会将恶意脚本储存到数据库,当再访问相同页面时,服务器会返回给浏览器,这意味着访问这个页面的访客都会执行恶意脚本,因此储存型XSS的危害会更大。
XSS可以劫持访问或者盗用cookie进行无密码登录等
XSS常见的危害:
具体操作:
1.攻击者通过在目标网站注入恶意脚本来完成cookie盗用
2.请求代理
目前主要的应对XSS攻击的手段还是转码和过滤
过滤:对