一、防抖
1. 函数防抖原理
频繁触发事件时,停止操作后等待指定空闲时间,才执行 1 次函数;中途再次触发就重置等待计时
类比公交:有人陆续刷卡上车就推迟发车,没人继续刷卡、空窗期到了才开车。
2. 防抖应用场景
-
鼠标 onmousemove 移动事件
-
滚动滚动条 onscroll 事件
-
搜索框输入查询(输入完停顿再发请求,避免频繁接口)
-
表单实时校验、防止按钮重复高频点击
-
按钮提交事件
-
浏览器窗口大小改变 onresize 事件
3. 防抖策略:
每次触发事件 → 开启延时定时器;如果计时中再次触发,清空旧定时器、重新计时;等待期全程无新触发,定时器到期执行目标函数。
4. debounce 库实现节流
debounce (underscorejs.org/) 函数所做的事情就是,在用户停止某个操作一段时间之后才执行相应的监听函数,而不是在用户操作的过程当中,浏览器触发多少次事件,就执行多少次监听函数。
<div id="box"></div>
<button id="cancel">取消防抖</button>
<!-- 这个是官网上封装好的函数,我们可以直接用 -->
<script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
function dosomeing (event) {
console.log(event, '2')
box.innerHTML = count++;
return '想要的结果'
}
box.onmousemove = _.debounce(dosomeing, 300, true);
5. 定时器实现防抖
/**
* @param {*} func 要执行的事件函数
* @param {*} awit 多少秒执行上面的事件函数
* @param {*} immediate 是否立刻执行 true:表示立刻执行 false: 表示等到一定时间内再执行
*/
function debounce (func, awit, immediate) {
// 定时器返回的变量ID, 返回结果为 result
let timeout, result;
// 定义一个对象 decounced, 方便赋 取消防抖操作 decounced.cancel
let decounced = function () {
// 改变执行函数内部 this 的指向
let context = this;
// 保存函数内部的事件对象
let args = arguments;
// clearTimeout() 方法取消由 setTimeout() 方法设置的定时操作
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, awit);
if (callNow) result = func.apply(context, args);
} else {
// 不会立刻执行
timeout = setTimeout(() => {
func.apply(context, args);
}, awit);
}
return result;
}
// 取消防抖操作
// 增加取消效果(点击取消按钮取消发送请求)
decounced.cancel = function () {
clearTimeout(timeout);
// 当前是闭包,需把 timeout 设置为 null, 防止内存泄露
timeout = null;
}
return decounced;
}
let box = document.querySelector('#box');
let cancelBtn = document.querySelector('#cancel');
let count = 0;
function dosomeing (event) {
console.log(event, '2')
box.innerHTML = count++;
return '想要的结果'
}
// dosome 返回的是一个【decounced 对象】
let dosome = debounce(dosomeing, 300);
// 取消防抖
cancelBtn.onclick = function () {
dosome.cancel()
}
// _.debounce是一个高阶函数,参数也是函数,返回的也是一个函数
box.onmousemove = dosome;
// 这个是调用官网上现存的防抖函数 _.debounce
box.onmousemove = _.debounce(dosomeing, 300, true);
二、节流
1. 定义
固定时间间隔内,函数最多执行 1 次,到时间就触发,无视中间频繁点击
类比:公交固定 10 分钟发一班,哪怕中途挤满人、不停有人上车,没到发车点也不走,到点准时发车。
和防抖一句话区分
防抖:停手等一会再执行(多次触发全合并成 1 次)→ 搜索框输入
节流:固定周期必执行 1 次(匀速限流)→ 滚动监听
2. 应用场景
-
监听页面的滚动事件;窗口
resize缩放 -
鼠标移动事件;
-
用户频繁点击按钮操作;
-
游戏中的一些设计(如飞船发射导弹的频率)
3. underscore 库引入实现节流
<div id="box"></div>
<!-- 这个是官网上封装好的函数,我们可以直接用 -->
<script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
let box = document.querySelector('#box');
let count = 0;
function dosomeing (event) {
box.innerHTML = count++;
}
// 这个是调用官网上现存的防抖函数 _.debounce
/**
* 默认情况下,throttle将在你调用的第一时间尽快执行这个function,并且,如果你在wait周期内调用任意次数的函数,都将尽快的被覆盖。
* 如果你想禁用第一次首先执行的话,传递{leading: false},
* 还有如果你想禁用最后一次执行的话,传递{trailing: false}。
* 但是 leading 和 trailing 同时为 false 的时候,会有 bug
*/
box.onmousemove = _.throttle(dosomeing, 2000, {
trailing: false,
leading: false,
});
4. 定时器实现节流
定时器版本的节流函数具有两个特点:
特点一: n 秒后才会执行第一次(定时器到了时间后才会触发)
特点二:停止触发后节流函数还会执行一次(因为该函数是延迟执行的,当停止触发时其任务已经加入队列中,所以停止后还会执行一次)
定时器实现节流效果图如下:
// 通过定时器实现节流,第一次不执行,最后一次会执行事件函数。
function throttle (func, awit) {
let args, context, timeout;
return function () {
args = arguments;
context = this;
// 当定义器变量为 null 时, 执行该函数
if (!timeout) {
timeout = setTimeout(() => {
// awit 时间周期内,把定时器设置为 null
timeout = null;
func(context, args);
}, awit);
}
}
}
let count = 0;
function dosomeing (event) {
box.innerHTML = count++;
return '想要的结果'
}
let box = document.querySelector('#box');
box.onmousemove = throttle(dosomeing, 2000)
5. 时间戳实现节流
时间戳版本的节流函数具有两个特点:
特点一:开始触发后会立即执行(因为 lastTime 开始会被赋值为0);
特点二: 停止触发后不再执行(因为该函数是同步任务,在触发的时候就会进行相应的判断,所以就不存在停止触发后再执行的情况)。
时间戳实现节流效果:
6. 控制首尾,定时器和时间戳结合
// 通过时间戳和定时器结合实现第一次和最后一次都会执行。
/**
* @param {*} func 要执行的事件函数
* @param {*} awit 时间周期
* @param {*} options 第一次和最后一次是否会执行事件函数
* @returns
*/
function throttle (func, awit, options) {
let args, context, timeout;
let old = 0;
// 如果用户没有传 options 第三个参数,设置为 {}
if(!options) options = {};
let later = function () {
old = new Date().valueOf();
// awit 时间周期内,把定时器设置为 null
timeout = null;
func(context, args);
}
return function () {
args = arguments;
context = this;
let now = new Date().valueOf();
// 第一次不执行并且 之前的时间戳存在,把当前的时间赋值给 old
if(!options.leading && !old) {
old = now;
}
if (now - old > awit) {
// 第一次会直接执行
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func(context, args);
old = now;
} else if (!timeout && options.trailing) {
// 最后一次也会执行
timeout = setTimeout(later, awit);
}
}
}
let count = 0;
function dosomeing (event) {
box.innerHTML = count++;
return '想要的结果'
}
let box = document.querySelector('#box');
box.onmousemove = throttle(dosomeing, 2000, {
// 第一次会执行【leading: true】 , 最后一次会执行【trailing: true】
// 第一次不会执行【leading: false , 最后一次会执行【trailing: true】
// 第一次会执行【leading: true】 , 最后一次不会执行【trailing: false】
// 第一次不会执行【leading: false , 最后一次不会执行【trailing: false】这种情况暂时做不到,跟时间有关系
leading: true,
trailing: false,
})
三 、跨域
1. js 的同源策略
同源策略概念及作用:
概念: 协议、域名、端口三者全部相同 = 同源;任意一个不一样 = 跨域
作用:防止钓鱼网站通过
<iframe>嵌套银行页面,用 JS 窃取页面输入的账号密码;浏览器安全机制,阻止 AJAX 跨域请求、跨域 DOM 读取。
例:
`http://a:80` 和 `https://a:80`(协议不同→跨域)、
`http://a:80`和`http://b:80`(域名不同→跨域)
为什么要有同源限制?
我们举例说明:比如一个黑客程序,他利用 Iframe 把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过 Javascript 读取到你的表单中 input 中的内容,这样用户名,密码就轻松到手了。
// HTML iframe: 行内框架
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
2. vue前端跨域解决方案
为什么会出现跨域:
浏览器访问非同源的网址时,会被限制访问,出现跨域问题.
主流 3 种跨域解决方案
1、JSONP 跨域(前端实现,只支持 GET 请求)
原理:
<script src="地址">标签不受同源限制,动态创建 script 标签请求接口,后端返回回调函数(数据)格式的 JS 代码。
缺点:只能 GET,不能 POST,现在新项目少用。
// JSONP简易写法
function cb(res){console.log(res)}
let script = document.createElement('script')
script.src = 'http://xxx/api?callback=cb'
document.body.appendChild(script)
2、cors跨域, 跨域资源共享【生产最常用,后端配置】:
原理:后端在响应头添加
Access-Control-Allow-Origin放行域名,浏览器放行跨域 AJAX,GET/POST 全支持。
后端配置示例(Node/Java 同理):
// 允许所有域名跨域
res.setHeader('Access-Control-Allow-Origin','*')
// 只放行指定域名
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
3、补充:Nginx 反向代理(服务端解决跨域)
nginx 中转请求,前端请求同域 nginx,nginx 转发到后端接口,浏览器看是同源无跨域。
4、 开发环境:Vue 配置代理 proxy(webpack/vite:
只在本地开发生效,打包上线无效,上线仍需要 CORS/Nginx。
通过请求本地的服务器,然后本地的服务器再去请求远程的服务器(后端部署接口的服务器), 最后本地服务器再将请求回来的数据返回给浏览器 (本地服务器和浏览器之前不存在跨域)
// vue-cli vue.config.js
devServer:{
proxy:{
'/api':{
target:'http://后端真实地址',
changeOrigin:true
}
}
}
vue代理服务器proxy跨域两个关键点:
本地服务器(利用node.js创建的本地服务器进行代理,也叫代理服务器)和浏览器之间不存在 跨域服务器和服务器之间不存在跨域
话不多说,直接上代码:
首先创建一个 vue.config.js文件
// 假设要请求的接口是:http://40.00.100.100:3002/api/user/add
module.exports = {
devServer:{
host:'localhost', // 本地主机
port:5000, // 端口号的配置
open:true, // 自动打开浏览器
proxy:{
'/api': { // 拦截以 /api 开头的接口
//设置你调用的接口域名和端口号 别忘了加http
target: 'http://40.00.100.100:3002',
changeOrigin: true, //这里true表示实现跨域
secure: false, // 如果是https接口,需要配置这个参数
pathRewrite: {
'^/api':'/'
}
},
// 假如又有一个接口是:http://40.00.100.100:3002/get/list/add
// 那就再配置一个 get的,如下:
'/get': { // 拦截以 /get 开头的接口
target: 'http://40.00.100.100:3002',//设置你调用的接口域名和端口号 别忘了加http
changeOrigin: true, //这里true表示实现跨域
secure: false, // 如果是https接口,需要配置这个参数
pathRewrite: {
'^/api':'/' //这里理解成用‘/api’代替target里面的地址,
}
}
// 调用的时候直接 /get/list/add 就可以了
}
}
}
// 注意:修改了配置文件后一定要重启才会生效;
我们可以利用axios的baseUrl直接默认值是 api,这样我们每次访问的时候,自动补上这个api前缀,就不需要我们自己手工在每个接口上面写这个前缀了
在入口文件里面配置如下:
import axios from 'axios'
Vue.prototype.$http = axios
axios.defaults.baseURL = 'api' // 后面发现,其实不加这个感觉也好像可以
如果这配置 'api/' 会默认读取本地的域
如果只是开发环境测试,上面那种就足够了,如果区分生产和开发环境
就需要如下配置分环境配置跨域:
创建一个 api.config.js 文件(其实随便命名都可以)
const isPro = Object.is(process.env.NODE_ENV, 'production')
// 如果是生产环境,就用线上的接口;
module.exports = {
baseUrl: isPro ? 'http://www.vnshop.cn/api/' : 'api/'
}
然后在main.js 里面引入,这样可以保证动态的匹配生产和开发的定义前缀
import apiConfig from './api.config'
Vue.prototype.$http = axios
import axios from 'axios'
axios.defaults.baseURL = apiConfig.baseUrl
经过上面配置后,在dom里面可以这样轻松的访问,也不需要在任何组件里面引入axios模块了
async getData(){
const res = await this.$http.get('/api/user/add');
console.log(res);
},
小结:
代理跨域的主要方式是利用服务器请求服务器的方式避过跨域问题来实现的.大概的流程:
浏览器===>代理服务器===>目标服务器.
3. 最经典的跨域方案
详情:zhuanlan.zhihu.com/p/100529816
一、jsonp
jsonp本质上是一个Hack,它利用<script>标签不受同源策略限制的特性进行跨域操作。
jsonp优点:
实现简单
兼容性非常好
jsonp的缺点:
只支持get请求(因为<script>标签只能get)
有安全性问题,容易遭受xss攻击
需要服务端配合jsonp进行一定程度的改造
jsonp的实现:
function JSONP({
url,
params,
callbackKey,
callback
}) {
// 在参数里制定 callback 的名字
params = params || {}
params[callbackKey] = 'jsonpCallback'
// 预留 callback
window.jsonpCallback = callback
// 拼接参数字符串
const paramKeys = Object.keys(params)
const paramString = paramKeys
.map(key => `${key}=${params[key]}`)
.join('&')
// 插入 DOM 元素
const script = document.createElement('script')
script.setAttribute('src', `${url}?${paramString}`)
document.body.appendChild(script)
}
JSONP({
url: 'http://s.weibo.com/ajax/jsonp/suggestion',
params: {
key: 'test',
},
callbackKey: '_cb',
callback(result) {
console.log(result.data)
}
})
二、最方便的跨域方案Nginx
nginx是一款极其强大的web服务器,其优点就是轻量级、启动快、高并发。
现在的新项目中nginx几乎是首选,我们用node或者java开发的服务通常都需要经过nginx的反向代理。
反向代理的原理很简单,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再
讲请求转发给node或者java服务,这样就规避了同源策略。
三、其它跨域方案
1. HTML5 XMLHttpRequest 有一个API,postMessage()方法允许来自不同源的脚本采用异步方式
进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
2. WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向
对方发送或接收数据,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了,
因此可以跨域。
3. window.name + iframe:window.name属性值在不同的页面(甚至不同域名)加载后依旧存在,
并且可以支持非常长的 name 值,我们可以利用这个特点进行跨域。
4. location.hash + iframe:a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页
面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
4. document.domain + iframe: 该方式只能用于二级域名相同的情况下,
比如 a.test.com 和 b.test.com 适用于该方式,我们只需要给页面
添加 document.domain ='test.com' 表示二级
域名都相同就可以实现跨域,两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
二、浏览器渲染页面、输入网址 url
1. 网页输入网址到渲染完整流程
a. 整体流程
DNS 解析 → TCP 三次握手 → HTTP 请求 → 服务器返回资源 → 浏览器解析渲染页面 → TCP 四次挥手断开连接
b. DNS 域名解析(域名→IP):
大致可以分为如下步骤:
浏览器根据请求的 URL 先去浏览器 DNS 缓存里找 域名 对应的 IP 地址,
没有找到的话去查 操作系统缓存 (hosts 文件) 找 域名 对应的 IP 地址,
最后没有找到的话交给向 DNS 服务器 递归 + 迭代解析,拿到目标服务器真实 IP。向服务器发起请求;
· 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、CSS、JS、images等);
· 浏览器对加载到的资源(HTML、CSS、JS、images等)进行语法解析, 建立相应的内部数据结构(如HTML的DOM);
· 载入解析到的资源文件,渲染页面,完成。
c. TCP 三次握手
(建立连接,浏览器 = 客户端,网站 = 服务端)
通俗比喻:打电话接通
- 第一次握手(客户端→服务端) 客户端向服务端发送请求,等待服务端确认。\
- 第二次握手(服务端→客户端) 服务端收到这个请求,它会进行确认,并且会回复一个指令。\
- 第三次握手(客户端→服务端) 客户端收到服务器的回复指令,并且返回确认,然后服务器就会返回数据给客户端。
注意: tcp 三次握手,他们携带的数据不是正式的数据。
✅ 握手完毕,TCP 通道打通,才能发 HTTP 网页数据。
总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。确保两边收发功能全正常,避免无效连接浪费资源。不然容易出现丢包的现象。
d. HTTP 请求(在已经打通的 TCP 管道里发数据)
浏览器组装请求包:请求地址、请求方式 (get/post)、请求头 (cookie、浏览器信息等),顺着 TCP 通道发给服务器,相当于打通电话后,说出你想要什么页面。
e. 服务器返回资源
后端收到请求,查数据库、拼页面,把 html、css、js、图片打包成响应报文,原路通过 TCP 通道传回浏览器,相当于对方把你要的资料念 / 发给你。
f. 浏览器解析渲染页面
- 拆 HTML → DOM 树(页面骨架)
- 拆 CSS → CSSOM 样式树(衣服样式)
- 两棵树合并:渲染树
- 回流 Layout:算每个盒子大小、位置
- 重绘 Paint:上色、画图片,页面展示出来⚠️ 遇到 JS 会暂停解析,先执行 JS,JS 会改 DOM/CSS。
j. TCP 四次挥手(断开连接,挂电话
在断开连接之前客户端和服务器都处于ESTABLISHED(已连接)状态,双方都可以主动断开连接,以客户端主动断开连接为优。
数据全部传完,两边没事了,断开通道
- 客户端→服务端:我这边发完了,我不再发新数据了(FIN)
- 服务端→客户端:收到,我先把剩下数据发完(ACK)
- 服务端→客户端:我数据也发完了,我也关发送了(FIN)
- 客户端→服务端:收到,正式挂断(ACK)
为啥四次?TCP 全双工,两边都能收发,各自说完才能分别关闭,不能一下挂断。
2. 浏览器是如何渲染 UI 的? (装修房子类比)
-
解析 HTML → DOM 树(毛坯骨架)
拿到 HTML 文本,拆标签,搭出页面所有标签的层级结构,只知道有什么标签,没有样式。
-
解析 CSS → CSSOM 样式表(衣服 / 装修图纸)
解析所有 css,生成样式规则:字体、颜色、宽高、边距。
-
合并生成 RenderTree(装好家具的户型图)
把 DOM 和 CSS 配对,只保留看得见的元素(
display:none的标签不进渲染树)。 -
Layout 回流 / 布局(丈量尺寸、定位置)
挨个算每个盒子:多大、在屏幕哪个 xy 坐标,占多大地方。
改宽高、边距、位置 → 整段重新算布局 = 回流(开销大) -
Paint 重绘(上色画图,屏幕出页面)
按坐标填充颜色、图片、文字,画面正式显示。
只改颜色、背景 → 不用算布局,只上色 = 重绘(开销小)
补充关键坑
遇到<script>JS 标签,暂停 HTML、CSS 解析,优先下载并执行 JS,JS 会改动 DOM/CSS,拖慢渲染。
3. 网址分析
www.baidu.com(大白话)
- URL:统一资源定位符 = 网址全称域名是好记的名字,IP 是服务器门牌号,域名就是 IP 的别名映射。
- https:安全传输协议在 TCP 和 HTTP 中间包了一层 SSL/TLS 加密,传输的数据加密防窃听、篡改;http 是明文不安全。
- www:服务器主机名,代表百度的 WEB 服务主机。
- baidu.com:顶级域名,代表百度这家企业的域名主体。
紧接着访问流程
- DNS 解析:把baidu.com翻译成服务器真实 IP 地址(先查浏览器缓存→本机 hosts→DNS 服务器)
- 拿到 IP → HTTPS 握手(SSL 证书校验、加密协商)
- TCP 三次握手建立连接 → 发送 http 请求拿页面资源
- 服务器返回 html、css、js → 浏览器解析渲染页面
- 数据传完,TCP 四次挥手断开连接
四、性能优化
1. 网页性能优化的方式
1.压缩 css, js, 图片
2.减少 http 请求次数, 合并 css、js 、合并图片(雪碧图)
3.使用 CDN(第三方库,例如图片和文件都引用第三方链接)
4.减少 dom 元素数量
5.图片懒加载
6.静态资源另外用无 cookie 的域名
7.减少 dom 的访问(缓存 dom)
8.巧用事件委托
9.样式表置顶、脚本置低
10. [iPhone开发:iPhone iOS 3.x 与 iOS 4.x 的图片兼容处理]
想必大家都知道,在iOS 4.x上,要兼容iPhone 3Gs和iPhone4 的高清屏幕,只需要做两套图片。
image.png和image@2x.png 然后 UIImage *aImage = [UIImage imageNamed:"imag"];
优化图解:
css 放顶部:
css 在加载过程中不会影响到 DOM 树的生成,但是会影响到 Render 树的生成,进而影响到layout,
所以一般来说,style 的 link 标签需要尽量放在 head 里面,因为在解析DOM 树的时候是自上而下的,而 css 样式又是通过异步加载的,这样的话,解析 DOM 树下的 body 节点和加载 css 样式能尽可能的并行,加快 Render 树的生成的速度。
js放底部:
js 脚本应该放在底部,原因在于 js 线程与 GUI 渲染线程是互斥的关系,
如果 js 放在首部,当下载执行js的时候,会影响渲染行程绘制页面,js 的作用主要是处理交互,而交互必须得先让页面呈现才能进行,所以为了保证用户体验,尽量让页面先绘制出来。
2. 移动端性能优化
1. 尽量使用 css3 动画,开启硬件加速
2. 适当使用 touch 时间代替 click 时间
3. 避免使用 css3 渐变阴影效果
4. 可以用 transform: translateZ(0) 来开启硬件加速
5. 不滥用 float。float 在渲染时计算量比较大,尽量减少使用
4. 不滥用 web 字体。web 字体需要下载,解析,重绘当前页面
6. 合理使用 requestAnimationFrame 动画代替 setTimeout
7. css 中的属性(css3 transitions、css3 3D transforms、opacity、webGL、video)会触发
GUP 渲染,耗电
3. 图片懒加载?
-
监听浏览器的滚动事件,结合 clientHeight、offsetHeight、scrollTop、scrollHeight 等等变量计算当前图片(offsetTop, 当前图片屏幕顶部的距离)是否在可视区域,如果在,则替换 src 加载图片,当然这个滚动事件要主要节流。
-
当页面滚动的时间被触发 -> 执行加载图片操作 -> 判断图片是否在可视区域内 -> 在,则动态将 data-src 的值赋予该图片
-
原生:
src先不放真实地址,用data-src存图片链接;进入视口把data-src赋值给src。 -
HTML 原生属性:
<img loading="lazy">浏览器自动懒加载。
- 图片懒加载源码
<body>
<img src="./loading.gif" data-src="./img_1.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_2.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_3.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_5.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_6.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_7.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_8.jpg" alt="" srcset="">
<img src="./loading.gif" data-src="./img_9.jpg" alt="" srcset="">
</body>
<script>
// 获取图片的数量
let imgLength = document.getElementsByTagName('img').length;
// 获取所有图片的集合
let imgArr = document.getElementsByTagName('img');
lazyImg();
// 监听滚动条事件,后面跟着是一个函数, lazyImg.
window.onscroll = lazyImg;
function lazyImg () {
// 得到当前设置的可视高度
let clientHeight = document.documentElement.clientHeight;
// 得到滚动滚动的距离
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for (var i = 0; i < imgLength; i++) {
// 如果 img[i]到 屏幕顶部的距离 小于屏幕当前可视高度加上滚动条滚动的距离,则说明当前该图片是在可视区范围内
if (imgArr[i].offsetTop < clientHeight + scrollTop) {
// 避免重新赋值
if (imgArr[i].getAttribute('src') === './loading.gif') {
// 把在可视区范围内的图片的src 替换成 真正的 资源 url 路径
imgArr[i].src = imgArr[i].getAttribute('data-src')
}
}
}
}
</script>
4. 数据懒加载
不在页面打开瞬间一次性加载全部图片 / 列表数据,等到元素快要滚动进入可视区域,再去发请求加载资源。 举例:淘宝商品列表,一屏只加载眼前 20 条,往下滑到位置才拉剩下数据。
作用
- 减少首屏请求,加快页面打开速度;
- 节省流量,用户没滑到的图片 / 数据不加载。
实现原理:
(现在主流)IntersectionObserver 监听元素是否进入视口,比监听 scroll 节流性能更好。
实现想法:下拉列表数据懒加载(分页 / 触底加载)
监听滚动,滚动快到底部 → 请求下一页接口,追加列表。
5. 网络安全
dos攻击:恶意的向服务器发起几千万个请求,便会建立几千万个连接,服务器便会奔溃,内存爆掉。
解决办法:网易易盾:先请求第三方
跨站点攻击:招商银行登录了成功了可以转账会带上cookie去转账,a.com 登录成功了会有 cookie, b.com去请求a.com的接口, a.com的接口会自动带上这个cookie,钱就会被转走了
登录成功了服务器会向浏览器设置一个cookie,这个cookie是根据接口的域名来的,不是根据网址的域名来的。
解决办法:后端校验一下refer请求头来源,或者换成 token
跨站脚本攻击: a.com 引用 了 b.com 的 js 链接,这个 b.com 就能读取 a.com 的cookie
解决办法: 给 cookie 设置 httponly: true, 只能http进行传来传去。不可进行 document.cookie来读取值,或者再设置安全同源策略, 请求响应求设置Content-Security-Policy: script-src 'self'
五、浏览器相关
1. 浏览器的主要组成部分是什么
1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,
其他显示的各个部分都属于用户界面。
2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
3. 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,
并将解析后的内容显示在屏幕上。
4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,
而在底层使用操作系统的用户界面方法。
6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范
(HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
2. 常见的浏览器内核有哪些?
主要分成两部分:
渲染引擎(layout engineer或Rendering Engine)和JS引擎。
1. 渲染引擎:
负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示
方式然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的
效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都
需要内核。
2. JS引擎则:
解析和执行javascript来实现网页的动态效果。
最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。
常见内核
IE(内核:trident); internet explorer
火狐浏览器mozilla firefox (内核:gecko)
谷歌浏览器chrome(内核:webkit)
opera浏览器(内核:presto)
3. HTTP 和 HTTPS
1. HTTP 协议通常承载与 TCP 协议之上,在 HTTP 和 TCP 之间添加一个安全协议层
(SSL 或 TSL),这个时候,就成了我们常说的 HTTPS
特点:
原理
HTTP 跑在 TCP 上;HTTPS = HTTP + SSL/TLS 加密层,在 HTTP、TCP 中间多加一层安全加密。
一句话总结
HTTPS 就是加了 SSL 加密、带安全证书的 HTTP,牺牲一点性能换传输安全。
补充:HTTPS 握手流程
浏览器连服务器 → 拿 CA 证书校验真伪 → 协商加密算法 → 生成对称密钥,后续数据全加密收发。
对比
六、token
1. 使用基于token的登录流程
1. 客户端使用用户名跟密码请求登录
2. 服务端收到请求,去验证用户名与密码
3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回
请求的数据
2. 单点登录的三种方式
登录功能通常都是基于 Cookie 来实现的。当用户登录成功后,一般会将登录状态记录到 Session 中,或者是给用户签发一个 Token,然后浏览器将Session 的 ID 或 Token 保存到 Cookie 中,浏览器在之后的每次请求中携带它们。当服务端收到请求后,通过验证 Cookie 中的信息来判断用户是否登录 。
3.单点登录(Single Sign On, SSO)
单点登录指的是:单点登录 SSO:登录一个系统,旗下所有关联子系统免登
1、Session+Cookie(同域名单点)
- 用户登录主站,服务端生成
Session存用户信息,返回SessionID 写入 Cookie; - 同域名所有子项目,浏览器自动带 Cookie;
- 服务端拿 SessionID 查 Session,验证登录。
- 缺点:跨域名 Cookie 不互通,只适合同域名项目
2、Token 方案(前后端分离、跨域常用)
- 登录成功后端生成加密 Token(JWT 居多,自带用户信息)返回前端;
- 前端存
localStorage/Cookie,每次请求在请求头携带 Token; - 后端解密 Token 校验;
- 优点:跨域友好,不用服务端存 Session,分布式首选
3、SSO 认证中心(多域名、多系统通用单点)
- 统一认证中心:所有项目跳转同一个登录页;
- 登录成功,认证中心下发全局凭证,各子系统凭凭证完成登录;
- 例:阿里系淘宝、支付宝、优酷一次登录全平台免登
实现sessionid或者token多域共享主要有三种方式,父域cookie、认证中心、localstorage
八、JSX
什么是JSX:
JSX 是 JS 扩展语法,JS 内嵌 HTML,打包编译为原生 JS,代码直观、编译期报错、性能更好。
打包编译阶段,Babel 自动把 JSX 转成原生 JS (createElement) ,浏览器只执行普通 JS,无运行副作用。
// JSX写法:JS中直接写html let dom = <div>你好</div>
JSX特点:
执行效率高:编译时提前优化 JS 代码,比原生拼接 DOM 更快 类型安全:编译阶段校验语法,写错标签、属性打包直接报错,不用等到运行出错
编码简洁:JS 和页面标签写在一起,不用拼接字符串,写模板更快、可读性强
九、腾讯云 api 自动发布 + 刷新 CDN
腾讯云 通过API实现自动发布并刷新 CDN 缓存的功能,主要可通过以下两种方式实现:
1、CI/CD 工具触发(Jenkins/GitLabCI)
项目打包构建完,流水线里调用腾讯云 CDN 刷新 API,构建成功自动清空 CDN 旧缓存,一般 5~10 分钟全网节点生效。
2、使用 版本Git控制工具 自动同步
通过 Git** 的Webhook功能关联CDN刷新操作。当代码库发生变更时,自动触发CDN缓存刷新请求,适合频繁迭代更新。
具体实现步骤:
-
登录腾讯云后台,获取 CDN 调用秘钥 (SecretId/SecretKey)
-
在 Jenkins/GitLabCI 或接口脚本里接入腾讯 CDN 刷新 API
-
设置触发时机:代码提交 / 项目打包完成后自动执行刷新
该方案适用于动态内容更新频繁的场景,可显著提升网站响应速度与用户体验
作用
前端资源更新后自动清 CDN 缓存,用户立刻访问最新页面,不用手动后台点刷新,提升页面加载速度。
十、 采用虚拟表格渲染优化大数据表格展示卡顿问题
采用虚拟表格渲染技术可有效解决大数据表格展示卡顿问题。该技术通过仅渲染可视区域内的数据,动态加载和销毁DOM节点,显著减少内存占用和渲染压力。
1. 核心原理
不是一次性渲染全量 DOM,只渲染当前屏幕 + 缓冲区里能看见的行滚动表格时:
- 划出可视区的行 → 销毁 DOM
- 新滚进屏幕的行 → 动态生成 DOM靠顶部 / 底部空白占位元素撑起整体高度,保证滚动条长度和全量列表一致,DOM 数量永远维持几十条,解决上万条数据 DOM 过多造成页面卡顿。
2、三步实现
- 计算可视行数:容器高度 ÷ 单行高度 = 一屏能展示多少行(例:容器 1000px、行高 50px,一屏 20 行)
- 可视区渲染 + 占位:只渲染当前区间数据;用空白 div 占位,维持列表总高度、滚动条正常
- 增设缓冲区:上下各预留 5~10 行缓冲数据,避免滚动瞬间频繁创建销毁 DOM、闪屏
3、适用场景
- ✅ 数据>5000 行:上万 / 百万级大数据表格,必用虚拟列表优化
- ❌ 数据<5000 行:直接全量渲染即可,虚拟列表反而增加额外计算开销
4. 总结
虚拟列表通过按需渲染可视区域 DOM、动态增删节点 + 占位撑开滚动高度,大幅减少 DOM 数量,优化大数据表格滚动卡顿。
十、前端渲染10000条数据
1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)
原理
首屏只渲染一页数据,滚动滑到列表底部时,再请求下一页接口、追加渲染数据,不用一次性渲染 10000 条 DOM,减少首屏卡顿。
判断触底两种方案
- 原生
getBoundingClientRect(图中标注)在列表最底部放占位 DOM,通过>dom.getBoundingClientRect().top ≤ 可视区clientHeight,占位进入可视区 = 触底,触发下一页加载。- 新方案:
IntersectionObserver监听底部占位,进入视口自动加载,比监听 scroll + 节流性能更好。
实现逻辑
- 初始化:请求第 1 页数据,渲染首屏列表;
- 监听滚动 / 监听底部占位元素;
- 滚动到底部 → 请求下一页,数据拼接到原有列表末尾;
- 全部数据加载完毕,关闭加载监听。
和虚拟列表区别
懒加载分页:数据分批拉取、DOM 越来越多(一万条最终 DOM 还是上万),适合几千~一万条; 虚拟列表:一次性拿全数据、DOM 永远只保持一屏几十条,十万 + 超大列表首选。
总结: 下拉懒加载靠监听底部占位元素是否进入可视区,触底分页拉新数据,分批渲染优化首屏卡顿。
2. 虚拟长列表 + 分片渲染
虚拟列表核心原理
- 只渲染可视区数据:上万 / 百万条数据一次性全拿到,但屏幕外的行一律不生成 DOM;
- 用 padding 撑高度:靠列表容器的
padding-top / padding-bottom撑开盒子高度,让滚动条长度和全量列表一模一样; - 滚动动态替换 DOM:滑出屏幕的 DOM 销毁、新进入视口的 DOM 新建,页面 DOM 数量永远只有一屏几十条。
- 备注:
antd4.0+ Select下拉框内置虚拟滚动,大数据下拉不卡顿
setTimeout 分片处理百万数据(multistep 函数)
作用
一次性循环百万数据会阻塞 JS 主线程、页面卡死;利用setTimeout(25ms)把大批量任务拆分,浏览器一帧只做一小段任务,剩余丢到下一个宏任务,不阻塞渲染。
代码逻辑
// 把渲染任务切成多段,每隔 25ms 执行一段,**浏览器中间有空隙渲染页面,不会白屏卡死**。
function multistep(steps,args,callback){
var tasks = steps.concat() // 拷贝任务数组
setTimeout(function(){
var task = tasks.shift() // 取出第一个任务执行
task.apply(null, args || [])
if(tasks.length>0){
// 还有任务,25ms后继续执行下一个
setTimeout(arguments.callee,25)
}else{
callback() // 全部执行完走回调
}
},25)
}
两种大数据优化区分
- 下拉懒加载(分页) :分批从后端拿数据,DOM 随下拉越来越多;
- 虚拟列表:一次性拿全数据,DOM 始终保持少量,百万级最优;
- 分片渲染:前端拿到巨量全量数据后,用定时器拆分渲染,防止首次渲染卡死。
总结:
虚拟列表靠可视区渲染 + padding 占位控 DOM 数量;
海量数据首次渲染用setTimeout分片拆分任务,避免 JS 阻塞。
六、虚拟 DOM
1. 虚拟 DOM 是什么
短时间连续改 10 次 DOM,不立刻碰真实页面 DOM,所有修改先存在 JS 对象(虚拟 DOM)里,最后对比差异,只一次性更新一次真实 DOM,减少反复操作 DOM 的损耗。
2. 虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
-
JS 对象模拟 DOM(造替身) 用普通 JS 对象仿照页面 DOM 结构,把标签名、属性、子节点写成对象,这个对象就是虚拟 DOM,不碰真实页面。
-
diff 算法(找差别)数据更新后生成新的虚拟 DOM 树,拿新、旧两棵虚拟 DOM 对比,找出哪里改了、删了、新增了,只记录变化部分。
-
**patch 补丁(改真实页面)**拿着 diff 找到的改动记录,只去真实 DOM 上修改对应节点,没变化的 DOM 一概不动。
3. 虚拟 DOM 的优缺点?
✅ 优点:
-
保底性能手动频繁删改 DOM 容易写崩性能;虚拟 DOM 统一算法优化,再差也不会烂,下限稳,不用自己手动优化 DOM。
-
不用手动操作 DOM只改数据,框架靠虚拟 DOM 自动更新页面,少写 DOM 操作代码,开发快。
-
跨平台好用真实 DOM 只在浏览器才有,虚拟 DOM 就是 JS 对象:能用于 SSR 服务端渲染、小程序、App (Weex) 多端复用一套逻辑。
❌ 缺点
- 没法极致性能优化框架为了通用兼容所有场景,代码做不到针对性定制优化;极致高性能场景,原生手写 DOM 比虚拟 DOM 更快。
- 超大列表首次渲染更慢大批量 DOM 一次性插入时,多了一层虚拟 DOM 对比计算,速度不如直接
innerHTML。
总结
虚拟 DOM = JS 替身 DOM,批量合并 DOM 修改、省心好跨端,但极致性能不如原生 DOM。