前端算法
1.合并两个有序链表
- 递归方式
将两个升序链表合并为一个新的升序链表并返回, 新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入: 1->2->4, 1->3->4
输出: 1->1->2->3->4->4
const mergeTwoLists = function(l1, l2){
if(l1 === null){
return l2
}
if(l2 === null){
return l1
}
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2)
return l1
}else{
l2.next = mergeTwoLists(l1, l2.next)
return l2
}
}
/**
M,N是两条链表l1, l2的长度
时间复杂度: O(M+N)
空间复杂度: O(M+N)
*/
2.通过迭代的方式实现
var mergeTwoLists = function (l1, l2){
const prehead = new ListNode(-1)
let prev= prehead
while(l1 != null && l2!=null){
if(l1.val <= l2.val){
prev.next = l1;
l1 = l1.next
}else{
prev.next =l2
l2= l2.next
}
prev= prev.next
}
prev.next = l1 === null ? l2: l1
return prehead.next
}
2. 括号生成
数组n代表生成括号的对数, 请你设计一个函数, 用于能够生成所有可能的并且
有效的括号组合
示例:
输入: n=3
输出: ['((()))', '(()())','(())()','()(())','()()()']
const generateParenthesis = function (n) {
const res= [];
function dfs(l, r, str){
if(l == n && r == n){
return res.push(str)
}
// 当l小于r的时候不满足条件, 剪枝
if(l< r){
retrun
}
// l小于n 的时候可以插入左括号, 最多可以插入n个
if(l<n){
dfs(l+1,r, str+'(')
}
// r<l的时候, 可以插入右括号
if(r<l){
dfs(l , r+1, ')')
}
}
dfs(0, 0, '')
return res
}
时间复杂度: o(2^N)
空间复杂度: o(2^N)
合并K个排序链表
描述:
合并k个排序链表, 返回合并后的排序链表, 情分析和描述算法的复杂度
示例:
输入: [1->4->5, 1->3->4,2->6]
输出: 1->1->2->3->4->4->5->6
两数之和
题目: 给定一个数组nums和一个目标值target, 在该数组中找出和为目标值的两个数
输入: nums[8, 2, 6, 5, 4, 1, 3]; target: 7
输出: [2, 5]
// 时间复杂度o(n), 空间复杂度为o(n)
const twoNumsAdd = function (arr, target){
if(Array.isArray(arr)){
// 使用map将遍历过的数组存储起来, 空间换时间
let map = new Map()
for(let i=0; i< arr.length; i++){
let val = target- arr[i]
if(map.has(val)){
return [val, arr[i]]
}else{
map.set(arr[i], i)
}
}
}
}
三数之和
题目: 给定一个数组nums, 判断nums中是否存在三个元素a,b,c, 使得a+b+
c = target, 找出所有满足条件且不重复的三元组合
输入: nums:[5,2,1,1,3,4,6];target:8
输出: [[1,1,6], [1,2,5], [1,3,4]]
// 利用双指针的方式, 将三数之和转化为两数之和
const findTree = function (arr, target){
// 先将数组从小到大排列
arr.sort((a,b) => a-b)
for(let i=0; i< arr.length; i++){
// 跳过重复的arr[i]的值, 比如[2,1,1]跳过第二个1
if(arr[i] && arr[i] === arr[i-1]) continue
let left = i+1
let right = arr.length-1
// 双指针left, right
while(left < right){
let sum = arr[i]+ arr[left]+arr[right]
if(sum> target){
right--
}else if(sum< target){
left++
}else{
// 相等的时候, 先取出arr[left], 然后left++, 两步合成一步, arr[right--]同样的逻辑
result.push(arr[i], arr[left], arr[right])
while(arr[left] === arr[left-1]){
left ++
}
while(arr[right] === arr[right+1]){
right--
}
}
}
}
return result
}
版本号排序
题目: 输入一组版本号, 输出从大到小的排序
输入: ['2.1.0.1', '0.402.1', '10.2.1', '5.1.2', '1.0.4.5']
输出: ['10.2.1', '5.1.2', '2.1.0.1', '1.0.4.5', '0.402.1']
const versionSort = function (nums){
return nums.sort((a, b) => {
let i=0;
const arr1 = a.split('.')
const arr2 = b.split('.')
while(true){
const s1 = arr1[i]
const s2= arr2[i]
i++;
// 若s1或者es2不存在, 说明相同的位置已比较完成, 接下来比较
// arr1与arr2的长度, 长的版本号大
if(s1 === undefined || s2 ===undefined){
return arr2.length - arr1.length
}
if(s1===s2) continue
return s2 -s1
}
})
}
找出第一个不重复的字符
题目: 输入一个字符串, 找到第一个不重复的字符的下标
输入: 'abcabcde'
输出: 6
// 时间复杂度o(n), 空间复杂度o(n)
const findIndex = function (str){
if(!str) return -1;
// 使用map存储每个字符出现的次数
let map = new Map()
let arr= str.split('')
for(let-i=0; i< arr.length; i++){
const key= arr[i]
const mapVal = map[arr[i]]
map[key] = mapVal? mapVal+1:1
}
// 在遍历一遍找到出现一次的下标
for(let i =0; i< arr.length; i++){
if(map.has(arr[i] === 1){
return i
}
}
return -1
}
字符串所有排列组合
题目: 输入一个字符串,打印出该字符串中,所有字符的排列组合
输入: 'abc'
输出: ['abc', 'acb', 'bca', 'bac', 'cab', 'cba']
function permute (str){
if(str.length === 0) return ['']
let result=[];
let first = str[0]
let subPermutes = permute(str.slice(1));
for(let subPermute of subPermutes){
for(let i=0; i<= subPermute.length; i++){
result.push(subPermute.slice(0, 1)+first, subPermute.slice(i))
}
}
return result
}
第二种方式实现:
/**
* 交换数组中两个元素的位置
* @param {Array} arr 数组
* @param {number} i 第一个元素的索引
* @param {number} j 第二个元素的索引
*/
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 回溯函数,用于生成排列组合
* @param {Array} arr 字符数组
* @param {number} start 开始位置
* @param {string[]} result 结果数组
*/
function backtrack(arr,start, result){
if(start === arr.length-1){
result.push(arr.join(''))
return ;
}
for(let i= start; i< arr.length; i++){
swap(arr, start, i)
backtrack(arr, start+1, result)
swap(arr, start, i)
}
}
funciton permute(str){
let result = [];
let arr = str.split('')
backtrack(arr, 0, result)
return result
}
冒泡排序
时间复杂度为O(n^2) 稳定排序算法
const bubbleSort = function (arr){
const length = arr.length;
// 外层循环控制, 排序进行多少轮
for(let i=0; i< length;i++){
// 内层循环用于每一轮的数据比较
// 注意 j的长度范围是length-i-1
for(let j=0; j< length-i-1; i++){
// 相邻元素, 大的放到后面
if(arr[j] > arr[j+1]){
// 交换位置
[arr[j], arr[j+1]] = arr[arr[j+1], arr[j]]
}
}
}
return arr
}
选择排序
时间复杂度O(n^2) 不稳定
const selectSort = function (arr){
// 定义index存储最小值得下标
let index;
// 外层循环控制, 排序进行多少轮
for(let i=0; i< arr.length; i++){
index= i;
// 内层循环用于每一轮的数据比较
// 注意j的起始范围是i+1
for(let j=i+1;j<arr.length; j++){
// 寻找最小值
if(arr[j]< arr[index]){
// 保存最小值得下标
index = j
}
}
if(index !== i) {
[arr[i], arr[index]] = [arr[index], arr[j]]
}
}
return arr
}
插入排序
时间复杂度为O(n^2), 稳定
const inserSort = function (arr) {
// 从第二个元素开始遍历序列
for(let i=1; i< arr.length; i++){
let j=i;
// 记录要插入的目标元素
let target = arr[j];
// 从target所在位置向前遍历, 直至找到一个比目标元素小的元素,
// 目标元素插入到该元素之后的位置
while(j> 0&& arr[j-1]> target){
// 移动前一个元素的位置, 将其向后移动一个位置
arr[j] = arr[j-1]
j--
}
arr[j] = target
}
return arr
}
快速排序
时间复杂未O(n) 不稳定
const quickSort = function (list){
// 当list.length < =1 的时候, 推出递归
if(list.lengtn <=1) return list
// 找到中间节点
let mid = Math.floor(list.length/2)
// 以中间节点为基准, 比该节点大的参数让在right数组中, 比该节点小的放在left数组中
let base = list.splice(mid, 1)[0]
let left =[]
let right =[]
for(let i=0; i< list.length; i++){
if(list[i]>base){
right.push(list[i])
}else{
left.push(list[i])
}
}
return quickSort(left).concat(base, quickSort(right))
}
实现方式2
// 交换数组中的两个元素的位置
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j]= temp
}
// 划分函数 , 将函数划分为两部分, 左边元素小于等于pivot, 右边大于等于pivot
function partition(arr, low, high){
let pivot = arr[high]
let i= (low -1)
for(let j=low;j<high-1;j++){
if(arr[j]<= pivot){
i++;
swap(arr, i, j)
}
}
swap(arr, i+1, high)
return i+1
}
// 快速排序函数
function quickSort(arr, low=0, high = arr.length-1){
if(low< right){
let pi = partition(arr, low, right)
quickSort(arr, low, pi-1)
quickSort(arr, pi+1, high)
}
return arr
}
归并排序
时间复杂度O(N)稳定
funciton MergeSort(array){
function merge(left, right){
let [l, r]=[0, 0]
let result=[];
// 从left和right区域中各取出第一个元素, 比较它们的大小
while(l< left.length && r< right.length){
// 将较小的元素添加到result中,然后从较小元素坐在的区域内取出
// 下一个元素, 继续进行比较
if(left[i]< right[t]){
result.push(left[i])
l++
}else{
result.push(right[r])
r++
}
}
// 如果left或者right有一个为空, 则直接将另一方的所有元素依次添加到result中
result = result.concat(left.slice(1,left.length))
result = result.concat(right.slice(r, right.length))
return result
}
let len = array.length;
// 当每个子序列中仅有1个元素的时候返回
if(len <=1){
return array;
}
// 将给定的列表分为两部分
let num= Math.floor(len/2)
let left = MergeSort(array.slice(0, num))
let right = MergeSort(array.slice(num, len))
return merge(left, right)
}
列表转成树
题目: 输入一组列表如下。 转换成树形结构 输入:
[
{ id: 1, title: "child1", parentId: 0 },
{ id: 2, title: "child2", parentId: 0 },
{ id: 3, title: "child1_1", parentId: 1 },
{ id: 4, title: "child1_2", parentId: 1 },
{ id: 5, title: "child2_1", parentId: 2 }
]
输出:
tree=[
{
"id": 1,
"title": "child1",
"parentId": 0,
"children": [
{
"id": 3,
"title": "child1_1",
"parentId": 1
},
{
"id": 4,
"title": "child1_2",
"parentId": 1
}
]
},
{
"id": 2,
"title": "child2",
"parentId": 0,
"children": [
{
"id": 5,
"title": "child2_1",
"parentId": 2
}
]
}
]
funciton listToTree(data){
// 使用对象重新存储数据, 空间换时间
let map = {}
let treeData =[]
// 遍历原始数据data, 存到map中, id为key, 值为数据
for(let i=0; i< data.length; i++){
map[data[i].id] = data[i]
}
// 遍历对象
for(let i in map){
if(map[i].parentId){
if(!map[map[i.parentId]].children){
map[map[i].parentId].children =[]
}
map[map[i].parentId].children.push(map[i])
}else{
treeData.push(map[i])
}
}
return treeData
}
深度优先遍历
题目: 对树进行遍历, 从第一个节点开始, 遍历其子节点, 直到它的所有子节点都被遍历完毕, 然后在遍历它的兄弟节点 输入:
tree=[
{
"id": 1,
"title": "child1",
"parentId": 0,
"children": [
{
"id": 3,
"title": "child1_1",
"parentId": 1
},
{
"id": 4,
"title": "child1_2",
"parentId": 1
}
]
},
{
"id": 2,
"title": "child2",
"parentId": 0,
"children": [
{
"id": 5,
"title": "child2_1",
"parentId": 2
}
]
}
]
输出: [1,3,4,2,5]
// 递归版本
function deepTree(tree, arr=[]){
if(!tree || !tree.length) return arr;
tree.forEach(data => {
arr.push(data.id)
// 遍历子树
data.children && deepTree(data.children, arr)
})
return arr
}
// 非递归版本
funciton deeepTree(tree){
if(!tree || !tree.length) return
let arr = [];
let stack =[]
// 先将第一层节点放入栈中
let len = tree.length
for(let i=0; i< len; i++){
stack.push(tree[j])
}
let node;
while(stack.length){
// 获取当前第一个节点
node = stack.shift()
arr.push(node.id)
// 如果该节点有子节点, 继续添加进入栈中
if(node,children && node.children.length){
stack = node.children.concat(stack)
}
}
return arr
}
广度优先遍历
题目: 以横向的维度对树进行遍历, 从第一个节点开始, 依次遍历其素有的兄弟节点, 再遍历第一个节点的子节点, 一层层的向下遍历 输入:
tree=[
{
"id": 1,
"title": "child1",
"parentId": 0,
"children": [
{
"id": 3,
"title": "child1_1",
"parentId": 1
},
{
"id": 4,
"title": "child1_2",
"parentId": 1
}
]
},
{
"id": 2,
"title": "child2",
"parentId": 0,
"children": [
{
"id": 5,
"title": "child2_1",
"parentId": 2
}
]
}
]
输出:[1,2,3,4,5]
function rangeTree(tree){
if(!tree || !tree.length) return ;
let arr = [];
let node, list = [...tree]
// 取出当前节点
while(node = list.shift()){
arr.push(node.id)
node. children && list.push(...node.children)
}
return arr
}
买卖股票问题
题目: 给定一个整数数组, 其中第i个元素代表了第i天的股票价格; 非负整数fee代表了交易过票的手续费用, 求返回获的利润的最大值 输入: arr:[1, 12, 13, 9 , 15, 8, 6, 16] feee: 2 输出: 22
// 贪心算法求解
function buyStock(list, fee){
// min为当前的最小值, 即买入
let min = list[0]
let sum = 0;
for(let i =1; i< list.length; i++){
// 从1开始, 依次判断
if(list[i]<min){
// 寻找数组的最小值
min = list[i]
}else{
// 计算如果当前卖出是否赚钱
let temp = list[i]- min- fee
if(temp> 0){
// 赚钱, 存数据
sum+= temp
//重新计算min, 分两种情况, 如果后面继续涨, 则默认继续持有
// 若后面跌, 则以后面的价格重新买入
min=list[i]-fee
}
}
}
return sum
}
斐波那契数列
题目: 从第3项开始,当前项等于前两项之和: 1 1 2 3 5 8 13 21 ……,计算第n项的值
输入: 10
输出: 89
function fib(n){
// 使用dp数组, 将之前计算的结果存起来, 防治栈溢出
if(n === 0) return 0;
if(n ===1) return 1
let dp = [0,1];
for(let i=2;i<=n ; i++){
dp[i] = dp[i-1]+dp[i-2]
}
return dp[n]
}
实现方式2 ,利用三个变量形式, 动态规划
function fib(n){
if(n===0) return 0
if(n ===1) return 1
lett rn2=0, rn_1 =1, rn;
for(let i=2; i<=n ; i++){
rn = rn_1+rn_2
rn_2 = rn_1
rn_1 = rn
}
return rn
}
如何解决页面请求大规模并发问题
滑动窗口 算法, 专门来流量控制的
背景
数据采集平台, 低代码平台, 有序相对稳定发送到后端
方案
class RequestQueue{
constructor(maxConcurrent){
this.maxConcurrent = 0; // 最大请求并发
this.currentConcurrent =0; // 当前并发请求数
this.queue = [] // 请求队列
}
add(request){
return new Promise((resolve, reject) => {
this.queue.push({request, resolve, rejext})
this.processQueue()
})
}
processQueue(){
if(this.Queue.length > 0 && this.currentConCurrent< this.maxConsurrent){
const {request, resolve, reject} = this.queue.shift()
this.currentConsurrent++;
request()
.then(resolve)
.catch(reject)
.finally(() => {
this.currentXConcurrent--;
this.processQueue()
})
}
}
}
大文件上传怎么处理
场景: 上百兆适合使用
经常遇到的问题:
- 网络断开, 之前传的没了
- 传着传着,网络波动 结果啥都没有了
- 关机了, 想接着传, 做不到
专业术语
- 断点续传
- 断开重连重传
- 切片上传
方案:
-
前端切片 chunk 1024M(1047576k) 按照500k切片, const size = 1048576/500
-
将切片传递给后端,切的片要取名: hash, index
-
后端组合切片
-
前端切片: 主进程会卡顿, 利用web-worker多线程切片, 处理了完成交给主进程发送
-
切完后, 将bolb 存储到IndexDB, 下次用来进来以后, 嗅探一下IndexDB里面是否存在未完成上传的切片, 有就尝试继续上传
-
websocket, 实时通知和请求序列的控制, 现在一般用的是wss
大文件上传的源码
<body>
<input type='file' id='fileInput' >
<button onclick='uploadFile()'>upload</button>
<script>
const CHUNK_SIZE= 5*1024*1024 // 每块大小为5MB
function uploadFile(){
const file = document.getElementById('fileInput')[0]
if(!file){
alert('请选择文件')
return
}
const totalChunks = Math.ceil(file.size/ CHUNK_SIZE)
let currenkChunk =0;
function uploadChunk(){
if(currentChunk >= totalChunk){
console.log('上传文件')
return
}
const start = currentChunk * CHUNK_SIZE
const end = Math.min(start + CHUNK_SIZE, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumer', currentChunk +1)
formData.append('totalChunks', totalChunks)
fetch('/upload', {
method:'POST',
body: formData
})
.then(response => {
if(response.ok){
currentChunk++
uploadChunk() // 递归调用上传 下一部分
}else{
console.log('')
}
})
.catch(error => {
console.log('上床异常')
})
}
uploadChunk()
}
</script>
</body>
在前端怎么实现页面截图
交代背景
- 像飞书文档 , 内容分在列表页想要查看
- 内容导出为png格式
- 设计类软件, 出图
方案
- canvas
- puppeteer 无头表格, 无头ui
- html2canvas
- 上传cdn
落地
全页面截图还是局部截图
- 截图工具的时候, 需要考虑通用性, selector body header dom
- 设计具体协议
h5移动后端如何做适配
背景
项目想支持PC,移动端
方案
- 根据端来开发不同页面(成本较高)
- 根据不同端加载不同的css样式(可取)
- 根据响应式, 来运行不同的样式规则(常用)
- style预处理器来做
考虑的问题:
- 设置视窗, 通过元信息配置 meta
<meta name='viewport' content='width = device-width, inital-scale =1.0'>
- 掌握媒体查询
body{
font-szie: 16px;
}
// 但是在某一些设备尺寸, 这个size是要更改的
@media(min-width: 780px) and(max-width: 1024px){
body{
font-size: 18px
}
}
- 弹性布局 flex布局
- 图片的响应式
<picture>
<source srcset='image-large.jpg' media='(min-width:800px)'>
<source srcset='image-medium.jpg' media='(min-width: 400px)'>
<img src='image-small.jpg' alt='response Image' >
</picture>
- rem rem单位的基础值由html的font-size决定
html{
font-size: 16px;
}
.header{
font-size: 1rem;
}
如何修改第三方的npm包
方案
- 稳定库, node_modules直接修改
- patch方案 'patch-package' 自动化 'pnpm i patch-package postinstall'
{
'scripts':{
'postinstall' :'patch-package'
}
}
创建补丁 npx pacth-package rspack 这个时候会在项目生成'patchs/rspack+1.0.0.patch'
- fork (github fork) 直接改源码, 源码修改之后, 构建,发布到npm私服
使用同一个链接, 如何实现pc打开的是web应用, 手机打开的是一个h5应用
方案
区分 pc mobile
识别端
- js识别userAgent
当QPS达到峰值的时候, 该如何处理
背景
当前端应用的QPS达到峰值的时候, 会对服务器和应用的性能造成很大的压力, 甚至可能导致系统崩溃, 为了解决这个问题, 我们需要采取一系列的措施优化和刮半年里高并发请求。
方案:
-
请求限流 以node.js为例, 限流
-
请求合并
-
短时间内的请求进行合并, 以此来降低服务端的压力
-
请求缓存
-
任务队列
如何实现网页加载进度条
web应用中如何对静态资源加载失败的场景做降级处理
场景
- 图片
- css文件
- javascript文件
- cdn
- 字体文件
- 服务端渲染
解决方案
- 占位图 来描述图片
- 重试机制(404, 无权限)
- 上报
<img src=''alt='' onerror='handleImgeError(this)'
function handleImageError(image){
image.error = null // 防止死循环
image.src ='placeholder.png' // 使用占位图
}
处理css文件
资源没有加载到
- 关键性样式, 通过内联
- 备用样式
- 上报
<head>
<style>
body{
font-family:''
}
</style>
<link rel='stylesheet' href='styles.css' onerror='handleCssError()'>
</head>
function handleCssError(){
// 加载备用样式
const fallbackCss = document.createElement('link')
fallback.rel = 'stylesheet'
fallbackCss.href='fallback-styles.css'
document.head.appenChild(fallbackCss)
}
javascript 文件处理
网络异常, 导致资源没有加载 解决:
- 内联脚本
- 备用脚本处理
- 上报
cdn
- 本地备份, 如果cdn出错了, 就使用本地备份
- 动态切换, 切到另外一个有用的cdn服务
<head>
<script src='https://cdn....' onerror='handleCdnError()'
</head>
function handleCssError(){
// 加载本地备份
const fallbackScript = document.createElement('script')
fallbackScript.src ='beifen.js'
document.head.appendChild(fallbackScript)
}
怎么设计一个全站请求耗时的统计工具
背景
通过这个统计工具, 可以更清晰看到整个站点的性能情况,首屏加载时间(FP/FCP)
- 监控请求耗时 http, 中间件,axios
- 前端监控, 监控整个请求相关并且记录耗时数据
- 后端监控:后端记录
- 数据汇总: 数据清洗加工, 数据可视化,可视化图表
(function (){
const originaXhrOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.opne= function (...args){
this._startTime = performance.now()
this.addEventListener('load', function (){
const duration = performance.now() - this._startTime
console.log(`XHR${args[1]took${duration}}ms`)
reportRequestDuration(args[1], duration, 'XHR')
})
originalXhrOpen.apply(this, args)
}
const originalFetch = window.fetch
window.fetch = async function (...args){
const startTime = performance.now()
const response = await originalFetch.apply(this, args)
const duration = performance.now() - startTime
reportRequestDuration(args[0], duration, 'Fetch')
return response
}
function reportRequestDuration(url, duration, type){
fetch('', {
method:'post',
headers:{'Content-Type':'application/json'}
body:JSON.strinify({url, duration, type })
})
}
})()
说说对函数编程思想的理解
基本概念
- 函数封装的方式解决问题, 纯函数, 没有副作用, 相同输入得到相同的输出
- 不可变
- 高阶函数, 函数柯里化
- 函数组合,类似于面向对象继承 总结:
- 可测试性吗, 更好写单元测试
- 可维护性
- 并发
- 简洁
请你说说对dns协议的理解
将域名映射到ip上
域名解析的整个过程
浏览器渲染原理
- 用户输入域名
- 检查自身DNS缓存
- 操作系统的DNS缓存
- 本地域名服务器
- 根据本地DNS服务器去查找跟DNS服务器, 顶级域名服务器, 权威DNS服务器
- 返回结果, 浏览器缓存并向IP发起请求
DNS 的记录类型
- A记录: 将域名映射到IPV4地址
- AAAA记录: 将域名映射到IPV6地址
- CNAME记录; 将一个域名映射到另一个域名
- MX记录: 指定邮件服务器
- TXT: 文本信息存储, 域名验证,SPF记录
DNS常见问题
- DNS解析慢
- DNS预解析
- 使用CDN, CDN节点用户就近
- 减少外部资源请求,自己的域名+oss+cdn
- DNS劫持
- HTTPS, 证书保证传输安全性
- DNSSES:DNS安全扩展
优化:
- DNS缓存
- nslookup
- dig
- 在线: dns.google.com dnschecker.org
美团电影院,怎么实现一个电影选座的功能
使用canvas来实现选座功能
实现思路:
- canvas基础处理
- 座位绘制
- 交互添加
- 座位状态的管理, 数据结构设计
- 性能优化与美化
具体实现: 初始化操作如下
<head>
<style>
canvas{
border: 1px solid #000;
display: block;
margin: 0 auto;
}
</style>
<body>
<canvas id='cinemaCanvas' width='800' height='600'/>
<script srcc='cinema.js'></script>
</body>
</head>
绘制座位
cosnt canvas = document.getElementById('cinemaCanvas')
const ctx = canvas.getContext('2d')
const rows = 10
const cols = 15
const seatSize= 40
const seatSpacing = 10
const seats = []
for(let row =0; row< rows; rows++){
const seatRow =[];
for(let col=0; col< cols; col++){
seatRow.push({
x:col * (seatSize + seatSpacing),
y: row * (seatSize + seatSpacing),
status: 'available'
})
}
seats.push(seatRow)
}
function drawSeats(){
seats.forEach((row) => {
row.forEach((seat) => {
ctx.fillStyle = getSeatColor(seat.status)
ctx.fillRect(seat.x, seat.y, seatSize, seatSize)
})
})
}
function getSeatColor(status){
switch(status){
case 'available':
return 'green'
case 'selected':
return 'blue'
case 'unavailable':
return 'red'
default:
return 'grey'
}
}
drawSeats()
说说图片性能优化的方案
方案对比
- loading='lazy'
<img src='iamge.jpg' alt='example image' loading='lazy' >
- intersection observer
<img data-src='image.jpg' alt='example image' class='lazyload'>
<script>
document.addEventListener('DOMContenxtLoaded', function (){
const lazyImages = document.querySelectorAll('img.lazyload')
if('IntersectionObserve' in window){
const observer = new IntersectionObserve(function(entries, observe){
entries.forEach(entry => {
if(entry.isIntersecting){
const img = entry.target
img.src = img.dataset.src
imhg.classList.remove('lazyload')
observer.unobserver(img)
}
})
})
lazyImages.forEach(img => {
oberver.oberve(img)
})
}else{
lazyImage.forEach(img => {
img.src = img.dataset.src
})
}
})
</script>
- 滚动事件来做 监听scroll事件 最原始的做法
<img data-src='image.jpg' alt='' class ='lazyload'>
<script>
document.addEventListener('DOMContentLoaded', function (){
const lazyImages = document.querySelectAll('img.lazyload')
const lazyLoad = () => {
lazyImages.forEach(img => {
if(img.getBoundingClientRect().top < window.innerHeight && img.getBoundingClientRect().bottom > 0 && getComputedStyle(img).display !== 'none'){
img.src= img.dataset.src
img.classList.remove('lazyload')
}
})
if(lazyImage.length === 0){
document.removeEventListener('scroll', lazyload)
window.removeEventListener('resize', lazyload)
window.removeEventListener('orientationchange', lazyload)
}
}
document.addEventListener('scroll', lazyload)
window.addEventListener('resize', lazyload)
window.addEventListener('orientationchange', lazyload)
})
</script>
服务端渲染
服务端渲染的理解? 服务端渲染概述: 服务端渲染: 在服务器端将网页内容渲染为完整的HTML, 然后发送到客户端浏览器进行显示。 客户端渲染: 在浏览器端通过javsscript动态生成页面内容
ssr的优势与挑战 优势:
- 提高首屏渲染速度 用户无需等待javascript架子啊和执行即可查看到完成内容, 显著提升用户体验
- seo优化 搜索引擎可以直接抓取完整的HTML内容, 有助于提高网站在搜索结果中的排名
- 共享连接时显示预览 在社交媒体上分享链接的时候, 能够正确显示页面预览信息, 增加内容的传播性
挑战:
- 开发复杂度增加 需要处理服务器渲染逻辑, 设计Node.js等后端技术, 与前端开发结合更复杂
- 服务器压力增大 服务器需要承担渲染页面的工作, 可能导致性能瓶颈, 尤其是在高并发场景下
- 状态管理复杂 需要在服务器和客户端之间同步状态, 确保数据一致性, 避免因状态不同导致的错误