基础知识
js事件流
- 一个完整事件流从window开始,最后回到window 的过程
- 事件流分为三个阶段:捕获阶段、目标过程、冒泡阶段
跨域
- 同源策略
- 同源:协议+域名+端口 三者相同 限制一下行为
- cookie、LocalStorage和IndexDB无法读取
- Dom和js对象无法获得
- Ajax无法发送 有三个标签是允许跨域加载资源
<img src><link href=xxx><script src=xxx>img,link,script 都可以跨域 比如引入外部地址,cdn的库 img 可用于统计打点,可使用第三方统计服务
- 解决方案
- jsop跨域
function jsonp() { let script = document.createElement('script') script.type = 'text/javascript' script.src = `http://www.baidu.com?callback=onBack` document.body.appendChild(script) function onBack() { alert('执行onBack') } } jsonp() </script>- document.domain + iframe跨域
两个页面都通过js强制设置
document.domain为基础主域,就实 现了同域
var user='parent' document.domain='domain.com' // iframe 获取父元素 window.parent.user- postMessage 跨域
weakMap
class MyTest{};
let my =new MyTest(); // 对象
let map = new WeakMap(); // key 只能是对象
let map2 = new Map();
map.set(my,1);
my = null; // 当你给一个变量设置为null的时候 不会马上回收,
map 运用的时候,不会被回收掉,weakMap引用的对象被设置null,后续会被清空。
compose函数
function compose(...fns) {
return function (...args) {
const lastFn = fns.pop();
const r = lastFn(...args);
return fns.reduceRight((prev, current) => {
return current(prev)
}, r)
}
}
发布订阅模式
const util = require('util')
function EventEmitter() {
this._event = {}
}
EventEmitter.prototype.on = function (eventname, callback) {
if (!this._event) {
this._event = {}
}
if (this._event[eventname]) {
this._event[eventname].push(callback)
} else {
this._event[eventname] = [callback]
}
}
EventEmitter.prototype.emit = function (eventname, ...arg) {
this._event[eventname].forEach((callback) => callback(...arg))
}
EventEmitter.prototype.off = function (eventname, callback) {
if (this._event && this._event[eventname]) {
this._event[eventname] = this._event[eventname].filter(
(x) => x !== callback && x.l !== callback
)
}
}
EventEmitter.prototype.once = function (eventname, callback) {
const one = () => {
callback()
this.off(eventname, on)
}
one.l = callback
this.on(eventname, one)
}
项目实战
后端一次丢给你10万条数据,前端你要怎么处理?
- 前端懒加载+分页
import React, { useRef } from 'react';
import { debounce } from 'lodash'
import './App.less';
function App() {
const prevY = useRef(0)
const poll = useRef();
let arr = [];
for (let i = 0; i < 100000; i++) {
arr.push({
name: `name${i}`,
title: `title${i}`,
text: `text${i}`,
uid: `uid${i}`
})
}
let curPage = 1;
let pageSize = 16;
const [list, setList] = React.useState([]);
function scrollAndLoading() {
// 判读用户向下滚动
if (window.scrollY > prevY.current) {
prevY.current = window.scrollY
const pollTop = poll.current.getBoundingClientRect().top - 25;
if (pollTop < window.innerHeight) {
curPage++;
setList(arr.slice(0, pageSize * curPage))
}
}
}
React.useEffect(() => {
poll.current = document.querySelector('#poll');
const getData = debounce(scrollAndLoading, 300);
window.addEventListener('scroll', getData, false)
setList(arr.slice(0, 16))
return () => {
window.removeEventListener('scroll', getData, false)
}
}, [])
Axios取消重复请求
import axios, { AxiosRequestConfig } from 'axios';
const pendingRequest = new Map();
function generateRegKey(config){
const {method,url,params,data} = config;
return [method,url,JSON.stringify(params),JSON.stringify(data)].join("&")
}
function addPendingRequest(config:AxiosRequestConfig){
const requestKey = generateRegKey(config);
config.cancelToken = new axios.CancelToken((cancel)=>{
if(!pendingRequest.has(requestKey)){
pendingRequest.set(requestKey,cancel)
}
})
}
function removePendingRequest(config){
const requestKey = generateRegKey(config);
if(pendingRequest.has(requestKey)){
const cancelToken = pendingRequest.get(requestKey);
cancelToken(requestKey);
pendingRequest.delete(requestKey)
}
}
axios.interceptors.request.use(function(config){
removePendingRequest(config);
addPendingRequest(config)
},(err)=>{
return Promise.reject(err)
});
axios.interceptors.response.use((response)=>{
removePendingRequest(response.config);
return response;
},(error)=>{
removePendingRequest(error.config ||{});
if(axios.isCancel(error)){
console.log('已取消重复请求'+ error.message)
}else{
console.log('进行异常处理')
}
return Promise.reject(error)
})
前端大文件分片上传&断点续传
- 文件根据内容生成hash值
self.importScripts('https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js');
self.onmessage = (e) => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
console.log(spark, 'spark')
let percentage = 0;
let count = 0;
const loadNext = (index) => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunkList[index].file)
reader.onload = (e) => {
count++
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end(),
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
loadNext(count)
}
}
}
loadNext(0)
}
- vue示例相关代码
<template>
<div class="hello">
<input type="file" @change="handleFileChange" />
<el-button @click="handleUpload">上传</el-button>
<el-button @click="handleMerge">合并</el-button>
<el-button @click="handleFast">秒传</el-button>
<el-button @click="handlePause">暂停</el-button>
</div>
</template>
<script>
const SIZE = 10 * 1024 * 1024; // 10MB切片
import request from "../api/request";
export default {
name: "Home",
props: {
msg: String,
},
data: () => ({
container: {
file: null,
},
data: [],
requestList: [],
}),
computed: {
uploadPercentage() {
if (!this.container.file || !this.data.length) return 0;
const loaded = this.data
.map((item) => item.size * item.percentage)
.reduce((acc, cur) => acc + cur);
console.log(
parseInt((loaded / this.container.file.size).toFixed(2)),
"loaded=="
);
return parseInt((loaded / this.container.file.size).toFixed(2));
},
Worker,
},
methods: {
handlePause() {
console.log("暂停");
this.requestList.forEach((xhr) => {
console.log(xhr, "xhr===");
xhr?.abort();
});
this.requestList = [];
},
handleFileChange(e) {
const [file] = e.target.files;
Object.assign(this.$data, this.$options.data());
this.container.file = file;
},
// 生成断点续传的hash
calculateHash(fileChunkList) {
return new Promise((resolve) => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = (e) => {
const { percentage, hash } = e.data;
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({
file: file.slice(cur, cur + size), // 调用Blob上方法切割字节 返回Blob
});
cur += size;
}
return fileChunkList;
},
// 文件秒传:服务器上已上传相同文件对比hash 相同则直接提示成功
async verifyUpload(filename, fileHash) {
const result = await request({
url: "http://localhost:3000/verify",
headers: {
"content-type": "application/json",
},
data: JSON.stringify({
filename,
fileHash,
}),
});
console.log(result, "ssss===");
// return JSON.parse(data);
},
//上传切片
async uploadChunks() {
const requestList = this.data
.map(({ chunk, hash, index }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData, index };
})
.map(async ({ formData, index }) =>
request({
url: "http://localhost:3000/upload",
data: formData,
requestList: this.requestList,
onProgress: this.createProgressHandler(this.data[index], index),
})
);
await Promise.all(requestList); // 并发切片
// 合并切片
// await this.mergeRequest();
},
createProgressHandler(item, index) {
return (e) => {
item.percentage = parseInt(String((e.loaded / e.total) * 100));
this.data.splice(index, 1, item);
};
},
async handleMerge() {
await this.mergeRequest();
},
async handleFast() {
if (!this.container.file) return;
// 获取切片列表
const fileChunkList = this.createFileChunk(this.container.file);
this.container.hash = await this.calculateHash(fileChunkList);
// 文件秒传 看是否上传过
const result = await this.verifyUpload(
this.container.file.name,
this.container.hash
);
console.log(result, "result");
// if (!shouldUpload) {
// alert("上传成功");
// return;
// }
},
async handleUpload() {
if (!this.container.file) return;
// 获取切片列表
const fileChunkList = this.createFileChunk(this.container.file);
this.container.hash = await this.calculateHash(fileChunkList);
// 文件秒传 看是否上传过
// const { shouldUpload } = await this.verifyUpload(
// this.container.file.name,
// this.container.hash
// );
// if (!shouldUpload) {
// alert("上传成功");
// return;
// }
this.data = fileChunkList.map(({ file }, index) => {
return {
fileHash: this.container.hash,
percentage: 0,
chunk: file,
index,
hash: this.container.hash + "-" + index, // 文件名 + 数组下标
};
});
console.log("upLoad==");
await this.uploadChunks();
},
async mergeRequest() {
await request({
url: "http://localhost:3000/merge",
headers: {
"content-type": "application/json",
},
data: JSON.stringify({
filename: this.container.file.name,
size: SIZE,
}),
});
},
},
};
</script>
- request 相关封装
const request = ({
url,
method = "post",
data,
headers = {},
onProgress = e => e,
requestList,
}) => {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = onProgress
xhr.open(method, url);
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key])
})
xhr.send(data);
xhr.onload = (e) => {
// 将请求成功的xhr 从列表中删除
if (requestList) {
const xhrIndex = requestList.findIndex(item => item === xhr)
xhrIndex > -1 && requestList.splice(xhrIndex, 1);
}
resolve({
data: e.target.resolve
})
};
requestList?.push(xhr)
})
}
export default request;
判断文件的上传类型
1.通过文件头信息判断文件类型 常见文件头部:
- jpeg(jpg):FFDBFF;PNG(png):89504E47;gif:47494638
- psd:38425053;pdf:255044462D312E
async function blobToString(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function () {
const res = reader.result
.split("")
.map((v) => v.charCodeAt(v))
.map((v) => v.toString(16).toUpperCase())
.map((v) => v.padStart(2, "0"))
.join(" ");
resolve(res);
};
reader.readAsBinaryString(blob);
});
}
async function isJpg(file) {
const res = await blobToString(file.slice(0, 3))
return res === 'FF DB FF'
}
async function isPng(file) {
const res = await blobToString(file.slice(0, 4))
return res === '89 50 4E 47'
}
async function isGif(file) {
const res = await blobToString(file.slice(0, 4))
return res === '47 49 46 38'
}
vue长列表虚拟滚动技术
虚拟滚动,根据容器可视区域的列表数量,监听用户滑动或滚动事件,动态截取长列表数据中的部分数据渲染到页面上,动态使用空白占位填充容器上下滚动区域内容,模拟实现原生滚动效果。
- VScroll插件
<template>
<div
class="scroll-container"
ref="scrollConainer"
@scroll.passive="handleScroll"
>
<div :style="blankFill">
<div class="listItem" v-for="(item, index) in showDataList" :key="index">
<slot :thisItem="item" />
</div>
<div v-if="isRequestStatus">
<h2>{{ msg }}</h2>
</div>
</div>
</div>
</template>
<script>
export default {
name: "index",
props: {
msg: {
default: () => "小二正在努力,请等待。。。",
type: String,
},
// 记录单条数据的高度
oneHeight: {
default: () => 100,
type: Number,
},
// 请求数据的url
requestUrl: {
default: () => "http://192.168.1.107:4000/data?num=",
type: String,
},
// 请求数据的条数
requestNum: {
default: () => 10,
type: Number,
},
},
data() {
return {
allDataList: [],
containSize: 0,
startIndex: 0,
isRequestStatus: true,
currentScrollTop: 0,
};
},
activated() {
this.$nextTick(() => {
this.$refs.scrollConainer.scrollTop = this.currentScrollTop;
});
},
computed: {
endIndex() {
let endIndex = this.startIndex + this.containSize;
if (!this.allDataList[endIndex]) {
endIndex = this.allDataList.length - 1;
}
return endIndex;
},
showDataList() {
const arr = this.allDataList.slice(this.startIndex, this.endIndex);
return arr;
},
blankFill() {
return {
paddingTop: this.startIndex * this.oneHeight + "px",
paddingBottom:
(this.allDataList.length - this.endIndex) * this.oneHeight + "px",
};
},
},
methods: {
getNewsData() {
this.isRequestStatus = true;
return this.$axios
.get(`${this.requestUrl}${this.requestNum}`)
.then((res) => {
this.isRequestStatus = false;
return res.data.list;
})
.catch(() => {
this.isRequestStatus = false;
return false;
});
},
getContainerSize() {
const offsetHeight = this.$refs.scrollConainer.offsetHeight;
this.containSize = Math.floor(offsetHeight / this.oneHeight) + 2;
console.log(offsetHeight, this.containSize, "containerSize");
},
handleScroll() {
let fps = 30;
let interval = 1000 / fps;
let then = Date.now();
requestAnimationFrame(() => {
let now = Date.now();
this.setDataStart();
if (now - then >= interval) {
then = now;
requestAnimationFrame(arguments.callee);
}
});
},
async setDataStart() {
this.currentScrollTop = this.$refs.scrollConainer.scrollTop;
this.currentIndex = ~~(this.currentScrollTop / this.oneHeight);
if (this.currentIndex === this.startIndex) return;
this.startIndex = this.currentIndex;
if (
this.startIndex + this.containSize > this.allDataList.length - 1 &&
!this.isRequestStatus
) {
let newList = await this.getNewsData();
if (!newList) return;
this.allDataList = [...this.allDataList, ...newList];
}
},
},
async created() {
const result = await this.getNewsData();
if (!result) return;
this.allDataList = result;
},
mounted() {
this.getContainerSize();
window.onresize = this.getContainerSize;
window.onorientationchange = this.getContainerSize;
},
};
</script>
<style scoped>
.scroll-container {
height: 100vh;
overflow-y: scroll;
}
.title {
font-size: 16px;
font-weight: bold;
text-align: left;
}
.listItem {
height: 100px;
}
.itemContent {
display: flex;
justify-content: space-between;
}
</style>
- vue 中虚拟列表调用
<template>
<div class="news-box">
<VScroll
:msg="msg"
:oneHeight="oneHeight"
:requestUrl="requestUrl"
:requestNum="requestNum"
v-slot:default="oneItem"
>
<router-link
:to="{ path: '/article?id=' + oneItem.thisItem.id }"
class="itemContent"
>
<div class="itemleft">
<div class="title">{{ oneItem.thisItem.title }}</div>
<div class="bottomContent">
<div class="from">{{ oneItem.thisItem.from }}</div>
<div class="date">{{ oneItem.thisItem.date }}</div>
</div>
</div>
<div class="img">
<img class="img2" :src="imgList[oneItem.thisItem.image]" alt="" />
</div>
</router-link>
</VScroll>
</div>
</template>
<script>
import imgList from "../imgs/index";
export default {
name: "index",
data() {
return {
imgList,
msg: "小二正在努力,请耐心等待...",
oneHeight: 100,
requestUrl: "http://192.168.1.107:4000/data?num=",
requestNum: 10,
};
},
activated() {},
computed: {},
methods: {},
};
</script>
<style scoped>
.news-box {
height: 100%;
/* padding-bottom: 50px; */
}
.title {
font-size: 16px;
font-weight: bold;
text-align: left;
}
.listItem {
height: 100px;
}
.itemContent {
display: flex;
justify-content: space-between;
}
.itemleft {
width: 220px;
}
.img {
width: 150px;
height: 80px;
background: pink;
overflow: hidden;
}
.img2 {
height: auto;
max-width: 100%;
max-height: 100%;
vertical-align: bottom;
object-fit: fill;
}
.bottomContent {
margin-top: 10px;
display: flex;
justify-content: space-between;
}
.from {
font-size: 12px;
color: #999;
}
.date {
font-size: 12px;
color: #999;
}
</style>
下拉刷新
/**
* @param element 元素
* @param callback 回调
*/
function downRefresh(element, callback) {
let startY; // 开始下拉时候纵坐标
let distance // 本次下拉距离
let originalTop = element.offsetTop; // 刚开始元素距离顶部的高度
let startTop;
element.addEventListener('touchstart', function (event) {
if(element.scrollTop <= 0){
startTop = element.offsetTop;
startY = event.touches[0].pageY; // 当前点击纵坐标
element.addEventListener('touchmove', touchMove);
element.addEventListener('touchend', touchEnd);
}
function touchMove(event) {
let pageY = event.touches[0].pageY;
if(pageY>startY){
distance = pageY - startY //当前移动距离
element.style.top = startTop + distance + 'px'
}else{
element.removeEventListener('touchmove', touchMove);
element.removeEventListener('touchend', touchEnd);
}
}
function touchEnd(event) {
element.removeEventListener('touchmove', touchMove);
element.removeEventListener('touchend', touchEnd);
if (distance > 30) {
callback()
}
function _back() {
let currentTop = element.offsetTop;
if (currentTop - originalTop >= 1) {
element.style.top = currentTop - 1 + 'px';
requestAnimationFrame(_back);
} else {
element.style.top = originalTop + 'px'
}
}
requestAnimationFrame(_back)
}
})
}
计算刷新率(帧数)
var then = Date.now();
var count = 0;
function nextTime() {
requestAnimationFrame(function () {
count++;
if (count % 60 === 0) {
var time = (Date.now() - then) / count;
var ms = Math.round(time * 1000) / 1000;
var fps = Math.round(1000 / ms)
console.log(`count:${count}\t ${ms}:ms;fps:${fps}`)
}
nextTime()
})
}
nextTime()
flat函数实现
function flat(arr) {
if (!arr instanceof Array) {
return arr
};
let isDeep = arr.some(item => (item instanceof Array))
if (!isDeep) return arr;
let res = Array.prototype.concat.apply([], arr);
res = flat(res);
return res;
}
深拷贝
// fn,undefined 会被过滤掉, date会被转成时间,reg:{}变成空对象
let obj = {
product: { name: "传感器", version: '123' },
age: 12,
fn: function () { return 1 },
date: new Date(),
reg: /\d/g,
a: undefined,
b: null
}
const copyObj = JSON.parse(JSON.stringify(obj));
function deepClone(obj, hash = new WeakMap()) {
// 判断是undefined 和null
if (obj == null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
// 要不是数组,要不是对象
if (hash.has(obj)) return hash.get(obj);
let cloneObj = new obj.constructor;
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash)
}
}
return cloneObj;
}
Object.create 实现
function create(parentPropertype) {
let Fn = function () { }
Fn.prototype = parentPropertype;
return new Fn();
}
es6 class
class Animal {
constructor(name) {
this.name = name;
this.eat = "吃肉"
}
say() {
console.log(this, 'this')
}
}
let animal = new Animal();
let say = animal.say;
say(); // this is undefined
判断变量的类型
1. typeof 不能判断对象类型 [] ,{}
2. constructor 可以找到这个变量是通过谁构造出来的
3. instanceif 判断谁是谁的实例 __proto__
4. Object.prototype.toString.call() 缺陷就是不能细分谁是谁的实例
函数柯里化
const currying = (fn, arr = []) => {
let len = fn.length;
return function (...arg) {
arr = [...arr, ...arg];
if (arr.length < len) {
return currying(fn, arr)
} else {
return fn(...arr)
}
}
}
function isType(type,value) {
return Object.prototype.toString.call(value) === `[object ${type}]`
}
判断相等
function isObjectEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length !== bProps.length) return false
for (var i = 0; i < aProps.length; i++) {
const propName = aProps[i]
const propA = a[propName]
const propB = b[propName]
if (typeof propA === 'object') {
return isObjectEqual(propA, propB)
} else if (propA !== propB) {
return false
}
}
return true
}
aliplayer播放器
const playerCssUrl =
"https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css";
const playerJsUrl = `https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-h5-min.js`;
export function loadJs(url: string) {
return new Promise(function (resolve, reject) {
let script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onerror = reject;
script.onload = resolve;
document.head.appendChild(script);
});
}
export function loadCss(href: string) {
return new Promise(function (resolve, reject) {
let link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}
export async function loadPlayer() {
await loadCss(playerCssUrl);
await loadJs(playerJsUrl);
}
export default class VideoPlayer {
id: string;
videoUrl: string;
width: string;
height: string;
player: any;
cover: string;
constructor(props: any) {
this.id = props.id;
this.cover = props.cover;
this.videoUrl = props.videoUrl;
this.width = props.width || "100%";
this.height = props.height || "450px";
this.player = null;
this.init();
}
async init() {
await loadPlayer();
this.player = new Aliplayer({
id: this.id,
cover: this.cover,
source: this.videoUrl,
width: this.width,
height: this.height,
playsinline: true,
isLive: false, // 直播
replay: false, // 循环直播
preload: true, // 播放器字段加载
autoplay: true, // 自动播放
diagnosisButtonVisible: false, //是否显示检测按钮
keyShortCuts: true, //是否启用快捷键
keyFastForwardStep: 5, //快进快退的时间长度
controlBarVisibility: "hover", // 控制条的显示方式:鼠标悬停
useH5Prism: true, //启用H5播放器
});
}
pause() {
this.player.pause();
}
seek(time: number) {
this.player.seek(time);
}
getCurrentTime() {
this.player.getCurrentTime();
}
getStatus() {
this.player.getStatus();
}
mute() {
this.player.mute();
}
replay() {
this.player.replay();
}
}