一、防抖
函数节流: 是指一定时间内 js 方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。
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. 定义
节流指一定时间内 js 方法会执行一次,即执行函数的频率是固定的。类似于10 分钟一趟的公交车,不管10 分钟内有多少人在公交站等,10 分钟一到就会按时发车
2. 应用场景
-
监听页面的滚动事件;
-
鼠标移动事件;
-
用户频繁点击按钮操作;
-
游戏中的一些设计(如飞船发射导弹的频率)
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 的同源策略
概念:
同源策略是客户端脚本(尤其是Netscape Navigator2.0,其目的是防止某个文档或脚本从多个不同源装载。这里的同源策略指的是: 协议,域名,端口 相同,同源策略是一种安全协议。指一段脚本只能读取来自同一来源的窗口和文档的属性。
为什么要有同源限制?
我们举例说明:比如一个黑客程序,他利用 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前端跨域解决方案
为什么会出现跨域:
浏览器访问非同源的网址时,会被限制访问,出现跨域问题.
常见的跨域有三种:
1.jspn跨域,
原理:动态生成script标签,通过script标签引入接口地址(因为script标签不存在跨域的)
2.cors跨域(后端开启) :
全称 “跨域资源共享”,原理:它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制
3.vue代理服务器proxy跨域:
通过请求本地的服务器,然后本地的服务器再去请求远程的服务器(后端部署接口的服务器),
最后本地服务器再将请求回来的数据返回给浏览器(本地服务器和浏览器之前不存在跨域)
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. 图解如下:
b. 回答一:
大致可以分为如下7步:
浏览器根据请求的 URL 先去
浏览器 DNS 缓存里找 域名 对应的 IP 地址,
没有找到的话去操作系统 DNS 缓存 里找 域名 对应的 IP 地址,
再没有找到去 hosts 文件里找 域名 对应的 IP 地址,
最后没有找到的话交给 DNS(域名解析服务器) 进行域名解析,
找到真实 IP 地址,
向服务器发起请求;
· 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、CSS、JS、images等);
· 浏览器对加载到的资源(HTML、CSS、JS、images等)进行语法解析,
建立相应的内部数据结构(如HTML的DOM);
· 载入解析到的资源文件,渲染页面,完成。
c. TCP 三次握手
1.客户端向服务端发送请求,等待服务端确认。
2.服务端收到这个请求,它会进行确认,并且会回复一个指令。
3.客户端收到服务器的回复指令,并且返回确认,然后服务器就会返回数据给客户端。
注意:tcp 三次握手,他们携带的数据不是正式的数据。
总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。
cc. 四次挥手
在断开连接之前客户端和服务器都处于**
ESTABLISHED(已连接)状态**,双方都可以主动断开连接,以客户端主动断开连接为优。
d. 回答二:
先阅读这篇科普性质的:从 URL 输入到页面展现到底发生什么? 先阅读篇文章:从输入 URL 开始建立前端知识体系。
- 输入 URL 后解析出协议、主机、端口、路径等信息,并构造一个 HTTP 请求。
- 强缓存。
- 协商缓存。
-
DNS 域名解析。(字节面试被虐后,是时候搞懂 DNS 了)
-
TCP 连接。
总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。
-
http 请求。
-
服务器处理请求并返回 HTTP 报文。
-
浏览器渲染页面。
- 断开 TCP 连接。
2. 浏览器是如何渲染 UI 的?
1.浏览器获取 HTML 文件,然后对文件进行解析,形成 DOM Tree
2.与此同时,进行 CSS 解析,生成 css 结构体
3.接着将 DOM Tree 与 css 结构体合成为 Render Tree
4.接着进入布局(Layout)阶段,计算布局信息,也就是为每个节点分配一个应出现在屏幕上的确切坐标
5.调用 UI 引擎去渲染, 遍历Render Tree的节点,并将元素呈现出来, 最后成了我们所看到的页面。
3. 网址分析
三、 三次握手和四次挥手
1. 状态
listen: 侦听来自远方TCP端口的连接请求;
syn-send: 在发送连接请求后等待匹配的连接请求;
syn-received: 在收到和发送—个连接请求后等待对连接请求的确认;
established: 代表一个打开的连接,数据可以传送给用户;
fin-wait-1: 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
fin-wait-2: 从远程TCP等待连接中断请求;
close-wait: 等待从本地用户发来的连接中断请求;
closing: 等待远程TCP对连接中断的确认;
last-ack: 等待原来发向匹程TCP的选按中酊月水的确认;
time-wait: 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
close: 没有任何连接状态;
2. 为什么需要三次握手,两次不可以吗
第一次握手,客户端发送网络包,服务端收到了,这样服务端确定了客户端的发送能力是正常的;\
第二次握手,服务端发包,客户端收到了,这样客户端确定了服务端的接收、发送能力时正常的。但是此时服务端不能确定客户端的接收能力;\
于是第三次握手,客户端发包,服务器收到,服务器确认了客户端的接收发送能力是正常的。\
因此,需要三次握手才能确认双方的接收与发送能力是否正常。
3. 三次握手过程
刚开始客户端处于closed状态,服务端处于listen状态。
第一次握手:客户端给服务端发送一个SYN报文,并指明客户端的初始化序列号SN(seq=x),之后客户端处于SYN-SEND状态。
第二次握手:服务端收到客户端的SYN报文后,返回自己的SYN,并且指定了自己的初始化序列号ISN(seq=y),同时会把客户端的ISN+1(x+1)作为ACK的值,表示自己已经收到了客户端的SYN,这时服务器处于SYN-RECIVED状态。
第三次握手:客户端收到SYN报文后,会发送一个ACK报文(y+1),并且指定当前序列号(x+1),也是把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISED状态(established: 代表一个打开的连接,数据可以传送给用户)。
服务器收到ACK报文后,也处于ESTABLISED状态,此时,双方建立的连接。
4. 三次握手的作用
确认双方的接收与发送能力
指定自己的初始化序列号,为后面的可靠传输做准备
5.三次握手过程中可以携带数据吗
**第三次握手的时候,是可以携带数据的。**但是,第一次、第二次握手绝对不可以携带数据假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发 SYN 报文的话(因为攻击者根本就不用管服务器的接收、发送能力是否正常,它就是要攻击你),这会让服务器花费很多时间、内存空间来接收这些报文。
简单的记忆就是,请求连接/接收 即 SYN = 1的时候不能携带数据
而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以当然能正常发送/携带数据了。
6. 四次挥手
刚开始双方都处于ESTABLISED状态,假如是客户端先发起关闭请求
1)第一次挥手:客户端发送一个 FIN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。此时客户端处于 FIN_WAIT1 状态,等待服务端的确认。
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
2)第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
CLOSE-WAIT - 等待从本地用户发来的连接中断请求;此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待 2)状态,等待服务端发出的连接释放报文段。
FIN-WAIT-2 - 从远程TCP等待连接中断请求;
3)第三次挥手:如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,等待客户端的确认。
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
4)第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值(seq=u+1),此时客户端处于 TIME_WAIT(时间等待)状态。
TIME-WAIT - 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
7. 为什么要四次挥手
于 TCP 的半关闭(half-close)特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
举个例子:A 和 B 打电话,通话即将结束后,A 说 “我没啥要说的了”,B 回答 “我知道了”,于是 A 向 B 的连接释放了。但是 B 可能还会有要说的话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,于是 B 向 A 的连接释放了,这样整个通话就结束了。
四、性能优化
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 的值赋予该图片
-
图片懒加载源码
<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. 数据懒加载
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
详情:www.cnblogs.com/sunlong88/p…
1. HTTP 协议通常承载与 TCP 协议之上,在 HTTP 和 TCP 之间添加一个安全协议层
(SSL 或 TSL),这个时候,就成了我们常说的 HTTPS
特点:
1.`HTTP` 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
2.`HTTP` 是不安全的,而 HTTPS 是安全的
3.`HTTP` 标准端口是80 ,而 HTTPS 的标准端口是443
4.`在OSI` 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
5.`HTTP` 无法加密,而HTTPS 对传输的数据进行加密,证的网络协议,安全性高于HTTP协议。
6.`HTTP`无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书,一般免费证书少,因而需要一定费用
六、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 中的信息来判断用户是否登录 。
2.单点登录(Single Sign On, SSO)
单点登录指的是:用户只需登录一次,就可访问同一帐号平台下的多个应用系统。
实现sessionid或者token多域共享主要有三种方式,父域cookie、认证中心、localstorage
实现方式
(1)父域cookie共享机制(适用于同域系统)
原理:将Cookie的
domain属性设置为父域名(如example.com),path设置为根路径(/),这样所有子域名(如app1.example.com、app2.example.com)均可共享该Cookie。用户登录主应用后,子应用通过父域Cookie验证身份。特点:简单易实现,但仅支持同域下的系统,且依赖浏览器对父域Cookie的支持。
基于父域Cookie共享机制 缺点:不支持跨主域名
认证中心模式(适用于跨域系统)
- 原理:部署独立的认证中心(如CAS Server),用户统一在认证中心登录,认证中心生成Token(如Service Ticket或JWT)并返回给子系统。子系统通过验证Token确认用户状态。2
- 特点:支持跨域系统,安全性较高,但需要维护独立的认证中心服务。2 可以部署一个认证中心,用来专门负责处理登录请求。
检查token:用户访问某个应用系统,应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统未登录,跳转至认证中心。认证中心根据 Cookie (认证中心的)判断用户是否在其他应用系统已登录。
返回url:如果发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已登录,会跳转回目标 URL ,让token拼接在目标 URL 的后面,回传给目标应用系统。
验证token:应用系统拿到 Token 之后,还需要向认证中心验证Token ,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,直接返回正常的响应。
分布式Session或Redis共享机制
- 原理:将Session信息存储在Redis等集中式存储中,多个应用通过连接Redis共享Session数据。用户登录后,Session ID被序列化存储在Redis中,子系统通过Redis查询Session状态。34
- 特点:适用于微服务架构,但需注意Redis的单点故障风险及数据一致性问题。34
总结:选择实现方式需结合系统架构(同域/跨域)、安全性需求及维护成本。例如,微服务架构推荐使用分布式Session或Token机制;传统Web应用可采用父域Cookie或认证中心模式
八、JSX
什么是JSX:JSX=javascript xml就是Javascript和XML结合的一种格式。是 JavaScript 的语法扩展,只要你把HTML代码写在JS里,那就是JSX。
在实际开发中,JSX 在产品打包阶段都已经编译成纯 JavaScript,不会带来任何副作用,反而会让代码更加直观并易于维护。官方定义是:类 XML 语法的 ECMAScript 扩展。
JSX特点:
JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。 它是类型安全的,在编译过程中就能发现错误。
使用 JSX 编写模板更加简单快速。
九、腾讯云api实现自动发布和刷新cdn功能
腾讯云** 通过API实现自动发布并刷新 CDN** 缓存的功能,主要可通过以下两种方式实现:
结合 CI/CD工具** 自动刷新
在代码构建或发布流程中集成腾讯云CDN API接口,通过 Jenkins** 、 GitLab CI** 等工具触发缓存刷新。例如,当代码更新后自动调用API强制节点缓存过期,通常5-1 0分钟生效。 12
使用 版本控制工具** 自动同步
通过 Git** 的Webhook功能关联CDN刷新操作。当代码库发生变更时,自动触发CDN缓存刷新请求,适用于持续更新的场景。 13
具体实现步骤:
- 在腾讯云CDN控制台获取API密钥
- 配置CI/CD工具调用API接口(参考腾讯云官方文档)
- 设定触发条件(如代码提交、构建完成等) 14
该方案适用于动态内容更新频繁的场景,可显著提升网站响应速度与用户体验
九、 采用虚拟表格渲染优化大数据表格展示卡顿问题
采用虚拟表格渲染技术可有效解决大数据表格展示卡顿问题。该技术通过仅渲染可视区域内的数据,动态加载和销毁DOM节点,显著减少内存占用和渲染压力。 12
核心原理
虚拟表格通过限制浏览器渲染的DOM节点数量实现性能优化。当滚动时,仅更新可视区域内的数据,而非整个数据集。例如,当用户滚动时,只有当前屏幕显示的行会被渲染,离开视口的行会被销毁,新进入视口的行被动态生成。 13
实现步骤
- 计算可视区域行数:根据容器高度和行高计算当前可视区域可容纳的行数(如容器高度为1000px,行高50px,则可视区域可容纳20行)。
- 动态渲染数据:仅渲染可视区域内的数据行,并创建占位元素维持滚动条长度。 3
- 缓冲区域控制:通常设置5-10行缓冲区域,避免滚动时频繁重绘DOM节点。 45
适用场景
适用于数据量超过5000行的场景,可流畅处理万级至百万级数据。若数据量小于5000行,直接渲染更高效。
十、前端渲染10000条数据
1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)
懒加载和分页方式一般用于做长列表优化, 类似于表格的分页功能, 具体思路就是用户每次只加载能看见的数据, 当滚动到底部时再去加载《未完待续》的数据.
懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下:
这里我们通过监听 window 的 scroll 事件以及对 poll 元素使用getBoundingClientRect 来获取 poll 元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.
function scrollAndLoading() {
if(window.scrollY > prevY) { // 判断用户是否向下滚动
prevY = window.scrollY
if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
// 请求《未完待续》数据
}
}
}
useEffect(() => {
// something code
const getData = debounce(scrollAndLoading, 300)
window.addEventListener('scroll', getData, false)
return () => {
window.removeEventListener('scroll', getData, false)
}
}, [])
其中prevY存储的是窗口上一次滚动的距离, 只有在向下滚动并且滚动高度大于上一次时才更新其值.
至于分页的逻辑, 原生javascript实现分页也很简单, 我们通过定义几个维度:
- curPage当前的页数
- pageSize 每一页展示的数量
- data 传入的数据量
有了这几个条件,我们的基本能分页功能就可以完成了. 前端分页的核心代码如下:
let data = [];
let curPage = 1;
let pageSize = 16;
let prevY = 0;
// other code...
function scrollAndLoading() {
if(window.scrollY > prevY) { // 判断用户是否向下滚动
prevY = window.scrollY
if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
curPage++
setList(searchData.slice(0, pageSize * curPage))
}
}
}
2. 虚拟长列表
使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)
虚拟滚动技术也可以用来优化长列表, 其核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容,实现思路也非常简单.
第一个点我们可以使用 js 缓冲器来分片处理 100 万条数据, 思路代码如下:
function multistep(steps,args,callback){
var tasks = steps.concat();
setTimeout(function(){
var task = tasks.shift();
task.apply(null, args || []); //调用Apply参数必须是数组
if(tasks.length > 0){
setTimeout(arguments.callee, 25);
}else{
callback();
}
},25);
}
六、虚拟 DOM
1. 虚拟 DOM 的优缺点?
虚拟 DOM:
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟
DOM 不会立即操作 DOM ,而是将这 10 次更新的diff内容保存到本地一个JS 对象中,最终将这个 JS
对象一次性 attch 到 DOM 树上,再进行后续操作,避免大量无谓的计算量。
所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在JS 对象(虚拟 DOM )上,
操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,
交由浏览器去绘制
优势:
1. 保证性能下限:
框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是
普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟
DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
2. 无需手动操作 DOM:
我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据
双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
3. 跨平台:
虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便
地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
1. 无法进行极致优化:
虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中
虚拟 DOM 无法进行针对性的极致优化。
首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML插入慢。
2. 虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
1. 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
2. diff 算法 — 比较两棵虚拟 DOM 树的差异;
3. pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。